@checksub_team/peaks_timeline
Version:
JavaScript UI component for displaying audio waveforms
692 lines (543 loc) • 21.7 kB
JavaScript
/**
* @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;
});