UNPKG

@checksub_team/peaks_timeline

Version:

JavaScript UI component for displaying audio waveforms

692 lines (543 loc) 21.7 kB
/** * @file * * Defines the {@link lines} class. * * @module lines */ define([ './segments-group', './line-group', './line-indicator', '../utils' ], function( SegmentsGroup, LineGroup, LineIndicator, Utils) { 'use strict'; function LineGroups(peaks, view, layer) { this._peaks = peaks; this._view = view; this._layer = layer; this._lineGroupsById = {}; this._lineGroupsByPosition = {}; this._autoAddToLayer = false; this._areSourceInteractionsAllowed = true; this._areSegmentInteractionsAllowed = true; this._automaticallyCreatedLineId = null; this._automaticLineCreationPosition = null; this._automaticLineCreationMouseY = null; this._automaticLineCreationTimeout = null; this._segmentsGroups = {}; this._segmentsGroupToLine = {}; this._lineIndicator = new LineIndicator( peaks, view, document.getElementById('line-indicator-container') ); this._peaks.on('handler.view.setFrameOffsetY', this.refreshLineYs.bind(this)); this._peaks.on('handler.lines.add', this._onLinesAdd.bind(this)); this._peaks.on('handler.lines.remove', this._onLinesRemove.bind(this)); this._peaks.on('model.line.update', this._onLineUpdate.bind(this)); this._peaks.on('handler.sources.show', this._onSourcesWrappingChanged.bind(this)); this._peaks.on('handler.sources.hide', this._onSourcesWrappingChanged.bind(this)); this._peaks.on('model.segment.updated', this._onSegmentsUpdate.bind(this)); this._peaks.on('handler.segments.add', this._onSegmentsAdd.bind(this)); this._peaks.on('handler.segments.remove', this._onSegmentsRemove.bind(this)); this._peaks.on('handler.segments.remove_all', this._onSegmentsRemoveAll.bind(this)); this._peaks.on('main.overrideInteractions', this.overrideInteractions.bind(this)); this._peaks.on('main.allowInteractions', this.allowInteractions.bind(this)); this._peaks.on('line.heightChanged', this._onLineHeightChanged.bind(this)); this._peaks.on('segments.dragend', this._onSegmentUpdated.bind(this)); this._peaks.on('lineIndicator.drag', this._onIndicatorDrag.bind(this)); } LineGroups.prototype.getLineGroupsById = function() { return this._lineGroupsById; }; LineGroups.prototype.fitToView = function() { this._lineIndicator.fitToView(); }; LineGroups.prototype._onLinesAdd = function(lines) { lines.forEach(function(line) { this._createLineGroup(line); }.bind(this)); }; LineGroups.prototype._onLinesRemove = function(lines) { lines.forEach(function(line) { this._removeLine(line); }.bind(this)); }; LineGroups.prototype._onLineUpdate = function(line) { const lineGroup = this._lineGroupsById[line.id]; if (!lineGroup) { this._peaks.logger('peaks.lines.update(): line not found: ' + line.id); return; } if (line.position !== lineGroup.getPosition()) { this._setLinePosition(line.id, line.position); } this._lineIndicator.updateIndicator(line); }; LineGroups.prototype._onSegmentsAdd = function(segments) { var self = this; segments.forEach(function(segment) { if (!self._segmentsGroups[segment.line]) { self._segmentsGroups[segment.line] = new SegmentsGroup(self._peaks, self._view, true); } self._segmentsGroups[segment.line].onSegmentsAdd([segment]); if (Utils.objectHasProperty(self._segmentsGroupToLine, segment.line)) { self._segmentsGroupToLine[segment.line].refreshSegmentsHeight(); } }); }; LineGroups.prototype._onSegmentsUpdate = function(segment) { this._segmentsGroups[segment.line].onSegmentsUpdate(segment); }; LineGroups.prototype._onSegmentUpdated = function() { for (var lineId in this._segmentsGroups) { if (Utils.objectHasProperty(this._segmentsGroups, lineId) && this._segmentsGroups[lineId].hasUpdatedSegments()) { this._segmentsGroups[lineId].onSegmentUpdated(); } } }; LineGroups.prototype._onSegmentsRemove = function(segments) { var self = this; segments.forEach(function(segment) { self._segmentsGroups[segment.line].onSegmentsRemove([segment]); }); }; LineGroups.prototype._onSegmentsRemoveAll = function(lineId) { this._segmentsGroups[lineId].onSegmentsRemoveAll(); if (Utils.objectHasProperty(this._segmentsGroupToLine, lineId)) { this._segmentsGroupToLine[lineId].refreshSegmentsHeight(); } }; LineGroups.prototype._onLineHeightChanged = function() { this.refreshLineYs(); }; LineGroups.prototype._onSourcesWrappingChanged = function(sources) { sources.forEach(function(source) { if (this._lineGroupsById[source.lineId]) { this._lineGroupsById[source.lineId].updateLineHeight(source, 'wrappingChanged'); } }.bind(this)); }; LineGroups.prototype.addSource = function(source, sourceGroup, sourcesAround) { const lineGroup = this._lineGroupsById[source.lineId]; if (!lineGroup) { this._peaks.logger('peaks.lines.addSource(): line group not found for source ' + source.id + ' with lineId: ' + source.lineId); this._peaks.logger('Available line groups: ' + JSON.stringify(Object.keys(this._lineGroupsById))); return; } lineGroup.addSource(source, sourceGroup, sourcesAround); }; LineGroups.prototype.addSegments = function(segmentsGroupId, lineId) { const lineGroup = this._lineGroupsById[lineId]; lineGroup.allowInteractions(this._areSegmentInteractionsAllowed); lineGroup.addSegments(this._segmentsGroups[segmentsGroupId]); this._segmentsGroupToLine[segmentsGroupId] = lineGroup; this._setInteractions(lineGroup.getId()); this.refreshLineYs(); }; LineGroups.prototype.removeSource = function(source) { if (!this._lineGroupsById[source.lineId]) { return null; } return this._lineGroupsById[source.lineId].removeSource(source); }; LineGroups.prototype.removeSourceGroup = function(source) { if (!this._lineGroupsById[source.lineId]) { return null; } return this._lineGroupsById[source.lineId].removeSourceGroup(source); }; LineGroups.prototype._removeLine = function(line) { var lineGroup = this._lineGroupsById[line.id]; if (this._automaticallyCreatedLineId === line.id) { this._automaticallyCreatedLineId = null; } lineGroup.destroy(); delete this._lineGroupsByPosition[line.position]; delete this._lineGroupsById[line.id]; this._lineIndicator.removeIndicator(line.id, false); this.refreshLineYs(); return lineGroup; }; LineGroups.prototype.isLineVisible = function(lineId) { return this._lineGroupsById[lineId] ? this._lineGroupsById[lineId].isVisible() : false; }; LineGroups.prototype.getVisibleLines = function() { var lineIds = {}; for (var id in this._lineGroupsById) { if (Utils.objectHasProperty(this._lineGroupsById, id)) { if (this._lineGroupsById[id].isVisible()) { lineIds[id] = true; } } } return lineIds; }; LineGroups.prototype.getSourcesOnLineAfter = function(lineId, time) { return this._lineGroupsById[lineId].getSourcesAfter(time); }; /** * Returns all segments in the given segment group or displayed line whose start time is at or after the given time. * * @param {String|Number} lineId A rendered line identifier or a segment group identifier. * @param {Number} time * @returns {Array<Segment>} */ LineGroups.prototype.getSegmentsOnLineAfter = function(lineId, time) { const lineGroup = this._lineGroupsById[lineId] || this._segmentsGroupToLine[lineId]; if (!lineGroup) { return []; } return lineGroup.getSegmentsAfter(time); }; LineGroups.prototype.getSegmentsGroups = function() { return this._segmentsGroups; }; LineGroups.prototype.addToLayer = function(layer) { for (var id in this._lineGroupsById) { if (Utils.objectHasProperty(this._lineGroupsById, id)) { this._lineGroupsById[id].addToLayer(layer); } } this._autoAddToLayer = true; }; LineGroups.prototype.height = function() { var height = 2 * this._peaks.options.padding; for (var position in this._lineGroupsByPosition) { if (Utils.objectHasProperty(this._lineGroupsByPosition, position)) { height += this._lineGroupsByPosition[position].lineHeight() + this._peaks.options.interline; } } height -= this._peaks.options.interline; return height; }; LineGroups.prototype.linesLength = function() { var length = 0; for (var position in this._lineGroupsByPosition) { if (Utils.objectHasProperty(this._lineGroupsByPosition, position)) { var lineLength = this._lineGroupsByPosition[position].lineLength(); if (lineLength > length) { length = lineLength; } } } return length; }; LineGroups.prototype.stopAutomaticLineCreation = function() { if (this._automaticLineCreationTimeout) { clearTimeout(this._automaticLineCreationTimeout); } this._automaticLineCreationTimeout = null; this._automaticLineCreationPosition = null; this._automaticLineCreationMouseY = null; this._automaticallyCreatedLineId = null; }; LineGroups.prototype.manageAutomaticLineCreation = function(newLinePosition, initialPosition, sources, mouseY) { if (!Utils.isNullOrUndefined(this._automaticallyCreatedLineId)) { return; } else if ( !Utils.isNullOrUndefined(this._automaticLineCreationPosition) && this._automaticLineCreationPosition !== newLinePosition ) { this.stopAutomaticLineCreation(); } if (this._automaticLineCreationTimeout) { return; } this._automaticLineCreationPosition = newLinePosition; this._automaticLineCreationMouseY = mouseY; this._automaticLineCreationTimeout = setTimeout(function() { this._automaticLineCreationTimeout = null; const currentLine = this._lineGroupsByPosition[initialPosition]; sources = sources.filter(function(source) { return currentLine.hasSource(source.id); }); if (sources.length === 0) { this.stopAutomaticLineCreation(); return; } const posAround = this.getLinesAroundPos(currentLine.getPosition()); const targetPosIsJustBefore = newLinePosition <= currentLine.getPosition() && newLinePosition > posAround[0]; const targetPosIsJustAfter = newLinePosition >= currentLine.getPosition() && newLinePosition <= posAround[1]; if ( sources.length === currentLine.countRemainingElements() && (targetPosIsJustBefore || targetPosIsJustAfter) ) { this.stopAutomaticLineCreation(); return; } this._peaks.addLine({ position: newLinePosition, indicatorType: this._peaks.options.defaultLineIndicatorType, indicatorText: this._peaks.options.defaultLineIndicatorText, indicatorSubText: this._peaks.options.defaultLineIndicatorSubText }, true); const automaticallyCreatedLineGroup = this._lineGroupsByPosition[newLinePosition]; if (!automaticallyCreatedLineGroup) { this._peaks.logger( 'peaks.lines.manageAutomaticLineCreation(): line group not found for position ' + newLinePosition ); return; } this._automaticallyCreatedLineId = automaticallyCreatedLineGroup.getId(); this._moveSourcesToPositionIfPossible(sources, newLinePosition); // Notify that sources have been moved to a new line (for ghost preview updates) this._peaks.emit('sources.delayedLineChange', sources); }.bind(this), this._peaks.options.automaticLineCreationDelay); }; LineGroups.prototype.manageVerticalPosition = function(sources, startTime, endTime, mouseX, mouseY) { if (Utils.isNullOrUndefined(mouseX)) { return; } const currentLineGroup = this._lineGroupsById[sources[0].lineId]; const position = currentLineGroup.getPosition(); const linePos = this.getLinesUnderY(mouseY); if (currentLineGroup.isLocked()) { return; } if (linePos[0] !== linePos[1]) { this.manageAutomaticLineCreation(linePos[0] + 1, position, sources, mouseY); } else { this.stopAutomaticLineCreation(); const targetLineGroup = this._lineGroupsByPosition[linePos[0]]; if ( !Utils.isNullOrUndefined(mouseX) && linePos[0] !== position && !targetLineGroup.isLocked() && !targetLineGroup.isSegmentsLine() ) { var mouseTime = this._view.pixelsToTime(mouseX + this._view.getFrameOffset()); var sourceDuration = Utils.roundTime(endTime - startTime); this._moveSourcesToPositionIfPossible(sources, linePos[0], mouseTime, sourceDuration); } } }; LineGroups.prototype._hasSpaceForSourceAt = function(sourcesAround, sourceDuration) { if (sourcesAround.overlapping) { return false; } else if (!sourcesAround.right) { return true; } else { var leftLimit = sourcesAround.left ? sourcesAround.left.endTime : 0; var rightLimit = sourcesAround.right.startTime; return sourceDuration <= Utils.roundTime(rightLimit - leftLimit); } }; LineGroups.prototype.getLineByPosition = function(pos) { return this._lineGroupsByPosition[pos]; }; LineGroups.prototype.getLastPosition = function() { var keys = Object.keys(this._lineGroupsByPosition); return keys.pop(); }; LineGroups.prototype.getLinesUnderY = function(y) { var height; var pos = [-1, this.getLastPosition() + 1]; for (var position in this._lineGroupsByPosition) { if (Utils.objectHasProperty(this._lineGroupsByPosition, position)) { height = this._lineGroupsByPosition[position].lineHeight(); if (y > this._lineGroupsByPosition[position].y()) { pos[0] = Math.max(pos[0], parseInt(position, 10)); } if (y < this._lineGroupsByPosition[position].y() + height) { pos[1] = Math.min(pos[1], parseInt(position, 10)); } } } return pos; }; LineGroups.prototype.getLinesAroundPos = function(pos) { var returnPos = [-1, this.getLastPosition() + 1]; for (var position in this._lineGroupsByPosition) { if (Utils.objectHasProperty(this._lineGroupsByPosition, position)) { var p = parseInt(position, 10); if (p < pos) { returnPos[0] = p; } else if (p > pos) { returnPos[1] = p; break; } } } return returnPos; }; LineGroups.prototype._moveSourcesToPositionIfPossible = function(sources, targetPosition, targetTime, sourcesDuration ) { const lineGroup = this._lineGroupsByPosition[targetPosition]; if (!lineGroup || lineGroup.isSegmentsLine()) { return false; } if (Utils.isNullOrUndefined(targetTime)) { targetTime = sources[0].startTime; } const sourcesAround = lineGroup.getSourcesAround(targetTime); if (!this._hasSpaceForSourceAt(sourcesAround, sourcesDuration)) { return false; } this._moveSourcesToLine(sources, sourcesAround, lineGroup.getId()); return true; }; // This code assumes that all sources belong to a single line // and are sorted by their start time LineGroups.prototype._moveSourcesToLine = function(sources, sourcesAround, targetLineId) { sources.forEach(function(source) { const sourceGroup = this.removeSource(source); source.lineId = targetLineId; this.addSource(source, sourceGroup, sourcesAround); sourcesAround.left = source; }.bind(this)); this.refreshLineYs(); }; LineGroups.prototype.refreshLineYs = function() { var y = this._peaks.options.padding - this._view.getFrameOffsetY(); var anyChange = false; // This code assumes that lines are ordered in ascending order of position // This is the case as the keys are integers for (var position in this._lineGroupsByPosition) { if (Utils.objectHasProperty(this._lineGroupsByPosition, position)) { if (this._lineGroupsByPosition[position].y() !== y) { this._lineGroupsByPosition[position].y(y); anyChange = true; } y += this._lineGroupsByPosition[position].lineHeight() + this._peaks.options.interline; } } if (anyChange) { this._view.updateTimeline(); } this._lineIndicator.refreshIndicators(); }; LineGroups.prototype._setLinePosition = function(lineId, position) { const lineGroup = this._lineGroupsById[lineId]; if (!lineGroup || position === lineGroup.getPosition()) { return; } const isPositionAvailable = Utils.isNullOrUndefined(this._lineGroupsByPosition[position]); if (isPositionAvailable) { this._lineGroupsByPosition[position] = lineGroup; lineGroup.updatePosition(position); return; } delete this._lineGroupsByPosition[lineGroup.getPosition()]; var currentPos; const newLinesByPosition = Object.assign({},this._lineGroupsByPosition); // This code assumes that lines are ordered in ascending order of position // This is the case as the keys are integers for (var pos in this._lineGroupsByPosition) { if (Utils.objectHasProperty(this._lineGroupsByPosition, pos)) { currentPos = parseInt(pos, 10); if (Utils.objectHasProperty(this._lineGroupsByPosition, position) && currentPos >= position) { var newPosition = currentPos + 1; this._lineGroupsByPosition[currentPos].updatePosition(newPosition); newLinesByPosition[newPosition] = this._lineGroupsByPosition[currentPos]; if (!this._lineGroupsByPosition[newPosition]) { break; } } } } lineGroup.updatePosition(position); newLinesByPosition[position] = lineGroup; this._lineGroupsByPosition = newLinesByPosition; }; LineGroups.prototype._createLineGroup = function(line) { const lineGroup = new LineGroup(this._peaks, this._view, line); this._lineGroupsById[line.id] = lineGroup; this._setLinePosition(line.id, line.position); this._lineIndicator.addIndicator(lineGroup); if (this._autoAddToLayer) { lineGroup.addToLayer(this._layer); } this._setInteractions(line.id); this.refreshLineYs(); return line.id; }; LineGroups.prototype._onIndicatorDrag = function(lineId, y) { const lineGroup = this._lineGroupsById[lineId]; if (!lineGroup) { return; } const positions = this.getLinesUnderY(y); if (positions[0] !== positions[1] || positions[0] === lineGroup.getPosition()) { return; } const targetPos = positions[0]; const targetLineGroup = this._lineGroupsByPosition[targetPos]; const targetCenterY = targetLineGroup.y() + (targetLineGroup.lineHeight() / 2); const movingDown = targetPos > lineGroup.getPosition(); if ((movingDown && y < targetCenterY) || (!movingDown && y > targetCenterY)) { return; } if (targetPos === lineGroup.getPosition() + 1) { this._setLinePosition(targetLineGroup.getId(), lineGroup.getPosition()); } else { this._setLinePosition(lineId, targetPos); } this.refreshLineYs(); this._peaks.emit('line.moved', lineGroup.getLine()); }; LineGroups.prototype.updateSegments = function(frameStartTime, frameEndTime) { for (var lineId in this._segmentsGroups) { if (Utils.objectHasProperty(this._segmentsGroups, lineId)) { this._segmentsGroups[lineId].updateSegments(frameStartTime, frameEndTime); } } }; LineGroups.prototype.manageCollision = function(sources, newStartTime, newEndTime) { return this._lineGroupsById[sources[0].lineId].manageCollision(sources, newStartTime, newEndTime); }; LineGroups.prototype.manageOrder = function(sources, startTime, endTime) { // Assuming all ordered elements have the same position return this._lineGroupsById[sources[0].lineId].manageOrder(sources, startTime, endTime); }; LineGroups.prototype._setInteractions = function(lineId) { const lineGroup = this._lineGroupsById[lineId]; if (this._areInteractionsOverridden) { lineGroup.allowInteractions(this._areInteractionsAllowed); } else { lineGroup.allowInteractions( lineGroup.isSegmentsLine() ? this._areSegmentInteractionsAllowed : this._areSourceInteractionsAllowed ); } }; LineGroups.prototype.overrideInteractions = function(bool, areInteractionsAllowed) { this._areInteractionsOverridden = typeof bool !== 'undefined' ? bool : this._areInteractionsOverridden; this._areInteractionsAllowed = typeof areInteractionsAllowed !== 'undefined' ? areInteractionsAllowed : this._areInteractionsAllowed; for (var id in this._lineGroupsById) { if (Utils.objectHasProperty(this._lineGroupsById, id)) { this._setInteractions(id); } } }; LineGroups.prototype.allowInteractions = function(forSources, forSegments) { this._areSourceInteractionsAllowed = !Utils.isNullOrUndefined(forSources) ? forSources : this._areSourceInteractionsAllowed; this._areSegmentInteractionsAllowed = !Utils.isNullOrUndefined(forSegments) ? forSegments : this._areSegmentInteractionsAllowed; for (var id in this._lineGroupsById) { if (Utils.objectHasProperty(this._lineGroupsById, id)) { this._setInteractions(id); } } }; return LineGroups; });