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