@checksub_team/peaks_timeline
Version:
JavaScript UI component for displaying audio waveforms
719 lines (572 loc) • 20.7 kB
JavaScript
/**
* @file
*
* Defines the {@link SourcesLayer} class.
*
* @module sources-layer
*/
define([
'./source-group',
'./lines',
'./data-retriever',
'./utils',
'./invoker',
'konva'
], function(
SourceGroup,
Lines,
DataRetriever,
Utils,
Invoker,
Konva) {
'use strict';
/**
* Creates a Konva.Layer that displays sources on the timeline.
*
* @class
* @alias SourcesLayer
*
* @param {Peaks} peaks
* @param {WaveformOverview|WaveformZoomView} view
* @param {Boolean} allowEditing
*/
function SourcesLayer(peaks, view, allowEditing) {
this._peaks = peaks;
this._view = view;
this._allowEditing = allowEditing;
this._sourcesGroup = {};
this._layer = new Konva.Layer();
this._dataRetriever = new DataRetriever(peaks);
this._lines = new Lines(peaks, view, this);
this._lines.addToLayer(this);
this._loadedData = {};
this._debouncedRescale = new Invoker().debounce(
this._rescale, 150
);
this._peaks.on('sources.add', this._onSourcesAdd.bind(this));
this._peaks.on('sources.destroy', this._onSourcesDestroy.bind(this));
this._peaks.on('sources.show', this._onSourcesShow.bind(this));
this._peaks.on('sources.hide', this._onSourcesHide.bind(this));
this._peaks.on('sources.setSelected', this._onSourcesSetSelected.bind(this));
this._peaks.on('source.update', this._onSourceUpdate.bind(this));
this._peaks.on('data.retrieved', this._onDataRetrieved.bind(this));
this._peaks.on('sources.refresh', this._onSourcesRefresh.bind(this));
this._peaks.on('segments.show', this._onSegmentsShow.bind(this));
this._peaks.on('options.set.line_height', this._onOptionsLineHeightChange.bind(this));
this._peaks.on('source.setIndicators', this.setIndicators.bind(this));
}
SourcesLayer.prototype.fitToView = function() {
this._lines.fitToView();
};
SourcesLayer.prototype.getLoadedData = function(id) {
return this._loadedData[id];
};
SourcesLayer.prototype.setLoadedData = function(id, data) {
this._loadedData[id] = data;
};
SourcesLayer.prototype.getSegmentsGroups = function() {
return this._lines.getSegmentsGroups();
};
SourcesLayer.prototype.add = function(element) {
this._layer.add(element);
};
/**
* Adds the layer to the given {Konva.Stage}.
*
* @param {Konva.Stage} stage
*/
SourcesLayer.prototype.addToStage = function(stage) {
stage.add(this._layer);
};
SourcesLayer.prototype.enableEditing = function(enable) {
this._allowEditing = enable;
};
SourcesLayer.prototype.isEditingEnabled = function() {
return this._allowEditing;
};
SourcesLayer.prototype._onOptionsLineHeightChange = function(oldHeight) {
var positions = [];
for (var sourceId in this._sourcesGroup) {
if (Utils.objectHasProperty(this._sourcesGroup, sourceId)) {
var source = this._sourcesGroup[sourceId].getSource();
if (!positions.includes(source.position)) {
this._lines.changeLineHeight(
oldHeight,
this._peaks.options.lineHeight
);
positions.push(source.position);
}
this._removeSource(source);
this._addSourceGroup(source);
}
}
if (positions) {
var frameOffset = this._view.getFrameOffset();
var width = this._view.getWidth();
this.updateSources(
this._view.pixelsToTime(frameOffset),
this._view.pixelsToTime(frameOffset + width)
);
}
};
SourcesLayer.prototype._onSourceUpdate = function(source) {
var redraw = false;
var sourceGroup = this._sourcesGroup[source.id];
var frameOffset = this._view.getFrameOffset();
var width = this._view.getWidth();
var frameStartTime = this._view.pixelsToTime(frameOffset);
var frameEndTime = this._view.pixelsToTime(frameOffset + width);
if (sourceGroup) {
this._removeSource(source);
redraw = true;
}
if (source.isVisible(frameStartTime, frameEndTime)) {
this._addSourceGroup(source);
redraw = true;
}
if (redraw) {
this.updateSources(frameStartTime, frameEndTime);
}
};
SourcesLayer.prototype._onSourcesSetSelected = function(sources) {
sources.forEach(function(source) {
const sourceGroup = this._sourcesGroup[source.id];
if (sourceGroup) {
sourceGroup.setSelected();
}
}.bind(this));
this.draw();
};
SourcesLayer.prototype._onSourcesShow = function(sources) {
var self = this;
sources.forEach(function(source) {
self._sourcesGroup[source.id].setWrapping(false, true);
});
this._layer.draw();
};
SourcesLayer.prototype._onSourcesAdd = function(sources) {
var self = this;
var frameOffset = self._view.getFrameOffset();
var width = self._view.getWidth();
var frameStartTime = self._view.pixelsToTime(frameOffset);
var frameEndTime = self._view.pixelsToTime(frameOffset + width);
sources.forEach(function(source) {
self._addSourceGroup(source, source.isVisible(frameStartTime, frameEndTime));
});
this._view.updateTimelineLength();
this.updateSources(frameStartTime, frameEndTime);
};
SourcesLayer.prototype._onSourcesDestroy = function(sources, shouldBlockEvent) {
var self = this;
sources.forEach(function(source) {
self._removeSource(source, true, shouldBlockEvent);
});
this._view.updateTimelineLength();
this._layer.draw();
};
SourcesLayer.prototype._onSourcesShow = function(sources) {
var self = this;
sources.forEach(function(source) {
self._sourcesGroup[source.id].setWrapping(false, true);
});
this._layer.draw();
};
SourcesLayer.prototype._onSourcesHide = function(sources) {
var self = this;
sources.forEach(function(source) {
self._sourcesGroup[source.id].setWrapping(true, true);
});
this._layer.draw();
};
SourcesLayer.prototype._onDataRetrieved = function(data, source, url) {
if (data) {
var type = data.type.split('/')[0];
var sourceGroup = this._sourcesGroup[source.id];
if (sourceGroup) {
switch (type) {
case 'image':
sourceGroup.addImagePreview(data.content, url, true);
break;
case 'video':
sourceGroup.addVideoPreview(data.content, url, true);
break;
case 'binary':
case 'text':
case 'application':
case 'other':
case 'audio':
sourceGroup.addAudioPreview(type, data.content, url, true);
break;
default:
// Type not handled
}
}
}
};
SourcesLayer.prototype._onSourcesRefresh = function() {
this._layer.draw();
};
SourcesLayer.prototype._onSegmentsShow = function(lineId, position) {
this._lines.addSegments(lineId, position);
this._view.updateTimelineLength();
this._layer.draw();
};
/**
* Creates the Konva UI objects for a given source.
*
* @private
* @param {Source} source
* @returns {Kanva.Group}
*/
SourcesLayer.prototype._createSourceGroup = function(source) {
return new SourceGroup(source, this._peaks, this, this._view);
};
/**
* Adds a Konva UI object to the layer for a given source.
*
* @private
* @param {Source} source
* @returns {Konva.Group}
*/
SourcesLayer.prototype._addSourceGroup = function(source, startDataRetrieval = true) {
var sourceGroup = this._createSourceGroup(source);
this._sourcesGroup[source.id] = sourceGroup;
this._lines.addSourceGroup(sourceGroup, source.position);
// After creating and referencing the new group, we can start data retrieval
if (startDataRetrieval) {
this._dataRetriever.retrieveData(source);
}
return sourceGroup;
};
SourcesLayer.prototype.setIndicators = function(source) {
var sourceGroup = this._sourcesGroup[source.id];
if (sourceGroup) {
sourceGroup.createIndicators();
this._layer.draw();
}
};
/**
* Updates the positions of all displayed sources in the view.
*
* @param {Number} startTime The start of the visible range in the view,
* in seconds.
* @param {Number} endTime The end of the visible range in the view,
* in seconds.
*/
SourcesLayer.prototype.updateSources = function(startTime, endTime) {
// Update segments
this._lines.updateSegments(startTime, endTime);
// Update sources in visible time range.
var sources = this.findSources(startTime, endTime);
// Should implement virtualization on Y
var count = sources.length;
sources.forEach(this._updateSource.bind(this));
this._lines.setOffsetY(this._view.getFrameOffsetY());
count += this._removeInvisibleSources(startTime, endTime);
if (count > 0) {
this._layer.draw();
}
};
SourcesLayer.prototype.onSourcesGroupDragStart = function(element) {
this._initialFrameOffset = this._view.getFrameOffset();
this._mouseDownX = this._view.getPointerPosition().x;
this._selectedElements = {};
const draggedElementId = element.currentTarget.attrs.sourceId;
const shouldDragSelectedElements = Object.keys(this._view.getSelectedElements()).includes(
draggedElementId
);
this._nonSelectedElement = shouldDragSelectedElements ?
null :
[this._sourcesGroup[draggedElementId].getSource()];
};
SourcesLayer.prototype.onSourcesGroupDragEnd = function() {
const updatedSources = (this._nonSelectedElement || Object.values(this._view.getSelectedElements())).map(
function(source) {
const sourceGroup = this._sourcesGroup[source.id];
if (sourceGroup) {
sourceGroup.prepareDragEnd();
}
return source;
}.bind(this)
);
this._view.drawSourcesLayer();
this._view.updateTimelineLength();
this._selectedElements = {};
this._peaks.emit('sources.updated', updatedSources);
};
SourcesLayer.prototype.onSourcesGroupDrag = function(draggedElement) {
this._view.updateWithAutoScroll(this._updateSourcesGroup.bind(this));
return {
x: draggedElement.absolutePosition().x,
y: draggedElement.absolutePosition().y
};
};
SourcesLayer.prototype._updateSourcesGroup = function() {
var mousePos = Math.min(
this._view.getWidth() - this._peaks.options.autoScrollThreshold * this._view.getWidth(),
Math.max(
0,
this._view.getPointerPosition().x
)
);
const diff = mousePos - this._mouseDownX;
const currentFrameOffset = this._view.getFrameOffset();
const mousePosY = this._view.getPointerPosition().y;
var newEnd = 0;
var shouldRedraw = false;
(this._nonSelectedElement || Object.values(this._view.getSelectedElements())).forEach(function(source) {
if (!this._selectedElements[source.id]) {
this._selectedElements[source.id] = {
startX: this._view.timeToPixels(source.startTime),
endX: this._view.timeToPixels(source.endTime)
};
}
const { startX, endX } = this._selectedElements[source.id];
newEnd = Math.max(newEnd, source.endTime);
shouldRedraw = this.updateSource(
source,
startX + diff + (currentFrameOffset - this._initialFrameOffset),
endX + diff + (currentFrameOffset - this._initialFrameOffset),
mousePosY
) || shouldRedraw;
}.bind(this));
if (shouldRedraw) {
this.draw();
}
this._view.setTimelineLength(
this._view.timeToPixels(newEnd) + this._view.getWidth()
);
};
SourcesLayer.prototype.findSources = function(startTime, endTime) {
var sources = this._peaks.sources.find(startTime, endTime);
var positions = this._lines.getVisibleLines();
return sources.filter(
function(source) {
return positions[source.position];
}
);
};
SourcesLayer.prototype.updateSource = function(source, newStartX, newEndX, newY) {
var newXs = {
startX: newStartX,
endX: newEndX
};
if (this._peaks.options.canMoveSourcesBetweenLines) {
this.manageVerticalPosition(source, newY);
}
newXs = this.manageSourceOrder(source, newStartX, newEndX);
newXs = this.manageCollision(source, newXs.startX, newXs.endX);
source.updateTimes(
newXs.startX !== null ? this._view.pixelsToTime(newXs.startX) : null,
newXs.endX !== null ? this._view.pixelsToTime(newXs.endX) : null
);
if (newXs) {
this._updateSource(
source
);
return true;
}
return false;
};
SourcesLayer.prototype.manageVerticalPosition = function(source, newY) {
return this._lines.manageVerticalPosition(source, newY);
};
SourcesLayer.prototype.manageSourceOrder = function(source, newStartX, newEndX) {
return this._lines.manageSourceOrder(source, newStartX, newEndX);
};
SourcesLayer.prototype.manageCollision = function(source, newStartX, newEndX) {
return this._lines.manageCollision(source, newStartX, newEndX);
};
/**
* @private
*/
SourcesLayer.prototype._updateSource = function(source) {
var sourceGroup = this._findOrAddSourceGroup(source);
sourceGroup.update();
};
SourcesLayer.prototype._findOrAddSourceGroup = function(source) {
var sourceGroup = this._sourcesGroup[source.id];
if (!sourceGroup) {
sourceGroup = this._addSourceGroup(source);
}
return sourceGroup;
};
/**
* Removes any sources that are not visible, i.e., are not within and do not
* overlap the given time range.
*
* @private
* @param {Number} startTime The start of the visible time range, in seconds.
* @param {Number} endTime The end of the visible time range, in seconds.
* @returns {Number} The number of sources removed.
*/
SourcesLayer.prototype._removeInvisibleSources = function(startTime, endTime) {
var count = 0;
for (var sourceId in this._sourcesGroup) {
if (Utils.objectHasProperty(this._sourcesGroup, sourceId)) {
var source = this._sourcesGroup[sourceId].getSource();
if (!this._isSourceVisible(source, startTime, endTime)) {
this._removeSource(source);
count++;
}
}
}
return count;
};
SourcesLayer.prototype._isSourceVisible = function(source, startTime, endTime) {
return source.isVisible(startTime, endTime) && this._lines.isLineVisible(source.position);
};
/**
* Get all visible sources.
*
* @private
* @returns {Array<Source>} The visible sources.
*/
SourcesLayer.prototype._getVisibleSources = function() {
var frameOffset = this._view.getFrameOffset();
var width = this._view.getWidth();
var frameStartTime = this._view.pixelsToTime(frameOffset);
var frameEndTime = this._view.pixelsToTime(frameOffset + width);
var visibleSources = [];
for (var sourceId in this._sourcesGroup) {
if (Utils.objectHasProperty(this._sourcesGroup, sourceId)) {
var source = this._sourcesGroup[sourceId].data;
if (source.isVisible(frameStartTime, frameEndTime)) {
visibleSources.push(source);
}
}
}
return visibleSources;
};
SourcesLayer.prototype._getOverlappedSources = function() {
var self = this;
var sources = this._getVisibleSources();
return sources.reduce(function(result, source) {
sources.forEach(function(source2) {
if (self._sourcesGroupOverlapped(source, source2)) {
if (!result.includes(source2.id)) {
result.push(source2);
}
}
});
return result;
}, []);
};
/**
* Removes the given source from the view.
*
* @param {Source} source
*/
SourcesLayer.prototype._removeSource = function(source, isPermanent, shouldBlockEvent) {
var sourceGroup = this._sourcesGroup[source.id];
if (sourceGroup) {
delete this._sourcesGroup[source.id];
this._lines.removeSourceGroup(source, isPermanent);
sourceGroup.destroy();
}
if (isPermanent && !shouldBlockEvent) {
this._peaks.emit('source.destroyed', source);
}
};
/**
* Toggles visibility of the sources layer.
*
* @param {Boolean} visible
*/
SourcesLayer.prototype.setVisible = function(visible) {
this._layer.setVisible(visible);
};
SourcesLayer.prototype.draw = function() {
this._layer.draw();
};
SourcesLayer.prototype.listening = function(bool) {
this._layer.listening(bool);
};
SourcesLayer.prototype.stopDrag = function() {
this._layer.stopDrag();
};
SourcesLayer.prototype.getSourceGroupById = function(sourceId) {
return this._sourcesGroup[sourceId];
};
SourcesLayer.prototype.getSourcesOnLineAfter = function(lineId, time) {
return this._lines.getSourcesOnLineAfter(lineId, time);
};
SourcesLayer.prototype._sourcesOverlapped = function(source1, source2) {
var endsLater = (source1.startTime < source2.startTime)
&& (source1.endTime > source2.startTime);
var startsEarlier = (source1.startTime > source2.startTime)
&& (source1.startTime < source2.endTime);
return endsLater || startsEarlier;
};
SourcesLayer.prototype.rescale = function(debounce) {
// this._lines.rescale();
if (debounce) {
this._debouncedRescale();
}
else {
this._rescale();
}
};
SourcesLayer.prototype._rescale = function() {
var id, audioPreviews, urls = [], self = this;
for (id in this._sourcesGroup) {
if (Utils.objectHasProperty(this._sourcesGroup, id)) {
audioPreviews = this._sourcesGroup[id].getAudioPreview();
audioPreviews.forEach(function(audioPreview) {
if (self._shouldResampleAudio(audioPreview.url, urls)) {
self._loadedData[audioPreview.url + '-scaled'] = {
data: self._resampleAudio(audioPreview.url),
scale: self._view.getTimeToPixelsScale()
};
urls.push(audioPreview.url);
}
});
}
}
this._layer.draw();
};
SourcesLayer.prototype._shouldResampleAudio = function(audioUrl, urls) {
return this._loadedData[audioUrl + '-scaled']
&& !urls.includes(audioUrl)
&& this._loadedData[audioUrl + '-scaled'].scale !== this._view.getTimeToPixelsScale();
};
SourcesLayer.prototype._resampleAudio = function(audioUrl) {
return this._loadedData[audioUrl].resample({
scale: this._loadedData[audioUrl].sample_rate
/ this._view.getTimeToPixelsScale()
});
};
SourcesLayer.prototype.destroy = function() {
this._peaks.off('sources.add', this._onSourcesAdd);
this._peaks.off('sources.destroy', this._onSourcesDestroy);
this._peaks.off('sources.show', this._onSourcesShow);
this._peaks.off('sources.hide', this._onSourcesHide);
this._peaks.off('source.update', this._onSourceUpdate);
this._peaks.off('data.retrieved', this._onDataRetrieved);
this._peaks.off('sources.refresh', this._onSourcesRefresh);
this._peaks.off('segments.show', this._onSegmentsShow);
this._peaks.off('options.set.line_height', this._onOptionsLineHeightChange);
this._peaks.off('source.setIndicators', this.setIndicators);
};
SourcesLayer.prototype.getHeight = function() {
return this._layer.getHeight();
};
SourcesLayer.prototype.getFullHeight = function() {
return this._lines.height();
};
SourcesLayer.prototype.getLength = function() {
return this._lines.linesLength();
};
SourcesLayer.prototype.getLineByPosition = function(pos) {
return this._lines.getLineByPosition(pos);
};
/**
* Object for storing data and UI of a source.
*
* @typedef {Object} CompleteSource
* @global
* @property {Source} data
* @property {Konva.Group} ui
*/
return SourcesLayer;
});