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