@checksub_team/peaks_timeline
Version:
JavaScript UI component for displaying audio waveforms
802 lines (648 loc) • 22.9 kB
JavaScript
/**
* @file
*
* Defines the {@link SegmentsGroup} class.
*
* @module segments-group
*/
define([
'./segment-shape',
'./utils',
'konva'
], function(
SegmentShape,
Utils,
Konva) {
'use strict';
/**
* Creates a Konva.Group that displays segment markers against the audio
* waveform.
*
* @class
* @alias SegmentsGroup
*
* @param {Peaks} peaks
* @param {WaveformOverview|WaveformZoomView} view
* @param {Boolean} allowEditing
*/
function SegmentsGroup(peaks, view, allowEditing) {
this._peaks = peaks;
this._view = view;
this._allowEditing = allowEditing;
this._firstSegmentId = null;
this._segments = {};
this._lastSegmentId = null;
this._segmentShapes = {};
this._group = new Konva.Group();
this._updatedSegments = [];
this._isMagnetized = false;
this._peaks.on('segments.setMagnetizing', this.setMagnetizing.bind(this));
this._peaks.on('segment.setIndicators', this.setIndicators.bind(this));
this._peaks.on('segments.relative_ids_refreshed', this._onRelativeIdsRefreshed.bind(this));
}
SegmentsGroup.prototype._onRelativeIdsRefreshed = function() {
for (var id in this._segmentShapes) {
if (Utils.objectHasProperty(this._segmentShapes, id)) {
var segmentShape = this._segmentShapes[id];
var newText = '#'
+ segmentShape._segment.relativeId
+ ' '
+ Utils.removeLineBreaks(segmentShape._segment.labelText);
if (newText === segmentShape._label.text) {
return;
}
segmentShape._label.setText(newText);
}
}
};
SegmentsGroup.prototype.isEmpty = function() {
return Object.keys(this._segments).length === 0;
};
/**
* Adds the group to the given {Konva.Group}.
*
* @param {Konva.Group} group
*/
SegmentsGroup.prototype.addToGroup = function(group) {
group.add(this._group);
};
SegmentsGroup.prototype.moveToTop = function() {
this._group.moveToTop();
};
SegmentsGroup.prototype.enableEditing = function(enable) {
this._allowEditing = enable;
};
SegmentsGroup.prototype.isEditingEnabled = function() {
return this._allowEditing;
};
SegmentsGroup.prototype.y = function(value) {
return this._group.y(value);
};
SegmentsGroup.prototype.getActiveSegment = function(time) {
var activeSegment = null;
var currentSegment = null;
var nextSegmentId = null;
do {
if (!currentSegment) {
currentSegment = this._segments[this._firstSegmentId];
}
else {
currentSegment = this._segments[currentSegment.nextSegmentId];
}
if (currentSegment) {
if (currentSegment.segment.startTime > time) {
// We didn't find an active segment and will not in the remainings segments
break;
}
if (currentSegment.segment.startTime <= time && currentSegment.segment.endTime > time) {
activeSegment = currentSegment.segment;
break;
}
}
else {
break;
}
nextSegmentId = currentSegment.nextSegmentId;
} while (nextSegmentId);
return activeSegment;
};
SegmentsGroup.prototype.onSegmentsUpdate = function(segment) {
if (this._segments[segment.id]) {
var redraw = false;
var segmentShape = this._segmentShapes[segment.id];
var frameOffset = this._view.getFrameOffset();
var width = this._view.getWidth();
var frameStartTime = this._view.pixelsToTime(frameOffset);
var frameEndTime = this._view.pixelsToTime(frameOffset + width);
this._deleteSegment(segment);
this._addSegment(segment);
if (segmentShape) {
this._removeSegment(segment);
redraw = true;
}
if (segment.isVisible(frameStartTime, frameEndTime)) {
this._addSegmentShape(segment);
redraw = true;
}
if (redraw) {
this.updateSegments(frameStartTime, frameEndTime);
}
}
};
SegmentsGroup.prototype.onSegmentUpdated = function() {
this._peaks.emit('segments.updated', this._updatedSegments);
this._updatedSegments = [];
};
SegmentsGroup.prototype.onSegmentsAdd = function(segments) {
var self = this;
var frameOffset = self._view.getFrameOffset();
var width = self._view.getWidth();
var frameStartTime = self._view.pixelsToTime(frameOffset);
var frameEndTime = self._view.pixelsToTime(frameOffset + width);
segments.forEach(function(segment) {
self._addSegment(segment);
});
self.updateSegments(frameStartTime, frameEndTime);
};
SegmentsGroup.prototype.onSegmentsRemove = function(segments) {
var self = this;
segments.forEach(function(segment) {
var index = self._updatedSegments.indexOf(segment);
if (index > -1) {
self._updatedSegments.splice(index, 1);
}
self._removeSegment(segment);
self._deleteSegment(segment);
});
this._draw();
};
SegmentsGroup.prototype.onSegmentsRemoveAll = function() {
this._group.removeChildren();
this._firstSegmentId = null;
this._segments = {};
this._lastSegmentId = null;
this._segmentShapes = {};
this._draw();
};
SegmentsGroup.prototype._addSegment = function(segment) {
var newSegment = {
segment: segment,
prevSegmentId: null,
nextSegmentId: null
};
if (this._firstSegmentId) {
var currentSegment = null;
do {
if (!currentSegment) {
currentSegment = this._segments[this._firstSegmentId];
}
else {
currentSegment = this._segments[currentSegment.nextSegmentId];
}
if (segment.startTime <= currentSegment.segment.startTime) {
if (currentSegment.prevSegmentId) {
this._segments[currentSegment.prevSegmentId].nextSegmentId = segment.id;
newSegment.prevSegmentId = currentSegment.prevSegmentId;
}
else {
this._firstSegmentId = segment.id;
}
currentSegment.prevSegmentId = segment.id;
newSegment.nextSegmentId = currentSegment.segment.id;
this._segments[segment.id] = newSegment;
break;
}
} while (currentSegment.nextSegmentId);
if (!newSegment.prevSegmentId && !newSegment.nextSegmentId) {
currentSegment.nextSegmentId = segment.id;
newSegment.prevSegmentId = currentSegment.segment.id;
this._segments[segment.id] = newSegment;
this._lastSegmentId = segment.id;
}
}
else {
this._firstSegmentId = segment.id;
this._segments[segment.id] = newSegment;
this._lastSegmentId = segment.id;
}
};
SegmentsGroup.prototype._deleteSegment = function(segment) {
if (this._segments[segment.id].prevSegmentId) {
this._segments[this._segments[segment.id].prevSegmentId].nextSegmentId
= this._segments[segment.id].nextSegmentId;
}
if (this._segments[segment.id].nextSegmentId) {
this._segments[this._segments[segment.id].nextSegmentId].prevSegmentId
= this._segments[segment.id].prevSegmentId;
}
if (this._firstSegmentId === segment.id) {
this._firstSegmentId = this._segments[segment.id].nextSegmentId;
}
if (this._lastSegmentId === segment.id) {
this._lastSegmentId = this._segments[segment.id].prevSegmentId;
}
delete this._segments[segment.id];
};
SegmentsGroup.prototype.getSegmentsGroupLength = function() {
if (this._segments[this._lastSegmentId]) {
return this._view.timeToPixels(this._segments[this._lastSegmentId].segment.endTime);
}
return 0;
};
/**
* Creates the Konva UI objects for a given segment.
*
* @private
* @param {Segment} segment
* @returns {SegmentShape}
*/
SegmentsGroup.prototype._createSegmentShape = function(segment) {
return new SegmentShape(segment, this._peaks, this, this._view);
};
/**
* Adds a Konva UI object to the group for a given segment.
*
* @private
* @param {Segment} segment
* @returns {SegmentShape}
*/
SegmentsGroup.prototype._addSegmentShape = function(segment) {
var segmentShape = this._createSegmentShape(segment);
segmentShape.addToGroup(this._group, this);
this._segmentShapes[segment.id] = segmentShape;
return segmentShape;
};
SegmentsGroup.prototype.updateSegmentsOnMove = function(segment, marker) {
this._updateSegments(segment, marker);
};
/**
* Updates the positions of all displayed segments in the view.
*
* @param {Number} startTime The start of the visible range in the view,
* in seconds.
* @param {Number} endTime The end of the visible range in the view,
* in seconds.
*/
SegmentsGroup.prototype.updateSegments = function(startTime, endTime) {
// Update segments in visible time range.
var segments = this.find(startTime, endTime);
var count = segments.length;
segments.forEach(this._updateSegment.bind(this));
// TODO: in the overview all segments are visible, so no need to check
count += this._removeInvisibleSegments(startTime, endTime);
if (count > 0) {
this._draw();
}
};
SegmentsGroup.prototype.find = function(startTime, endTime) {
var currentSegment = null;
var visibleSegments = [];
if (this._firstSegmentId) {
do {
if (!currentSegment) {
currentSegment = this._segments[this._firstSegmentId];
}
else {
currentSegment = this._segments[currentSegment.nextSegmentId];
}
if (currentSegment.segment.isVisible(startTime, endTime)) {
visibleSegments.push(currentSegment.segment);
}
else if (visibleSegments.length) {
break;
}
} while (currentSegment.nextSegmentId);
}
return visibleSegments;
};
SegmentsGroup.prototype._draw = function() {
this._view.drawSourcesLayer();
};
/**
* @private
* @param {Segment} segment
*/
SegmentsGroup.prototype._updateSegment = function(segment) {
var segmentShape = this._findOrAddSegmentShape(segment);
segmentShape.update();
};
SegmentsGroup.prototype.getCurrentHeight = function() {
var currentHeight = 0;
for (var id in this._segmentShapes) {
if (Utils.objectHasProperty(this._segmentShapes, id)) {
currentHeight = this._segmentShapes[id].getSegmentHeight();
break;
}
}
if (!currentHeight) {
if (this.isEmpty()) {
currentHeight = this._peaks.options.emptyLineHeight;
}
else {
currentHeight = this._peaks.options.segmentHeight;
}
}
return currentHeight;
};
/**
* @private
* @param {Segment} segment
*/
SegmentsGroup.prototype._findOrAddSegmentShape = function(segment) {
var segmentShape = this._segmentShapes[segment.id];
if (!segmentShape) {
segmentShape = this._addSegmentShape(segment);
}
return segmentShape;
};
/**
* Removes any segments that are not visible, i.e., are not within and do not
* overlap the given time range.
*
* @private
* @param {Number} startTime The start of the visible time range, in seconds.
* @param {Number} endTime The end of the visible time range, in seconds.
* @returns {Number} The number of segments removed.
*/
SegmentsGroup.prototype._removeInvisibleSegments = function(startTime, endTime) {
var count = 0;
for (var segmentId in this._segmentShapes) {
if (Utils.objectHasProperty(this._segmentShapes, segmentId)) {
var segment = this._segmentShapes[segmentId].getSegment();
if (!segment.isVisible(startTime, endTime)) {
this._removeSegment(segment);
count++;
}
}
}
return count;
};
SegmentsGroup.prototype.getVisibleSegments = function() {
return this._getVisibleSegments();
};
SegmentsGroup.prototype._getVisibleSegments = function() {
var frameOffset = this._view.getFrameOffset();
var width = this._view.getWidth();
var frameStartTime = this._view.pixelsToTime(frameOffset);
var frameEndTime = this._view.pixelsToTime(frameOffset + width);
var visibleSegments = [];
for (var segmentId in this._segmentShapes) {
if (Utils.objectHasProperty(this._segmentShapes, segmentId)) {
var segment = this._segmentShapes[segmentId]._segment;
if (segment.isVisible(frameStartTime, frameEndTime)) {
visibleSegments.push(segment);
}
}
}
return visibleSegments;
};
SegmentsGroup.prototype.setMagnetizing = function(bool) {
this._isMagnetized = bool;
};
SegmentsGroup.prototype.isMagnetized = function() {
return this._isMagnetized;
};
SegmentsGroup.prototype.setIndicators = function(segment) {
var segmentShape = this._segmentShapes[segment.id];
if (segmentShape) {
segmentShape.createIndicators();
this._draw();
}
};
SegmentsGroup.prototype.addToUpdatedSegments = function(segment) {
if (this._updatedSegments.indexOf(segment) === -1) {
this._updatedSegments.push(segment);
}
};
SegmentsGroup.prototype.updateSegment = function(segment, newStartX, newEndX) {
var newXs = this.manageCollision(segment, newStartX, newEndX);
if (newXs.startX !== null) {
segment.startTime = this._view.pixelsToTime(newXs.startX);
}
if (newXs.endX !== null) {
segment.endTime = this._view.pixelsToTime(newXs.endX);
}
if (newXs) {
this._updateSegment(segment);
this.addToUpdatedSegments(segment);
this._draw();
}
};
SegmentsGroup.prototype.manageCollision = function(segment, newStartX, newEndX) {
var newStartTime = null;
var newEndTime = null;
var startLimited = false;
var endLimited = false;
var segmentMagnetThreshold, width, previousSegment, nextSegment, newXs;
if (this._isMagnetized) {
segmentMagnetThreshold = this._view.pixelsToTime(
this._peaks.options.segmentMagnetThreshold
);
if (newStartX !== null && newEndX !== null) {
width = newEndX - newStartX;
}
}
if (newStartX !== null) {
// startMarker changed
newStartTime = this._view.pixelsToTime(newStartX);
if (this._segments[segment.id].prevSegmentId) {
// there is another segment to the left
previousSegment = this._segments[this._segments[segment.id].prevSegmentId].segment;
if (this._isMagnetized) {
if (newStartTime < previousSegment.endTime + segmentMagnetThreshold) {
newStartX = this._view.timeToPixels(previousSegment.endTime);
if (width) {
newEndX = newStartX + width;
}
return {
startX: newStartX,
endX: newEndX
};
}
}
else if (segment.startTime >= newStartTime) {
// startMarker moved to the left
if (newStartTime < previousSegment.endTime) {
// there is collision
if (previousSegment.editable) {
if (previousSegment.startTime + previousSegment.minSize > newStartTime) {
newStartTime = previousSegment.startTime + previousSegment.minSize;
startLimited = true;
}
if (previousSegment.endTime !== newStartTime) {
newXs = this.manageCollision(
previousSegment,
this._view.timeToPixels(previousSegment.startTime),
this._view.timeToPixels(newStartTime)
);
if (newXs.startX !== null) {
previousSegment.startTime = this._view.pixelsToTime(newXs.startX);
}
if (newXs.endX !== null) {
previousSegment.endTime = this._view.pixelsToTime(newXs.endX);
}
this._updateSegment(previousSegment);
this.addToUpdatedSegments(previousSegment);
}
}
else {
newStartTime = previousSegment.endTime;
startLimited = true;
}
}
}
}
else {
if (newStartTime < 0) {
newStartTime = 0;
startLimited = true;
}
}
}
if (newEndX !== null) {
// endMarker changed
newEndTime = this._view.pixelsToTime(newEndX);
if (this._segments[segment.id].nextSegmentId) {
// there is another segment to the right
nextSegment = this._segments[this._segments[segment.id].nextSegmentId].segment;
if (this._isMagnetized) {
if (newEndTime > nextSegment.startTime - segmentMagnetThreshold) {
newEndX = this._view.timeToPixels(nextSegment.startTime);
if (width) {
newStartX = newEndX - width;
}
return {
startX: newStartX,
endX: newEndX
};
}
}
else if (segment.endTime <= newEndTime) {
// endMarker moved to the right
if (newEndTime > nextSegment.startTime) {
// there is collision
if (nextSegment.editable) {
if (nextSegment.endTime - nextSegment.minSize < newEndTime) {
newEndTime = nextSegment.endTime - nextSegment.minSize;
endLimited = true;
}
if (nextSegment.startTime !== newEndTime) {
newXs = this.manageCollision(
nextSegment,
this._view.timeToPixels(newEndTime),
this._view.timeToPixels(nextSegment.endTime)
);
if (newXs.startX !== null) {
nextSegment.startTime = this._view.pixelsToTime(newXs.startX);
}
if (newXs.endX !== null) {
nextSegment.endTime = this._view.pixelsToTime(newXs.endX);
}
this._updateSegment(nextSegment);
this.addToUpdatedSegments(nextSegment);
}
}
else {
newEndTime = nextSegment.startTime;
endLimited = true;
}
}
}
}
else {
// No limits on the right
}
}
// Check for minimal size of segment
if (newStartTime !== null && newEndTime !== null) {
if (Utils.roundTime(newEndTime - newStartTime) < segment.minSize) {
if (previousSegment && nextSegment) {
if (Utils.roundTime(nextSegment.startTime - previousSegment.endTime) < segment.minSize) {
return {
startX: null,
endX: null
};
}
}
if (startLimited) {
newEndTime = newStartTime + segment.minSize;
}
else if (endLimited) {
newStartTime = newEndTime - segment.minSize;
}
}
}
else if (newStartTime !== null) {
if (Utils.roundTime(segment.endTime - newStartTime) < segment.minSize) {
newStartTime = segment.endTime - segment.minSize;
}
}
else if (newEndTime !== null) {
if (Utils.roundTime(newEndTime - segment.startTime) < segment.minSize) {
newEndTime = segment.startTime + segment.minSize;
}
}
var output = {
startX: null,
endX: null
};
if (newStartTime !== null) {
output.startX = this._view.timeToPixels(newStartTime);
}
if (newEndTime !== null) {
output.endX = this._view.timeToPixels(newEndTime);
}
return output;
};
SegmentsGroup.prototype._getOverlappedSegments = function() {
var self = this;
var segments = this._getVisibleSegments();
return segments.reduce(function(result, segment) {
segments.forEach(function(segment2) {
if (self._segmentsOverlapped(segment, segment2)) {
if (!result.includes(segment2.id)) {
result.push(segment2);
}
}
});
return result;
}, []);
};
/**
* Removes the given segment from the view.
*
* @param {Segment} segment
*/
SegmentsGroup.prototype._removeSegment = function(segment) {
var segmentShape = this._segmentShapes[segment.id];
if (segmentShape) {
delete this._segmentShapes[segment.id];
segmentShape.destroy();
}
};
/**
* Toggles visibility of the segments layer.
*
* @param {Boolean} visible
*/
SegmentsGroup.prototype.setVisible = function(visible) {
this._group.setVisible(visible);
};
SegmentsGroup.prototype.draw = function() {
this._draw();
};
SegmentsGroup.prototype._segmentsOverlapped = function(segment1, segment2) {
var endsLater = (segment1.startTime < segment2.startTime)
&& (segment1.endTime > segment2.startTime);
var startsEarlier = (segment1.startTime > segment2.startTime)
&& (segment1.startTime < segment2.endTime);
return endsLater || startsEarlier;
};
SegmentsGroup.prototype.destroy = function() {
this._peaks.off('segments.setMagnetizing', this.setMagnetizing);
this._peaks.off('segment.setIndicators', this.setIndicators);
this._peaks.off('segments.relative_ids_refreshed', this._onRelativeIdsRefreshed);
};
SegmentsGroup.prototype.fitToView = function() {
for (var segmentId in this._segmentShapes) {
if (Utils.objectHasProperty(this._segmentShapes, segmentId)) {
var segmentShape = this._segmentShapes[segmentId];
segmentShape.fitToView();
}
}
};
SegmentsGroup.prototype.contains = function(segment) {
for (var id in this._segments) {
if (Utils.objectHasProperty(this._segments, id)) {
if (id === segment.id) {
return true;
}
}
}
return false;
};
SegmentsGroup.prototype.getHeight = function() {
return this._group.getHeight();
};
return SegmentsGroup;
});