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@488
|
14 export abstract class VerticallyBounded<T> {
|
dev@488
|
15 abstract get range(): T;
|
dev@392
|
16 }
|
dev@392
|
17
|
dev@488
|
18 export abstract class VerticalScaleRenderer<T> extends VerticallyBounded<T> {
|
dev@488
|
19 abstract renderScale(range: 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 console.warn('waves base render playhead');
|
dev@488
|
90 const cursor = new Waves.helpers.CursorLayer({
|
dev@488
|
91 height: this.height,
|
dev@488
|
92 color: colour,
|
dev@488
|
93 });
|
dev@488
|
94 cursor.currentPosition = initialTime;
|
dev@488
|
95 return {
|
dev@488
|
96 update: currentTime => {
|
dev@488
|
97 cursor.currentPosition = currentTime;
|
dev@488
|
98 cursor.update();
|
dev@488
|
99 },
|
dev@488
|
100 remove: this.addLayer(cursor)
|
dev@488
|
101 };
|
dev@488
|
102 }
|
dev@488
|
103
|
dev@386
|
104 private update(): void {
|
dev@383
|
105 if (!this.waveTrack || !this.mFeature) {
|
dev@383
|
106 return;
|
dev@383
|
107 }
|
dev@386
|
108 this.clearTimeline();
|
dev@392
|
109 this.cachedFeatureLayers = this.featureLayers;
|
dev@392
|
110 for (const layer of this.cachedFeatureLayers) {
|
dev@389
|
111 this.addLayer(layer);
|
dev@383
|
112 }
|
dev@383
|
113 if (this.postAddMap) {
|
dev@392
|
114 this.cachedFeatureLayers.forEach(this.postAddMap);
|
dev@383
|
115 }
|
dev@383
|
116 }
|
dev@383
|
117
|
dev@383
|
118
|
dev@386
|
119 private renderTimeline(): Timeline {
|
dev@386
|
120 const track: HTMLElement = this.trackContainer.nativeElement;
|
dev@354
|
121 track.innerHTML = '';
|
dev@383
|
122 if (this.duration >= 0) {
|
dev@364
|
123 const width: number = track.getBoundingClientRect().width;
|
dev@383
|
124 this.timeline.pixelsPerSecond = width / this.duration;
|
dev@364
|
125 this.timeline.visibleWidth = width;
|
dev@364
|
126 }
|
dev@354
|
127 this.waveTrack = this.timeline.createTrack(
|
dev@354
|
128 track,
|
dev@383
|
129 this.height,
|
dev@376
|
130 this.id
|
dev@354
|
131 );
|
dev@354
|
132
|
dev@354
|
133 if ('ontouchstart' in window) {
|
dev@354
|
134 attachTouchHandlerBodges(
|
dev@386
|
135 track,
|
dev@354
|
136 this.timeline
|
dev@354
|
137 );
|
dev@354
|
138 }
|
dev@383
|
139 this.resetTimelineState();
|
dev@354
|
140 }
|
dev@354
|
141
|
dev@354
|
142 // TODO can likely be removed, or use waves-ui methods
|
dev@386
|
143 private clearTimeline(): void {
|
dev@354
|
144 // loop through layers and remove them, waves-ui provides methods for this but it seems to not work properly
|
dev@406
|
145 const track = this.waveTrack;
|
dev@406
|
146 if (track.layers.length === 0) { return; }
|
dev@406
|
147 const trackLayers: Layer[] = Array.from(track.layers as Layer[]);
|
dev@406
|
148 while (trackLayers.length) {
|
dev@406
|
149 this.removeLayer(trackLayers.pop());
|
dev@354
|
150 }
|
dev@383
|
151 this.resetTimelineState();
|
dev@356
|
152 }
|
dev@356
|
153
|
dev@406
|
154 private removeLayer(layer: Layer) {
|
dev@406
|
155 if (this.layers.includes(layer) && this.waveTrack) {
|
dev@406
|
156 const timeContextChildren = this.timeline.timeContext._children;
|
dev@406
|
157 this.waveTrack.remove(layer);
|
dev@406
|
158 this.layers.splice(this.layers.indexOf(layer), 1);
|
dev@406
|
159 const index = timeContextChildren.indexOf(layer.timeContext);
|
dev@406
|
160 if (index >= 0) {
|
dev@406
|
161 timeContextChildren.splice(index, 1);
|
dev@406
|
162 }
|
dev@406
|
163 layer.destroy();
|
dev@406
|
164 }
|
dev@406
|
165 }
|
dev@406
|
166
|
dev@383
|
167 private resetTimelineState(): void {
|
dev@356
|
168 // time axis
|
dev@356
|
169 const timeAxis = new Waves.helpers.TimeAxisLayer({
|
dev@384
|
170 height: this.height,
|
dev@356
|
171 color: '#b0b0b0'
|
dev@356
|
172 });
|
dev@389
|
173 this.addLayer(timeAxis, true);
|
dev@356
|
174 this.timeline.state = new Waves.states.CenteredZoomState(this.timeline);
|
dev@364
|
175 this.timeline.tracks.update(); // TODO this is problematic, shared state across components
|
dev@354
|
176 }
|
dev@354
|
177
|
dev@354
|
178
|
dev@354
|
179 // TODO can likely use methods in waves-ui directly
|
dev@406
|
180 addLayer(layer: Layer,
|
dev@406
|
181 isAxis: boolean = false): LayerRemover {
|
dev@389
|
182 const timeContext = this.timeline.timeContext;
|
dev@354
|
183 if (!layer.timeContext) {
|
dev@412
|
184 if (isAxis) {
|
dev@412
|
185 layer.setTimeContext(timeContext);
|
dev@412
|
186 } else {
|
dev@412
|
187 const layerTimeContext = new Waves.core.LayerTimeContext(timeContext);
|
dev@412
|
188 if (this.duration) {
|
dev@412
|
189 layerTimeContext.duration = this.duration;
|
dev@412
|
190 }
|
dev@412
|
191 layer.setTimeContext(layerTimeContext);
|
dev@412
|
192 }
|
dev@354
|
193 }
|
dev@389
|
194 this.waveTrack.add(layer);
|
dev@354
|
195 this.layers.push(layer);
|
dev@354
|
196 layer.render();
|
dev@354
|
197 layer.update();
|
dev@406
|
198 return () => this.removeLayer(layer);
|
dev@354
|
199 }
|
dev@354
|
200
|
dev@354
|
201 seekStart(): void {
|
dev@354
|
202 this.zoomOnMouseDown = this.timeline.timeContext.zoom;
|
dev@354
|
203 this.offsetOnMouseDown = this.timeline.timeContext.offset;
|
dev@354
|
204 }
|
dev@354
|
205
|
dev@354
|
206 seekEnd(x: number): void {
|
dev@354
|
207 const hasSameZoom: boolean = this.zoomOnMouseDown ===
|
dev@354
|
208 this.timeline.timeContext.zoom;
|
dev@354
|
209 const hasSameOffset: boolean = this.offsetOnMouseDown ===
|
dev@354
|
210 this.timeline.timeContext.offset;
|
dev@354
|
211 if (hasSameZoom && hasSameOffset) {
|
dev@354
|
212 this.seek(x);
|
dev@354
|
213 }
|
dev@354
|
214 }
|
dev@354
|
215
|
dev@354
|
216 seek(x: number): void {
|
dev@354
|
217 if (this.timeline) {
|
dev@354
|
218 const timeContext: any = this.timeline.timeContext;
|
dev@354
|
219 if (this.onSeek) {
|
dev@354
|
220 this.onSeek(timeContext.timeToPixel.invert(x) - timeContext.offset);
|
dev@354
|
221 }
|
dev@354
|
222 }
|
dev@354
|
223 }
|
dev@354
|
224 }
|
dev@389
|
225
|
dev@389
|
226 export abstract class VerticallyBoundedWavesComponent
|
dev@389
|
227 <T extends ShapedFeatureData> extends WavesComponent<T>
|
dev@488
|
228 implements VerticalScaleRenderer<[number, number]> {
|
dev@389
|
229 abstract range: [number, number];
|
dev@389
|
230
|
dev@389
|
231 renderScale(range: [number, number]): void {
|
dev@389
|
232 this.addLayer(new Waves.helpers.ScaleLayer({
|
dev@389
|
233 tickColor: this.colour,
|
dev@389
|
234 textColor: this.colour,
|
dev@389
|
235 height: this.height,
|
dev@389
|
236 yDomain: range
|
dev@389
|
237 }));
|
dev@389
|
238 }
|
dev@389
|
239 }
|
dev@392
|
240
|
cannam@473
|
241 export abstract class VerticallyBinnedWavesComponent
|
cannam@473
|
242 <T extends ShapedFeatureData> extends WavesComponent<T>
|
dev@488
|
243 implements VerticalScaleRenderer<string[]> {
|
dev@488
|
244 abstract range: string[];
|
cannam@473
|
245
|
dev@488
|
246 renderScale(binNames: string[]): void {
|
cannam@473
|
247 this.addLayer(new Waves.helpers.DiscreteScaleLayer({
|
cannam@473
|
248 tickColor: this.colour,
|
cannam@473
|
249 textColor: this.colour,
|
cannam@473
|
250 height: this.height,
|
cannam@473
|
251 binNames
|
cannam@473
|
252 }));
|
cannam@473
|
253 }
|
cannam@473
|
254 }
|
cannam@473
|
255
|
dev@392
|
256 export abstract class InspectableVerticallyBoundedComponent
|
dev@392
|
257 <T extends ShapedFeatureData> extends VerticallyBoundedWavesComponent<T>
|
dev@392
|
258 implements VerticalValueInspectorRenderer {
|
dev@392
|
259
|
dev@392
|
260 private wrappedSeekHandler: OnSeekHandler;
|
dev@392
|
261 private highlight: HighlightLayer;
|
dev@392
|
262
|
dev@392
|
263 @Input() set onSeek(handler: OnSeekHandler) {
|
dev@392
|
264 this.wrappedSeekHandler = (x: number) => {
|
dev@392
|
265 handler(x);
|
dev@397
|
266 this.updatePosition(x);
|
dev@397
|
267 };
|
dev@397
|
268 }
|
dev@397
|
269
|
dev@397
|
270 get updatePosition() {
|
dev@397
|
271 return (currentTime: number): void => {
|
dev@394
|
272 if (this.highlight) {
|
dev@397
|
273 this.highlight.currentPosition = currentTime;
|
dev@394
|
274 this.highlight.update();
|
dev@394
|
275 }
|
dev@392
|
276 };
|
dev@392
|
277 }
|
dev@392
|
278
|
dev@392
|
279 get onSeek(): OnSeekHandler {
|
dev@392
|
280 return this.wrappedSeekHandler;
|
dev@392
|
281 }
|
dev@392
|
282
|
dev@392
|
283
|
dev@394
|
284 renderInspector(range: [number, number], unit?: string): void {
|
dev@394
|
285 if (range) {
|
dev@394
|
286 this.highlight = new Waves.helpers.HighlightLayer(
|
dev@394
|
287 this.cachedFeatureLayers,
|
dev@394
|
288 {
|
dev@394
|
289 opacity: 0.7,
|
dev@394
|
290 height: this.height,
|
dev@394
|
291 color: '#c33c54', // TODO pass in?
|
dev@394
|
292 labelOffset: 38,
|
dev@394
|
293 yDomain: range,
|
dev@396
|
294 unit: unit || ''
|
dev@394
|
295 }
|
dev@394
|
296 );
|
dev@394
|
297 this.addLayer(this.highlight);
|
dev@394
|
298 }
|
dev@392
|
299 }
|
dev@392
|
300 }
|