annotate src/app/visualisations/waves-base.component.ts @ 509:041468f553e1 tip master

Merge pull request #57 from LucasThompson/fix/session-stack-max-call-stack Fix accidental recursion in PersistentStack
author Lucas Thompson <LucasThompson@users.noreply.github.com>
date Mon, 27 Nov 2017 11:04:30 +0000
parents c39df81c4dae
children
rev   line source
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@497 8 import {countingIdProvider} from 'piper-js/web-worker';
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 }