dev@170
|
1 /**
|
dev@170
|
2 * Created by lucast on 21/03/2017.
|
dev@170
|
3 */
|
dev@231
|
4 import {
|
dev@231
|
5 ChangeDetectionStrategy,
|
dev@231
|
6 Component,
|
dev@231
|
7 Input,
|
dev@408
|
8 OnDestroy,
|
dev@456
|
9 OnInit,
|
dev@456
|
10 Output,
|
dev@456
|
11 EventEmitter
|
dev@236
|
12 } from '@angular/core';
|
dev@348
|
13 import {naivePagingMapper} from '../visualisations/WavesJunk';
|
dev@408
|
14 import {OnSeekHandler} from '../playhead/PlayHeadHelpers';
|
dev@361
|
15 import {
|
dev@381
|
16 defaultColourGenerator,
|
dev@361
|
17 HigherLevelFeatureShape,
|
dev@361
|
18 KnownShapedFeature
|
dev@361
|
19 } from '../visualisations/FeatureUtilities';
|
dev@408
|
20 import {
|
dev@408
|
21 RenderLoopService,
|
dev@408
|
22 TaskRemover
|
dev@408
|
23 } from '../services/render-loop/render-loop.service';
|
dev@456
|
24 import {DomSanitizer} from '@angular/platform-browser';
|
dev@170
|
25
|
dev@350
|
26 export interface Item {
|
dev@350
|
27 id: string;
|
dev@200
|
28 hasSharedTimeline: boolean;
|
dev@200
|
29 title?: string;
|
dev@200
|
30 description?: string;
|
dev@224
|
31 progress?: number;
|
dev@350
|
32 }
|
dev@350
|
33
|
dev@350
|
34 export interface PendingRootAudioItem extends Item {
|
dev@350
|
35 uri: string;
|
dev@453
|
36 mimeType?: string;
|
dev@456
|
37 isExportable?: boolean;
|
dev@350
|
38 }
|
dev@378
|
39 export interface RootAudioItem extends PendingRootAudioItem {
|
dev@350
|
40 audioData: AudioBuffer;
|
dev@350
|
41 }
|
dev@350
|
42
|
dev@350
|
43 export interface PendingAnalysisItem extends Item {
|
dev@350
|
44 parent: RootAudioItem;
|
dev@350
|
45 extractorKey: string;
|
dev@350
|
46 }
|
dev@350
|
47
|
dev@396
|
48 export type AnalysisItem = PendingAnalysisItem & KnownShapedFeature & {
|
dev@396
|
49 unit?: string
|
dev@396
|
50 };
|
dev@361
|
51
|
dev@361
|
52 export function isItem(item: Item): item is Item {
|
dev@361
|
53 return item.id != null && item.hasSharedTimeline != null;
|
dev@350
|
54 }
|
dev@350
|
55
|
dev@350
|
56 export function isPendingRootAudioItem(item: Item): item is PendingRootAudioItem {
|
dev@361
|
57 return isItem(item) && typeof (item as RootAudioItem).uri === 'string';
|
dev@350
|
58 }
|
dev@350
|
59
|
dev@350
|
60 export function isRootAudioItem(item: Item): item is RootAudioItem {
|
dev@410
|
61 return item && isPendingRootAudioItem(item) &&
|
dev@351
|
62 (item as RootAudioItem).audioData instanceof AudioBuffer;
|
dev@350
|
63 }
|
dev@350
|
64
|
dev@350
|
65 export function isPendingAnalysisItem(item: Item): item is AnalysisItem {
|
dev@350
|
66 const downcast = (item as AnalysisItem);
|
dev@350
|
67 return isRootAudioItem(downcast.parent)
|
dev@350
|
68 && typeof downcast.extractorKey === 'string';
|
dev@350
|
69 }
|
dev@350
|
70
|
dev@350
|
71 export function isAnalysisItem(item: Item): item is AnalysisItem {
|
dev@350
|
72 const downcast = (item as AnalysisItem);
|
dev@361
|
73 return isPendingAnalysisItem(item) &&
|
dev@361
|
74 downcast.shape != null &&
|
dev@361
|
75 downcast.collected != null;
|
dev@350
|
76 }
|
dev@350
|
77
|
dev@350
|
78 // these should probably be actual concrete types with their own getUri methods
|
dev@350
|
79 export function getRootUri(item: Item): string {
|
dev@350
|
80 if (isPendingRootAudioItem(item)) {
|
dev@350
|
81 return item.uri;
|
dev@350
|
82 }
|
dev@350
|
83 if (isPendingAnalysisItem(item)) {
|
dev@350
|
84 return item.parent.uri;
|
dev@350
|
85 }
|
dev@350
|
86 throw new Error('Invalid item: No URI property set.');
|
dev@170
|
87 }
|
dev@170
|
88
|
dev@170
|
89 @Component({
|
dev@170
|
90 selector: 'ugly-analysis-item',
|
dev@170
|
91 templateUrl: './analysis-item.component.html',
|
dev@231
|
92 styleUrls: ['./analysis-item.component.css'],
|
dev@231
|
93 changeDetection: ChangeDetectionStrategy.OnPush
|
dev@170
|
94 })
|
dev@408
|
95 export class AnalysisItemComponent implements OnInit, OnDestroy {
|
dev@224
|
96
|
dev@408
|
97 // TODO should be TimelineTimeContext?
|
dev@408
|
98 @Input() set timeline(timeline: Timeline) {
|
dev@408
|
99 this.mTimeline = timeline;
|
dev@408
|
100 this.resetRemoveAnimation();
|
dev@408
|
101 }
|
dev@408
|
102
|
dev@408
|
103 get timeline(): Timeline {
|
dev@408
|
104 return this.mTimeline;
|
dev@408
|
105 }
|
dev@408
|
106
|
dev@408
|
107 @Input() set isActive(isActive: boolean) {
|
dev@408
|
108 this.removeAnimation();
|
dev@408
|
109 this.mIsActive = isActive;
|
dev@408
|
110 if (isActive) {
|
dev@408
|
111 this.resetRemoveAnimation();
|
dev@408
|
112 }
|
dev@408
|
113 }
|
dev@408
|
114
|
dev@408
|
115 get isActive() {
|
dev@408
|
116 return this.mIsActive;
|
dev@408
|
117 }
|
dev@408
|
118
|
dev@350
|
119 @Input() item: Item;
|
dev@285
|
120 @Input() contentWidth: number;
|
dev@348
|
121 @Input() onSeek: OnSeekHandler;
|
dev@456
|
122 @Output() remove: EventEmitter<Item>;
|
dev@408
|
123 // TODO move / re-think - naivePagingMapper feels like a big ol' bodge
|
dev@408
|
124 private removeAnimation: TaskRemover;
|
dev@224
|
125 private hasProgressOnInit = false;
|
dev@408
|
126 private mIsActive: boolean;
|
dev@408
|
127 private mTimeline: Timeline;
|
dev@224
|
128
|
dev@456
|
129 constructor(private renderLoop: RenderLoopService,
|
dev@456
|
130 private sanitizer: DomSanitizer) {
|
dev@456
|
131 this.remove = new EventEmitter<Item>();
|
dev@456
|
132 }
|
dev@348
|
133
|
dev@224
|
134 ngOnInit(): void {
|
dev@408
|
135 this.resetRemoveAnimation();
|
dev@231
|
136 this.hasProgressOnInit = this.item.progress != null;
|
dev@224
|
137 }
|
dev@224
|
138
|
dev@224
|
139 isLoading(): boolean {
|
dev@231
|
140 return this.hasProgressOnInit && this.item.progress < 100;
|
dev@224
|
141 }
|
dev@348
|
142
|
dev@348
|
143 isAudioItem(): boolean {
|
dev@408
|
144 return this.item && isRootAudioItem(this.item);
|
dev@348
|
145 }
|
dev@361
|
146
|
dev@410
|
147 isPending(): boolean {
|
dev@410
|
148 return this.item &&
|
dev@410
|
149 !isRootAudioItem(this.item) && !isAnalysisItem(this.item) &&
|
dev@410
|
150 (isPendingAnalysisItem(this.item) || isPendingRootAudioItem(this.item));
|
dev@410
|
151 }
|
dev@410
|
152
|
dev@361
|
153 getFeatureShape(): HigherLevelFeatureShape | null {
|
dev@361
|
154 return !isPendingRootAudioItem(this.item) &&
|
dev@361
|
155 isAnalysisItem(this.item) ? this.item.shape : null;
|
dev@361
|
156 }
|
dev@381
|
157
|
dev@412
|
158 getDuration(): number | null {
|
dev@412
|
159 if (isRootAudioItem(this.item)) {
|
dev@412
|
160 return this.item.audioData.duration;
|
dev@412
|
161 }
|
dev@412
|
162 if (isAnalysisItem(this.item)) {
|
dev@412
|
163 return this.item.parent.audioData.duration;
|
dev@412
|
164 }
|
dev@412
|
165 }
|
dev@412
|
166
|
dev@381
|
167 getNextColour(): string {
|
dev@381
|
168 return defaultColourGenerator.next().value;
|
dev@381
|
169 }
|
dev@408
|
170
|
dev@408
|
171 ngOnDestroy(): void {
|
dev@408
|
172 this.removeAnimation();
|
dev@408
|
173 }
|
dev@408
|
174
|
dev@456
|
175 private sanitize(url: string) {
|
dev@456
|
176 return this.sanitizer.bypassSecurityTrustUrl(url);
|
dev@456
|
177 }
|
dev@456
|
178
|
dev@456
|
179 private generateFilename(item: PendingRootAudioItem): string {
|
dev@456
|
180 // TODO this is too brittle, and will often produce the wrong result
|
dev@456
|
181 // i.e. audio/mpeg results in .mpeg, when .mp3 is likely desired
|
dev@456
|
182 const mimeParts = item.mimeType ? item.mimeType.split('/') : [];
|
dev@456
|
183 const extension = mimeParts.length === 2 ? mimeParts[1] : '';
|
dev@456
|
184 return `${item.title}.${extension}`;
|
dev@456
|
185 }
|
dev@456
|
186
|
dev@408
|
187 private resetRemoveAnimation(): void {
|
dev@408
|
188 if (this.removeAnimation) {
|
dev@408
|
189 this.removeAnimation();
|
dev@408
|
190 }
|
dev@408
|
191 const createPagingTask = () => {
|
dev@408
|
192 const pagingMapper = naivePagingMapper(this.timeline);
|
dev@408
|
193 return this.renderLoop.addPlayingTask(currentTime => {
|
dev@408
|
194 pagingMapper(currentTime);
|
dev@408
|
195 });
|
dev@408
|
196 };
|
dev@408
|
197 // only add a pager to audio items, it can drive the feature items
|
dev@408
|
198 const remover = this.timeline && this.isAudioItem() ?
|
dev@408
|
199 createPagingTask() : () => {};
|
dev@408
|
200 this.removeAnimation = () => {
|
dev@408
|
201 remover();
|
dev@408
|
202 this.removeAnimation = () => {};
|
dev@408
|
203 };
|
dev@408
|
204 }
|
dev@170
|
205 }
|