dev@354: /** dev@354: * Created by lucast on 26/05/2017. dev@354: */ dev@384: import {AfterViewInit, ElementRef, Input, ViewChild} from '@angular/core'; dev@354: import {OnSeekHandler} from '../playhead/PlayHeadHelpers'; dev@354: import {attachTouchHandlerBodges} from './WavesJunk'; dev@354: import Waves from 'waves-ui-piper'; dev@376: import {countingIdProvider} from 'piper/client-stubs/WebWorkerStreamingClient'; dev@383: import {ShapedFeatureData} from './FeatureUtilities'; dev@376: dev@376: const trackIdGenerator = countingIdProvider(0); dev@354: dev@389: // has to be an abstract class vs as interface for Angular's DI dev@389: export abstract class VerticallyBounded { dev@389: abstract get range(): [number, number]; dev@392: } dev@392: dev@392: export abstract class VerticalScaleRenderer extends VerticallyBounded { dev@389: abstract renderScale(range: [number, number]): void; dev@389: } dev@389: dev@392: export abstract class VerticalValueInspectorRenderer dev@392: extends VerticalScaleRenderer { dev@392: // TODO how do I know these layers are actually 'describable'? dev@394: abstract renderInspector(range: [number, number], unit?: string): void; dev@397: abstract get updatePosition(): OnSeekHandler; dev@392: } dev@392: dev@406: export type LayerRemover = () => void; dev@406: dev@383: export abstract class WavesComponent dev@383: implements AfterViewInit { dev@384: @ViewChild('track') trackContainer: ElementRef; dev@354: @Input() set width(width: number) { dev@354: if (this.timeline) { dev@354: requestAnimationFrame(() => { dev@354: this.timeline.timeContext.visibleWidth = width; dev@354: this.timeline.tracks.update(); dev@354: }); dev@354: } dev@354: } dev@354: @Input() timeline: Timeline; dev@354: @Input() onSeek: OnSeekHandler; dev@379: @Input() colour: string; dev@412: @Input() duration: number; dev@383: @Input() set feature(feature: T) { dev@383: this.mFeature = feature; dev@383: this.update(); dev@383: } dev@383: dev@383: get feature(): T { dev@383: return this.mFeature; dev@383: } dev@354: dev@387: private layers: Layer[]; dev@387: private zoomOnMouseDown: number; dev@387: private offsetOnMouseDown: number; dev@387: private waveTrack: Track; dev@387: private mFeature: T; dev@387: private id: string; dev@383: protected abstract get featureLayers(): Layer[]; dev@392: protected cachedFeatureLayers: Layer[]; dev@383: protected postAddMap: (value: Layer, index: number, array: Layer[]) => void; dev@406: height: number; dev@354: dev@354: constructor() { dev@354: this.layers = []; dev@376: this.id = trackIdGenerator.next().value; dev@354: } dev@354: dev@383: ngAfterViewInit(): void { dev@384: this.height = dev@384: this.trackContainer.nativeElement.getBoundingClientRect().height; dev@386: this.renderTimeline(); dev@383: this.update(); dev@383: } dev@383: dev@386: private update(): void { dev@383: if (!this.waveTrack || !this.mFeature) { dev@383: return; dev@383: } dev@386: this.clearTimeline(); dev@392: this.cachedFeatureLayers = this.featureLayers; dev@392: for (const layer of this.cachedFeatureLayers) { dev@389: this.addLayer(layer); dev@383: } dev@383: if (this.postAddMap) { dev@392: this.cachedFeatureLayers.forEach(this.postAddMap); dev@383: } dev@383: } dev@383: dev@383: dev@386: private renderTimeline(): Timeline { dev@386: const track: HTMLElement = this.trackContainer.nativeElement; dev@354: track.innerHTML = ''; dev@383: if (this.duration >= 0) { dev@364: const width: number = track.getBoundingClientRect().width; dev@383: this.timeline.pixelsPerSecond = width / this.duration; dev@364: this.timeline.visibleWidth = width; dev@364: } dev@354: this.waveTrack = this.timeline.createTrack( dev@354: track, dev@383: this.height, dev@376: this.id dev@354: ); dev@354: dev@354: if ('ontouchstart' in window) { dev@354: attachTouchHandlerBodges( dev@386: track, dev@354: this.timeline dev@354: ); dev@354: } dev@383: this.resetTimelineState(); dev@354: } dev@354: dev@354: // TODO can likely be removed, or use waves-ui methods dev@386: private clearTimeline(): void { dev@354: // loop through layers and remove them, waves-ui provides methods for this but it seems to not work properly dev@406: const track = this.waveTrack; dev@406: if (track.layers.length === 0) { return; } dev@406: const trackLayers: Layer[] = Array.from(track.layers as Layer[]); dev@406: while (trackLayers.length) { dev@406: this.removeLayer(trackLayers.pop()); dev@354: } dev@383: this.resetTimelineState(); dev@356: } dev@356: dev@406: private removeLayer(layer: Layer) { dev@406: if (this.layers.includes(layer) && this.waveTrack) { dev@406: const timeContextChildren = this.timeline.timeContext._children; dev@406: this.waveTrack.remove(layer); dev@406: this.layers.splice(this.layers.indexOf(layer), 1); dev@406: const index = timeContextChildren.indexOf(layer.timeContext); dev@406: if (index >= 0) { dev@406: timeContextChildren.splice(index, 1); dev@406: } dev@406: layer.destroy(); dev@406: } dev@406: } dev@406: dev@383: private resetTimelineState(): void { dev@356: // time axis dev@356: const timeAxis = new Waves.helpers.TimeAxisLayer({ dev@384: height: this.height, dev@356: color: '#b0b0b0' dev@356: }); dev@389: this.addLayer(timeAxis, true); dev@356: this.timeline.state = new Waves.states.CenteredZoomState(this.timeline); dev@364: this.timeline.tracks.update(); // TODO this is problematic, shared state across components dev@354: } dev@354: dev@354: dev@354: // TODO can likely use methods in waves-ui directly dev@406: addLayer(layer: Layer, dev@406: isAxis: boolean = false): LayerRemover { dev@389: const timeContext = this.timeline.timeContext; dev@354: if (!layer.timeContext) { dev@412: if (isAxis) { dev@412: layer.setTimeContext(timeContext); dev@412: } else { dev@412: const layerTimeContext = new Waves.core.LayerTimeContext(timeContext); dev@412: if (this.duration) { dev@412: layerTimeContext.duration = this.duration; dev@412: } dev@412: layer.setTimeContext(layerTimeContext); dev@412: } dev@354: } dev@389: this.waveTrack.add(layer); dev@354: this.layers.push(layer); dev@354: layer.render(); dev@354: layer.update(); dev@406: return () => this.removeLayer(layer); dev@354: } dev@354: dev@354: seekStart(): void { dev@354: this.zoomOnMouseDown = this.timeline.timeContext.zoom; dev@354: this.offsetOnMouseDown = this.timeline.timeContext.offset; dev@354: } dev@354: dev@354: seekEnd(x: number): void { dev@354: const hasSameZoom: boolean = this.zoomOnMouseDown === dev@354: this.timeline.timeContext.zoom; dev@354: const hasSameOffset: boolean = this.offsetOnMouseDown === dev@354: this.timeline.timeContext.offset; dev@354: if (hasSameZoom && hasSameOffset) { dev@354: this.seek(x); dev@354: } dev@354: } dev@354: dev@354: seek(x: number): void { dev@354: if (this.timeline) { dev@354: const timeContext: any = this.timeline.timeContext; dev@354: if (this.onSeek) { dev@354: this.onSeek(timeContext.timeToPixel.invert(x) - timeContext.offset); dev@354: } dev@354: } dev@354: } dev@354: } dev@389: dev@389: export abstract class VerticallyBoundedWavesComponent dev@389: extends WavesComponent dev@392: implements VerticalScaleRenderer { dev@389: abstract range: [number, number]; dev@389: dev@389: renderScale(range: [number, number]): void { dev@389: this.addLayer(new Waves.helpers.ScaleLayer({ dev@389: tickColor: this.colour, dev@389: textColor: this.colour, dev@389: height: this.height, dev@389: yDomain: range dev@389: })); dev@389: } dev@389: } dev@392: dev@392: export abstract class InspectableVerticallyBoundedComponent dev@392: extends VerticallyBoundedWavesComponent dev@392: implements VerticalValueInspectorRenderer { dev@392: dev@392: private wrappedSeekHandler: OnSeekHandler; dev@392: private highlight: HighlightLayer; dev@392: dev@392: @Input() set onSeek(handler: OnSeekHandler) { dev@392: this.wrappedSeekHandler = (x: number) => { dev@392: handler(x); dev@397: this.updatePosition(x); dev@397: }; dev@397: } dev@397: dev@397: get updatePosition() { dev@397: return (currentTime: number): void => { dev@394: if (this.highlight) { dev@397: this.highlight.currentPosition = currentTime; dev@394: this.highlight.update(); dev@394: } dev@392: }; dev@392: } dev@392: dev@392: get onSeek(): OnSeekHandler { dev@392: return this.wrappedSeekHandler; dev@392: } dev@392: dev@392: dev@394: renderInspector(range: [number, number], unit?: string): void { dev@394: if (range) { dev@394: this.highlight = new Waves.helpers.HighlightLayer( dev@394: this.cachedFeatureLayers, dev@394: { dev@394: opacity: 0.7, dev@394: height: this.height, dev@394: color: '#c33c54', // TODO pass in? dev@394: labelOffset: 38, dev@394: yDomain: range, dev@396: unit: unit || '' dev@394: } dev@394: ); dev@394: this.addLayer(this.highlight); dev@394: } dev@392: } dev@392: }