annotate src/app/visualisations/waves-base.component.ts @ 488:64ed45a0bad3

Introduce PlayheadRenderer, implement in the waves base. Make VerticallyBounded and VerticalScaleRenderer generic and remove bin equivalents. Forward calls for a PlayheadRenderer from VerticalScaleComponent on to its children. Also update other components accordingly.
author Lucas Thompson <dev@lucas.im>
date Wed, 05 Jul 2017 18:42:12 +0100
parents de23ea6bcd0d
children ab43880f1cd5
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@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 }