@checksub_team/peaks_timeline
Version:
JavaScript UI component for displaying audio waveforms
424 lines (351 loc) • 11 kB
JavaScript
/**
* @file
*
* Defines the {@link TimelineSegments} class.
*
* @module timeline-segments
*/
define([
'colors.css',
'./segment',
'./utils'
], function(Colors, Segment, Utils) {
'use strict';
/**
* Segment parameters.
*
* @typedef {Object} SegmentOptions
* @global
* @property {Number} startTime Segment start time, in seconds.
* @property {Number} endTime Segment end time, in seconds.
* @property {Boolean=} editable If <code>true</code> the segment start and
* end times can be adjusted via the user interface.
* Default: <code>false</code>.
* @property {String=} color Segment waveform color.
* Default: a random color.
* @property {String=} labelText Segment label text.
* Default: an empty string.
* @property {String=} id A unique segment identifier.
* Default: an automatically generated identifier.
*/
/**
* Handles all functionality related to the adding, removing and manipulation
* of segments.
*
* @class
* @alias TimelineSegments
*
* @param {Peaks} peaks The parent Peaks object.
*/
function TimelineSegments(peaks) {
this._peaks = peaks;
this._segments = [];
this._segmentsById = {};
this._segmentIdCounter = 0;
this._colorIndex = 0;
}
/**
* Returns a new unique segment id value.
*
* @private
* @returns {String}
*/
TimelineSegments.prototype._getNextSegmentId = function() {
return 'peaks.segment.' + this._segmentIdCounter++;
};
var colors = [
Colors.navy,
Colors.blue,
Colors.aqua,
Colors.teal,
// Colors.olive,
// Colors.lime,
// Colors.green,
Colors.yellow,
Colors.orange,
Colors.red,
Colors.maroon,
Colors.fuchsia,
Colors.purple
// Colors.black,
// Colors.gray,
// Colors.silver
];
/**
* @private
* @returns {String}
*/
TimelineSegments.prototype._getSegmentColor = function() {
if (this._peaks.options.randomizeSegmentColor) {
if (++this._colorIndex === colors.length) {
this._colorIndex = 0;
}
return colors[this._colorIndex];
}
else {
return this._peaks.options.segmentColor;
}
};
/**
* Adds a new segment object.
*
* @private
* @param {Segment} segment
*/
TimelineSegments.prototype._addSegment = function(segment) {
this._segments.push(segment);
this._segmentsById[segment.id] = segment;
};
/**
* Creates a new segment object.
*
* @private
* @param {SegmentOptions} options
* @return {Segment}
*/
TimelineSegments.prototype._createSegment = function(options) {
// Watch for anyone still trying to use the old
// createSegment(startTime, endTime, ...) API
if (!Utils.isObject(options)) {
throw new TypeError('peaks.segments.add(): expected a Segment object parameter');
}
var segment = new Segment(
this._peaks,
Utils.isNullOrUndefined(options.id) ? this._getNextSegmentId() : options.id,
options.startTime,
options.endTime,
options.duration,
options.labelText,
options.color || this._getSegmentColor(),
options.textColor || '#000000',
options.handleTextColor || '#000000',
options.hoverColor,
options.warningColor,
options.opacity || 1,
options.borderColor,
options.borderWidth,
options.borderRadius,
options.editable,
options.allowDeletion || false,
options.line,
options.indicators
);
return segment;
};
/**
* Returns all segments.
*
* @returns {Array<Segment>}
*/
TimelineSegments.prototype.getSegments = function() {
return this._segments;
};
/**
* Add segments to the given line so they can be displayed.
*/
TimelineSegments.prototype.addSegmentsToPosition = function(lineId, position) {
this._peaks.emit('segments.show', lineId, position);
};
/**
* Returns the segment with the given id, or <code>null</code> if not found.
*
* @param {String} id
* @returns {Segment|null}
*/
TimelineSegments.prototype.getSegment = function(id) {
return this._segmentsById[id] || null;
};
/**
* Returns all segments that overlap a given point in time.
*
* @param {Number} time
* @returns {Array<Segment>}
*/
TimelineSegments.prototype.getSegmentsAtTime = function(time) {
return this._segments.filter(function(segment) {
return time >= segment.startTime && time < segment.endTime;
});
};
TimelineSegments.prototype.setMagnetizing = function(bool) {
this._peaks.emit('segments.setMagnetizing', bool);
};
/**
* Returns all segments that overlap a given time region.
*
* @param {Number} startTime The start of the time region, in seconds.
* @param {Number} endTime The end of the time region, in seconds.
*
* @returns {Array<Segment>}
*/
TimelineSegments.prototype.find = function(startTime, endTime) {
return this._segments.filter(function(segment) {
return segment.isVisible(startTime, endTime);
});
};
/**
* Adds one or more segments to the timeline.
*
* @param {SegmentOptions|Array<SegmentOptions>} segmentOrSegments
*/
TimelineSegments.prototype.add = function(/* segmentOrSegments */) {
var self = this;
var segments = Array.isArray(arguments[0]) ?
arguments[0] :
Array.prototype.slice.call(arguments);
segments = segments.map(function(segmentOptions) {
var segment = self._createSegment(segmentOptions);
if (Utils.objectHasProperty(self._segmentsById, segment.id)) {
var seg = Object.values(self._segmentsById)[0];
throw new Error('peaks.segments.add(): duplicate id. \n\n Segment I = ' +
JSON.stringify(
{
id: seg.id,
startTime: seg.startTime,
endTime: seg.endTime
}
) + ' Segment II = ' +
JSON.stringify(
{
id: segment.id,
startTime: segment.startTime,
endTime: segment.endTime
}
)
);
}
return segment;
});
segments.forEach(function(segment) {
self._addSegment(segment);
});
this.refreshRelativeIds();
this._peaks.emit('segments.add', segments);
};
/**
* Set relative ids for all segments.
*
* @private
*/
TimelineSegments.prototype.refreshRelativeIds = function() {
var idsByPosition = {};
this._segments.sort(function(a, b) {
return a.startTime - b.startTime;
}).forEach(function(segment) {
segment.relativeId = (idsByPosition[segment.line] || 0) + 1;
idsByPosition[segment.line] = segment.relativeId;
});
this._peaks.emit('segments.relative_ids_refreshed');
};
/**
* Returns the indexes of segments that match the given predicate.
*
* @private
* @param {Function} predicate Predicate function to find matching segments.
* @returns {Array<Number>} An array of indexes into the segments array of
* the matching elements.
*/
TimelineSegments.prototype._findSegment = function(predicate) {
var indexes = [];
for (var i = 0, length = this._segments.length; i < length; i++) {
if (predicate(this._segments[i])) {
indexes.push(i);
}
}
return indexes;
};
/**
* Removes the segments at the given array indexes.
*
* @private
* @param {Array<Number>} indexes The array indexes to remove.
* @returns {Array<Segment>} The removed {@link Segment} objects.
*/
TimelineSegments.prototype._removeIndexes = function(indexes) {
var removed = [];
for (var i = 0; i < indexes.length; i++) {
var index = indexes[i] - removed.length;
var itemRemoved = this._segments.splice(index, 1)[0];
delete this._segmentsById[itemRemoved.id];
removed.push(itemRemoved);
}
return removed;
};
/**
* Removes all segments that match a given predicate function.
*
* After removing the segments, this function also emits a
* <code>segments.remove</code> event with the removed {@link Segment}
* objects.
*
* @private
* @param {Function} predicate A predicate function that identifies which
* segments to remove.
* @returns {Array<Segment>} The removed {@link Segment} objects.
*/
TimelineSegments.prototype._removeSegments = function(predicate) {
var indexes = this._findSegment(predicate);
var removed = this._removeIndexes(indexes);
this.refreshRelativeIds();
this._peaks.emit('segments.remove', removed);
return removed;
};
/**
* Removes the given segment.
*
* @param {Segment} segment The segment to remove.
* @returns {Array<Segment>} The removed segment.
*/
TimelineSegments.prototype.remove = function(segment) {
return this._removeSegments(function(s) {
return s === segment;
});
};
/**
* Removes any segments with the given id.
*
* @param {String} id
* @returns {Array<Segment>} The removed {@link Segment} objects.
*/
TimelineSegments.prototype.removeById = function(segmentId) {
return this._removeSegments(function(segment) {
return segment.id === segmentId;
});
};
/**
* Removes any segments with the given start time, and optional end time.
*
* @param {Number} startTime Segments with this start time are removed.
* @param {Number?} endTime If present, only segments with both the given
* start time and end time are removed.
* @returns {Array<Segment>} The removed {@link Segment} objects.
*/
TimelineSegments.prototype.removeByTime = function(startTime, endTime) {
endTime = (typeof endTime === 'number') ? endTime : 0;
var fnFilter;
if (endTime > 0) {
fnFilter = function(segment) {
return segment.startTime === startTime && segment.endTime === endTime;
};
}
else {
fnFilter = function(segment) {
return segment.startTime === startTime;
};
}
return this._removeSegments(fnFilter);
};
/**
* Removes all segments.
*
* After removing the segments, this function emits a
* <code>segments.remove_all</code> event.
*/
TimelineSegments.prototype.removeAll = function(lineId) {
var indexes = this._findSegment(function(segment) {
return segment.line === lineId;
});
this._removeIndexes(indexes);
this._peaks.emit('segments.remove_all', lineId);
};
return TimelineSegments;
});