UNPKG

@checksub_team/peaks_timeline

Version:

JavaScript UI component for displaying audio waveforms

719 lines (572 loc) 20.7 kB
/** * @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; });