mirror of
https://github.com/nasa/openmct.git
synced 2024-12-19 05:07:52 +00:00
Merge branch 'open806' into open-master
This commit is contained in:
commit
617691ab24
@ -22,7 +22,7 @@
|
||||
{
|
||||
"key": "PlotController",
|
||||
"implementation": "PlotController.js",
|
||||
"depends": [ "$scope", "telemetryFormatter", "telemetrySubscriber" ]
|
||||
"depends": [ "$scope", "telemetryFormatter", "telemetryHandler" ]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -30,13 +30,13 @@ define(
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
function PlotController($scope, telemetryFormatter, telemetrySubscriber) {
|
||||
function PlotController($scope, telemetryFormatter, telemetryHandler) {
|
||||
var subPlotFactory = new SubPlotFactory(telemetryFormatter),
|
||||
modeOptions = new PlotModeOptions([], subPlotFactory),
|
||||
subplots = [],
|
||||
cachedObjects = [],
|
||||
updater,
|
||||
subscription,
|
||||
handle,
|
||||
domainOffset;
|
||||
|
||||
// Populate the scope with axis information (specifically, options
|
||||
@ -77,7 +77,7 @@ define(
|
||||
// new subscription.) This will clear the plot.
|
||||
function recreateUpdater() {
|
||||
updater = new PlotUpdater(
|
||||
subscription,
|
||||
handle,
|
||||
($scope.axes[0].active || {}).key,
|
||||
($scope.axes[1].active || {}).key
|
||||
);
|
||||
@ -85,8 +85,8 @@ define(
|
||||
|
||||
// Handle new telemetry data in this plot
|
||||
function updateValues() {
|
||||
if (subscription) {
|
||||
setupModes(subscription.getTelemetryObjects());
|
||||
if (handle) {
|
||||
setupModes(handle.getTelemetryObjects());
|
||||
}
|
||||
if (updater) {
|
||||
updater.update();
|
||||
@ -95,29 +95,44 @@ define(
|
||||
update();
|
||||
}
|
||||
|
||||
// Display new historical data as it becomes available
|
||||
function addHistoricalData(domainObject, series) {
|
||||
updater.addHistorical(domainObject, series);
|
||||
modeOptions.getModeHandler().plotTelemetry(updater);
|
||||
update();
|
||||
}
|
||||
|
||||
// Issue a new request for historical telemetry
|
||||
function requestTelemetry() {
|
||||
if (handle && updater) {
|
||||
handle.request({}, addHistoricalData);
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new subscription; telemetrySubscriber gets
|
||||
// to do the meaningful work here.
|
||||
function subscribe(domainObject) {
|
||||
if (subscription) {
|
||||
subscription.unsubscribe();
|
||||
if (handle) {
|
||||
handle.unsubscribe();
|
||||
}
|
||||
subscription = domainObject && telemetrySubscriber.subscribe(
|
||||
handle = domainObject && telemetryHandler.handle(
|
||||
domainObject,
|
||||
updateValues,
|
||||
true // Lossless
|
||||
);
|
||||
if (subscription) {
|
||||
setupModes(subscription.getTelemetryObjects());
|
||||
setupAxes(subscription.getMetadata());
|
||||
if (handle) {
|
||||
setupModes(handle.getTelemetryObjects());
|
||||
setupAxes(handle.getMetadata());
|
||||
recreateUpdater();
|
||||
requestTelemetry();
|
||||
}
|
||||
}
|
||||
|
||||
// Release the current subscription (called when scope is destroyed)
|
||||
function releaseSubscription() {
|
||||
if (subscription) {
|
||||
subscription.unsubscribe();
|
||||
subscription = undefined;
|
||||
if (handle) {
|
||||
handle.unsubscribe();
|
||||
handle = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
94
platform/features/plot/src/elements/PlotLine.js
Normal file
94
platform/features/plot/src/elements/PlotLine.js
Normal file
@ -0,0 +1,94 @@
|
||||
/*global define,Float32Array*/
|
||||
|
||||
define(
|
||||
['./PlotSeriesWindow'],
|
||||
function (PlotSeriesWindow) {
|
||||
"use strict";
|
||||
|
||||
|
||||
function PlotLine(buffer) {
|
||||
|
||||
// Insert a time-windowed data series into the buffer
|
||||
function insertSeriesWindow(seriesWindow) {
|
||||
var count = seriesWindow.getPointCount();
|
||||
|
||||
function doInsert() {
|
||||
var firstTimestamp = seriesWindow.getDomainValue(0),
|
||||
lastTimestamp = seriesWindow.getDomainValue(count - 1),
|
||||
startIndex = buffer.findInsertionIndex(firstTimestamp),
|
||||
endIndex = buffer.findInsertionIndex(lastTimestamp);
|
||||
|
||||
// Does the whole series fit in between two adjacent indexes?
|
||||
if ((startIndex === endIndex) && startIndex > -1) {
|
||||
// Insert it in between
|
||||
buffer.insert(seriesWindow, startIndex);
|
||||
} else {
|
||||
// Split it up, and add the two halves
|
||||
seriesWindow.split().forEach(insertSeriesWindow);
|
||||
}
|
||||
}
|
||||
|
||||
// Only insert if there are points to insert
|
||||
if (count > 0) {
|
||||
doInsert();
|
||||
}
|
||||
}
|
||||
|
||||
function createWindow(series, domain, range) {
|
||||
return new PlotSeriesWindow(
|
||||
series,
|
||||
domain,
|
||||
range,
|
||||
0,
|
||||
series.getPointCount()
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
/**
|
||||
* Add a point to this plot line.
|
||||
* @param {number} domainValue the domain value
|
||||
* @param {number} rangeValue the range value
|
||||
*/
|
||||
addPoint: function (domainValue, rangeValue) {
|
||||
var index;
|
||||
// Make sure we got real/useful values here...
|
||||
if (domainValue !== undefined && rangeValue !== undefined) {
|
||||
index = buffer.findInsertionIndex(domainValue);
|
||||
|
||||
// Already in the buffer? Skip insertion
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Insert the point
|
||||
if (!buffer.insertPoint(domainValue, rangeValue, index)) {
|
||||
// If insertion failed, trim from the beginning...
|
||||
buffer.trim(1);
|
||||
// ...and try again.
|
||||
buffer.insertPoint(domainValue, rangeValue, index);
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Add a series of telemetry data to this plot line.
|
||||
* @param {TelemetrySeries} series the data series
|
||||
* @param {string} [domain] the key indicating which domain
|
||||
* to use when looking up data from this series
|
||||
* @param {string} [range] the key indicating which range
|
||||
* to use when looking up data from this series
|
||||
*/
|
||||
addSeries: function (series, domain, range) {
|
||||
// Should try to add via insertion if a
|
||||
// clear insertion point is available;
|
||||
// if not, should split and add each half.
|
||||
// Insertion operation also needs to factor out
|
||||
// redundant timestamps, for overlapping data
|
||||
insertSeriesWindow(createWindow(series, domain, range));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return PlotLine;
|
||||
}
|
||||
);
|
240
platform/features/plot/src/elements/PlotLineBuffer.js
Normal file
240
platform/features/plot/src/elements/PlotLineBuffer.js
Normal file
@ -0,0 +1,240 @@
|
||||
/*global define,Float32Array*/
|
||||
|
||||
define(
|
||||
[],
|
||||
function () {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Contains the buffer used to draw a plot.
|
||||
* @param {number} domainOffset number to subtract from domain values
|
||||
* @param {number} initialSize initial buffer size
|
||||
* @param {number} maxSize maximum buffer size
|
||||
* @constructor
|
||||
*/
|
||||
function PlotLineBuffer(domainOffset, initialSize, maxSize) {
|
||||
var buffer = new Float32Array(initialSize * 2),
|
||||
rangeExtrema = [ Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY ],
|
||||
length = 0;
|
||||
|
||||
// Binary search for an insertion index
|
||||
function binSearch(value, min, max) {
|
||||
var mid = Math.floor((min + max) / 2),
|
||||
found = buffer[mid * 2];
|
||||
|
||||
// Collisions are not wanted
|
||||
if (found === value) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Otherwise, if we're down to a single index,
|
||||
// we've found our insertion point
|
||||
if (min >= max) {
|
||||
// Compare the found timestamp with the search
|
||||
// value to decide if we'll insert after or before.
|
||||
return min + ((found < value) ? 1 : 0);
|
||||
}
|
||||
|
||||
// Finally, do the recursive step
|
||||
if (found < value) {
|
||||
return binSearch(value, mid + 1, max);
|
||||
} else {
|
||||
return binSearch(value, min, mid - 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Increase the size of the buffer
|
||||
function doubleBufferSize() {
|
||||
var sz = Math.min(maxSize * 2, buffer.length * 2),
|
||||
canDouble = sz > buffer.length,
|
||||
doubled = canDouble && new Float32Array(sz);
|
||||
|
||||
if (canDouble) {
|
||||
doubled.set(buffer); // Copy contents of original
|
||||
buffer = doubled;
|
||||
}
|
||||
|
||||
return canDouble;
|
||||
}
|
||||
|
||||
// Decrease the size of the buffer
|
||||
function halveBufferSize() {
|
||||
var sz = Math.max(initialSize * 2, buffer.length / 2),
|
||||
canHalve = sz < buffer.length;
|
||||
|
||||
if (canHalve) {
|
||||
buffer = new Float32Array(buffer.subarray(0, sz));
|
||||
}
|
||||
|
||||
return canHalve;
|
||||
}
|
||||
|
||||
// Set a value in the buffer
|
||||
function setValue(index, domainValue, rangeValue) {
|
||||
buffer[index * 2] = domainValue - domainOffset;
|
||||
buffer[index * 2 + 1] = rangeValue;
|
||||
// Track min/max of range values (min/max for
|
||||
// domain values can be read directly from buffer)
|
||||
rangeExtrema[0] = Math.min(rangeExtrema[0], rangeValue);
|
||||
rangeExtrema[1] = Math.max(rangeExtrema[1], rangeValue);
|
||||
}
|
||||
|
||||
return {
|
||||
/**
|
||||
* Get the WebGL-displayable buffer of points to plot.
|
||||
* @returns {Float32Array} displayable buffer for this line
|
||||
*/
|
||||
getBuffer: function () {
|
||||
return buffer;
|
||||
},
|
||||
/**
|
||||
* Get the number of points stored in this buffer.
|
||||
* @returns {number} the number of points stored
|
||||
*/
|
||||
getLength: function () {
|
||||
return length;
|
||||
},
|
||||
/**
|
||||
* Get the min/max range values that are currently in this
|
||||
* buffer. Unlike range extrema, these will change as the
|
||||
* buffer gets trimmed.
|
||||
* @returns {number[]} min, max domain values
|
||||
*/
|
||||
getDomainExtrema: function () {
|
||||
// Since these are ordered in the buffer, assume
|
||||
// these are the values at the first and last index
|
||||
return [
|
||||
buffer[0] + domainOffset,
|
||||
buffer[length * 2 - 2] + domainOffset
|
||||
];
|
||||
},
|
||||
/**
|
||||
* Get the min/max range values that have been observed for this
|
||||
* buffer. Note that these values may have been trimmed out at
|
||||
* some point.
|
||||
* @returns {number[]} min, max range values
|
||||
*/
|
||||
getRangeExtrema: function () {
|
||||
return rangeExtrema;
|
||||
},
|
||||
/**
|
||||
* Remove values from this buffer.
|
||||
* Normally, values are removed from the start
|
||||
* of the buffer; a truthy value in the second argument
|
||||
* will cause values to be removed from the end.
|
||||
* @param {number} count number of values to remove
|
||||
* @param {boolean} [fromEnd] true if the most recent
|
||||
* values should be removed
|
||||
*/
|
||||
trim: function (count, fromEnd) {
|
||||
// If we're removing values from the start...
|
||||
if (!fromEnd) {
|
||||
// ...do so by shifting buffer contents over
|
||||
buffer.set(buffer.subarray(2 * count));
|
||||
}
|
||||
// Reduce used buffer size accordingly
|
||||
length -= count;
|
||||
// Finally, if less than half of the buffer is being
|
||||
// used, free up some memory.
|
||||
if (length < buffer.length / 4) {
|
||||
halveBufferSize();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Insert data from the provided series at the specified
|
||||
* index. If this would exceed the buffer's maximum capacity,
|
||||
* this operation fails and the buffer is unchanged.
|
||||
* @param {TelemetrySeries} series the series to insert
|
||||
* @param {number} index the index at which to insert this
|
||||
* series
|
||||
* @returns {boolean} true if insertion succeeded; otherwise
|
||||
* false
|
||||
*/
|
||||
insert: function (series, index) {
|
||||
var sz = series.getPointCount(),
|
||||
i;
|
||||
|
||||
// Don't allow append after the end; that doesn't make sense
|
||||
index = Math.min(index, length);
|
||||
|
||||
// Resize if necessary
|
||||
while (sz > ((buffer.length / 2) - length)) {
|
||||
if (!doubleBufferSize()) {
|
||||
// Can't make room for this, insertion fails
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Shift data over if necessary
|
||||
if (index < length) {
|
||||
buffer.set(
|
||||
buffer.subarray(index * 2, length * 2),
|
||||
(index + sz) * 2
|
||||
);
|
||||
}
|
||||
|
||||
// Insert data into the set
|
||||
for (i = 0; i < sz; i += 1) {
|
||||
setValue(
|
||||
i + index,
|
||||
series.getDomainValue(i),
|
||||
series.getRangeValue(i)
|
||||
);
|
||||
}
|
||||
|
||||
// Increase the length
|
||||
length += sz;
|
||||
|
||||
// Indicate that insertion was successful
|
||||
return true;
|
||||
},
|
||||
/**
|
||||
* Append a single data point.
|
||||
*/
|
||||
insertPoint: function (domainValue, rangeValue, index) {
|
||||
// Don't allow
|
||||
index = Math.min(length, index);
|
||||
|
||||
// Ensure there is space for this point
|
||||
if (length >= (buffer.length / 2)) {
|
||||
if (!doubleBufferSize()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Put the data in the buffer
|
||||
setValue(length, domainValue, rangeValue);
|
||||
|
||||
// Update length
|
||||
length += 1;
|
||||
|
||||
// Indicate that this was successful
|
||||
return true;
|
||||
},
|
||||
/**
|
||||
* Find an index for inserting data with this
|
||||
* timestamp. The second argument indicates whether
|
||||
* we are searching for insert-before or insert-after
|
||||
* positions.
|
||||
* Timestamps are meant to be unique, so if a collision
|
||||
* occurs, this will return -1.
|
||||
* @param {number} timestamp timestamp to insert
|
||||
* @returns {number} the index for insertion (or -1)
|
||||
*/
|
||||
findInsertionIndex: function (timestamp) {
|
||||
var value = timestamp - domainOffset;
|
||||
|
||||
// Handle empty buffer case and check for an
|
||||
// append opportunity (which is most common case for
|
||||
// real-time data so is optimized-for) before falling
|
||||
// back to a binary search for the insertion point.
|
||||
return (length < 1) ? 0 :
|
||||
(value > buffer[length * 2 - 2]) ? length :
|
||||
binSearch(value, 0, length - 1);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return PlotLineBuffer;
|
||||
}
|
||||
);
|
47
platform/features/plot/src/elements/PlotSeriesWindow.js
Normal file
47
platform/features/plot/src/elements/PlotSeriesWindow.js
Normal file
@ -0,0 +1,47 @@
|
||||
/*global define*/
|
||||
define(
|
||||
[],
|
||||
function () {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Provides a window on a telemetry data series, to support
|
||||
* insertion into a plot line.
|
||||
*/
|
||||
function PlotSeriesWindow(series, domain, range, start, end) {
|
||||
return {
|
||||
getPointCount: function () {
|
||||
return end - start;
|
||||
},
|
||||
getDomainValue: function (index) {
|
||||
return series.getDomainValue(index + start, domain);
|
||||
},
|
||||
getRangeValue: function (index) {
|
||||
return series.getRangeValue(index + start, range);
|
||||
},
|
||||
split: function () {
|
||||
var mid = Math.floor((end + start) / 2);
|
||||
return ((end - start) > 1) ?
|
||||
[
|
||||
new PlotSeriesWindow(
|
||||
series,
|
||||
domain,
|
||||
range,
|
||||
start,
|
||||
mid
|
||||
),
|
||||
new PlotSeriesWindow(
|
||||
series,
|
||||
domain,
|
||||
range,
|
||||
mid,
|
||||
end
|
||||
)
|
||||
] : [];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return PlotSeriesWindow;
|
||||
}
|
||||
);
|
@ -5,7 +5,8 @@
|
||||
* the conversion from data API to displayable buffers.
|
||||
*/
|
||||
define(
|
||||
function () {
|
||||
['./PlotLine', './PlotLineBuffer'],
|
||||
function (PlotLine, PlotLineBuffer) {
|
||||
'use strict';
|
||||
|
||||
var MAX_POINTS = 86400,
|
||||
@ -17,117 +18,182 @@ define(
|
||||
* Float32Array for each trace, and tracks the boundaries of the
|
||||
* data sets (since this is convenient to do during the same pass).
|
||||
* @constructor
|
||||
* @param {Telemetry[]} datas telemetry data objects
|
||||
* @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
|
||||
*/
|
||||
function PlotUpdater(subscription, domain, range, maxPoints) {
|
||||
var max = [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
|
||||
min = [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY],
|
||||
x,
|
||||
y,
|
||||
domainOffset,
|
||||
buffers = {},
|
||||
lengths = {},
|
||||
lengthArray = [],
|
||||
bufferArray = [];
|
||||
function PlotUpdater(handle, domain, range, maxPoints) {
|
||||
var ids = [],
|
||||
lines = {},
|
||||
dimensions = [0, 0],
|
||||
origin = [0, 0],
|
||||
domainExtrema,
|
||||
rangeExtrema,
|
||||
bufferArray = [],
|
||||
domainOffset;
|
||||
|
||||
// Double the size of a Float32Array
|
||||
function doubleSize(buffer) {
|
||||
var doubled = new Float32Array(buffer.length * 2);
|
||||
doubled.set(buffer); // Copy contents of original
|
||||
return doubled;
|
||||
// Look up a domain object's id (for mapping, below)
|
||||
function getId(domainObject) {
|
||||
return domainObject.getId();
|
||||
}
|
||||
|
||||
// Make sure there is enough space in a buffer to accomodate a
|
||||
// new point at the specified index. This will updates buffers[id]
|
||||
// if necessary.
|
||||
function ensureBufferSize(buffer, id, index) {
|
||||
// Check if we don't have enough room
|
||||
if (index > (buffer.length / 2 - 1)) {
|
||||
// If we don't, can we expand?
|
||||
if (index < maxPoints) {
|
||||
// Double the buffer size
|
||||
buffer = buffers[id] = doubleSize(buffer);
|
||||
} else {
|
||||
// Just shift the existing buffer
|
||||
buffer.set(buffer.subarray(2));
|
||||
}
|
||||
}
|
||||
|
||||
return buffer;
|
||||
// Check if this set of ids matches the current set of ids
|
||||
// (used to detect if line preparation can be skipped)
|
||||
function idsMatch(nextIds) {
|
||||
return nextIds.map(function (id, index) {
|
||||
return ids[index] === id;
|
||||
}).reduce(function (a, b) {
|
||||
return a && b;
|
||||
}, true);
|
||||
}
|
||||
|
||||
// Add data to the plot.
|
||||
function addData(obj) {
|
||||
var id = obj.getId(),
|
||||
index = lengths[id] || 0,
|
||||
buffer = buffers[id],
|
||||
domainValue = subscription.getDomainValue(obj, domain),
|
||||
rangeValue = subscription.getRangeValue(obj, range);
|
||||
// Prepare plot lines for this group of telemetry objects
|
||||
function prepareLines(telemetryObjects) {
|
||||
var nextIds = telemetryObjects.map(getId),
|
||||
next = {};
|
||||
|
||||
// If we don't already have a data buffer for that ID,
|
||||
// make one.
|
||||
if (!buffer) {
|
||||
buffer = new Float32Array(INITIAL_SIZE);
|
||||
buffers[id] = buffer;
|
||||
// Detect if we already have everything we need prepared
|
||||
if (ids.length === nextIds.length && idsMatch(nextIds)) {
|
||||
// Nothing to prepare, move on
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure there's data to add, and then add it
|
||||
if (domainValue !== undefined && rangeValue !== undefined &&
|
||||
(index < 1 || domainValue !== buffer[index * 2 - 2])) {
|
||||
// Use the first observed domain value as a domainOffset
|
||||
domainOffset = domainOffset !== undefined ?
|
||||
domainOffset : domainValue;
|
||||
// Ensure there is space for the new buffer
|
||||
buffer = ensureBufferSize(buffer, id, index);
|
||||
// Account for shifting that may have occurred
|
||||
index = Math.min(index, maxPoints - 1);
|
||||
// Update the buffer
|
||||
buffer[index * 2] = domainValue - domainOffset;
|
||||
buffer[index * 2 + 1] = rangeValue;
|
||||
// Update length
|
||||
lengths[id] = Math.min(index + 1, maxPoints);
|
||||
// Observe max/min range values
|
||||
max[1] = Math.max(max[1], rangeValue);
|
||||
min[1] = Math.min(min[1], rangeValue);
|
||||
// Built up a set of ids. Note that we can only
|
||||
// create plot lines after our domain offset has
|
||||
// been determined.
|
||||
if (domainOffset !== undefined) {
|
||||
// Update list of ids in use
|
||||
ids = nextIds;
|
||||
|
||||
// Create buffers for these objects
|
||||
bufferArray = ids.map(function (id) {
|
||||
var buffer = new PlotLineBuffer(
|
||||
domainOffset,
|
||||
INITIAL_SIZE,
|
||||
maxPoints
|
||||
);
|
||||
next[id] = lines[id] || new PlotLine(buffer);
|
||||
return buffer;
|
||||
});
|
||||
}
|
||||
|
||||
return buffer;
|
||||
// If there are no more lines, clear the domain offset
|
||||
if (Object.keys(next).length < 1) {
|
||||
domainOffset = undefined;
|
||||
}
|
||||
|
||||
// Update to the current set of lines
|
||||
lines = next;
|
||||
}
|
||||
|
||||
// Update min/max domain values for these objects
|
||||
function updateDomainExtrema(objects) {
|
||||
max[0] = Number.NEGATIVE_INFINITY;
|
||||
min[0] = Number.POSITIVE_INFINITY;
|
||||
objects.forEach(function (obj) {
|
||||
var id = obj.getId(),
|
||||
buffer = buffers[id],
|
||||
length = lengths[id],
|
||||
low = buffer[0] + domainOffset,
|
||||
high = buffer[length * 2 - 2] + domainOffset;
|
||||
max[0] = Math.max(high, max[0]);
|
||||
min[0] = Math.min(low, min[0]);
|
||||
});
|
||||
// Initialize the domain offset, based on these observed values
|
||||
function initializeDomainOffset(values) {
|
||||
domainOffset =
|
||||
((domainOffset === undefined) && (values.length > 0)) ?
|
||||
(values.reduce(function (a, b) {
|
||||
return (a || 0) + (b || 0);
|
||||
}, 0) / values.length) :
|
||||
domainOffset;
|
||||
}
|
||||
|
||||
// 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];
|
||||
}
|
||||
|
||||
// Update dimensions and origin based on extrema of plots
|
||||
function updateExtrema() {
|
||||
if (bufferArray.length > 0) {
|
||||
domainExtrema = bufferArray.map(function (lineBuffer) {
|
||||
return lineBuffer.getDomainExtrema();
|
||||
}).reduce(reduceExtrema);
|
||||
|
||||
rangeExtrema = bufferArray.map(function (lineBuffer) {
|
||||
return lineBuffer.getRangeExtrema();
|
||||
}).reduce(reduceExtrema);
|
||||
|
||||
dimensions = (rangeExtrema[0] === rangeExtrema[1]) ?
|
||||
[dimensionsOf(domainExtrema), 2.0 ] :
|
||||
[dimensionsOf(domainExtrema), dimensionsOf(rangeExtrema)];
|
||||
origin = [originOf(domainExtrema), originOf(rangeExtrema)];
|
||||
}
|
||||
}
|
||||
|
||||
// Add latest data for this domain object
|
||||
function addPointFor(domainObject) {
|
||||
var line = lines[domainObject.getId()];
|
||||
if (line) {
|
||||
line.addPoint(
|
||||
handle.getDomainValue(domainObject, domain),
|
||||
handle.getRangeValue(domainObject, range)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle new telemetry data
|
||||
function update() {
|
||||
var objects = subscription.getTelemetryObjects();
|
||||
bufferArray = objects.map(addData);
|
||||
lengthArray = objects.map(function (obj) {
|
||||
return lengths[obj.getId()];
|
||||
});
|
||||
updateDomainExtrema(objects);
|
||||
var objects = handle.getTelemetryObjects();
|
||||
|
||||
// Initialize domain offset if necessary
|
||||
if (domainOffset === undefined) {
|
||||
initializeDomainOffset(objects.map(function (obj) {
|
||||
return handle.getDomainValue(obj, domain);
|
||||
}).filter(function (value) {
|
||||
return typeof value === 'number';
|
||||
}));
|
||||
}
|
||||
|
||||
// Make sure lines are available
|
||||
prepareLines(objects);
|
||||
|
||||
// Add new data
|
||||
objects.forEach(addPointFor);
|
||||
|
||||
// Finally, update extrema
|
||||
updateExtrema();
|
||||
}
|
||||
|
||||
// Prepare buffers and related state for this object
|
||||
function prepare(telemetryObject) {
|
||||
var id = telemetryObject.getId();
|
||||
lengths[id] = 0;
|
||||
buffers[id] = new Float32Array(INITIAL_SIZE);
|
||||
lengthArray.push(lengths[id]);
|
||||
bufferArray.push(buffers[id]);
|
||||
// Add historical data for this domain object
|
||||
function setHistorical(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 (domainOffset === undefined) {
|
||||
initializeDomainOffset([
|
||||
series.getDomainValue(0, domain),
|
||||
series.getDomainValue(count - 1, domain)
|
||||
]);
|
||||
}
|
||||
|
||||
// Make sure lines are available
|
||||
prepareLines(handle.getTelemetryObjects());
|
||||
|
||||
// Look up the line for this domain object
|
||||
line = lines[domainObject.getId()];
|
||||
|
||||
// ...and put the data into it.
|
||||
if (line) {
|
||||
line.addSeries(series, domain, range);
|
||||
}
|
||||
|
||||
// Finally, update extrema
|
||||
updateExtrema();
|
||||
}
|
||||
|
||||
// Use a default MAX_POINTS if none is provided
|
||||
@ -136,7 +202,7 @@ define(
|
||||
// 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.
|
||||
subscription.getTelemetryObjects().forEach(prepare);
|
||||
update();
|
||||
|
||||
return {
|
||||
/**
|
||||
@ -146,10 +212,7 @@ define(
|
||||
* @returns {number[]} the dimensions which bound this data set
|
||||
*/
|
||||
getDimensions: function () {
|
||||
// Pad range if necessary
|
||||
return (max[1] === min[1]) ?
|
||||
[max[0] - min[0], 2.0 ] :
|
||||
[max[0] - min[0], max[1] - min[1]];
|
||||
return dimensions;
|
||||
},
|
||||
/**
|
||||
* Get the origin of this data set's boundary.
|
||||
@ -160,7 +223,7 @@ define(
|
||||
*/
|
||||
getOrigin: function () {
|
||||
// Pad range if necessary
|
||||
return (max[1] === min[1]) ? [ min[0], min[1] - 1.0 ] : min;
|
||||
return origin;
|
||||
},
|
||||
/**
|
||||
* Get the domain offset; this offset will have been subtracted
|
||||
@ -189,23 +252,17 @@ define(
|
||||
*
|
||||
* @returns {Float32Array[]} the buffers for these traces
|
||||
*/
|
||||
getBuffers: function () {
|
||||
getLineBuffers: function () {
|
||||
return bufferArray;
|
||||
},
|
||||
/**
|
||||
* Get the number of points in the buffer with the specified
|
||||
* index. Buffers are padded to minimize memory allocations,
|
||||
* so user code will need this information to know how much
|
||||
* data to plot.
|
||||
* @returns {number} the number of points in this buffer
|
||||
*/
|
||||
getLength: function (index) {
|
||||
return lengthArray[index] || 0;
|
||||
},
|
||||
/**
|
||||
* Update with latest data.
|
||||
*/
|
||||
update: update
|
||||
update: update,
|
||||
/**
|
||||
* Fill in historical data.
|
||||
*/
|
||||
addHistorical: setHistorical
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -34,11 +34,11 @@ define(
|
||||
subplot.setDomainOffset(prepared.getDomainOffset());
|
||||
|
||||
// Draw the buffers. Select color by index.
|
||||
subplot.getDrawingObject().lines = prepared.getBuffers().map(function (buf, i) {
|
||||
subplot.getDrawingObject().lines = prepared.getLineBuffers().map(function (buf, i) {
|
||||
return {
|
||||
buffer: buf,
|
||||
buffer: buf.getBuffer(),
|
||||
color: PlotPalette.getFloatColor(i),
|
||||
points: prepared.getLength(i)
|
||||
points: buf.getLength()
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -23,7 +23,7 @@ define(
|
||||
});
|
||||
|
||||
function plotTelemetryTo(subplot, prepared, index) {
|
||||
var buffer = prepared.getBuffers()[index];
|
||||
var buffer = prepared.getLineBuffers()[index];
|
||||
|
||||
// Track the domain offset, used to bias domain values
|
||||
// to minimize loss of precision when converted to 32-bit
|
||||
@ -33,9 +33,9 @@ define(
|
||||
// Draw the buffers. Always use the 0th color, because there
|
||||
// is one line per plot.
|
||||
subplot.getDrawingObject().lines = [{
|
||||
buffer: buffer,
|
||||
buffer: buffer.getBuffer(),
|
||||
color: PlotPalette.getFloatColor(0),
|
||||
points: prepared.getLength(index)
|
||||
points: buffer.getLength()
|
||||
}];
|
||||
|
||||
subplot.update();
|
||||
|
@ -11,9 +11,10 @@ define(
|
||||
describe("The plot controller", function () {
|
||||
var mockScope,
|
||||
mockFormatter,
|
||||
mockSubscriber,
|
||||
mockSubscription,
|
||||
mockHandler,
|
||||
mockHandle,
|
||||
mockDomainObject,
|
||||
mockSeries,
|
||||
controller;
|
||||
|
||||
|
||||
@ -30,28 +31,33 @@ define(
|
||||
"domainObject",
|
||||
[ "getId", "getModel", "getCapability" ]
|
||||
);
|
||||
mockSubscriber = jasmine.createSpyObj(
|
||||
mockHandler = jasmine.createSpyObj(
|
||||
"telemetrySubscriber",
|
||||
["subscribe"]
|
||||
["handle"]
|
||||
);
|
||||
mockSubscription = jasmine.createSpyObj(
|
||||
mockHandle = jasmine.createSpyObj(
|
||||
"subscription",
|
||||
[
|
||||
"unsubscribe",
|
||||
"getTelemetryObjects",
|
||||
"getMetadata",
|
||||
"getDomainValue",
|
||||
"getRangeValue"
|
||||
"getRangeValue",
|
||||
"request"
|
||||
]
|
||||
);
|
||||
mockSeries = jasmine.createSpyObj(
|
||||
'series',
|
||||
['getPointCount', 'getDomainValue', 'getRangeValue']
|
||||
);
|
||||
|
||||
mockSubscriber.subscribe.andReturn(mockSubscription);
|
||||
mockSubscription.getTelemetryObjects.andReturn([mockDomainObject]);
|
||||
mockSubscription.getMetadata.andReturn([{}]);
|
||||
mockSubscription.getDomainValue.andReturn(123);
|
||||
mockSubscription.getRangeValue.andReturn(42);
|
||||
mockHandler.handle.andReturn(mockHandle);
|
||||
mockHandle.getTelemetryObjects.andReturn([mockDomainObject]);
|
||||
mockHandle.getMetadata.andReturn([{}]);
|
||||
mockHandle.getDomainValue.andReturn(123);
|
||||
mockHandle.getRangeValue.andReturn(42);
|
||||
|
||||
controller = new PlotController(mockScope, mockFormatter, mockSubscriber);
|
||||
controller = new PlotController(mockScope, mockFormatter, mockHandler);
|
||||
});
|
||||
|
||||
it("provides plot colors", function () {
|
||||
@ -71,7 +77,7 @@ define(
|
||||
// Make an object available
|
||||
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
|
||||
// Should have subscribed
|
||||
expect(mockSubscriber.subscribe).toHaveBeenCalledWith(
|
||||
expect(mockHandler.handle).toHaveBeenCalledWith(
|
||||
mockDomainObject,
|
||||
jasmine.any(Function),
|
||||
true // Lossless
|
||||
@ -92,7 +98,7 @@ define(
|
||||
expect(controller.getSubPlots().length > 0).toBeTruthy();
|
||||
|
||||
// Broadcast data
|
||||
mockSubscriber.subscribe.mostRecentCall.args[1]();
|
||||
mockHandler.handle.mostRecentCall.args[1]();
|
||||
|
||||
controller.getSubPlots().forEach(function (subplot) {
|
||||
expect(subplot.getDrawingObject().lines)
|
||||
@ -104,17 +110,17 @@ define(
|
||||
// Make an object available
|
||||
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
|
||||
// Verify precondition - shouldn't unsubscribe yet
|
||||
expect(mockSubscription.unsubscribe).not.toHaveBeenCalled();
|
||||
expect(mockHandle.unsubscribe).not.toHaveBeenCalled();
|
||||
// Remove the domain object
|
||||
mockScope.$watch.mostRecentCall.args[1](undefined);
|
||||
// Should have unsubscribed
|
||||
expect(mockSubscription.unsubscribe).toHaveBeenCalled();
|
||||
expect(mockHandle.unsubscribe).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
it("changes modes depending on number of objects", function () {
|
||||
// Act like one object is available
|
||||
mockSubscription.getTelemetryObjects.andReturn([
|
||||
mockHandle.getTelemetryObjects.andReturn([
|
||||
mockDomainObject
|
||||
]);
|
||||
|
||||
@ -124,7 +130,7 @@ define(
|
||||
expect(controller.getModeOptions().length).toEqual(1);
|
||||
|
||||
// Act like one object is available
|
||||
mockSubscription.getTelemetryObjects.andReturn([
|
||||
mockHandle.getTelemetryObjects.andReturn([
|
||||
mockDomainObject,
|
||||
mockDomainObject,
|
||||
mockDomainObject
|
||||
@ -174,17 +180,26 @@ define(
|
||||
expect(controller.isRequestPending()).toBeFalsy();
|
||||
});
|
||||
|
||||
it("requests historical telemetry", function () {
|
||||
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
|
||||
expect(mockHandle.request).toHaveBeenCalled();
|
||||
mockHandle.request.mostRecentCall.args[1](
|
||||
mockDomainObject,
|
||||
mockSeries
|
||||
);
|
||||
});
|
||||
|
||||
it("unsubscribes when destroyed", function () {
|
||||
// Make an object available
|
||||
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
|
||||
// Make sure $destroy is what's listened for
|
||||
expect(mockScope.$on.mostRecentCall.args[0]).toEqual('$destroy');
|
||||
// Also verify precondition
|
||||
expect(mockSubscription.unsubscribe).not.toHaveBeenCalled();
|
||||
expect(mockHandle.unsubscribe).not.toHaveBeenCalled();
|
||||
// Destroy the scope
|
||||
mockScope.$on.mostRecentCall.args[1]();
|
||||
// Should have unsubscribed
|
||||
expect(mockSubscription.unsubscribe).toHaveBeenCalled();
|
||||
expect(mockHandle.unsubscribe).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
151
platform/features/plot/test/elements/PlotLineBufferSpec.js
Normal file
151
platform/features/plot/test/elements/PlotLineBufferSpec.js
Normal file
@ -0,0 +1,151 @@
|
||||
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
|
||||
|
||||
/**
|
||||
* MergeModelsSpec. Created by vwoeltje on 11/6/14.
|
||||
*/
|
||||
define(
|
||||
["../../src/elements/PlotLineBuffer"],
|
||||
function (PlotLineBuffer) {
|
||||
"use strict";
|
||||
|
||||
var TEST_INITIAL_SIZE = 10,
|
||||
TEST_MAX_SIZE = 40,
|
||||
TEST_DOMAIN_OFFSET = 42;
|
||||
|
||||
describe("A plot line buffer", function () {
|
||||
var mockSeries,
|
||||
testDomainValues,
|
||||
testRangeValues,
|
||||
buffer;
|
||||
|
||||
beforeEach(function () {
|
||||
testDomainValues = [ 1, 3, 7, 9, 14, 15 ];
|
||||
testRangeValues = [ 8, 0, 3, 9, 8, 11 ];
|
||||
mockSeries = jasmine.createSpyObj(
|
||||
"series",
|
||||
['getPointCount', 'getDomainValue', 'getRangeValue']
|
||||
);
|
||||
mockSeries.getPointCount.andCallFake(function () {
|
||||
return testDomainValues.length;
|
||||
});
|
||||
mockSeries.getDomainValue.andCallFake(function (i) {
|
||||
return testDomainValues[i];
|
||||
});
|
||||
mockSeries.getRangeValue.andCallFake(function (i) {
|
||||
return testRangeValues[i];
|
||||
});
|
||||
|
||||
buffer = new PlotLineBuffer(
|
||||
TEST_DOMAIN_OFFSET,
|
||||
TEST_INITIAL_SIZE,
|
||||
TEST_MAX_SIZE
|
||||
);
|
||||
|
||||
// Start with some data in there
|
||||
buffer.insert(mockSeries, 0);
|
||||
});
|
||||
|
||||
it("allows insertion of series data", function () {
|
||||
// Convert to a regular array for checking.
|
||||
// Verify that domain/ranges were interleaved and
|
||||
// that domain offset was adjusted for.
|
||||
expect(
|
||||
Array.prototype.slice.call(buffer.getBuffer()).slice(0, 12)
|
||||
).toEqual([ -41, 8, -39, 0, -35, 3, -33, 9, -28, 8, -27, 11]);
|
||||
expect(buffer.getLength()).toEqual(6);
|
||||
});
|
||||
|
||||
it("finds insertion indexes", function () {
|
||||
expect(buffer.findInsertionIndex(0)).toEqual(0);
|
||||
expect(buffer.findInsertionIndex(2)).toEqual(1);
|
||||
expect(buffer.findInsertionIndex(5)).toEqual(2);
|
||||
expect(buffer.findInsertionIndex(10)).toEqual(4);
|
||||
expect(buffer.findInsertionIndex(14.5)).toEqual(5);
|
||||
expect(buffer.findInsertionIndex(20)).toEqual(6);
|
||||
|
||||
// 9 is already in there, disallow insertion
|
||||
expect(buffer.findInsertionIndex(9)).toEqual(-1);
|
||||
});
|
||||
|
||||
it("allows insertion in the middle", function () {
|
||||
var head = [ -41, 8, -39, 0, -35, 3 ],
|
||||
tail = [ -33, 9, -28, 8, -27, 11];
|
||||
buffer.insert(mockSeries, 3);
|
||||
expect(
|
||||
Array.prototype.slice.call(buffer.getBuffer()).slice(0, 24)
|
||||
).toEqual(head.concat(head).concat(tail).concat(tail));
|
||||
expect(buffer.getLength()).toEqual(12);
|
||||
});
|
||||
|
||||
it("allows values to be trimmed from the start", function () {
|
||||
buffer.trim(2);
|
||||
expect(buffer.getLength()).toEqual(4);
|
||||
expect(
|
||||
Array.prototype.slice.call(buffer.getBuffer()).slice(0, 8)
|
||||
).toEqual([ -35, 3, -33, 9, -28, 8, -27, 11]);
|
||||
});
|
||||
|
||||
it("expands buffer when needed to accommodate more data", function () {
|
||||
var i;
|
||||
|
||||
// Initial underlying buffer should be twice initial size...
|
||||
// (Since each pair will take up two elements)
|
||||
expect(buffer.getBuffer().length).toEqual(20);
|
||||
|
||||
// Should be able to insert 6 series of 6 points each
|
||||
// (After that, we'll hit the test max of 40)
|
||||
for (i = 1; i < 15; i += 1) {
|
||||
expect(buffer.insertPoint(i * 10, Math.sin(i), i))
|
||||
.toBeTruthy();
|
||||
}
|
||||
|
||||
// Buffer should have expanded in the process
|
||||
expect(buffer.getBuffer().length).toEqual(40);
|
||||
|
||||
// Push to maximum size just to make sure...
|
||||
for (i = 1; i < 150; i += 1) {
|
||||
buffer.insertPoint(i * 10, Math.sin(i), i);
|
||||
}
|
||||
|
||||
expect(buffer.getBuffer().length).toEqual(80);
|
||||
});
|
||||
|
||||
it("ensures a maximum size", function () {
|
||||
var i;
|
||||
|
||||
// Should be able to insert 6 series of 6 points each
|
||||
// (After that, we'll hit the test max of 40)
|
||||
for (i = 1; i < 6; i += 1) {
|
||||
expect(buffer.getLength()).toEqual(6 * i);
|
||||
expect(buffer.insert(mockSeries, Number.POSITIVE_INFINITY))
|
||||
.toBeTruthy();
|
||||
}
|
||||
|
||||
// Should be maxed out now
|
||||
expect(buffer.getLength()).toEqual(36);
|
||||
expect(buffer.insert(mockSeries, Number.POSITIVE_INFINITY))
|
||||
.toBeFalsy();
|
||||
expect(buffer.getLength()).toEqual(36);
|
||||
|
||||
});
|
||||
|
||||
it("reduces buffer size when space is no longer needed", function () {
|
||||
// Check that actual buffer is sized to the initial size
|
||||
// (double TEST_INITIAL_SIZE, since two elements are needed per
|
||||
// point; one for domain, one for range)
|
||||
expect(buffer.getBuffer().length).toEqual(20);
|
||||
// Should have 6 elements now... grow to 24
|
||||
buffer.insert(mockSeries, Number.POSITIVE_INFINITY);
|
||||
buffer.insert(mockSeries, Number.POSITIVE_INFINITY);
|
||||
buffer.insert(mockSeries, Number.POSITIVE_INFINITY);
|
||||
// This should have doubled the actual buffer size
|
||||
expect(buffer.getBuffer().length).toEqual(80);
|
||||
// Remove some values
|
||||
buffer.trim(20);
|
||||
// Actual buffer size should have been reduced accordingly
|
||||
expect(buffer.getBuffer().length).toBeLessThan(80);
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
114
platform/features/plot/test/elements/PlotLineSpec.js
Normal file
114
platform/features/plot/test/elements/PlotLineSpec.js
Normal file
@ -0,0 +1,114 @@
|
||||
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
|
||||
|
||||
define(
|
||||
["../../src/elements/PlotLine"],
|
||||
function (PlotLine) {
|
||||
"use strict";
|
||||
|
||||
describe("A plot line", function () {
|
||||
var mockBuffer,
|
||||
mockSeries,
|
||||
testDomainBuffer,
|
||||
testRangeBuffer,
|
||||
testSeries,
|
||||
line;
|
||||
|
||||
beforeEach(function () {
|
||||
testDomainBuffer = [];
|
||||
testRangeBuffer = [];
|
||||
testSeries = [];
|
||||
|
||||
mockBuffer = jasmine.createSpyObj(
|
||||
'buffer',
|
||||
['findInsertionIndex', 'insert', 'insertPoint', 'trim']
|
||||
);
|
||||
mockSeries = jasmine.createSpyObj(
|
||||
'series',
|
||||
['getPointCount', 'getDomainValue', 'getRangeValue']
|
||||
);
|
||||
|
||||
mockSeries.getPointCount.andCallFake(function () {
|
||||
return testSeries.length;
|
||||
});
|
||||
mockSeries.getDomainValue.andCallFake(function (i) {
|
||||
return (testSeries[i] || [])[0];
|
||||
});
|
||||
mockSeries.getRangeValue.andCallFake(function (i) {
|
||||
return (testSeries[i] || [])[1];
|
||||
});
|
||||
|
||||
// Function like PlotLineBuffer, to aid in testability
|
||||
mockBuffer.findInsertionIndex.andCallFake(function (v) {
|
||||
var index = 0;
|
||||
if (testDomainBuffer.indexOf(v) !== -1) {
|
||||
return -1;
|
||||
}
|
||||
while ((index < testDomainBuffer.length) &&
|
||||
(testDomainBuffer[index] < v)) {
|
||||
index += 1;
|
||||
}
|
||||
return index;
|
||||
});
|
||||
mockBuffer.insert.andCallFake(function (series, index) {
|
||||
var domains = [], ranges = [], i;
|
||||
for (i = 0; i < series.getPointCount(); i += 1) {
|
||||
domains.push(series.getDomainValue(i));
|
||||
ranges.push(series.getRangeValue(i));
|
||||
}
|
||||
testDomainBuffer = testDomainBuffer.slice(0, index)
|
||||
.concat(domains)
|
||||
.concat(testDomainBuffer.slice(index));
|
||||
testRangeBuffer = testRangeBuffer.slice(0, index)
|
||||
.concat(ranges)
|
||||
.concat(testRangeBuffer.slice(index));
|
||||
return true;
|
||||
});
|
||||
mockBuffer.insertPoint.andCallFake(function (dv, rv, index) {
|
||||
testDomainBuffer.splice(index, 0, dv);
|
||||
testRangeBuffer.splice(index, 0, rv);
|
||||
return true;
|
||||
});
|
||||
|
||||
line = new PlotLine(mockBuffer);
|
||||
});
|
||||
|
||||
it("allows single point insertion", function () {
|
||||
line.addPoint(100, 200);
|
||||
line.addPoint(50, 42);
|
||||
line.addPoint(150, 12321);
|
||||
// Should have managed insertion index choices to get to...
|
||||
expect(testDomainBuffer).toEqual([50, 100, 150]);
|
||||
expect(testRangeBuffer).toEqual([42, 200, 12321]);
|
||||
});
|
||||
|
||||
it("allows series insertion", function () {
|
||||
testSeries = [ [ 50, 42 ], [ 100, 200 ], [ 150, 12321 ] ];
|
||||
line.addSeries(mockSeries);
|
||||
// Should have managed insertion index choices to get to...
|
||||
expect(testDomainBuffer).toEqual([50, 100, 150]);
|
||||
expect(testRangeBuffer).toEqual([42, 200, 12321]);
|
||||
});
|
||||
|
||||
it("splits series insertion when necessary", function () {
|
||||
testSeries = [ [ 50, 42 ], [ 100, 200 ], [ 150, 12321 ] ];
|
||||
line.addPoint(75, 1);
|
||||
line.addSeries(mockSeries);
|
||||
// Should have managed insertion index choices to get to...
|
||||
expect(testDomainBuffer).toEqual([50, 75, 100, 150]);
|
||||
expect(testRangeBuffer).toEqual([42, 1, 200, 12321]);
|
||||
});
|
||||
|
||||
it("attempts to remove points when insertion fails", function () {
|
||||
// Verify precondition - normally doesn't try to trim
|
||||
line.addPoint(1, 2);
|
||||
expect(mockBuffer.trim).not.toHaveBeenCalled();
|
||||
|
||||
// But if insertPoint fails, it should trim
|
||||
mockBuffer.insertPoint.andReturn(false);
|
||||
line.addPoint(2, 3);
|
||||
expect(mockBuffer.trim).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
74
platform/features/plot/test/elements/PlotSeriesWindowSpec.js
Normal file
74
platform/features/plot/test/elements/PlotSeriesWindowSpec.js
Normal file
@ -0,0 +1,74 @@
|
||||
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
|
||||
|
||||
define(
|
||||
["../../src/elements/PlotSeriesWindow"],
|
||||
function (PlotSeriesWindow) {
|
||||
"use strict";
|
||||
|
||||
describe("A plot's window on a telemetry series", function () {
|
||||
var mockSeries,
|
||||
testSeries,
|
||||
window;
|
||||
|
||||
beforeEach(function () {
|
||||
testSeries = [
|
||||
[ 0, 42 ],
|
||||
[ 10, 1 ],
|
||||
[ 20, 4 ],
|
||||
[ 30, 9 ],
|
||||
[ 40, 3 ]
|
||||
];
|
||||
|
||||
mockSeries = jasmine.createSpyObj(
|
||||
'series',
|
||||
['getPointCount', 'getDomainValue', 'getRangeValue']
|
||||
);
|
||||
|
||||
mockSeries.getPointCount.andCallFake(function () {
|
||||
return testSeries.length;
|
||||
});
|
||||
mockSeries.getDomainValue.andCallFake(function (i) {
|
||||
return testSeries[i][0];
|
||||
});
|
||||
mockSeries.getRangeValue.andCallFake(function (i) {
|
||||
return testSeries[i][1];
|
||||
});
|
||||
|
||||
window = new PlotSeriesWindow(
|
||||
mockSeries,
|
||||
"testDomain",
|
||||
"testRange",
|
||||
1,
|
||||
testSeries.length
|
||||
);
|
||||
});
|
||||
|
||||
it("provides a window upon a data series", function () {
|
||||
expect(window.getPointCount()).toEqual(4);
|
||||
expect(window.getDomainValue(0)).toEqual(10);
|
||||
expect(window.getRangeValue(0)).toEqual(1);
|
||||
});
|
||||
|
||||
it("looks up using specific domain/range keys", function () {
|
||||
window.getDomainValue(0);
|
||||
window.getRangeValue(0);
|
||||
expect(mockSeries.getDomainValue)
|
||||
.toHaveBeenCalledWith(1, 'testDomain');
|
||||
expect(mockSeries.getRangeValue)
|
||||
.toHaveBeenCalledWith(1, 'testRange');
|
||||
});
|
||||
|
||||
it("can be split into smaller windows", function () {
|
||||
var windows = window.split();
|
||||
expect(windows.length).toEqual(2);
|
||||
expect(windows[0].getPointCount()).toEqual(2);
|
||||
expect(windows[1].getPointCount()).toEqual(2);
|
||||
expect(windows[0].getDomainValue(0)).toEqual(10);
|
||||
expect(windows[1].getDomainValue(0)).toEqual(30);
|
||||
expect(windows[0].getRangeValue(0)).toEqual(1);
|
||||
expect(windows[1].getRangeValue(0)).toEqual(9);
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
@ -14,6 +14,7 @@ define(
|
||||
testRange,
|
||||
testDomainValues,
|
||||
testRangeValues,
|
||||
mockSeries,
|
||||
updater;
|
||||
|
||||
function makeMockDomainObject(id) {
|
||||
@ -33,6 +34,10 @@ define(
|
||||
"subscription",
|
||||
[ "getDomainValue", "getRangeValue", "getTelemetryObjects" ]
|
||||
);
|
||||
mockSeries = jasmine.createSpyObj(
|
||||
'series',
|
||||
['getPointCount', 'getDomainValue', 'getRangeValue']
|
||||
);
|
||||
testDomain = "testDomain";
|
||||
testRange = "testRange";
|
||||
testDomainValues = { a: 3, b: 7, c: 13 };
|
||||
@ -55,57 +60,14 @@ define(
|
||||
});
|
||||
|
||||
it("provides one buffer per telemetry object", function () {
|
||||
expect(updater.getBuffers().length).toEqual(3);
|
||||
expect(updater.getLineBuffers().length).toEqual(3);
|
||||
});
|
||||
|
||||
it("changes buffer count if telemetry object counts change", function () {
|
||||
mockSubscription.getTelemetryObjects
|
||||
.andReturn([makeMockDomainObject('a')]);
|
||||
updater.update();
|
||||
expect(updater.getBuffers().length).toEqual(1);
|
||||
});
|
||||
|
||||
it("maintains a buffer of received telemetry", function () {
|
||||
// Count should be large enough to trigger a buffer resize
|
||||
var count = 750,
|
||||
i;
|
||||
|
||||
// Increment values exposed by subscription
|
||||
function increment() {
|
||||
Object.keys(testDomainValues).forEach(function (k) {
|
||||
testDomainValues[k] += 1;
|
||||
testRangeValues[k] += 1;
|
||||
});
|
||||
}
|
||||
|
||||
// Simulate a lot of telemetry updates
|
||||
for (i = 0; i < count; i += 1) {
|
||||
updater.update();
|
||||
expect(updater.getLength(0)).toEqual(i + 1);
|
||||
expect(updater.getLength(1)).toEqual(i + 1);
|
||||
expect(updater.getLength(2)).toEqual(i + 1);
|
||||
increment();
|
||||
}
|
||||
|
||||
// Domain offset should be lowest domain value
|
||||
expect(updater.getDomainOffset()).toEqual(3);
|
||||
|
||||
// Test against initial values, offset by count,
|
||||
// as was the case during each update
|
||||
for (i = 0; i < count; i += 1) {
|
||||
expect(updater.getBuffers()[0][i * 2])
|
||||
.toEqual(3 + i - 3);
|
||||
expect(updater.getBuffers()[0][i * 2 + 1])
|
||||
.toEqual(123 + i);
|
||||
expect(updater.getBuffers()[1][i * 2])
|
||||
.toEqual(7 + i - 3);
|
||||
expect(updater.getBuffers()[1][i * 2 + 1])
|
||||
.toEqual(456 + i);
|
||||
expect(updater.getBuffers()[2][i * 2])
|
||||
.toEqual(13 + i - 3);
|
||||
expect(updater.getBuffers()[2][i * 2 + 1])
|
||||
.toEqual(789 + i);
|
||||
}
|
||||
expect(updater.getLineBuffers().length).toEqual(1);
|
||||
});
|
||||
|
||||
it("can handle delayed telemetry object availability", function () {
|
||||
@ -124,7 +86,7 @@ define(
|
||||
);
|
||||
|
||||
// Should have 0 buffers for 0 objects
|
||||
expect(updater.getBuffers().length).toEqual(0);
|
||||
expect(updater.getLineBuffers().length).toEqual(0);
|
||||
|
||||
// Restore the three objects the test subscription would
|
||||
// normally have.
|
||||
@ -132,30 +94,79 @@ define(
|
||||
updater.update();
|
||||
|
||||
// Should have 3 buffers for 3 objects
|
||||
expect(updater.getBuffers().length).toEqual(3);
|
||||
expect(updater.getLineBuffers().length).toEqual(3);
|
||||
});
|
||||
|
||||
it("accepts historical telemetry updates", function () {
|
||||
var mockObject = mockSubscription.getTelemetryObjects()[0];
|
||||
|
||||
it("shifts buffer upon expansion", function () {
|
||||
// Count should be large enough to hit buffer's max size
|
||||
var count = 1400,
|
||||
i;
|
||||
mockSeries.getPointCount.andReturn(3);
|
||||
mockSeries.getDomainValue.andCallFake(function (i) {
|
||||
return 1000 + i * 1000;
|
||||
});
|
||||
mockSeries.getRangeValue.andReturn(10);
|
||||
|
||||
// Initial update; should have 3 in first position
|
||||
// (a's initial domain value)
|
||||
// PlotLine & PlotLineBuffer are tested for most of the
|
||||
// details here, so just check for some expected side
|
||||
// effect; in this case, should see more points in the buffer
|
||||
expect(updater.getLineBuffers()[0].getLength()).toEqual(1);
|
||||
updater.addHistorical(mockObject, mockSeries);
|
||||
expect(updater.getLineBuffers()[0].getLength()).toEqual(4);
|
||||
});
|
||||
|
||||
it("clears the domain offset if no objects are present", function () {
|
||||
mockSubscription.getTelemetryObjects.andReturn([]);
|
||||
updater.update();
|
||||
expect(updater.getBuffers()[0][1]).toEqual(123);
|
||||
|
||||
// Simulate a lot of telemetry updates
|
||||
for (i = 0; i < count; i += 1) {
|
||||
testDomainValues.a += 1;
|
||||
testRangeValues.a += 1;
|
||||
updater.update();
|
||||
}
|
||||
|
||||
// Value at front of the buffer should have been pushed out
|
||||
expect(updater.getBuffers()[0][1]).not.toEqual(123);
|
||||
expect(updater.getDomainOffset()).toBeUndefined();
|
||||
});
|
||||
|
||||
it("handles empty historical telemetry updates", function () {
|
||||
// General robustness check for when a series is empty
|
||||
var mockObject = mockSubscription.getTelemetryObjects()[0];
|
||||
|
||||
mockSeries.getPointCount.andReturn(0);
|
||||
mockSeries.getDomainValue.andCallFake(function (i) {
|
||||
return 1000 + i * 1000;
|
||||
});
|
||||
mockSeries.getRangeValue.andReturn(10);
|
||||
|
||||
// PlotLine & PlotLineBuffer are tested for most of the
|
||||
// details here, so just check for some expected side
|
||||
// effect; in this case, should see more points in the buffer
|
||||
expect(updater.getLineBuffers()[0].getLength()).toEqual(1);
|
||||
updater.addHistorical(mockObject, mockSeries);
|
||||
expect(updater.getLineBuffers()[0].getLength()).toEqual(1);
|
||||
});
|
||||
|
||||
it("can initialize domain offset from historical telemetry", function () {
|
||||
var tmp = mockSubscription.getTelemetryObjects();
|
||||
|
||||
mockSubscription.getTelemetryObjects.andReturn([]);
|
||||
|
||||
// Reinstantiate with the empty subscription
|
||||
updater = new PlotUpdater(
|
||||
mockSubscription,
|
||||
testDomain,
|
||||
testRange
|
||||
);
|
||||
|
||||
// Restore subscription, provide some historical data
|
||||
mockSubscription.getTelemetryObjects.andReturn(tmp);
|
||||
mockSeries.getPointCount.andReturn(3);
|
||||
mockSeries.getDomainValue.andCallFake(function (i) {
|
||||
return 1000 + i * 1000;
|
||||
});
|
||||
mockSeries.getRangeValue.andReturn(10);
|
||||
|
||||
// PlotLine & PlotLineBuffer are tested for most of the
|
||||
// details here, so just check for some expected side
|
||||
// effect; in this case, should see more points in the buffer
|
||||
expect(updater.getDomainOffset()).toBeUndefined();
|
||||
updater.addHistorical(tmp[0], mockSeries);
|
||||
expect(updater.getDomainOffset()).toBeDefined();
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
}
|
||||
);
|
@ -57,18 +57,30 @@ define(
|
||||
// Prepared telemetry data
|
||||
mockPrepared = jasmine.createSpyObj(
|
||||
"prepared",
|
||||
[ "getDomainOffset", "getOrigin", "getDimensions", "getBuffers", "getLength" ]
|
||||
[
|
||||
"getDomainOffset",
|
||||
"getOrigin",
|
||||
"getDimensions",
|
||||
"getLineBuffers"
|
||||
]
|
||||
);
|
||||
|
||||
mockSubPlotFactory.createSubPlot.andCallFake(createMockSubPlot);
|
||||
|
||||
// Act as if we have three buffers full of data
|
||||
testBuffers = [["a"], ["b"], ["c"]];
|
||||
mockPrepared.getBuffers.andReturn(testBuffers);
|
||||
testBuffers = ['a', 'b', 'c'].map(function (id) {
|
||||
var mockBuffer = jasmine.createSpyObj(
|
||||
'buffer-' + id,
|
||||
['getBuffer', 'getLength']
|
||||
);
|
||||
mockBuffer.getBuffer.andReturn([id]);
|
||||
mockBuffer.getLength.andReturn(3);
|
||||
return mockBuffer;
|
||||
});
|
||||
mockPrepared.getLineBuffers.andReturn(testBuffers);
|
||||
mockPrepared.getDomainOffset.andReturn(1234);
|
||||
mockPrepared.getOrigin.andReturn([10, 10]);
|
||||
mockPrepared.getDimensions.andReturn([500, 500]);
|
||||
mockPrepared.getLength.andReturn(3);
|
||||
|
||||
// Clear out drawing objects
|
||||
testDrawingObjects = [];
|
||||
@ -104,7 +116,7 @@ define(
|
||||
// Make sure the right buffer was drawn to the
|
||||
// right subplot.
|
||||
testDrawingObject.lines.forEach(function (line, j) {
|
||||
expect(line.buffer).toEqual(testBuffers[j]);
|
||||
expect(line.buffer).toEqual(testBuffers[j].getBuffer());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -57,18 +57,25 @@ define(
|
||||
// Prepared telemetry data
|
||||
mockPrepared = jasmine.createSpyObj(
|
||||
"prepared",
|
||||
[ "getDomainOffset", "getOrigin", "getDimensions", "getBuffers", "getLength" ]
|
||||
[ "getDomainOffset", "getOrigin", "getDimensions", "getLineBuffers" ]
|
||||
);
|
||||
|
||||
mockSubPlotFactory.createSubPlot.andCallFake(createMockSubPlot);
|
||||
|
||||
// Act as if we have three buffers full of data
|
||||
testBuffers = [["a"], ["b"], ["c"]];
|
||||
mockPrepared.getBuffers.andReturn(testBuffers);
|
||||
testBuffers = ['a', 'b', 'c'].map(function (id) {
|
||||
var mockBuffer = jasmine.createSpyObj(
|
||||
'buffer-' + id,
|
||||
['getBuffer', 'getLength']
|
||||
);
|
||||
mockBuffer.getBuffer.andReturn([id]);
|
||||
mockBuffer.getLength.andReturn(3);
|
||||
return mockBuffer;
|
||||
});
|
||||
mockPrepared.getLineBuffers.andReturn(testBuffers);
|
||||
mockPrepared.getDomainOffset.andReturn(1234);
|
||||
mockPrepared.getOrigin.andReturn([10, 10]);
|
||||
mockPrepared.getDimensions.andReturn([500, 500]);
|
||||
mockPrepared.getLength.andReturn(3);
|
||||
|
||||
// Objects that will be drawn to in sub-plots
|
||||
testDrawingObjects = [];
|
||||
@ -104,7 +111,7 @@ define(
|
||||
// Make sure the right buffer was drawn to the
|
||||
// right subplot.
|
||||
expect(testDrawingObject.lines[0].buffer)
|
||||
.toEqual(testBuffers[i]);
|
||||
.toEqual(testBuffers[i].getBuffer());
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -6,11 +6,14 @@
|
||||
"SubPlot",
|
||||
"SubPlotFactory",
|
||||
"elements/PlotAxis",
|
||||
"elements/PlotLine",
|
||||
"elements/PlotLineBuffer",
|
||||
"elements/PlotPalette",
|
||||
"elements/PlotPanZoomStack",
|
||||
"elements/PlotPanZoomStackGroup",
|
||||
"elements/PlotPosition",
|
||||
"elements/PlotPreparer",
|
||||
"elements/PlotSeriesWindow",
|
||||
"elements/PlotTickGenerator",
|
||||
"elements/PlotUpdater",
|
||||
"modes/PlotModeOptions",
|
||||
|
@ -43,6 +43,11 @@
|
||||
"key": "telemetrySubscriber",
|
||||
"implementation": "TelemetrySubscriber.js",
|
||||
"depends": [ "$q", "$timeout" ]
|
||||
},
|
||||
{
|
||||
"key": "telemetryHandler",
|
||||
"implementation": "TelemetryHandler.js",
|
||||
"depends": [ "$q", "telemetrySubscriber" ]
|
||||
}
|
||||
],
|
||||
"licenses": [
|
||||
|
45
platform/telemetry/src/TelemetryDelegator.js
Normal file
45
platform/telemetry/src/TelemetryDelegator.js
Normal file
@ -0,0 +1,45 @@
|
||||
/*global define*/
|
||||
|
||||
define(
|
||||
[],
|
||||
function () {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Used to handle telemetry delegation associated with a
|
||||
* given domain object.
|
||||
*/
|
||||
function TelemetryDelegator($q) {
|
||||
return {
|
||||
/**
|
||||
* Promise telemetry-providing objects associated with
|
||||
* this domain object (either the domain object itself,
|
||||
* or the objects it delegates)
|
||||
* @returns {Promise.<DomainObject[]>} domain objects with
|
||||
* a telemetry capability
|
||||
*/
|
||||
promiseTelemetryObjects: function (domainObject) {
|
||||
// If object has been cleared, there are no relevant
|
||||
// telemetry-providing domain objects.
|
||||
if (!domainObject) {
|
||||
return $q.when([]);
|
||||
}
|
||||
|
||||
// Otherwise, try delegation first, and attach the
|
||||
// object itself if it has a telemetry capability.
|
||||
return $q.when(domainObject.useCapability(
|
||||
"delegation",
|
||||
"telemetry"
|
||||
)).then(function (result) {
|
||||
var head = domainObject.hasCapability("telemetry") ?
|
||||
[ domainObject ] : [],
|
||||
tail = result || [];
|
||||
return head.concat(tail);
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return TelemetryDelegator;
|
||||
}
|
||||
);
|
91
platform/telemetry/src/TelemetryHandle.js
Normal file
91
platform/telemetry/src/TelemetryHandle.js
Normal file
@ -0,0 +1,91 @@
|
||||
/*global define*/
|
||||
|
||||
define(
|
||||
[],
|
||||
function () {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* A telemetry handle acts as a helper in issuing requests for
|
||||
* new telemetry as well as subscribing to real-time updates
|
||||
* for the same telemetry series. This is exposed through the
|
||||
* `telemetryHandler` service.
|
||||
* @param $q Angular's $q, for promises
|
||||
* @param {TelemetrySubscription} subscription a subscription
|
||||
* to supplied telemetry
|
||||
*/
|
||||
function TelemetryHandle($q, subscription) {
|
||||
var seriesMap = {},
|
||||
self = Object.create(subscription);
|
||||
|
||||
// Request a telemetry series for this specific object
|
||||
function requestSeries(telemetryObject, request, callback) {
|
||||
var id = telemetryObject.getId(),
|
||||
telemetry = telemetryObject.getCapability('telemetry');
|
||||
|
||||
function receiveSeries(series) {
|
||||
// Store it for subsequent lookup
|
||||
seriesMap[id] = series;
|
||||
// Notify callback of new series data, if there is one
|
||||
if (callback) {
|
||||
callback(telemetryObject, series);
|
||||
}
|
||||
// Pass it along for promise-chaining
|
||||
return series;
|
||||
}
|
||||
|
||||
// Issue the request via the object's telemetry capability
|
||||
return telemetry.requestData(request).then(receiveSeries);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the most recently obtained telemetry data series associated
|
||||
* with this domain object.
|
||||
* @param {DomainObject} the domain object which has telemetry
|
||||
* data associated with it
|
||||
* @return {TelemetrySeries} the most recent telemetry series
|
||||
* (or undefined if there is not one)
|
||||
*/
|
||||
self.getSeries = function (domainObject) {
|
||||
var id = domainObject.getId();
|
||||
return seriesMap[id];
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Change the request duration.
|
||||
* @param {object|number} request the duration of historical
|
||||
* data to look at; or, the request to issue
|
||||
* @param {Function} [callback] a callback that will be
|
||||
* invoked as new data becomes available, with the
|
||||
* domain object for which new data is available.
|
||||
*/
|
||||
self.request = function (request, callback) {
|
||||
// Issue (and handle) the new request from this object
|
||||
function issueRequest(telemetryObject) {
|
||||
return requestSeries(telemetryObject, request, callback);
|
||||
}
|
||||
|
||||
// Map the request to all telemetry objects
|
||||
function issueRequests(telemetryObjects) {
|
||||
return $q.all(telemetryObjects.map(issueRequest));
|
||||
}
|
||||
|
||||
// If the request is a simple number, treat it as a duration
|
||||
request = (typeof request === 'number') ?
|
||||
{ duration: request } : request;
|
||||
|
||||
// Look up telemetry-providing objects from the subscription,
|
||||
// then issue new requests.
|
||||
return subscription.promiseTelemetryObjects()
|
||||
.then(issueRequests);
|
||||
};
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
return TelemetryHandle;
|
||||
|
||||
}
|
||||
);
|
33
platform/telemetry/src/TelemetryHandler.js
Normal file
33
platform/telemetry/src/TelemetryHandler.js
Normal file
@ -0,0 +1,33 @@
|
||||
/*global define*/
|
||||
|
||||
define(
|
||||
['./TelemetryHandle'],
|
||||
function (TelemetryHandle) {
|
||||
"use strict";
|
||||
|
||||
|
||||
/**
|
||||
* A TelemetryRequester provides an easy interface to request
|
||||
* telemetry associated with a set of domain objects.
|
||||
*
|
||||
* @constructor
|
||||
* @param $q Angular's $q
|
||||
*/
|
||||
function TelemetryHandler($q, telemetrySubscriber) {
|
||||
return {
|
||||
handle: function (domainObject, callback, lossless) {
|
||||
var subscription = telemetrySubscriber.subscribe(
|
||||
domainObject,
|
||||
callback,
|
||||
lossless
|
||||
);
|
||||
|
||||
return new TelemetryHandle($q, subscription);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return TelemetryHandler;
|
||||
|
||||
}
|
||||
);
|
@ -1,8 +1,8 @@
|
||||
/*global define*/
|
||||
|
||||
define(
|
||||
['./TelemetryQueue', './TelemetryTable'],
|
||||
function (TelemetryQueue, TelemetryTable) {
|
||||
['./TelemetryQueue', './TelemetryTable', './TelemetryDelegator'],
|
||||
function (TelemetryQueue, TelemetryTable, TelemetryDelegator) {
|
||||
"use strict";
|
||||
|
||||
|
||||
@ -31,7 +31,9 @@ define(
|
||||
* the callback once, with access to the latest data
|
||||
*/
|
||||
function TelemetrySubscription($q, $timeout, domainObject, callback, lossless) {
|
||||
var unsubscribePromise,
|
||||
var delegator = new TelemetryDelegator($q),
|
||||
unsubscribePromise,
|
||||
telemetryObjectPromise,
|
||||
latestValues = {},
|
||||
telemetryObjects = [],
|
||||
pool = lossless ? new TelemetryQueue() : new TelemetryTable(),
|
||||
@ -42,23 +44,7 @@ define(
|
||||
// This will either be the object in view, or object that
|
||||
// this object delegates its telemetry capability to.
|
||||
function promiseRelevantObjects(domainObject) {
|
||||
// If object has been cleared, there are no relevant
|
||||
// telemetry-providing domain objects.
|
||||
if (!domainObject) {
|
||||
return $q.when([]);
|
||||
}
|
||||
|
||||
// Otherwise, try delegation first, and attach the
|
||||
// object itself if it has a telemetry capability.
|
||||
return $q.when(domainObject.useCapability(
|
||||
"delegation",
|
||||
"telemetry"
|
||||
)).then(function (result) {
|
||||
var head = domainObject.hasCapability("telemetry") ?
|
||||
[ domainObject ] : [],
|
||||
tail = result || [];
|
||||
return head.concat(tail);
|
||||
});
|
||||
return delegator.promiseTelemetryObjects(domainObject);
|
||||
}
|
||||
|
||||
function updateValuesFromPool() {
|
||||
@ -152,8 +138,8 @@ define(
|
||||
// will be unsubscribe functions. (This must be a promise
|
||||
// because delegation is supported, and retrieving delegate
|
||||
// telemetry-capable objects may be an asynchronous operation.)
|
||||
unsubscribePromise =
|
||||
promiseRelevantObjects(domainObject)
|
||||
telemetryObjectPromise = promiseRelevantObjects(domainObject);
|
||||
unsubscribePromise = telemetryObjectPromise
|
||||
.then(cacheObjectReferences)
|
||||
.then(subscribeAll);
|
||||
|
||||
@ -239,6 +225,17 @@ define(
|
||||
*/
|
||||
getMetadata: function () {
|
||||
return metadatas;
|
||||
},
|
||||
/**
|
||||
* Get a promise for all telemetry-providing objects
|
||||
* associated with this subscription.
|
||||
* @returns {Promise.<DomainObject[]>} a promise for
|
||||
* telemetry-providing objects
|
||||
*/
|
||||
promiseTelemetryObjects: function () {
|
||||
// Unsubscribe promise is available after objects
|
||||
// are loaded.
|
||||
return telemetryObjectPromise;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
12
platform/telemetry/test/TelemetryDelegatorSpec.js
Normal file
12
platform/telemetry/test/TelemetryDelegatorSpec.js
Normal file
@ -0,0 +1,12 @@
|
||||
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
|
||||
|
||||
define(
|
||||
["../src/TelemetryDelegator"],
|
||||
function (TelemetryDelegator) {
|
||||
"use strict";
|
||||
|
||||
describe("The telemetry delegator", function () {
|
||||
|
||||
});
|
||||
}
|
||||
);
|
88
platform/telemetry/test/TelemetryHandleSpec.js
Normal file
88
platform/telemetry/test/TelemetryHandleSpec.js
Normal file
@ -0,0 +1,88 @@
|
||||
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
|
||||
|
||||
define(
|
||||
["../src/TelemetryHandle"],
|
||||
function (TelemetryHandle) {
|
||||
"use strict";
|
||||
|
||||
describe("A telemetry handle", function () {
|
||||
var mockQ,
|
||||
mockSubscription,
|
||||
mockDomainObject,
|
||||
mockTelemetry,
|
||||
mockSeries,
|
||||
mockCallback,
|
||||
handle;
|
||||
|
||||
function asPromise(v) {
|
||||
return (v || {}).then ? v : {
|
||||
then: function (callback) {
|
||||
return asPromise(callback(v));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
mockQ = jasmine.createSpyObj('$q', ['when', 'all']);
|
||||
mockSubscription = jasmine.createSpyObj(
|
||||
'subscription',
|
||||
['unsubscribe', 'getTelemetryObjects', 'promiseTelemetryObjects']
|
||||
);
|
||||
mockDomainObject = jasmine.createSpyObj(
|
||||
'domainObject',
|
||||
['getId', 'getCapability']
|
||||
);
|
||||
mockTelemetry = jasmine.createSpyObj(
|
||||
'telemetry',
|
||||
['requestData']
|
||||
);
|
||||
mockSeries = jasmine.createSpyObj(
|
||||
'series',
|
||||
['getPointCount', 'getDomainValue', 'getRangeValue']
|
||||
);
|
||||
mockCallback = jasmine.createSpy('callback');
|
||||
|
||||
// Simulate $q.all, at least for asPromise-provided promises
|
||||
mockQ.all.andCallFake(function (values) {
|
||||
return values.map(function (v) {
|
||||
var r;
|
||||
asPromise(v).then(function (value) { r = value; });
|
||||
return r;
|
||||
});
|
||||
});
|
||||
mockQ.when.andCallFake(asPromise);
|
||||
mockSubscription.getTelemetryObjects
|
||||
.andReturn([mockDomainObject]);
|
||||
mockSubscription.promiseTelemetryObjects
|
||||
.andReturn(asPromise([mockDomainObject]));
|
||||
mockDomainObject.getId.andReturn('testId');
|
||||
mockDomainObject.getCapability.andReturn(mockTelemetry);
|
||||
mockTelemetry.requestData.andReturn(asPromise(mockSeries));
|
||||
|
||||
handle = new TelemetryHandle(mockQ, mockSubscription);
|
||||
});
|
||||
|
||||
it("exposes subscription API", function () {
|
||||
// Should still expose methods from the provided subscription
|
||||
expect(handle.unsubscribe)
|
||||
.toBe(mockSubscription.unsubscribe);
|
||||
expect(handle.getTelemetryObjects)
|
||||
.toBe(mockSubscription.getTelemetryObjects);
|
||||
});
|
||||
|
||||
it("provides an interface for historical requests", function () {
|
||||
handle.request({}, mockCallback);
|
||||
expect(mockCallback).toHaveBeenCalledWith(
|
||||
mockDomainObject,
|
||||
mockSeries
|
||||
);
|
||||
});
|
||||
|
||||
it("provides the latest series for domain objects", function () {
|
||||
handle.request({});
|
||||
expect(handle.getSeries(mockDomainObject))
|
||||
.toEqual(mockSeries);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
64
platform/telemetry/test/TelemetryHandlerSpec.js
Normal file
64
platform/telemetry/test/TelemetryHandlerSpec.js
Normal file
@ -0,0 +1,64 @@
|
||||
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
|
||||
|
||||
define(
|
||||
["../src/TelemetryHandler"],
|
||||
function (TelemetryHandler) {
|
||||
"use strict";
|
||||
|
||||
describe("The telemetry handler", function () {
|
||||
// TelemetryHandler just provides a factory
|
||||
// for TelemetryHandle, so most real testing
|
||||
// should happen there.
|
||||
var mockQ,
|
||||
mockSubscriber,
|
||||
mockDomainObject,
|
||||
mockCallback,
|
||||
mockSubscription,
|
||||
handler;
|
||||
|
||||
beforeEach(function () {
|
||||
mockQ = jasmine.createSpyObj("$q", ["when"]);
|
||||
mockSubscriber = jasmine.createSpyObj(
|
||||
'telemetrySubscriber',
|
||||
['subscribe']
|
||||
);
|
||||
mockDomainObject = jasmine.createSpyObj(
|
||||
'domainObject',
|
||||
['getId', 'getCapability']
|
||||
);
|
||||
mockCallback = jasmine.createSpy('callback');
|
||||
mockSubscription = jasmine.createSpyObj(
|
||||
'subscription',
|
||||
[
|
||||
'unsubscribe',
|
||||
'getTelemetryObjects',
|
||||
'getRangeValue',
|
||||
'getDomainValue'
|
||||
]
|
||||
);
|
||||
|
||||
mockSubscriber.subscribe.andReturn(mockSubscription);
|
||||
|
||||
handler = new TelemetryHandler(mockQ, mockSubscriber);
|
||||
});
|
||||
|
||||
it("acts as a factory for subscription objects", function () {
|
||||
var handle = handler.handle(
|
||||
mockDomainObject,
|
||||
mockCallback
|
||||
);
|
||||
// Just verify that this looks like a TelemetrySubscription
|
||||
[
|
||||
"unsubscribe",
|
||||
"getTelemetryObjects",
|
||||
"getRangeValue",
|
||||
"getDomainValue",
|
||||
"request"
|
||||
].forEach(function (method) {
|
||||
expect(handle[method]).toEqual(jasmine.any(Function));
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
@ -184,6 +184,14 @@ define(
|
||||
it("fires callback when telemetry objects are available", function () {
|
||||
expect(mockCallback.calls.length).toEqual(1);
|
||||
});
|
||||
|
||||
it("exposes a promise for telemetry objects", function () {
|
||||
var mockCallback2 = jasmine.createSpy('callback');
|
||||
subscription.promiseTelemetryObjects().then(mockCallback2);
|
||||
|
||||
expect(mockCallback2)
|
||||
.toHaveBeenCalledWith([ mockDomainObject ]);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
@ -2,7 +2,10 @@
|
||||
"TelemetryAggregator",
|
||||
"TelemetryCapability",
|
||||
"TelemetryController",
|
||||
"TelemetryDelegator",
|
||||
"TelemetryFormatter",
|
||||
"TelemetryHandle",
|
||||
"TelemetryHandler",
|
||||
"TelemetryQueue",
|
||||
"TelemetrySubscriber",
|
||||
"TelemetrySubscription",
|
||||
|
Loading…
Reference in New Issue
Block a user