Victor Woeltjen aefad6fdd3 [Code Style] Continue refactor of Plot bundle
Continue refactoring Plot bundle to use prototypes,
WTD-1482
2015-08-13 10:26:04 -07:00

324 lines
12 KiB
JavaScript

/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*global define,Float32Array*/
define(
['./PlotLine', './PlotLineBuffer'],
function (PlotLine, PlotLineBuffer) {
'use strict';
var MAX_POINTS = 86400,
PADDING_RATIO = 0.10, // Padding percentage for top & bottom
INITIAL_SIZE = 675; // 1/128 of MAX_POINTS
/**
* The PlotPreparer is responsible for handling data sets and
* preparing them to be rendered. It creates a WebGL-plottable
* Float32Array for each trace, and tracks the boundaries of the
* data sets (since this is convenient to do during the same pass).
* @memberof platform/features/plot
* @constructor
* @param {TelemetryHandle} handle the handle to telemetry access
* @param {string} domain the key to use when looking up domain values
* @param {string} range the key to use when looking up range values
* @param {number} fixedDuration maximum plot duration to display
* @param {number} maxPoints maximum number of points to display
*/
function PlotUpdater(handle, domain, range, fixedDuration, maxPoints) {
this.handle = handle;
this.domain = domain;
this.range = range;
this.fixedDuration = fixedDuration;
this.maxPoints = maxPoints;
this.ids = [];
this.lines = {};
this.buffers = {};
this.bufferArray = [];
// Use a default MAX_POINTS if none is provided
this.maxPoints = maxPoints !== undefined ? maxPoints : MAX_POINTS;
this.dimensions = [0, 0];
this.origin = [0, 0];
// Initially prepare state for these objects.
// Note that this may be an empty array at this time,
// so we also need to check during update cycles.
this.update();
}
// Look up a domain object's id (for mapping, below)
function getId(domainObject) {
return domainObject.getId();
}
// Used in the reduce step of updateExtrema
function reduceExtrema(a, b) {
return [ Math.min(a[0], b[0]), Math.max(a[1], b[1]) ];
}
// Convert a domain/range extrema to plot dimensions
function dimensionsOf(extrema) {
return extrema[1] - extrema[0];
}
// Convert a domain/range extrema to a plot origin
function originOf(extrema) {
return extrema[0];
}
// Check if this set of ids matches the current set of ids
// (used to detect if line preparation can be skipped)
PlotUpdater.prototype.idsMatch = function (nextIds) {
var ids = this.ids;
return ids.length === nextIds.length &&
nextIds.every(function (id, index) {
return ids[index] === id;
});
};
// Prepare plot lines for this group of telemetry objects
PlotUpdater.prototype.prepareLines = function (telemetryObjects) {
var nextIds = telemetryObjects.map(getId),
next = {},
self = this;
// Detect if we already have everything we need prepared
if (this.idsMatch(nextIds)) {
// Nothing to prepare, move on
return;
}
// Built up a set of ids. Note that we can only
// create plot lines after our domain offset has
// been determined.
if (this.domainOffset !== undefined) {
// Update list of ids in use
this.ids = nextIds;
// Create buffers for these objects
this.bufferArray = this.ids.map(function (id) {
self.buffers[id] = self.buffers[id] || new PlotLineBuffer(
self.domainOffset,
INITIAL_SIZE,
self.maxPoints
);
next[id] =
self.lines[id] || new PlotLine(self.buffers[id]);
return self.buffers[id];
});
}
// If there are no more lines, clear the domain offset
if (Object.keys(next).length < 1) {
this.domainOffset = undefined;
}
// Update to the current set of lines
this.lines = next;
};
// Initialize the domain offset, based on these observed values
PlotUpdater.prototype.initializeDomainOffset = function (values) {
this.domainOffset =
((this.domainOffset === undefined) && (values.length > 0)) ?
(values.reduce(function (a, b) {
return (a || 0) + (b || 0);
}, 0) / values.length) :
this.domainOffset;
};
// Expand range slightly so points near edges are visible
PlotUpdater.prototype.expandRange = function () {
var padding = PADDING_RATIO * this.dimensions[1],
top;
padding = Math.max(padding, 1.0);
top = Math.ceil(this.origin[1] + this.dimensions[1] + padding / 2);
this.origin[1] = Math.floor(this.origin[1] - padding / 2);
this.dimensions[1] = top - this.origin[1];
};
// Update dimensions and origin based on extrema of plots
PlotUpdater.prototype.updateBounds = function () {
var bufferArray = this.bufferArray;
if (bufferArray.length > 0) {
this.domainExtrema = bufferArray.map(function (lineBuffer) {
return lineBuffer.getDomainExtrema();
}).reduce(reduceExtrema);
this.rangeExtrema = bufferArray.map(function (lineBuffer) {
return lineBuffer.getRangeExtrema();
}).reduce(reduceExtrema);
// Calculate best-fit dimensions
this.dimensions = [ this.domainExtrema, this.rangeExtrema ]
.map(dimensionsOf);
this.origin = [ this.domainExtrema, this.rangeExtrema ]
.map(originOf);
// Enforce some minimum visible area
this.expandRange();
// ...then enforce a fixed duration if needed
if (this.fixedDuration !== undefined) {
this.origin[0] = this.origin[0] + this.dimensions[0] -
this.fixedDuration;
this.dimensions[0] = this.fixedDuration;
}
}
};
// Add latest data for this domain object
PlotUpdater.prototype.addPointFor = function (domainObject) {
var line = this.lines[domainObject.getId()];
if (line) {
line.addPoint(
this.handle.getDomainValue(domainObject, this.domain),
this.handle.getRangeValue(domainObject, this.range)
);
}
};
/**
* Update with latest data.
*/
PlotUpdater.prototype.update = function update() {
var objects = this.handle.getTelemetryObjects(),
self = this;
// Initialize domain offset if necessary
if (this.domainOffset === undefined) {
this.initializeDomainOffset(objects.map(function (obj) {
return self.handle.getDomainValue(obj, self.domain);
}).filter(function (value) {
return typeof value === 'number';
}));
}
// Make sure lines are available
this.prepareLines(objects);
// Add new data
objects.forEach(function (domainObject, index) {
self.addPointFor(domainObject, index);
});
// Then, update extrema
this.updateBounds();
};
/**
* Get the dimensions which bound all data in the provided
* data sets. This is given as a two-element array where the
* first element is domain, and second is range.
* @returns {number[]} the dimensions which bound this data set
*/
PlotUpdater.prototype.getDimensions = function () {
return this.dimensions;
};
/**
* Get the origin of this data set's boundary.
* This is given as a two-element array where the
* first element is domain, and second is range.
* The domain value here is not adjusted by the domain offset.
* @returns {number[]} the origin of this data set's boundary
*/
PlotUpdater.prototype.getOrigin = function () {
return this.origin;
};
/**
* Get the domain offset; this offset will have been subtracted
* from all domain values in all buffers returned by this
* preparer, in order to minimize loss-of-precision due to
* conversion to the 32-bit float format needed by WebGL.
* @returns {number} the domain offset
* @memberof platform/features/plot.PlotUpdater#
*/
PlotUpdater.prototype.getDomainOffset = function () {
return this.domainOffset;
};
/**
* Get all renderable buffers for this data set. This will
* be returned as an array which can be correlated back to
* the provided telemetry data objects (from the constructor
* call) by index.
*
* Internally, these are flattened; each buffer contains a
* sequence of alternating domain and range values.
*
* All domain values in all buffers will have been adjusted
* from their original values by subtraction of the domain
* offset; this minimizes loss-of-precision resulting from
* the conversion to 32-bit floats, which may otherwise
* cause aliasing artifacts (particularly for timestamps)
*
* @returns {Float32Array[]} the buffers for these traces
* @memberof platform/features/plot.PlotUpdater#
*/
PlotUpdater.prototype.getLineBuffers = function () {
return this.bufferArray;
};
/**
* Fill in historical data.
*/
PlotUpdater.prototype.addHistorical = function (domainObject, series) {
var count = series ? series.getPointCount() : 0,
line;
// Nothing to do if it's an empty series
if (count < 1) {
return;
}
// Initialize domain offset if necessary
if (this.domainOffset === undefined) {
this.initializeDomainOffset([
series.getDomainValue(0, this.domain),
series.getDomainValue(count - 1, this.domain)
]);
}
// Make sure lines are available
this.prepareLines(this.handle.getTelemetryObjects());
// Look up the line for this domain object
line = this.lines[domainObject.getId()];
// ...and put the data into it.
if (line) {
line.addSeries(series, this.domain, this.range);
}
// Update extrema
this.updateBounds();
};
return PlotUpdater;
}
);