annotate src/app/visualisations/waves-base.component.ts @ 480:0fbba61603b3

Begin to add regions component
author Chris Cannam <cannam@all-day-breakfast.com>
date Thu, 20 Jul 2017 16:15:40 +0100
parents de23ea6bcd0d
children 64ed45a0bad3
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@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 }