@checksub_team/peaks_timeline
Version:
JavaScript UI component for displaying audio waveforms
702 lines (578 loc) • 19.1 kB
JavaScript
/**
* @file
*
* Defines the {@link line} class.
*
* @module line
*/
define([
'konva',
'./utils'
], function(Konva, Utils) {
'use strict';
function Line(peaks, view, y, id, position) {
this._peaks = peaks;
this._view = view;
this._id = id;
this._position = position;
this._firstSourceId = null;
this._sources = {};
this._sourcesGroup = {};
this._wrapped = false;
this._y = y;
this._group = new Konva.Group({
x: 0,
y: y - this._view.getFrameOffsetY(),
draggable: true,
dragBoundFunc: function() {
return {
x: this.absolutePosition().x,
y: this.absolutePosition().y
};
}
});
this._sourceHeights = {};
this._height = this._peaks.options.emptyLineHeight;
this._unwrappedCount = 0;
}
Line.prototype.getPosition = function() {
return this._position;
};
Line.prototype.getId = function() {
return this._id;
};
Line.prototype.isEmpty = function() {
return this.isSegmentsLine() ?
this._segmentsGroup.isEmpty() :
Object.keys(this._sources).length === 0;
};
Line.prototype.isSegmentsLine = function() {
return Boolean(this._segmentsGroup);
};
Line.prototype.updateSegments = function(frameStartTime, frameEndTime) {
if (this.isSegmentsLine()) {
this._segmentsGroup.updateSegments(frameStartTime, frameEndTime);
}
};
Line.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;
};
Line.prototype.lineHeight = function() {
return this._height;
};
Line.prototype.changeHeight = function(from, to) {
if (this._sourceHeights[from]) {
var oldHeight = this._height;
if (this._sourceHeights[to]) {
this._sourceHeights[to] += this._sourceHeights[from];
}
else {
this._sourceHeights[to] = this._sourceHeights[from];
}
if (to > this._height) {
this._height = to;
}
else if (from === this._height) {
this._height = 0;
for (var height in this._sourceHeights) {
if (Utils.objectHasProperty(this._sourceHeights, height)) {
var parsedHeight = parseInt(height, 10);
if (parsedHeight !== from) {
if (parsedHeight > this._height) {
this._height = parsedHeight;
}
}
}
}
}
if (this._height !== oldHeight) {
this._peaks.emit('line.heightChanged', this._position);
}
delete this._sourceHeights[from];
}
};
Line.prototype.updateLineHeight = function(sourceGroup, action) {
var oldHeight = this._height;
var sourceGroupHeight;
switch (action) {
case 'add':
sourceGroupHeight = sourceGroup.getCurrentHeight();
if (this._sourceHeights[sourceGroupHeight]) {
this._sourceHeights[sourceGroupHeight]++;
}
else {
this._sourceHeights[sourceGroupHeight] = 1;
if (sourceGroupHeight > this._height) {
this._height = sourceGroupHeight;
}
}
break;
case 'remove':
if (Object.keys(this._sources).length === 0) {
this._height = this._peaks.options.emptyLineHeight;
this._sourceHeights = {};
}
else {
sourceGroupHeight = sourceGroup.getCurrentHeight();
this._sourceHeights[sourceGroupHeight]--;
if (this._sourceHeights[sourceGroupHeight] === 0
&& sourceGroupHeight === this._height) {
delete this._sourceHeights[sourceGroupHeight];
this._height = 0;
for (var height in this._sourceHeights) {
if (Utils.objectHasProperty(this._sourceHeights, height)) {
var parsedHeight = parseInt(height, 10);
if (parsedHeight > this._height) {
this._height = parsedHeight;
}
}
}
}
}
break;
}
if (this._height !== oldHeight) {
this._peaks.emit('line.heightChanged', this._position);
}
};
Line.prototype.isVisible = function() {
return this._y < this._view.getFrameOffsetY() + this._view.getHeight()
&& this._view.getFrameOffsetY() < this._y + this._height;
};
Line.prototype.addToLayer = function(layer) {
layer.add(this._group);
};
Line.prototype.addSourceGroup = function(sourceGroup) {
var source = sourceGroup.getSource();
this._sourcesGroup[source.id] = sourceGroup;
if (!this._sources[source.id]) {
var newSource = {
source: source,
prevSourceId: null,
nextSourceId: null
};
if (this._firstSourceId) {
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;
var newTimes = this._canBePlacedBetween(
source.startTime,
source.endTime,
startLimit,
currentSource.source.startTime
);
if (newTimes) {
source.updateTimes(newTimes.startTime, newTimes.endTime);
if (currentSource.prevSourceId) {
this._sources[currentSource.prevSourceId].nextSourceId = source.id;
newSource.prevSourceId = currentSource.prevSourceId;
}
else {
this._firstSourceId = source.id;
}
currentSource.prevSourceId = source.id;
newSource.nextSourceId = currentSource.source.id;
this._sources[source.id] = newSource;
break;
}
}
} while (currentSource.nextSourceId);
if (!newSource.prevSourceId && !newSource.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;
newSource.prevSourceId = currentSource.source.id;
this._sources[source.id] = newSource;
}
}
else {
this._firstSourceId = source.id;
this._sources[source.id] = newSource;
}
this.updateLineHeight(sourceGroup, 'add');
}
if (!sourceGroup.getParent() || !sourceGroup.isDescendantOf(this._group)) {
sourceGroup.addToGroup(this._group);
}
};
Line.prototype.addSegments = function(segmentsGroup) {
this._segmentsGroup = segmentsGroup;
this._height = this._segmentsGroup.getCurrentHeight();
segmentsGroup.addToGroup(this._group);
};
Line.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._position);
}
}
};
Line.prototype._canBePlacedBetween = function(startTime, endTime, startLimit, endLimit) {
var timeWidth = endTime - startTime;
var newTimes = null;
if ((!endLimit && startTime > startLimit) || (startTime > startLimit && endTime < endLimit)) {
// Can be placed at its wanted position with wanted start/end time
newTimes = {
startTime: startTime,
endTime: endTime
};
}
else if (endLimit - startLimit >= timeWidth) {
// Can be placed at its wanted position but not with its wanted start/end time
if (startTime > startLimit) {
newTimes = {
startTime: endLimit - timeWidth,
endTime: endLimit
};
}
else {
newTimes = {
startTime: startLimit,
endTime: startLimit + timeWidth
};
}
}
return newTimes;
};
Line.prototype.removeSourceGroup = function(source, isPermanent) {
var sourceGroup = this._sourcesGroup[source.id];
delete this._sourcesGroup[source.id];
if (isPermanent) {
if (this._sources[source.id].prevSourceId) {
this._sources[this._sources[source.id].prevSourceId].nextSourceId
= this._sources[source.id].nextSourceId;
}
if (this._sources[source.id].nextSourceId) {
this._sources[this._sources[source.id].nextSourceId].prevSourceId
= this._sources[source.id].prevSourceId;
}
if (this._firstSourceId === source.id) {
this._firstSourceId = this._sources[source.id].nextSourceId;
}
delete this._sources[source.id];
this.updateLineHeight(sourceGroup, 'remove');
}
return sourceGroup;
};
Line.prototype.getKonvaGroup = function() {
return this._group;
};
Line.prototype.getY = function() {
return this._group.y();
};
Line.prototype.getInitialY = function() {
return this._y;
};
Line.prototype.y = function(value, changeInitialY) {
this._group.y(value);
if (changeInitialY) {
this._y = value;
}
};
Line.prototype.moveOnY = function(dy) {
this._y += dy;
this._group.y(this._group.y() + dy);
};
Line.prototype.manageSourceOrder = function(source, newStartX, newEndX) {
var cursorX = this._view.getPointerPosition().x;
var tmpXs;
var newXs = {
startX: newStartX,
endX: newEndX
};
var sourceWidth = this._view.timeToPixels(source.endTime - source.startTime);
if (newStartX !== null && newEndX !== null) {
if (this._sources[source.id].prevSourceId) {
// there is another source to the left
var previousStartX = this._view.timeToPixels(
this._sources[this._sources[source.id].prevSourceId].source.startTime
);
if (cursorX + this._view.getFrameOffset() < previousStartX) {
// we want to change order
tmpXs = this._changeSourcePosition(
source,
sourceWidth,
cursorX + this._view.getFrameOffset()
);
if (tmpXs) {
newXs.startX = tmpXs.startTime;
newXs.endX = tmpXs.endTime;
}
}
}
if (this._sources[source.id].nextSourceId) {
// there is another source to the right
var nextEndX = this._view.timeToPixels(
this._sources[this._sources[source.id].nextSourceId].source.endTime
);
if (cursorX + this._view.getFrameOffset() > nextEndX) {
// we want to change order
tmpXs = this._changeSourcePosition(
source,
sourceWidth,
cursorX + this._view.getFrameOffset()
);
if (tmpXs) {
newXs.startX = tmpXs.startTime;
newXs.endX = tmpXs.endTime;
}
}
}
}
return newXs;
};
Line.prototype._changeSourcePosition = function(source, sourceWidth, x) {
if (source.orderable && this._firstSourceId) {
var currentRange = {
start: null,
end: null
};
var startLimit = null;
var endLimit = null;
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 = this._view.timeToPixels(
currentRange.start.source.endTime
);
}
else {
startLimit = 0;
}
if (currentRange.end) {
endLimit = this._view.timeToPixels(
currentRange.end.source.startTime
);
}
else {
endLimit = null;
}
if (x > startLimit && (endLimit === null || x < endLimit)) {
var newTimes = this._canBePlacedBetween(
x,
x + sourceWidth,
startLimit,
endLimit
);
if (newTimes) {
source.updateTimes(
this._view.pixelsToTime(newTimes.startTime),
this._view.pixelsToTime(newTimes.endTime)
);
var prevSourceId = currentRange.start
? currentRange.start.source.id
: null;
this._moveSource(this._sources[source.id].source, prevSourceId);
return newTimes;
}
}
} while (currentRange.end);
}
return null;
};
Line.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;
};
Line.prototype.manageCollision = function(source, newStartX, newEndX) {
var originalStartTime = null;
var originalEndTime = null;
var newStartTime = null;
var newEndTime = null;
var startLimited = false;
var endLimited = false;
var newXs = {
startX: newStartX,
endX: newEndX,
updateTimelineLength: false
};
if (newStartX !== null) {
// startMarker changed
originalStartTime = this._view.pixelsToTime(newStartX);
newStartTime = originalStartTime;
if (source.startTime > newStartTime) {
// startMarker moved to the left
if (this._sources[source.id].prevSourceId) {
// there is another source to the left
var previousSource = this._sources[this._sources[source.id].prevSourceId]
.source;
if (newStartTime < previousSource.endTime) {
// there is collision
newStartTime = previousSource.endTime;
startLimited = true;
}
}
else {
if (newStartTime < 0) {
newStartTime = 0;
startLimited = true;
}
}
}
}
if (newEndX !== null) {
// endMarker changed
originalEndTime = this._view.pixelsToTime(newEndX);
newEndTime = originalEndTime;
if (source.endTime < newEndTime) {
// endMarker moved to the right
if (this._sources[source.id].nextSourceId) {
// there is another source to the right
var nextSource = this._sources[this._sources[source.id].nextSourceId]
.source;
if (newEndTime > nextSource.startTime) {
// there is collision
newEndTime = nextSource.startTime;
endLimited = true;
}
}
}
}
// Update the other edge if dragging and collision
if (newStartTime !== null && newEndTime !== null) {
var timeWidth = Utils.roundTime(originalEndTime - originalStartTime);
if (startLimited) {
newEndTime = newStartTime + timeWidth;
}
if (endLimited) {
newStartTime = newEndTime - timeWidth;
}
}
// Check for minimal size of source
if (newStartTime !== null && newEndTime === null) {
if (source.endTime - newStartTime < source.minSize) {
newStartTime = source.endTime - source.minSize;
}
}
else if (newEndTime !== null && newStartTime === null) {
if (newEndTime - source.startTime < source.minSize) {
newEndTime = source.startTime + source.minSize;
}
}
else {
if (newEndTime - newStartTime < source.minSize) {
if (source.endTime !== newEndTime) {
newEndTime = newStartTime + source.minSize;
}
if (source.startTime !== newStartTime) {
newStartTime = newEndTime - source.minSize;
}
}
}
if (newStartTime !== null && newStartTime !== originalStartTime) {
newXs.startX = this._view.timeToPixels(newStartTime);
}
if (newEndTime !== null && newEndTime !== originalEndTime) {
newXs.endX = this._view.timeToPixels(newEndTime);
if (this._sources[source.id].nextSourceId === null) {
newXs.updateTimelineLength = true;
}
}
return newXs;
};
// Line.prototype.rescale = function() {
// for (var sourceId in this._sourcesGroup) {
// if (Utils.objectHasProperty(this._sourcesGroup, sourceId)) {
// this._sourcesGroup[sourceId].rescale();
// }
// }
// };
Line.prototype.getSourcesAfter = function(time) {
const sources = [];
var currentId = this._firstSourceId;
while (currentId) {
var lineSource = this._sources[currentId];
if (lineSource.source.startTime >= time) {
while (currentId) {
lineSource = this._sources[currentId];
sources.push(lineSource.source);
currentId = lineSource.nextSourceId;
}
break;
}
currentId = lineSource.nextSourceId;
}
return sources;
};
Line.prototype.updatePosition = function(pos) {
for (var sourceId in this._sources) {
if (Utils.objectHasProperty(this._sources, sourceId)) {
this._sources[sourceId].source.position = pos;
}
}
this._position = pos;
};
Line.prototype.destroy = function() {
this._firstSourceId = null;
this._sources = {};
this._sourcesGroup = {};
this._group.destroy();
};
Line.prototype.allowInteractions = function(bool) {
this._group.listening(bool);
};
return Line;
});