UNPKG

@checksub_team/peaks_timeline

Version:

JavaScript UI component for displaying audio waveforms

802 lines (648 loc) 22.9 kB
/** * @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; });