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