@checksub_team/peaks_timeline 1.4.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,697 @@
1
+ /**
2
+ * @file
3
+ *
4
+ * Defines the {@link SegmentsGroup} class.
5
+ *
6
+ * @module segments-group
7
+ */
8
+
9
+ define([
10
+ './segment-shape',
11
+ './utils',
12
+ 'konva'
13
+ ], function(
14
+ SegmentShape,
15
+ Utils,
16
+ Konva) {
17
+ 'use strict';
18
+
19
+ /**
20
+ * Creates a Konva.Group that displays segment markers against the audio
21
+ * waveform.
22
+ *
23
+ * @class
24
+ * @alias SegmentsGroup
25
+ *
26
+ * @param {Peaks} peaks
27
+ * @param {WaveformOverview|WaveformZoomView} view
28
+ * @param {Boolean} allowEditing
29
+ */
30
+
31
+ function SegmentsGroup(peaks, view, allowEditing) {
32
+ this._peaks = peaks;
33
+ this._view = view;
34
+ this._allowEditing = allowEditing;
35
+
36
+ this._firstSegmentId = null;
37
+ this._segments = {};
38
+ this._lastSegmentId = null;
39
+
40
+ this._segmentShapes = {};
41
+ this._group = new Konva.Group();
42
+
43
+ this._updatedSegments = [];
44
+
45
+ this._isMagnetized = false;
46
+
47
+ this._peaks.on('segment.updated', this._onSegmentsUpdate.bind(this));
48
+ this._peaks.on('segments.add', this._onSegmentsAdd.bind(this));
49
+ this._peaks.on('segments.remove', this._onSegmentsRemove.bind(this));
50
+ this._peaks.on('segments.remove_all', this._onSegmentsRemoveAll.bind(this));
51
+ this._peaks.on('segments.dragend', this._onSegmentUpdated.bind(this));
52
+ this._peaks.on('segments.setMagnetizing', this.setMagnetizing.bind(this));
53
+ }
54
+
55
+ /**
56
+ * Adds the group to the given {Konva.Group}.
57
+ *
58
+ * @param {Konva.Group} group
59
+ */
60
+
61
+ SegmentsGroup.prototype.addToGroup = function(group) {
62
+ group.add(this._group);
63
+ };
64
+
65
+ SegmentsGroup.prototype.moveToTop = function() {
66
+ this._group.moveToTop();
67
+ };
68
+
69
+ SegmentsGroup.prototype.enableEditing = function(enable) {
70
+ this._allowEditing = enable;
71
+ };
72
+
73
+ SegmentsGroup.prototype.isEditingEnabled = function() {
74
+ return this._allowEditing;
75
+ };
76
+
77
+ SegmentsGroup.prototype.y = function(value) {
78
+ return this._group.y(value);
79
+ };
80
+
81
+ SegmentsGroup.prototype.getActiveSegment = function(time) {
82
+ var activeSegment = null;
83
+ var currentSegment = null;
84
+ var nextSegmentId = null;
85
+
86
+ do {
87
+ if (!currentSegment) {
88
+ currentSegment = this._segments[this._firstSegmentId];
89
+ }
90
+ else {
91
+ currentSegment = this._segments[currentSegment.nextSegmentId];
92
+ }
93
+
94
+ if (currentSegment) {
95
+ if (currentSegment.segment.startTime > time) {
96
+ // We didn't find an active segment and will not in the remainings segments
97
+ break;
98
+ }
99
+
100
+ if (currentSegment.segment.startTime <= time && currentSegment.segment.endTime > time) {
101
+ activeSegment = currentSegment.segment;
102
+ break;
103
+ }
104
+ }
105
+ else {
106
+ break;
107
+ }
108
+
109
+ nextSegmentId = currentSegment.nextSegmentId;
110
+ } while (nextSegmentId);
111
+
112
+ return activeSegment;
113
+ };
114
+
115
+ SegmentsGroup.prototype._onSegmentsUpdate = function(segment) {
116
+ if (this._segments[segment.id]) {
117
+ var redraw = false;
118
+ var segmentShape = this._segmentShapes[segment.id];
119
+ var frameOffset = this._view.getFrameOffset();
120
+ var width = this._view.getWidth();
121
+ var frameStartTime = this._view.pixelsToTime(frameOffset);
122
+ var frameEndTime = this._view.pixelsToTime(frameOffset + width);
123
+
124
+ this._deleteSegment(segment);
125
+ this._addSegment(segment);
126
+
127
+ if (segmentShape) {
128
+ this._removeSegment(segment);
129
+ redraw = true;
130
+ }
131
+
132
+ if (segment.isVisible(frameStartTime, frameEndTime)) {
133
+ this._addSegmentShape(segment);
134
+ redraw = true;
135
+ }
136
+
137
+ if (redraw) {
138
+ this.updateSegments(frameStartTime, frameEndTime);
139
+ }
140
+ }
141
+ };
142
+
143
+ SegmentsGroup.prototype._onSegmentUpdated = function() {
144
+ this._peaks.emit('segments.updated', this._updatedSegments);
145
+ this._updatedSegments = [];
146
+ };
147
+
148
+ SegmentsGroup.prototype._onSegmentsAdd = function(segments) {
149
+ var self = this;
150
+
151
+ var frameOffset = self._view.getFrameOffset();
152
+ var width = self._view.getWidth();
153
+
154
+ var frameStartTime = self._view.pixelsToTime(frameOffset);
155
+ var frameEndTime = self._view.pixelsToTime(frameOffset + width);
156
+
157
+ segments.forEach(function(segment) {
158
+ self._addSegment(segment);
159
+ });
160
+
161
+ self.updateSegments(frameStartTime, frameEndTime);
162
+ };
163
+
164
+ SegmentsGroup.prototype._onSegmentsRemove = function(segments) {
165
+ var self = this;
166
+
167
+ segments.forEach(function(segment) {
168
+ var index = self._updatedSegments.indexOf(segment);
169
+
170
+ if (index > -1) {
171
+ self._updatedSegments.splice(index, 1);
172
+ }
173
+
174
+ self._removeSegment(segment);
175
+ self._deleteSegment(segment);
176
+ });
177
+
178
+ this._draw();
179
+ };
180
+
181
+ SegmentsGroup.prototype._onSegmentsRemoveAll = function() {
182
+ this._group.removeChildren();
183
+ this._firstSegmentId = null;
184
+ this._segments = {};
185
+ this._lastSegmentId = null;
186
+
187
+ this._segmentShapes = {};
188
+
189
+ this._draw();
190
+ };
191
+
192
+ SegmentsGroup.prototype._addSegment = function(segment) {
193
+ var newSegment = {
194
+ segment: segment,
195
+ prevSegmentId: null,
196
+ nextSegmentId: null
197
+ };
198
+
199
+ if (this._firstSegmentId) {
200
+ var currentSegment = null;
201
+
202
+ do {
203
+ if (!currentSegment) {
204
+ currentSegment = this._segments[this._firstSegmentId];
205
+ }
206
+ else {
207
+ currentSegment = this._segments[currentSegment.nextSegmentId];
208
+ }
209
+
210
+ if (segment.startTime <= currentSegment.segment.startTime) {
211
+ if (currentSegment.prevSegmentId) {
212
+ this._segments[currentSegment.prevSegmentId].nextSegmentId = segment.id;
213
+ newSegment.prevSegmentId = currentSegment.prevSegmentId;
214
+ }
215
+ else {
216
+ this._firstSegmentId = segment.id;
217
+ }
218
+
219
+ currentSegment.prevSegmentId = segment.id;
220
+ newSegment.nextSegmentId = currentSegment.segment.id;
221
+
222
+ this._segments[segment.id] = newSegment;
223
+ break;
224
+ }
225
+ } while (currentSegment.nextSegmentId);
226
+
227
+ if (!newSegment.prevSegmentId && !newSegment.nextSegmentId) {
228
+ currentSegment.nextSegmentId = segment.id;
229
+ newSegment.prevSegmentId = currentSegment.segment.id;
230
+ this._segments[segment.id] = newSegment;
231
+ this._lastSegmentId = segment.id;
232
+ }
233
+ }
234
+ else {
235
+ this._firstSegmentId = segment.id;
236
+ this._segments[segment.id] = newSegment;
237
+ this._lastSegmentId = segment.id;
238
+ }
239
+ };
240
+
241
+ SegmentsGroup.prototype._deleteSegment = function(segment) {
242
+ if (this._segments[segment.id].prevSegmentId) {
243
+ this._segments[this._segments[segment.id].prevSegmentId].nextSegmentId
244
+ = this._segments[segment.id].nextSegmentId;
245
+ }
246
+
247
+ if (this._segments[segment.id].nextSegmentId) {
248
+ this._segments[this._segments[segment.id].nextSegmentId].prevSegmentId
249
+ = this._segments[segment.id].prevSegmentId;
250
+ }
251
+
252
+ if (this._firstSegmentId === segment.id) {
253
+ this._firstSegmentId = this._segments[segment.id].nextSegmentId;
254
+ }
255
+
256
+ if (this._lastSegmentId === segment.id) {
257
+ this._lastSegmentId = this._segments[segment.id].prevSegmentId;
258
+ }
259
+
260
+ delete this._segments[segment.id];
261
+ };
262
+
263
+ SegmentsGroup.prototype.getSegmentsGroupLength = function() {
264
+ if (this._segments[this._lastSegmentId]) {
265
+ return this._view.timeToPixels(this._segments[this._lastSegmentId].segment.endTime);
266
+ }
267
+
268
+ return 0;
269
+ };
270
+
271
+ /**
272
+ * Creates the Konva UI objects for a given segment.
273
+ *
274
+ * @private
275
+ * @param {Segment} segment
276
+ * @returns {SegmentShape}
277
+ */
278
+
279
+ SegmentsGroup.prototype._createSegmentShape = function(segment) {
280
+ return new SegmentShape(segment, this._peaks, this, this._view);
281
+ };
282
+
283
+ /**
284
+ * Adds a Konva UI object to the group for a given segment.
285
+ *
286
+ * @private
287
+ * @param {Segment} segment
288
+ * @returns {SegmentShape}
289
+ */
290
+
291
+ SegmentsGroup.prototype._addSegmentShape = function(segment) {
292
+ var segmentShape = this._createSegmentShape(segment);
293
+
294
+ segmentShape.addToGroup(this._group, this);
295
+
296
+ this._segmentShapes[segment.id] = segmentShape;
297
+
298
+ return segmentShape;
299
+ };
300
+
301
+ SegmentsGroup.prototype.updateSegmentsOnMove = function(segment, marker) {
302
+ this._updateSegments(segment, marker);
303
+ };
304
+
305
+ /**
306
+ * Updates the positions of all displayed segments in the view.
307
+ *
308
+ * @param {Number} startTime The start of the visible range in the view,
309
+ * in seconds.
310
+ * @param {Number} endTime The end of the visible range in the view,
311
+ * in seconds.
312
+ */
313
+
314
+ SegmentsGroup.prototype.updateSegments = function(startTime, endTime) {
315
+ // Update segments in visible time range.
316
+ var segments = this._peaks.segments.find(startTime, endTime);
317
+
318
+ var count = segments.length;
319
+
320
+ segments.forEach(this._updateSegment.bind(this));
321
+
322
+ // TODO: in the overview all segments are visible, so no need to check
323
+ count += this._removeInvisibleSegments(startTime, endTime);
324
+
325
+ if (count > 0) {
326
+ this._draw();
327
+ }
328
+ };
329
+
330
+ SegmentsGroup.prototype._draw = function() {
331
+ this._view.drawSourcesLayer();
332
+ };
333
+
334
+ /**
335
+ * @private
336
+ * @param {Segment} segment
337
+ */
338
+
339
+ SegmentsGroup.prototype._updateSegment = function(segment) {
340
+ var segmentShape = this._findOrAddSegmentShape(segment);
341
+
342
+ segmentShape.update();
343
+ };
344
+
345
+ SegmentsGroup.prototype.getCurrentHeight = function() {
346
+ var currentHeight = 0;
347
+
348
+ for (var id in this._segmentShapes) {
349
+ if (Utils.objectHasProperty(this._segmentShapes, id)) {
350
+ currentHeight = this._segmentShapes[id].getSegmentHeight();
351
+ break;
352
+ }
353
+ }
354
+
355
+ if (!currentHeight && this._segments) {
356
+ currentHeight = this._peaks.options.segmentHeight;
357
+ }
358
+
359
+ return currentHeight;
360
+ };
361
+
362
+ /**
363
+ * @private
364
+ * @param {Segment} segment
365
+ */
366
+
367
+ SegmentsGroup.prototype._findOrAddSegmentShape = function(segment) {
368
+ var segmentShape = this._segmentShapes[segment.id];
369
+
370
+ if (!segmentShape) {
371
+ segmentShape = this._addSegmentShape(segment);
372
+ }
373
+
374
+ return segmentShape;
375
+ };
376
+
377
+ /**
378
+ * Removes any segments that are not visible, i.e., are not within and do not
379
+ * overlap the given time range.
380
+ *
381
+ * @private
382
+ * @param {Number} startTime The start of the visible time range, in seconds.
383
+ * @param {Number} endTime The end of the visible time range, in seconds.
384
+ * @returns {Number} The number of segments removed.
385
+ */
386
+
387
+ SegmentsGroup.prototype._removeInvisibleSegments = function(startTime, endTime) {
388
+ var count = 0;
389
+
390
+ for (var segmentId in this._segmentShapes) {
391
+ if (Utils.objectHasProperty(this._segmentShapes, segmentId)) {
392
+ var segment = this._segmentShapes[segmentId].getSegment();
393
+
394
+ if (!segment.isVisible(startTime, endTime)) {
395
+ this._removeSegment(segment);
396
+ count++;
397
+ }
398
+ }
399
+ }
400
+
401
+ return count;
402
+ };
403
+
404
+ SegmentsGroup.prototype.getVisibleSegments = function() {
405
+ return this._getVisibleSegments();
406
+ };
407
+
408
+ SegmentsGroup.prototype._getVisibleSegments = function() {
409
+ var frameOffset = this._view.getFrameOffset();
410
+ var width = this._view.getWidth();
411
+ var frameStartTime = this._view.pixelsToTime(frameOffset);
412
+ var frameEndTime = this._view.pixelsToTime(frameOffset + width);
413
+
414
+ var visibleSegments = [];
415
+
416
+ for (var segmentId in this._segmentShapes) {
417
+ if (Utils.objectHasProperty(this._segmentShapes, segmentId)) {
418
+ var segment = this._segmentShapes[segmentId]._segment;
419
+
420
+ if (segment.isVisible(frameStartTime, frameEndTime)) {
421
+ visibleSegments.push(segment);
422
+ }
423
+ }
424
+ }
425
+
426
+ return visibleSegments;
427
+ };
428
+
429
+ SegmentsGroup.prototype.setMagnetizing = function(bool) {
430
+ this._isMagnetized = bool;
431
+ };
432
+
433
+ SegmentsGroup.prototype.isMagnetized = function() {
434
+ return this._isMagnetized;
435
+ };
436
+
437
+ SegmentsGroup.prototype.addToUpdatedSegments = function(segment) {
438
+ if (this._updatedSegments.indexOf(segment) === -1) {
439
+ this._updatedSegments.push(segment);
440
+ }
441
+ };
442
+
443
+ SegmentsGroup.prototype.updateSegment = function(segment, newStartX, newEndX) {
444
+ var newXs = this.manageCollision(segment, newStartX, newEndX);
445
+
446
+ if (newXs.startX !== null) {
447
+ segment.startTime = this._view.pixelsToTime(newXs.startX);
448
+ }
449
+
450
+ if (newXs.endX !== null) {
451
+ segment.endTime = this._view.pixelsToTime(newXs.endX);
452
+ }
453
+
454
+ if (newXs) {
455
+ this._updateSegment(segment);
456
+
457
+ this.addToUpdatedSegments(segment);
458
+
459
+ this._draw();
460
+ }
461
+ };
462
+
463
+ SegmentsGroup.prototype.manageCollision = function(segment, newStartX, newEndX) {
464
+ var newStartTime = null;
465
+ var newEndTime = null;
466
+ var startLimited = false;
467
+ var endLimited = false;
468
+ var segmentMagnetThreshold, width;
469
+
470
+ if (this._isMagnetized) {
471
+ segmentMagnetThreshold = this._view.pixelsToTime(
472
+ this._peaks.options.segmentMagnetThreshold
473
+ );
474
+
475
+ if (newStartX !== null && newEndX !== null) {
476
+ width = newEndX - newStartX;
477
+ }
478
+ }
479
+
480
+ if (newStartX !== null) {
481
+ // startMarker changed
482
+ newStartTime = this._view.pixelsToTime(newStartX);
483
+
484
+ if (this._segments[segment.id].prevSegmentId) {
485
+ // there is another segment to the left
486
+ var previousSegment = this._segments[this._segments[segment.id].prevSegmentId].segment;
487
+
488
+ if (this._isMagnetized) {
489
+ if (newStartTime < previousSegment.endTime + segmentMagnetThreshold) {
490
+ newStartX = this._view.timeToPixels(previousSegment.endTime);
491
+ if (width) {
492
+ newEndX = newStartX + width;
493
+ }
494
+
495
+ return {
496
+ startX: newStartX,
497
+ endX: newEndX
498
+ };
499
+ }
500
+ }
501
+ else if (segment.startTime > newStartTime) {
502
+ // startMarker moved to the left
503
+ if (newStartTime < previousSegment.endTime) {
504
+ // there is collision
505
+ if (previousSegment.startTime + previousSegment.minSize > newStartTime) {
506
+ newStartTime = previousSegment.startTime + previousSegment.minSize;
507
+ startLimited = true;
508
+ }
509
+
510
+ if (previousSegment.endTime !== newStartTime) {
511
+ previousSegment.endTime = newStartTime;
512
+ this._updateSegment(previousSegment);
513
+ this.addToUpdatedSegments(previousSegment);
514
+ }
515
+ }
516
+ }
517
+ }
518
+ else {
519
+ if (newStartTime < 0) {
520
+ newStartTime = 0;
521
+ startLimited = true;
522
+ }
523
+ }
524
+ }
525
+
526
+ if (newEndX !== null) {
527
+ // endMarker changed
528
+ newEndTime = this._view.pixelsToTime(newEndX);
529
+
530
+ if (this._segments[segment.id].nextSegmentId) {
531
+ // there is another segment to the right
532
+ var nextSegment = this._segments[this._segments[segment.id].nextSegmentId].segment;
533
+
534
+ if (this._isMagnetized) {
535
+ if (newEndTime > nextSegment.startTime - segmentMagnetThreshold) {
536
+ newEndX = this._view.timeToPixels(nextSegment.startTime);
537
+ if (width) {
538
+ newStartX = newEndX - width;
539
+ }
540
+
541
+ return {
542
+ startX: newStartX,
543
+ endX: newEndX
544
+ };
545
+ }
546
+ }
547
+ else if (segment.endTime < newEndTime) {
548
+ // endMarker moved to the right
549
+ if (newEndTime > nextSegment.startTime) {
550
+ // there is collision
551
+ if (nextSegment.endTime - nextSegment.minSize < newEndTime) {
552
+ newEndTime = nextSegment.endTime - nextSegment.minSize;
553
+ endLimited = true;
554
+ }
555
+
556
+ if (nextSegment.startTime !== newEndTime) {
557
+ nextSegment.startTime = newEndTime;
558
+ this._updateSegment(nextSegment);
559
+ this.addToUpdatedSegments(nextSegment);
560
+ }
561
+ }
562
+ }
563
+ }
564
+ else {
565
+ // No limits on the right
566
+ }
567
+ }
568
+
569
+ // Check for minimal size of segment
570
+ if (newStartTime !== null && newEndTime !== null) {
571
+ if (newEndTime - newStartTime < segment.minSize) {
572
+ if (startLimited) {
573
+ newEndTime = newStartTime + segment.minSize;
574
+ }
575
+ else if (endLimited) {
576
+ newStartTime = newEndTime - segment.minSize;
577
+ }
578
+ }
579
+ }
580
+ else if (newStartTime !== null) {
581
+ if (segment.endTime - newStartTime < segment.minSize) {
582
+ newStartTime = segment.endTime - segment.minSize;
583
+ }
584
+ }
585
+ else if (newEndTime !== null) {
586
+ if (newEndTime - segment.startTime < segment.minSize) {
587
+ newEndTime = segment.startTime + segment.minSize;
588
+ }
589
+ }
590
+
591
+ var output = {
592
+ startX: null,
593
+ endX: null
594
+ };
595
+
596
+ if (newStartTime !== null) {
597
+ output.startX = this._view.timeToPixels(newStartTime);
598
+ }
599
+
600
+ if (newEndTime !== null) {
601
+ output.endX = this._view.timeToPixels(newEndTime);
602
+ }
603
+
604
+ return output;
605
+ };
606
+
607
+ SegmentsGroup.prototype._getOverlappedSegments = function() {
608
+ var self = this;
609
+ var segments = this._getVisibleSegments();
610
+
611
+ return segments.reduce(function(result, segment) {
612
+ segments.forEach(function(segment2) {
613
+ if (self._segmentsOverlapped(segment, segment2)) {
614
+ if (!result.includes(segment2.id)) {
615
+ result.push(segment2);
616
+ }
617
+ }
618
+ });
619
+
620
+ return result;
621
+ }, []);
622
+ };
623
+
624
+ /**
625
+ * Removes the given segment from the view.
626
+ *
627
+ * @param {Segment} segment
628
+ */
629
+
630
+ SegmentsGroup.prototype._removeSegment = function(segment) {
631
+ var segmentShape = this._segmentShapes[segment.id];
632
+
633
+ if (segmentShape) {
634
+ delete this._segmentShapes[segment.id];
635
+ segmentShape.destroy();
636
+ }
637
+ };
638
+
639
+ /**
640
+ * Toggles visibility of the segments layer.
641
+ *
642
+ * @param {Boolean} visible
643
+ */
644
+
645
+ SegmentsGroup.prototype.setVisible = function(visible) {
646
+ this._group.setVisible(visible);
647
+ };
648
+
649
+ SegmentsGroup.prototype.draw = function() {
650
+ this._draw();
651
+ };
652
+
653
+ SegmentsGroup.prototype._segmentsOverlapped = function(segment1, segment2) {
654
+ var endsLater = (segment1.startTime < segment2.startTime)
655
+ && (segment1.endTime > segment2.startTime);
656
+ var startsEarlier = (segment1.startTime > segment2.startTime)
657
+ && (segment1.startTime < segment2.endTime);
658
+
659
+ return endsLater || startsEarlier;
660
+ };
661
+
662
+ SegmentsGroup.prototype.destroy = function() {
663
+ this._peaks.off('segment.updated', this._onSegmentsUpdate);
664
+ this._peaks.off('segments.add', this._onSegmentsAdd);
665
+ this._peaks.off('segments.remove', this._onSegmentsRemove);
666
+ this._peaks.off('segments.remove_all', this._onSegmentsRemoveAll);
667
+ this._peaks.off('segments.dragged', this._onSegmentsDragged);
668
+ };
669
+
670
+ SegmentsGroup.prototype.fitToView = function() {
671
+ for (var segmentId in this._segmentShapes) {
672
+ if (Utils.objectHasProperty(this._segmentShapes, segmentId)) {
673
+ var segmentShape = this._segmentShapes[segmentId];
674
+
675
+ segmentShape.fitToView();
676
+ }
677
+ }
678
+ };
679
+
680
+ SegmentsGroup.prototype.contains = function(segment) {
681
+ for (var id in this._segments) {
682
+ if (Utils.objectHasProperty(this._segments, id)) {
683
+ if (id === segment.id) {
684
+ return true;
685
+ }
686
+ }
687
+ }
688
+
689
+ return false;
690
+ };
691
+
692
+ SegmentsGroup.prototype.getHeight = function() {
693
+ return this._group.getHeight();
694
+ };
695
+
696
+ return SegmentsGroup;
697
+ });