UNPKG

@checksub_team/peaks_timeline

Version:

JavaScript UI component for displaying audio waveforms

765 lines (634 loc) 21.9 kB
/** * @file * * Defines the {@link lineGroup} class. * * @module lineGroup */ define([ './source-group', '../utils', 'konva' ], function(SourceGroup, Utils, Konva) { 'use strict'; function LineGroup(peaks, view, line) { this._peaks = peaks; this._view = view; this._line = line; this._position = null; this._firstSourceId = null; this._sources = {}; this._cachedStartSourceForActive = null; this._sourcesGroup = {}; this._wrapped = false; this._group = new Konva.Group({ draggable: true, dragBoundFunc: function() { return { x: this.absolutePosition().x, y: this.absolutePosition().y }; } }); this._sourceHeights = {}; this._height = this._peaks.options.emptyLineHeight; this._unwrappedCount = 0; } LineGroup.prototype.getSegmentsGroup = function() { return this._segmentsGroup; }; LineGroup.prototype.getPosition = function() { return this._position; }; LineGroup.prototype.getId = function() { return this._line.id; }; LineGroup.prototype.isLocked = function() { return this._line.locked; }; LineGroup.prototype.getLine = function() { return this._line; }; LineGroup.prototype.countRemainingElements = function() { return this.isSegmentsLine() ? this._segmentsGroup.countRemainingElements() : Object.keys(this._sources).length; }; LineGroup.prototype.isSegmentsLine = function() { return Boolean(this._segmentsGroup); }; LineGroup.prototype.updateSegments = function(frameStartTime, frameEndTime) { if (this.isSegmentsLine()) { this._segmentsGroup.updateSegments(frameStartTime, frameEndTime); } }; LineGroup.prototype.lineLength = function() { var length = 0; if (this.isSegmentsLine()) { return this._segmentsGroup.getSegmentsGroupLength(); } for (var sourceId in this._sources) { if (Utils.objectHasProperty(this._sources, sourceId)) { var sourceGroupLength = this._view.timeToPixels( this._sources[sourceId].source.endTime ); if (sourceGroupLength > length) { length = sourceGroupLength; } } } return length; }; LineGroup.prototype.lineHeight = function() { return this._height; }; LineGroup.prototype._addHeight = function(height) { if (this._sourceHeights[height]) { this._sourceHeights[height]++; } else { this._sourceHeights[height] = 1; if (height > this._height) { this._height = height; } } }; LineGroup.prototype._subtractHeight = function(height) { if (Object.keys(this._sources).length === 0) { this._height = this._peaks.options.emptyLineHeight; this._sourceHeights = {}; } else { this._sourceHeights[height]--; if (this._sourceHeights[height] === 0 && height === this._height) { delete this._sourceHeights[height]; this._height = 0; for (var sourceHeight in this._sourceHeights) { if (Utils.objectHasProperty(this._sourceHeights, sourceHeight)) { var parsedHeight = parseInt(sourceHeight, 10); if (parsedHeight > this._height) { this._height = parsedHeight; } } } } } }; LineGroup.prototype.updateLineHeight = function(source, action) { var oldHeight = this._height; var sourceGroup = this._sourcesGroup[source.id]; var sourceGroupHeight; switch (action) { case 'add': sourceGroupHeight = sourceGroup ? sourceGroup.getCurrentHeight() : SourceGroup.getHeights(source, this._peaks).current; this._addHeight(sourceGroupHeight); break; case 'remove': sourceGroupHeight = sourceGroup ? sourceGroup.getCurrentHeight() : SourceGroup.getHeights(source, this._peaks).current; this._subtractHeight(sourceGroupHeight); break; default: // wrappingChanged var { unwrapped, wrapped } = sourceGroup ? sourceGroup.getHeights() : SourceGroup.getHeights(source, this._peaks); this._addHeight(source.wrapped ? wrapped : unwrapped); this._subtractHeight(source.wrapped ? unwrapped : wrapped); } if (this._height !== oldHeight) { this._peaks.emit('line.heightChanged', this._line); } }; LineGroup.prototype.isVisible = function() { return this.y() < this._view.getHeight() && this.y() + this._height > 0; }; LineGroup.prototype.addToLayer = function(layer) { layer.add(this._group); }; /** * Adds a source to the line. * @param {Source} source - The source to add. * @param {SourceGroup} sourceGroup - The source group of the source (optional). * @returns {void} */ LineGroup.prototype.addSource = function(source, sourceGroup, sourcesAround) { if (sourceGroup) { this._sourcesGroup[source.id] = sourceGroup; // Only move to this group if not currently being dragged // (during drag, source stays in drag layer for z-order) if (!sourceGroup.isDragged()) { if (!sourceGroup.getParent() || !sourceGroup.isDescendantOf(this._group)) { sourceGroup.moveTo(this._group); } } } if (!this._sources[source.id]) { var sourceData = { source: source, prevSourceId: null, nextSourceId: null }; if (Utils.isNullOrUndefined(this._firstSourceId)) { this._firstSourceId = source.id; this._sources[source.id] = sourceData; } else if (Utils.isNullOrUndefined(sourcesAround)) { this._addSourceWherePossible(sourceData); } else { if (sourcesAround.left) { this._sources[sourcesAround.left.id].nextSourceId = source.id; sourceData.prevSourceId = sourcesAround.left.id; } else { this._firstSourceId = source.id; } if (sourcesAround.right) { this._sources[sourcesAround.right.id].prevSourceId = source.id; sourceData.nextSourceId = sourcesAround.right.id; } this._sources[source.id] = sourceData; } this.updateLineHeight(source, 'add'); } }; LineGroup.prototype._addSourceWherePossible = function(sourceData) { const source = sourceData.source; var currentSource = null; do { if (!currentSource) { currentSource = this._sources[this._firstSourceId]; } else { currentSource = this._sources[currentSource.nextSourceId]; } if (source.endTime <= currentSource.source.startTime) { var startLimit = currentSource.prevSourceId ? this._sources[currentSource.prevSourceId].source.endTime : 0; const { newStartTime, newEndTime } = this._canBePlacedBetween( source.startTime, source.endTime, startLimit, currentSource.source.startTime ); if (!Utils.isNullOrUndefined(newStartTime) && !Utils.isNullOrUndefined(newEndTime)) { source.updateTimes(newStartTime, newEndTime); if (currentSource.prevSourceId) { this._sources[currentSource.prevSourceId].nextSourceId = source.id; sourceData.prevSourceId = currentSource.prevSourceId; } else { this._firstSourceId = source.id; } currentSource.prevSourceId = source.id; sourceData.nextSourceId = currentSource.source.id; this._sources[source.id] = sourceData; break; } } } while (currentSource.nextSourceId); if (!sourceData.prevSourceId && !sourceData.nextSourceId) { if (source.startTime < currentSource.source.endTime) { // Overlapping with last source var timeWidth = source.endTime - source.startTime; source.updateTimes( currentSource.source.endTime, currentSource.source.endTime + timeWidth ); } currentSource.nextSourceId = source.id; sourceData.prevSourceId = currentSource.source.id; this._sources[source.id] = sourceData; } }; LineGroup.prototype.addSegments = function(segmentsGroup) { this._segmentsGroup = segmentsGroup; this._height = this._segmentsGroup.getCurrentHeight(); segmentsGroup.moveTo(this._group); }; LineGroup.prototype.refreshSegmentsHeight = function() { if (this.isSegmentsLine) { var oldHeight = this._height; this._height = this._segmentsGroup.getCurrentHeight(); if (this._height !== oldHeight) { this._peaks.emit('line.heightChanged', this._line); } } }; LineGroup.prototype._canBePlacedBetween = function(startTime, endTime, startLimit, endLimit) { var timeWidth = Utils.roundTime(endTime - startTime); var newStartTime, newEndTime; if ((!endLimit && startTime > startLimit) || (startTime > startLimit && endTime < endLimit)) { // Can be placed at its wanted position with wanted start/end time newStartTime = startTime; newEndTime = endTime; } else if (Utils.roundTime(endLimit - startLimit) >= timeWidth) { // Can be placed at its wanted position but not with its wanted start/end time if (startTime > startLimit) { newStartTime = Utils.roundTime(endLimit - timeWidth); newEndTime = endLimit; } else { newStartTime = startLimit; newEndTime = Utils.roundTime(startLimit + timeWidth); } } return { newStartTime, newEndTime }; }; LineGroup.prototype.removeSourceGroup = function(source) { const sourceGroup = this._sourcesGroup[source.id]; delete this._sourcesGroup[source.id]; return sourceGroup; }; LineGroup.prototype.removeSource = function(source) { const sourceGroup = this.removeSourceGroup(source); var sourceData = this._sources[source.id]; delete this._sources[source.id]; if (Object.keys(this._sources).length === 0) { this._peaks.destroyLine(this._line.id, true); return sourceGroup; } if (sourceData.prevSourceId) { this._sources[sourceData.prevSourceId].nextSourceId = sourceData.nextSourceId; } if (sourceData.nextSourceId) { this._sources[sourceData.nextSourceId].prevSourceId = sourceData.prevSourceId; } if (this._firstSourceId === source.id) { this._firstSourceId = sourceData.nextSourceId; } this.updateLineHeight(source, 'remove'); return sourceGroup; }; LineGroup.prototype.getKonvaGroup = function() { return this._group; }; LineGroup.prototype.y = function(value) { if (typeof value !== 'number') { return this._group.y(); } this._group.y(value); }; LineGroup.prototype.manageOrder = function(sources, startTime, endTime) { const firstSource = sources[0]; const lastSource = sources[sources.length - 1]; const cursorTime = this._view.pixelsToTime(this._view.getPointerPosition().x); var newStartTime = startTime; var newEndTime = endTime; var tmpTimes; var sourceDuration = Utils.roundTime(endTime - startTime); if (typeof newStartTime === 'number' && typeof newEndTime === 'number') { if (this._sources[firstSource.id].prevSourceId) { // there is another source to the left var previousStartTime = this._sources[this._sources[firstSource.id].prevSourceId].source.startTime; if (Utils.roundTime(cursorTime + this._view.getTimeOffset()) < previousStartTime) { // we want to change order tmpTimes = this._changeSourcesPosition( sources, sourceDuration, cursorTime + this._view.getTimeOffset() ); if (typeof tmpTimes.newStartTime === 'number' && typeof tmpTimes.newEndTime === 'number') { newStartTime = tmpTimes.newStartTime; newEndTime = tmpTimes.newEndTime; } } } if (this._sources[lastSource.id].nextSourceId) { // there is another source to the right var nextEndTime = this._sources[this._sources[lastSource.id].nextSourceId].source.endTime; if (Utils.roundTime(cursorTime + this._view.getTimeOffset()) > nextEndTime) { // we want to change order tmpTimes = this._changeSourcesPosition( sources, sourceDuration, cursorTime + this._view.getTimeOffset() ); if (typeof tmpTimes.newStartTime === 'number' && typeof tmpTimes.newEndTime === 'number') { newStartTime = tmpTimes.newStartTime; newEndTime = tmpTimes.newEndTime; } } } } return { newStartTime, newEndTime }; }; LineGroup.prototype._changeSourcesPosition = function(sources, sourceDuration, time) { var currentRange = { start: null, end: null }; var startLimit = null; var endLimit = null; let newStartTime, newEndTime; do { if (!currentRange.end) { currentRange.end = this._sources[this._firstSourceId]; } else { currentRange.start = currentRange.end; currentRange.end = this._sources[currentRange.start.nextSourceId]; } if (currentRange.start) { startLimit = currentRange.start.source.endTime; } else { startLimit = 0; } if (currentRange.end) { endLimit = currentRange.end.source.startTime; } else { endLimit = null; } if (time > startLimit && (endLimit === null || time < endLimit)) { ({ newStartTime, newEndTime } = this._canBePlacedBetween( time, time + sourceDuration, startLimit, endLimit )); if (typeof newStartTime === 'number' && typeof newEndTime === 'number') { let prevSourceId = currentRange.start ? currentRange.start.source.id : null; sources.forEach(function(source) { this._moveSource(this._sources[source.id].source, prevSourceId); prevSourceId = source.id; }.bind(this)); } return { newStartTime, newEndTime }; } } while (currentRange.end); return { newStartTime, newEndTime }; }; LineGroup.prototype._moveSource = function(source, prevSourceId) { // Remove source from the list var sourceObj = this._sources[source.id]; var prevSource = this._sources[sourceObj.prevSourceId]; var nextSource = this._sources[sourceObj.nextSourceId]; if (prevSource) { this._sources[sourceObj.prevSourceId].nextSourceId = sourceObj.nextSourceId; } else { this._firstSourceId = sourceObj.nextSourceId; } if (nextSource) { this._sources[sourceObj.nextSourceId].prevSourceId = sourceObj.prevSourceId; } delete this._sources[source.id]; // Add source back to the list sourceObj.prevSourceId = prevSourceId; if (prevSourceId) { sourceObj.nextSourceId = this._sources[prevSourceId].nextSourceId; this._sources[prevSourceId].nextSourceId = source.id; } else { sourceObj.nextSourceId = this._firstSourceId; this._firstSourceId = source.id; } if (sourceObj.nextSourceId) { this._sources[sourceObj.nextSourceId].prevSourceId = source.id; } this._sources[source.id] = sourceObj; }; LineGroup.prototype.manageCollision = function(sources, newStartTime, newEndTime) { var originalStartTime = newStartTime; var originalEndTime = newEndTime; var startLimited = false; var endLimited = false; const firstSource = sources[0]; const lastSource = sources[sources.length - 1]; if (typeof newStartTime === 'number') { // startMarker changed if (this._sources[firstSource.id].prevSourceId) { // there is another source to the left var previousSource = this._sources[this._sources[firstSource.id].prevSourceId] .source; if (newStartTime < previousSource.endTime) { // there is collision newStartTime = previousSource.endTime; startLimited = true; } } else { if (newStartTime < 0) { newStartTime = 0; startLimited = true; } } } if (typeof newEndTime === 'number') { // endMarker changed if (this._sources[lastSource.id].nextSourceId) { // there is another source to the right var nextSource = this._sources[this._sources[lastSource.id].nextSourceId] .source; if (newEndTime > nextSource.startTime) { // there is collision newEndTime = nextSource.startTime; endLimited = true; } } } // Update the other edge if dragging and collision if (typeof newStartTime === 'number' && typeof newEndTime === 'number') { var timeWidth = originalEndTime - originalStartTime; if (startLimited) { newEndTime = Utils.roundTime(newStartTime + timeWidth); } if (endLimited) { newStartTime = Utils.roundTime(newEndTime - timeWidth); } } // Check for minimal size of source // We assume that only 1 source can be resized at a time if (typeof newStartTime === 'number' && typeof newEndTime !== 'number') { if (Utils.roundTime(sources[0].endTime - newStartTime) < sources[0].minSize) { newStartTime = Utils.roundTime(sources[0].endTime - sources[0].minSize); } } else if (typeof newEndTime === 'number' && typeof newStartTime !== 'number') { if (Utils.roundTime(newEndTime - sources[0].startTime) < sources[0].minSize) { newEndTime = Utils.roundTime(sources[0].startTime + sources[0].minSize); } } else { if (Utils.roundTime(newEndTime - newStartTime) < sources[0].minSize) { if (sources[0].endTime !== newEndTime) { newEndTime = Utils.roundTime(newStartTime + sources[0].minSize); } if (sources[0].startTime !== newStartTime) { newStartTime = Utils.roundTime(newEndTime - sources[0].minSize); } } } return { newStartTime, newEndTime }; }; LineGroup.prototype.getSourcesAfter = function(time) { const sources = []; var currentId = this._firstSourceId; while (currentId) { var sourceData = this._sources[currentId]; if (sourceData.source.startTime >= time) { while (currentId) { sourceData = this._sources[currentId]; sources.push(sourceData.source); currentId = sourceData.nextSourceId; } break; } currentId = sourceData.nextSourceId; } return sources; }; /** * Returns all segments on this line whose start time is at or after the given time. * * @param {Number} time * @returns {Array<Segment>} */ LineGroup.prototype.getSegmentsAfter = function(time) { if (!this.isSegmentsLine()) { return []; } return this._segmentsGroup.getSegmentsAfter(time); }; LineGroup.prototype.getSourcesAround = function(time) { var left = null; var right = null; var overlapping = null; var currentId = this._firstSourceId; while (currentId) { var sourceData = this._sources[currentId]; var source = sourceData.source; if (time < source.startTime) { right = source; break; } else if (time >= source.startTime && time <= source.endTime) { overlapping = source; break; } else { left = source; } currentId = sourceData.nextSourceId; } if (overlapping) { return { overlapping: overlapping }; } else { return { left: left, right: right }; } }; LineGroup.prototype.getActiveSource = function(time) { var activeSource = null; var previousSource = null; var currentSource = null; if (this._cachedStartSourceForActive) { if (this._cachedStartSourceForActive.startTime <= time) { if (this._cachedStartSourceForActive.endTime > time) { return this._cachedStartSourceForActive; } else { currentSource = this._sources[this._cachedStartSourceForActive.id]; } } } do { if (!currentSource) { currentSource = this._sources[this._firstSourceId]; } else { previousSource = currentSource; currentSource = this._sources[currentSource.nextSourceId]; } if (currentSource) { if (currentSource.source.startTime > time) { // We didn't find an active source and will not in the remaining sources if (previousSource) { this._cachedStartSourceForActive = previousSource.source; } break; } if (currentSource.source.startTime <= time && currentSource.source.endTime > time) { activeSource = currentSource.source; this._cachedStartSourceForActive = activeSource; break; } } else { break; } } while (currentSource.nextSourceId); return activeSource; }; LineGroup.prototype.updatePosition = function(pos) { this._line.position = pos; this._position = pos; }; LineGroup.prototype.hasSource = function(sourceId) { return Boolean(this._sources[sourceId]); }; LineGroup.prototype.destroy = function() { this._firstSourceId = null; this._sources = {}; this._sourcesGroup = {}; this._group.destroy(); }; LineGroup.prototype.allowInteractions = function(bool) { this._group.listening(bool); }; return LineGroup; });