Merge branch 'open806' into open-master

This commit is contained in:
Victor Woeltjen 2015-04-27 15:16:21 -07:00
commit 617691ab24
26 changed files with 1432 additions and 246 deletions

View File

@ -22,7 +22,7 @@
{
"key": "PlotController",
"implementation": "PlotController.js",
"depends": [ "$scope", "telemetryFormatter", "telemetrySubscriber" ]
"depends": [ "$scope", "telemetryFormatter", "telemetryHandler" ]
}
]
}

View File

@ -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;
}
}

View 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;
}
);

View 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;
}
);

View 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;
}
);

View File

@ -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
};
}

View File

@ -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()
};
});

View File

@ -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();

View File

@ -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();
});
});
}

View 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);
});
});
}
);

View 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();
});
});
}
);

View 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);
});
});
}
);

View File

@ -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();
});
});
}
);

View File

@ -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());
});
});
});

View File

@ -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());
});
});

View File

@ -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",

View File

@ -43,6 +43,11 @@
"key": "telemetrySubscriber",
"implementation": "TelemetrySubscriber.js",
"depends": [ "$q", "$timeout" ]
},
{
"key": "telemetryHandler",
"implementation": "TelemetryHandler.js",
"depends": [ "$q", "telemetrySubscriber" ]
}
],
"licenses": [

View 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;
}
);

View 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;
}
);

View 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;
}
);

View File

@ -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;
}
};
}

View 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 () {
});
}
);

View 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);
});
});
}
);

View 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));
});
});
});
}
);

View File

@ -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 ]);
});
});
}
);

View File

@ -2,7 +2,10 @@
"TelemetryAggregator",
"TelemetryCapability",
"TelemetryController",
"TelemetryDelegator",
"TelemetryFormatter",
"TelemetryHandle",
"TelemetryHandler",
"TelemetryQueue",
"TelemetrySubscriber",
"TelemetrySubscription",