dev@354
|
1 /**
|
dev@354
|
2 * Created by lucast on 26/05/2017.
|
dev@354
|
3 */
|
dev@384
|
4 import {AfterViewInit, ElementRef, Input, ViewChild} from '@angular/core';
|
dev@354
|
5 import {OnSeekHandler} from '../playhead/PlayHeadHelpers';
|
dev@354
|
6 import {attachTouchHandlerBodges} from './WavesJunk';
|
dev@354
|
7 import Waves from 'waves-ui-piper';
|
dev@376
|
8 import {countingIdProvider} from 'piper/client-stubs/WebWorkerStreamingClient';
|
dev@383
|
9 import {ShapedFeatureData} from './FeatureUtilities';
|
dev@376
|
10
|
dev@376
|
11 const trackIdGenerator = countingIdProvider(0);
|
dev@354
|
12
|
dev@389
|
13 // has to be an abstract class vs as interface for Angular's DI
|
dev@489
|
14 export abstract class VerticallyLabelled<T> {
|
dev@489
|
15 abstract get labels(): T;
|
dev@392
|
16 }
|
dev@392
|
17
|
dev@489
|
18 export abstract class VerticalScaleRenderer<T> extends VerticallyLabelled<T> {
|
dev@489
|
19 abstract renderScale(labels: T): void;
|
cannam@473
|
20 }
|
cannam@473
|
21
|
dev@392
|
22 export abstract class VerticalValueInspectorRenderer
|
dev@488
|
23 extends VerticalScaleRenderer<[number, number]> {
|
dev@392
|
24 // TODO how do I know these layers are actually 'describable'?
|
dev@394
|
25 abstract renderInspector(range: [number, number], unit?: string): void;
|
dev@397
|
26 abstract get updatePosition(): OnSeekHandler;
|
dev@392
|
27 }
|
dev@392
|
28
|
dev@488
|
29 export abstract class PlayheadManager {
|
dev@488
|
30 abstract update(time: number): void;
|
dev@488
|
31 abstract remove(): void;
|
dev@488
|
32 }
|
dev@488
|
33
|
dev@488
|
34 export abstract class PlayheadRenderer {
|
dev@488
|
35 abstract renderPlayhead(initialTime: number, colour: string): PlayheadManager;
|
dev@488
|
36 }
|
dev@488
|
37
|
dev@406
|
38 export type LayerRemover = () => void;
|
dev@406
|
39
|
dev@383
|
40 export abstract class WavesComponent<T extends ShapedFeatureData | AudioBuffer>
|
dev@488
|
41 implements AfterViewInit, PlayheadRenderer {
|
dev@488
|
42
|
dev@384
|
43 @ViewChild('track') trackContainer: ElementRef;
|
dev@354
|
44 @Input() set width(width: number) {
|
dev@354
|
45 if (this.timeline) {
|
dev@354
|
46 requestAnimationFrame(() => {
|
dev@354
|
47 this.timeline.timeContext.visibleWidth = width;
|
dev@354
|
48 this.timeline.tracks.update();
|
dev@354
|
49 });
|
dev@354
|
50 }
|
dev@354
|
51 }
|
dev@354
|
52 @Input() timeline: Timeline;
|
dev@354
|
53 @Input() onSeek: OnSeekHandler;
|
dev@379
|
54 @Input() colour: string;
|
dev@412
|
55 @Input() duration: number;
|
dev@383
|
56 @Input() set feature(feature: T) {
|
dev@383
|
57 this.mFeature = feature;
|
dev@383
|
58 this.update();
|
dev@383
|
59 }
|
dev@383
|
60
|
dev@383
|
61 get feature(): T {
|
dev@383
|
62 return this.mFeature;
|
dev@383
|
63 }
|
dev@354
|
64
|
dev@387
|
65 private layers: Layer[];
|
dev@387
|
66 private zoomOnMouseDown: number;
|
dev@387
|
67 private offsetOnMouseDown: number;
|
dev@387
|
68 private waveTrack: Track;
|
dev@387
|
69 private mFeature: T;
|
dev@387
|
70 private id: string;
|
dev@383
|
71 protected abstract get featureLayers(): Layer[];
|
dev@392
|
72 protected cachedFeatureLayers: Layer[];
|
dev@383
|
73 protected postAddMap: (value: Layer, index: number, array: Layer[]) => void;
|
dev@406
|
74 height: number;
|
dev@354
|
75
|
dev@354
|
76 constructor() {
|
dev@354
|
77 this.layers = [];
|
dev@376
|
78 this.id = trackIdGenerator.next().value;
|
dev@354
|
79 }
|
dev@354
|
80
|
dev@383
|
81 ngAfterViewInit(): void {
|
dev@384
|
82 this.height =
|
dev@384
|
83 this.trackContainer.nativeElement.getBoundingClientRect().height;
|
dev@386
|
84 this.renderTimeline();
|
dev@383
|
85 this.update();
|
dev@383
|
86 }
|
dev@383
|
87
|
dev@488
|
88 renderPlayhead(initialTime: number, colour: string): PlayheadManager {
|
dev@488
|
89 const cursor = new Waves.helpers.CursorLayer({
|
dev@488
|
90 height: this.height,
|
dev@488
|
91 color: colour,
|
dev@488
|
92 });
|
dev@488
|
93 cursor.currentPosition = initialTime;
|
dev@488
|
94 return {
|
dev@488
|
95 update: currentTime => {
|
dev@488
|
96 cursor.currentPosition = currentTime;
|
dev@488
|
97 cursor.update();
|
dev@488
|
98 },
|
dev@488
|
99 remove: this.addLayer(cursor)
|
dev@488
|
100 };
|
dev@488
|
101 }
|
dev@488
|
102
|
dev@386
|
103 private update(): void {
|
dev@383
|
104 if (!this.waveTrack || !this.mFeature) {
|
dev@383
|
105 return;
|
dev@383
|
106 }
|
dev@386
|
107 this.clearTimeline();
|
dev@392
|
108 this.cachedFeatureLayers = this.featureLayers;
|
dev@392
|
109 for (const layer of this.cachedFeatureLayers) {
|
dev@389
|
110 this.addLayer(layer);
|
dev@383
|
111 }
|
dev@383
|
112 if (this.postAddMap) {
|
dev@392
|
113 this.cachedFeatureLayers.forEach(this.postAddMap);
|
dev@383
|
114 }
|
dev@383
|
115 }
|
dev@383
|
116
|
dev@383
|
117
|
dev@386
|
118 private renderTimeline(): Timeline {
|
dev@386
|
119 const track: HTMLElement = this.trackContainer.nativeElement;
|
dev@354
|
120 track.innerHTML = '';
|
dev@383
|
121 if (this.duration >= 0) {
|
dev@364
|
122 const width: number = track.getBoundingClientRect().width;
|
dev@383
|
123 this.timeline.pixelsPerSecond = width / this.duration;
|
dev@364
|
124 this.timeline.visibleWidth = width;
|
dev@364
|
125 }
|
dev@354
|
126 this.waveTrack = this.timeline.createTrack(
|
dev@354
|
127 track,
|
dev@383
|
128 this.height,
|
dev@376
|
129 this.id
|
dev@354
|
130 );
|
dev@354
|
131
|
dev@354
|
132 if ('ontouchstart' in window) {
|
dev@354
|
133 attachTouchHandlerBodges(
|
dev@386
|
134 track,
|
dev@354
|
135 this.timeline
|
dev@354
|
136 );
|
dev@354
|
137 }
|
dev@383
|
138 this.resetTimelineState();
|
dev@354
|
139 }
|
dev@354
|
140
|
dev@354
|
141 // TODO can likely be removed, or use waves-ui methods
|
dev@386
|
142 private clearTimeline(): void {
|
dev@354
|
143 // loop through layers and remove them, waves-ui provides methods for this but it seems to not work properly
|
dev@406
|
144 const track = this.waveTrack;
|
dev@406
|
145 if (track.layers.length === 0) { return; }
|
dev@406
|
146 const trackLayers: Layer[] = Array.from(track.layers as Layer[]);
|
dev@406
|
147 while (trackLayers.length) {
|
dev@406
|
148 this.removeLayer(trackLayers.pop());
|
dev@354
|
149 }
|
dev@383
|
150 this.resetTimelineState();
|
dev@356
|
151 }
|
dev@356
|
152
|
dev@406
|
153 private removeLayer(layer: Layer) {
|
dev@406
|
154 if (this.layers.includes(layer) && this.waveTrack) {
|
dev@406
|
155 const timeContextChildren = this.timeline.timeContext._children;
|
dev@406
|
156 this.waveTrack.remove(layer);
|
dev@406
|
157 this.layers.splice(this.layers.indexOf(layer), 1);
|
dev@406
|
158 const index = timeContextChildren.indexOf(layer.timeContext);
|
dev@406
|
159 if (index >= 0) {
|
dev@406
|
160 timeContextChildren.splice(index, 1);
|
dev@406
|
161 }
|
dev@406
|
162 layer.destroy();
|
dev@406
|
163 }
|
dev@406
|
164 }
|
dev@406
|
165
|
dev@383
|
166 private resetTimelineState(): void {
|
dev@356
|
167 // time axis
|
dev@356
|
168 const timeAxis = new Waves.helpers.TimeAxisLayer({
|
dev@384
|
169 height: this.height,
|
dev@356
|
170 color: '#b0b0b0'
|
dev@356
|
171 });
|
dev@389
|
172 this.addLayer(timeAxis, true);
|
dev@356
|
173 this.timeline.state = new Waves.states.CenteredZoomState(this.timeline);
|
dev@364
|
174 this.timeline.tracks.update(); // TODO this is problematic, shared state across components
|
dev@354
|
175 }
|
dev@354
|
176
|
dev@354
|
177
|
dev@354
|
178 // TODO can likely use methods in waves-ui directly
|
dev@406
|
179 addLayer(layer: Layer,
|
dev@406
|
180 isAxis: boolean = false): LayerRemover {
|
dev@389
|
181 const timeContext = this.timeline.timeContext;
|
dev@354
|
182 if (!layer.timeContext) {
|
dev@412
|
183 if (isAxis) {
|
dev@412
|
184 layer.setTimeContext(timeContext);
|
dev@412
|
185 } else {
|
dev@412
|
186 const layerTimeContext = new Waves.core.LayerTimeContext(timeContext);
|
dev@412
|
187 if (this.duration) {
|
dev@412
|
188 layerTimeContext.duration = this.duration;
|
dev@412
|
189 }
|
dev@412
|
190 layer.setTimeContext(layerTimeContext);
|
dev@412
|
191 }
|
dev@354
|
192 }
|
dev@389
|
193 this.waveTrack.add(layer);
|
dev@354
|
194 this.layers.push(layer);
|
dev@354
|
195 layer.render();
|
dev@354
|
196 layer.update();
|
dev@406
|
197 return () => this.removeLayer(layer);
|
dev@354
|
198 }
|
dev@354
|
199
|
dev@354
|
200 seekStart(): void {
|
dev@354
|
201 this.zoomOnMouseDown = this.timeline.timeContext.zoom;
|
dev@354
|
202 this.offsetOnMouseDown = this.timeline.timeContext.offset;
|
dev@354
|
203 }
|
dev@354
|
204
|
dev@354
|
205 seekEnd(x: number): void {
|
dev@354
|
206 const hasSameZoom: boolean = this.zoomOnMouseDown ===
|
dev@354
|
207 this.timeline.timeContext.zoom;
|
dev@354
|
208 const hasSameOffset: boolean = this.offsetOnMouseDown ===
|
dev@354
|
209 this.timeline.timeContext.offset;
|
dev@354
|
210 if (hasSameZoom && hasSameOffset) {
|
dev@354
|
211 this.seek(x);
|
dev@354
|
212 }
|
dev@354
|
213 }
|
dev@354
|
214
|
dev@354
|
215 seek(x: number): void {
|
dev@354
|
216 if (this.timeline) {
|
dev@354
|
217 const timeContext: any = this.timeline.timeContext;
|
dev@354
|
218 if (this.onSeek) {
|
dev@354
|
219 this.onSeek(timeContext.timeToPixel.invert(x) - timeContext.offset);
|
dev@354
|
220 }
|
dev@354
|
221 }
|
dev@354
|
222 }
|
dev@354
|
223 }
|
dev@389
|
224
|
dev@389
|
225 export abstract class VerticallyBoundedWavesComponent
|
dev@389
|
226 <T extends ShapedFeatureData> extends WavesComponent<T>
|
dev@488
|
227 implements VerticalScaleRenderer<[number, number]> {
|
dev@489
|
228 abstract labels: [number, number];
|
dev@389
|
229
|
dev@489
|
230 renderScale(labels: [number, number]): void {
|
dev@389
|
231 this.addLayer(new Waves.helpers.ScaleLayer({
|
dev@389
|
232 tickColor: this.colour,
|
dev@389
|
233 textColor: this.colour,
|
dev@389
|
234 height: this.height,
|
dev@489
|
235 yDomain: labels
|
dev@389
|
236 }));
|
dev@389
|
237 }
|
dev@389
|
238 }
|
dev@392
|
239
|
cannam@473
|
240 export abstract class VerticallyBinnedWavesComponent
|
cannam@473
|
241 <T extends ShapedFeatureData> extends WavesComponent<T>
|
dev@488
|
242 implements VerticalScaleRenderer<string[]> {
|
dev@489
|
243 abstract labels: string[];
|
cannam@473
|
244
|
dev@489
|
245 renderScale(labels: string[]): void {
|
cannam@473
|
246 this.addLayer(new Waves.helpers.DiscreteScaleLayer({
|
cannam@473
|
247 tickColor: this.colour,
|
cannam@473
|
248 textColor: this.colour,
|
cannam@473
|
249 height: this.height,
|
dev@490
|
250 binNames: labels
|
cannam@473
|
251 }));
|
cannam@473
|
252 }
|
cannam@473
|
253 }
|
cannam@473
|
254
|
dev@392
|
255 export abstract class InspectableVerticallyBoundedComponent
|
dev@392
|
256 <T extends ShapedFeatureData> extends VerticallyBoundedWavesComponent<T>
|
dev@392
|
257 implements VerticalValueInspectorRenderer {
|
dev@392
|
258
|
dev@392
|
259 private wrappedSeekHandler: OnSeekHandler;
|
dev@392
|
260 private highlight: HighlightLayer;
|
dev@392
|
261
|
dev@392
|
262 @Input() set onSeek(handler: OnSeekHandler) {
|
dev@392
|
263 this.wrappedSeekHandler = (x: number) => {
|
dev@392
|
264 handler(x);
|
dev@397
|
265 this.updatePosition(x);
|
dev@397
|
266 };
|
dev@397
|
267 }
|
dev@397
|
268
|
dev@397
|
269 get updatePosition() {
|
dev@397
|
270 return (currentTime: number): void => {
|
dev@394
|
271 if (this.highlight) {
|
dev@397
|
272 this.highlight.currentPosition = currentTime;
|
dev@394
|
273 this.highlight.update();
|
dev@394
|
274 }
|
dev@392
|
275 };
|
dev@392
|
276 }
|
dev@392
|
277
|
dev@392
|
278 get onSeek(): OnSeekHandler {
|
dev@392
|
279 return this.wrappedSeekHandler;
|
dev@392
|
280 }
|
dev@392
|
281
|
dev@392
|
282
|
dev@394
|
283 renderInspector(range: [number, number], unit?: string): void {
|
dev@394
|
284 if (range) {
|
dev@394
|
285 this.highlight = new Waves.helpers.HighlightLayer(
|
dev@394
|
286 this.cachedFeatureLayers,
|
dev@394
|
287 {
|
dev@394
|
288 opacity: 0.7,
|
dev@394
|
289 height: this.height,
|
dev@394
|
290 color: '#c33c54', // TODO pass in?
|
dev@394
|
291 labelOffset: 38,
|
dev@394
|
292 yDomain: range,
|
dev@396
|
293 unit: unit || ''
|
dev@394
|
294 }
|
dev@394
|
295 );
|
dev@394
|
296 this.addLayer(this.highlight);
|
dev@394
|
297 }
|
dev@392
|
298 }
|
dev@392
|
299 }
|