mirror of
https://github.com/nasa/openmct.git
synced 2025-03-28 06:38:40 +00:00
Merge remote-tracking branch 'origin/open751' into open-master
This commit is contained in:
commit
61bb85df58
@ -22,7 +22,7 @@
|
|||||||
{
|
{
|
||||||
"key": "PlotController",
|
"key": "PlotController",
|
||||||
"implementation": "PlotController.js",
|
"implementation": "PlotController.js",
|
||||||
"depends": [ "$scope", "telemetryFormatter" ]
|
"depends": [ "$scope", "telemetryFormatter", "telemetrySubscriber" ]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
<span ng-controller="TelemetryController as telemetry">
|
|
||||||
<span ng-controller="PlotController as plot"
|
<span ng-controller="PlotController as plot"
|
||||||
ng-mouseleave="representation.showControls = false">
|
ng-mouseleave="representation.showControls = false">
|
||||||
|
|
||||||
@ -114,7 +113,7 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span class="t-wait-spinner loading" ng-show="telemetry.isRequestPending()">
|
<span class="t-wait-spinner loading" ng-show="plot.isRequestPending()">
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div ng-if="$last" class="gl-plot-axis-area gl-plot-x">
|
<div ng-if="$last" class="gl-plot-axis-area gl-plot-x">
|
||||||
@ -143,9 +142,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
|
||||||
|
|
||||||
|
@ -5,13 +5,13 @@
|
|||||||
*/
|
*/
|
||||||
define(
|
define(
|
||||||
[
|
[
|
||||||
"./elements/PlotPreparer",
|
"./elements/PlotUpdater",
|
||||||
"./elements/PlotPalette",
|
"./elements/PlotPalette",
|
||||||
"./elements/PlotAxis",
|
"./elements/PlotAxis",
|
||||||
"./modes/PlotModeOptions",
|
"./modes/PlotModeOptions",
|
||||||
"./SubPlotFactory"
|
"./SubPlotFactory"
|
||||||
],
|
],
|
||||||
function (PlotPreparer, PlotPalette, PlotAxis, PlotModeOptions, SubPlotFactory) {
|
function (PlotUpdater, PlotPalette, PlotAxis, PlotModeOptions, SubPlotFactory) {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
var AXIS_DEFAULTS = [
|
var AXIS_DEFAULTS = [
|
||||||
@ -30,10 +30,13 @@ define(
|
|||||||
*
|
*
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
function PlotController($scope, telemetryFormatter) {
|
function PlotController($scope, telemetryFormatter, telemetrySubscriber) {
|
||||||
var subPlotFactory = new SubPlotFactory(telemetryFormatter),
|
var subPlotFactory = new SubPlotFactory(telemetryFormatter),
|
||||||
modeOptions = new PlotModeOptions([], subPlotFactory),
|
modeOptions = new PlotModeOptions([], subPlotFactory),
|
||||||
subplots = [],
|
subplots = [],
|
||||||
|
cachedObjects = [],
|
||||||
|
updater,
|
||||||
|
subscription,
|
||||||
domainOffset;
|
domainOffset;
|
||||||
|
|
||||||
// Populate the scope with axis information (specifically, options
|
// Populate the scope with axis information (specifically, options
|
||||||
@ -45,38 +48,6 @@ define(
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Respond to newly-available telemetry data; update the
|
|
||||||
// drawing area accordingly.
|
|
||||||
function plotTelemetry() {
|
|
||||||
var prepared, datas, telemetry;
|
|
||||||
|
|
||||||
// Get a reference to the TelemetryController
|
|
||||||
telemetry = $scope.telemetry;
|
|
||||||
|
|
||||||
// Nothing to plot without TelemetryController
|
|
||||||
if (!telemetry) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure axes have been initialized (we will want to
|
|
||||||
// get the active axis below)
|
|
||||||
if (!$scope.axes) {
|
|
||||||
setupAxes(telemetry.getMetadata());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get data sets
|
|
||||||
datas = telemetry.getResponse();
|
|
||||||
|
|
||||||
// Prepare data sets for rendering
|
|
||||||
prepared = new PlotPreparer(
|
|
||||||
datas,
|
|
||||||
($scope.axes[0].active || {}).key,
|
|
||||||
($scope.axes[1].active || {}).key
|
|
||||||
);
|
|
||||||
|
|
||||||
modeOptions.getModeHandler().plotTelemetry(prepared);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trigger an update of a specific subplot;
|
// Trigger an update of a specific subplot;
|
||||||
// used in a loop to update all subplots.
|
// used in a loop to update all subplots.
|
||||||
function updateSubplot(subplot) {
|
function updateSubplot(subplot) {
|
||||||
@ -86,10 +57,13 @@ define(
|
|||||||
// Set up available modes (stacked/overlaid), based on the
|
// Set up available modes (stacked/overlaid), based on the
|
||||||
// set of telemetry objects in this plot view.
|
// set of telemetry objects in this plot view.
|
||||||
function setupModes(telemetryObjects) {
|
function setupModes(telemetryObjects) {
|
||||||
modeOptions = new PlotModeOptions(
|
if (cachedObjects !== telemetryObjects) {
|
||||||
telemetryObjects || [],
|
cachedObjects = telemetryObjects;
|
||||||
subPlotFactory
|
modeOptions = new PlotModeOptions(
|
||||||
);
|
telemetryObjects || [],
|
||||||
|
subPlotFactory
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update all sub-plots
|
// Update all sub-plots
|
||||||
@ -99,9 +73,47 @@ define(
|
|||||||
.forEach(updateSubplot);
|
.forEach(updateSubplot);
|
||||||
}
|
}
|
||||||
|
|
||||||
$scope.$watch("telemetry.getTelemetryObjects()", setupModes);
|
// Reinstantiate the plot updater (e.g. because we have a
|
||||||
$scope.$watch("telemetry.getMetadata()", setupAxes);
|
// new subscription.) This will clear the plot.
|
||||||
$scope.$on("telemetryUpdate", plotTelemetry);
|
function recreateUpdater() {
|
||||||
|
updater = new PlotUpdater(
|
||||||
|
subscription,
|
||||||
|
($scope.axes[0].active || {}).key,
|
||||||
|
($scope.axes[1].active || {}).key
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle new telemetry data in this plot
|
||||||
|
function updateValues() {
|
||||||
|
if (subscription) {
|
||||||
|
setupModes(subscription.getTelemetryObjects());
|
||||||
|
}
|
||||||
|
if (updater) {
|
||||||
|
updater.update();
|
||||||
|
modeOptions.getModeHandler().plotTelemetry(updater);
|
||||||
|
}
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new subscription; telemetrySubscriber gets
|
||||||
|
// to do the meaningful work here.
|
||||||
|
function subscribe(domainObject) {
|
||||||
|
if (subscription) {
|
||||||
|
subscription.unsubscribe();
|
||||||
|
}
|
||||||
|
subscription = domainObject && telemetrySubscriber.subscribe(
|
||||||
|
domainObject,
|
||||||
|
updateValues,
|
||||||
|
true // Lossless
|
||||||
|
);
|
||||||
|
if (subscription) {
|
||||||
|
setupModes(subscription.getTelemetryObjects());
|
||||||
|
setupAxes(subscription.getMetadata());
|
||||||
|
recreateUpdater();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.$watch('domainObject', subscribe);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
/**
|
/**
|
||||||
@ -156,7 +168,7 @@ define(
|
|||||||
*/
|
*/
|
||||||
setMode: function (mode) {
|
setMode: function (mode) {
|
||||||
modeOptions.setMode(mode);
|
modeOptions.setMode(mode);
|
||||||
plotTelemetry();
|
updateValues();
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Get all individual plots contained within this Plot view.
|
* Get all individual plots contained within this Plot view.
|
||||||
@ -169,7 +181,15 @@ define(
|
|||||||
/**
|
/**
|
||||||
* Explicitly update all plots.
|
* Explicitly update all plots.
|
||||||
*/
|
*/
|
||||||
update: update
|
update: update,
|
||||||
|
/**
|
||||||
|
* Check if a request is pending (to show the wait spinner)
|
||||||
|
*/
|
||||||
|
isRequestPending: function () {
|
||||||
|
// Placeholder; this should reflect request state
|
||||||
|
// when requesting historical telemetry
|
||||||
|
return false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
215
platform/features/plot/src/elements/PlotUpdater.js
Normal file
215
platform/features/plot/src/elements/PlotUpdater.js
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
/*global define,Float32Array*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepares data to be rendered in a GL Plot. Handles
|
||||||
|
* the conversion from data API to displayable buffers.
|
||||||
|
*/
|
||||||
|
define(
|
||||||
|
function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var MAX_POINTS = 86400,
|
||||||
|
INITIAL_SIZE = 675; // 1/128 of MAX_POINTS
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The PlotPreparer is responsible for handling data sets and
|
||||||
|
* preparing them to be rendered. It creates a WebGL-plottable
|
||||||
|
* Float32Array for each trace, and tracks the boundaries of the
|
||||||
|
* data sets (since this is convenient to do during the same pass).
|
||||||
|
* @constructor
|
||||||
|
* @param {Telemetry[]} datas telemetry data objects
|
||||||
|
* @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 = [];
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// If we don't already have a data buffer for that ID,
|
||||||
|
// make one.
|
||||||
|
if (!buffer) {
|
||||||
|
buffer = new Float32Array(INITIAL_SIZE);
|
||||||
|
buffers[id] = buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use a default MAX_POINTS if none is provided
|
||||||
|
maxPoints = maxPoints || MAX_POINTS;
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* Get the dimensions which bound all data in the provided
|
||||||
|
* data sets. This is given as a two-element array where the
|
||||||
|
* first element is domain, and second is range.
|
||||||
|
* @returns {number[]} the dimensions which bound this data set
|
||||||
|
*/
|
||||||
|
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]];
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Get the origin of this data set's boundary.
|
||||||
|
* This is given as a two-element array where the
|
||||||
|
* first element is domain, and second is range.
|
||||||
|
* The domain value here is not adjusted by the domain offset.
|
||||||
|
* @returns {number[]} the origin of this data set's boundary
|
||||||
|
*/
|
||||||
|
getOrigin: function () {
|
||||||
|
// Pad range if necessary
|
||||||
|
return (max[1] === min[1]) ? [ min[0], min[1] - 1.0 ] : min;
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Get the domain offset; this offset will have been subtracted
|
||||||
|
* from all domain values in all buffers returned by this
|
||||||
|
* preparer, in order to minimize loss-of-precision due to
|
||||||
|
* conversion to the 32-bit float format needed by WebGL.
|
||||||
|
* @returns {number} the domain offset
|
||||||
|
*/
|
||||||
|
getDomainOffset: function () {
|
||||||
|
return domainOffset;
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Get all renderable buffers for this data set. This will
|
||||||
|
* be returned as an array which can be correlated back to
|
||||||
|
* the provided telemetry data objects (from the constructor
|
||||||
|
* call) by index.
|
||||||
|
*
|
||||||
|
* Internally, these are flattened; each buffer contains a
|
||||||
|
* sequence of alternating domain and range values.
|
||||||
|
*
|
||||||
|
* All domain values in all buffers will have been adjusted
|
||||||
|
* from their original values by subtraction of the domain
|
||||||
|
* offset; this minimizes loss-of-precision resulting from
|
||||||
|
* the conversion to 32-bit floats, which may otherwise
|
||||||
|
* cause aliasing artifacts (particularly for timestamps)
|
||||||
|
*
|
||||||
|
* @returns {Float32Array[]} the buffers for these traces
|
||||||
|
*/
|
||||||
|
getBuffers: 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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return PlotUpdater;
|
||||||
|
|
||||||
|
}
|
||||||
|
);
|
@ -38,7 +38,7 @@ define(
|
|||||||
return {
|
return {
|
||||||
buffer: buf,
|
buffer: buf,
|
||||||
color: PlotPalette.getFloatColor(i),
|
color: PlotPalette.getFloatColor(i),
|
||||||
points: buf.length / 2
|
points: prepared.getLength(i)
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -35,7 +35,7 @@ define(
|
|||||||
subplot.getDrawingObject().lines = [{
|
subplot.getDrawingObject().lines = [{
|
||||||
buffer: buffer,
|
buffer: buffer,
|
||||||
color: PlotPalette.getFloatColor(0),
|
color: PlotPalette.getFloatColor(0),
|
||||||
points: buffer.length / 2
|
points: prepared.getLength(index)
|
||||||
}];
|
}];
|
||||||
|
|
||||||
subplot.update();
|
subplot.update();
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
|
/*global define,Promise,describe,it,expect,xit,beforeEach,waitsFor,jasmine*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MergeModelsSpec. Created by vwoeltje on 11/6/14.
|
* MergeModelsSpec. Created by vwoeltje on 11/6/14.
|
||||||
@ -11,12 +11,11 @@ define(
|
|||||||
describe("The plot controller", function () {
|
describe("The plot controller", function () {
|
||||||
var mockScope,
|
var mockScope,
|
||||||
mockFormatter,
|
mockFormatter,
|
||||||
mockTelemetry, // mock telemetry controller
|
mockSubscriber,
|
||||||
mockData,
|
mockSubscription,
|
||||||
mockDomainObject,
|
mockDomainObject,
|
||||||
controller;
|
controller;
|
||||||
|
|
||||||
function echo(i) { return i; }
|
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
mockScope = jasmine.createSpyObj(
|
mockScope = jasmine.createSpyObj(
|
||||||
@ -27,33 +26,32 @@ define(
|
|||||||
"formatter",
|
"formatter",
|
||||||
[ "formatDomainValue", "formatRangeValue" ]
|
[ "formatDomainValue", "formatRangeValue" ]
|
||||||
);
|
);
|
||||||
mockTelemetry = jasmine.createSpyObj(
|
|
||||||
"telemetry",
|
|
||||||
[ "getResponse", "getMetadata" ]
|
|
||||||
);
|
|
||||||
mockData = jasmine.createSpyObj(
|
|
||||||
"data",
|
|
||||||
[ "getPointCount", "getDomainValue", "getRangeValue" ]
|
|
||||||
);
|
|
||||||
mockDomainObject = jasmine.createSpyObj(
|
mockDomainObject = jasmine.createSpyObj(
|
||||||
"domainObject",
|
"domainObject",
|
||||||
[ "getId", "getModel", "getCapability" ]
|
[ "getId", "getModel", "getCapability" ]
|
||||||
);
|
);
|
||||||
|
mockSubscriber = jasmine.createSpyObj(
|
||||||
mockScope.telemetry = mockTelemetry;
|
"telemetrySubscriber",
|
||||||
mockTelemetry.getResponse.andReturn([mockData]);
|
["subscribe"]
|
||||||
mockData.getPointCount.andReturn(2);
|
|
||||||
mockData.getDomainValue.andCallFake(echo);
|
|
||||||
mockData.getRangeValue.andCallFake(echo);
|
|
||||||
|
|
||||||
controller = new PlotController(mockScope, mockFormatter);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("listens for telemetry updates", function () {
|
|
||||||
expect(mockScope.$on).toHaveBeenCalledWith(
|
|
||||||
"telemetryUpdate",
|
|
||||||
jasmine.any(Function)
|
|
||||||
);
|
);
|
||||||
|
mockSubscription = jasmine.createSpyObj(
|
||||||
|
"subscription",
|
||||||
|
[
|
||||||
|
"unsubscribe",
|
||||||
|
"getTelemetryObjects",
|
||||||
|
"getMetadata",
|
||||||
|
"getDomainValue",
|
||||||
|
"getRangeValue"
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
mockSubscriber.subscribe.andReturn(mockSubscription);
|
||||||
|
mockSubscription.getTelemetryObjects.andReturn([mockDomainObject]);
|
||||||
|
mockSubscription.getMetadata.andReturn([{}]);
|
||||||
|
mockSubscription.getDomainValue.andReturn(123);
|
||||||
|
mockSubscription.getRangeValue.andReturn(42);
|
||||||
|
|
||||||
|
controller = new PlotController(mockScope, mockFormatter, mockSubscriber);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("provides plot colors", function () {
|
it("provides plot colors", function () {
|
||||||
@ -66,16 +64,24 @@ define(
|
|||||||
.not.toEqual(controller.getColor(1));
|
.not.toEqual(controller.getColor(1));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not fail if telemetry controller is not in scope", function () {
|
it("subscribes to telemetry when a domain object appears in scope", function () {
|
||||||
mockScope.telemetry = undefined;
|
// Make sure we're using the right watch here
|
||||||
|
expect(mockScope.$watch.mostRecentCall.args[0])
|
||||||
// Broadcast data
|
.toEqual("domainObject");
|
||||||
mockScope.$on.mostRecentCall.args[1]();
|
// Make an object available
|
||||||
|
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
|
||||||
// Just want to not have an exception
|
// Should have subscribed
|
||||||
|
expect(mockSubscriber.subscribe).toHaveBeenCalledWith(
|
||||||
|
mockDomainObject,
|
||||||
|
jasmine.any(Function),
|
||||||
|
true // Lossless
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("draws lines when data becomes available", function () {
|
it("draws lines when data becomes available", function () {
|
||||||
|
// Make an object available
|
||||||
|
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
|
||||||
|
|
||||||
// Verify precondition
|
// Verify precondition
|
||||||
controller.getSubPlots().forEach(function (subplot) {
|
controller.getSubPlots().forEach(function (subplot) {
|
||||||
expect(subplot.getDrawingObject().lines)
|
expect(subplot.getDrawingObject().lines)
|
||||||
@ -83,11 +89,10 @@ define(
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Make sure there actually are subplots being verified
|
// Make sure there actually are subplots being verified
|
||||||
expect(controller.getSubPlots().length > 0)
|
expect(controller.getSubPlots().length > 0).toBeTruthy();
|
||||||
.toBeTruthy();
|
|
||||||
|
|
||||||
// Broadcast data
|
// Broadcast data
|
||||||
mockScope.$on.mostRecentCall.args[1]();
|
mockSubscriber.subscribe.mostRecentCall.args[1]();
|
||||||
|
|
||||||
controller.getSubPlots().forEach(function (subplot) {
|
controller.getSubPlots().forEach(function (subplot) {
|
||||||
expect(subplot.getDrawingObject().lines)
|
expect(subplot.getDrawingObject().lines)
|
||||||
@ -95,27 +100,39 @@ define(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("unsubscribes when domain object changes", function () {
|
||||||
|
// Make an object available
|
||||||
|
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
|
||||||
|
// Verify precondition - shouldn't unsubscribe yet
|
||||||
|
expect(mockSubscription.unsubscribe).not.toHaveBeenCalled();
|
||||||
|
// Remove the domain object
|
||||||
|
mockScope.$watch.mostRecentCall.args[1](undefined);
|
||||||
|
// Should have unsubscribed
|
||||||
|
expect(mockSubscription.unsubscribe).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
it("changes modes depending on number of objects", function () {
|
it("changes modes depending on number of objects", function () {
|
||||||
var expectedWatch = "telemetry.getTelemetryObjects()",
|
// Act like one object is available
|
||||||
watchFunction;
|
mockSubscription.getTelemetryObjects.andReturn([
|
||||||
|
mockDomainObject
|
||||||
|
]);
|
||||||
|
|
||||||
// Find the watch for telemetry objects, which
|
// Make an object available
|
||||||
// should change plot mode options
|
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
|
||||||
mockScope.$watch.calls.forEach(function (call) {
|
|
||||||
if (call.args[0] === expectedWatch) {
|
|
||||||
watchFunction = call.args[1];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
watchFunction([mockDomainObject]);
|
|
||||||
expect(controller.getModeOptions().length).toEqual(1);
|
expect(controller.getModeOptions().length).toEqual(1);
|
||||||
|
|
||||||
watchFunction([
|
// Act like one object is available
|
||||||
|
mockSubscription.getTelemetryObjects.andReturn([
|
||||||
mockDomainObject,
|
mockDomainObject,
|
||||||
mockDomainObject,
|
mockDomainObject,
|
||||||
mockDomainObject
|
mockDomainObject
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Make an object available
|
||||||
|
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
|
||||||
|
|
||||||
expect(controller.getModeOptions().length).toEqual(2);
|
expect(controller.getModeOptions().length).toEqual(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -151,6 +168,11 @@ define(
|
|||||||
expect(controller.stepBackPanZoom).not.toThrow();
|
expect(controller.stepBackPanZoom).not.toThrow();
|
||||||
expect(controller.unzoom).not.toThrow();
|
expect(controller.unzoom).not.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("indicates if a request is pending", function () {
|
||||||
|
// Placeholder; need to support requesting telemetry
|
||||||
|
expect(controller.isRequestPending()).toBeFalsy();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
@ -135,6 +135,22 @@ define(
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("provides access to a drawable object", function () {
|
||||||
|
expect(typeof subplot.getDrawingObject()).toEqual('object');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows a domain offset to be provided", function () {
|
||||||
|
// Domain object is needed to adjust canvas coordinates
|
||||||
|
// to avoid loss-of-precision associated with converting
|
||||||
|
// to 32 bit floats.
|
||||||
|
subplot.setDomainOffset(3);
|
||||||
|
subplot.update();
|
||||||
|
// Should have adjusted the origin accordingly
|
||||||
|
expect(subplot.getDrawingObject().origin[0])
|
||||||
|
.toEqual(2);
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
@ -1,4 +1,4 @@
|
|||||||
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
|
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine,Float32Array*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MergeModelsSpec. Created by vwoeltje on 11/6/14.
|
* MergeModelsSpec. Created by vwoeltje on 11/6/14.
|
||||||
@ -60,6 +60,13 @@ define(
|
|||||||
expect(preparer.getDimensions[1]).not.toEqual(0);
|
expect(preparer.getDimensions[1]).not.toEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("provides buffers", function () {
|
||||||
|
var datas = [makeMockData(0)],
|
||||||
|
preparer = new PlotPreparer(datas);
|
||||||
|
expect(preparer.getBuffers()[0] instanceof Float32Array)
|
||||||
|
.toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
161
platform/features/plot/test/elements/PlotUpdaterSpec.js
Normal file
161
platform/features/plot/test/elements/PlotUpdaterSpec.js
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine,Float32Array*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MergeModelsSpec. Created by vwoeltje on 11/6/14.
|
||||||
|
*/
|
||||||
|
define(
|
||||||
|
["../../src/elements/PlotUpdater"],
|
||||||
|
function (PlotUpdater) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
describe("A plot updater", function () {
|
||||||
|
var mockSubscription,
|
||||||
|
testDomain,
|
||||||
|
testRange,
|
||||||
|
testDomainValues,
|
||||||
|
testRangeValues,
|
||||||
|
updater;
|
||||||
|
|
||||||
|
function makeMockDomainObject(id) {
|
||||||
|
var mockDomainObject = jasmine.createSpyObj(
|
||||||
|
"object-" + id,
|
||||||
|
[ "getId", "getCapability", "getModel" ]
|
||||||
|
);
|
||||||
|
mockDomainObject.getId.andReturn(id);
|
||||||
|
return mockDomainObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
var ids = [ 'a', 'b', 'c' ],
|
||||||
|
mockObjects = ids.map(makeMockDomainObject);
|
||||||
|
|
||||||
|
mockSubscription = jasmine.createSpyObj(
|
||||||
|
"subscription",
|
||||||
|
[ "getDomainValue", "getRangeValue", "getTelemetryObjects" ]
|
||||||
|
);
|
||||||
|
testDomain = "testDomain";
|
||||||
|
testRange = "testRange";
|
||||||
|
testDomainValues = { a: 3, b: 7, c: 13 };
|
||||||
|
testRangeValues = { a: 123, b: 456, c: 789 };
|
||||||
|
|
||||||
|
mockSubscription.getTelemetryObjects.andReturn(mockObjects);
|
||||||
|
mockSubscription.getDomainValue.andCallFake(function (mockObject) {
|
||||||
|
return testDomainValues[mockObject.getId()];
|
||||||
|
});
|
||||||
|
mockSubscription.getRangeValue.andCallFake(function (mockObject) {
|
||||||
|
return testRangeValues[mockObject.getId()];
|
||||||
|
});
|
||||||
|
|
||||||
|
updater = new PlotUpdater(
|
||||||
|
mockSubscription,
|
||||||
|
testDomain,
|
||||||
|
testRange,
|
||||||
|
1350 // Smaller max size for easier testing
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("provides one buffer per telemetry object", function () {
|
||||||
|
expect(updater.getBuffers().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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can handle delayed telemetry object availability", function () {
|
||||||
|
// The case can occur where getTelemetryObjects() returns an
|
||||||
|
// empty array - specifically, while objects are still being
|
||||||
|
// loaded. The updater needs to be able to cope with that
|
||||||
|
// case.
|
||||||
|
var tmp = mockSubscription.getTelemetryObjects();
|
||||||
|
mockSubscription.getTelemetryObjects.andReturn([]);
|
||||||
|
|
||||||
|
// Reinstantiate with the empty subscription
|
||||||
|
updater = new PlotUpdater(
|
||||||
|
mockSubscription,
|
||||||
|
testDomain,
|
||||||
|
testRange
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should have 0 buffers for 0 objects
|
||||||
|
expect(updater.getBuffers().length).toEqual(0);
|
||||||
|
|
||||||
|
// Restore the three objects the test subscription would
|
||||||
|
// normally have.
|
||||||
|
mockSubscription.getTelemetryObjects.andReturn(tmp);
|
||||||
|
updater.update();
|
||||||
|
|
||||||
|
// Should have 3 buffers for 3 objects
|
||||||
|
expect(updater.getBuffers().length).toEqual(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it("shifts buffer upon expansion", function () {
|
||||||
|
// Count should be large enough to hit buffer's max size
|
||||||
|
var count = 1400,
|
||||||
|
i;
|
||||||
|
|
||||||
|
// Initial update; should have 3 in first position
|
||||||
|
// (a's initial domain value)
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
@ -57,7 +57,7 @@ define(
|
|||||||
// Prepared telemetry data
|
// Prepared telemetry data
|
||||||
mockPrepared = jasmine.createSpyObj(
|
mockPrepared = jasmine.createSpyObj(
|
||||||
"prepared",
|
"prepared",
|
||||||
[ "getDomainOffset", "getOrigin", "getDimensions", "getBuffers" ]
|
[ "getDomainOffset", "getOrigin", "getDimensions", "getBuffers", "getLength" ]
|
||||||
);
|
);
|
||||||
|
|
||||||
mockSubPlotFactory.createSubPlot.andCallFake(createMockSubPlot);
|
mockSubPlotFactory.createSubPlot.andCallFake(createMockSubPlot);
|
||||||
@ -68,6 +68,7 @@ define(
|
|||||||
mockPrepared.getDomainOffset.andReturn(1234);
|
mockPrepared.getDomainOffset.andReturn(1234);
|
||||||
mockPrepared.getOrigin.andReturn([10, 10]);
|
mockPrepared.getOrigin.andReturn([10, 10]);
|
||||||
mockPrepared.getDimensions.andReturn([500, 500]);
|
mockPrepared.getDimensions.andReturn([500, 500]);
|
||||||
|
mockPrepared.getLength.andReturn(3);
|
||||||
|
|
||||||
// Clear out drawing objects
|
// Clear out drawing objects
|
||||||
testDrawingObjects = [];
|
testDrawingObjects = [];
|
||||||
|
@ -57,7 +57,7 @@ define(
|
|||||||
// Prepared telemetry data
|
// Prepared telemetry data
|
||||||
mockPrepared = jasmine.createSpyObj(
|
mockPrepared = jasmine.createSpyObj(
|
||||||
"prepared",
|
"prepared",
|
||||||
[ "getDomainOffset", "getOrigin", "getDimensions", "getBuffers" ]
|
[ "getDomainOffset", "getOrigin", "getDimensions", "getBuffers", "getLength" ]
|
||||||
);
|
);
|
||||||
|
|
||||||
mockSubPlotFactory.createSubPlot.andCallFake(createMockSubPlot);
|
mockSubPlotFactory.createSubPlot.andCallFake(createMockSubPlot);
|
||||||
@ -68,6 +68,7 @@ define(
|
|||||||
mockPrepared.getDomainOffset.andReturn(1234);
|
mockPrepared.getDomainOffset.andReturn(1234);
|
||||||
mockPrepared.getOrigin.andReturn([10, 10]);
|
mockPrepared.getOrigin.andReturn([10, 10]);
|
||||||
mockPrepared.getDimensions.andReturn([500, 500]);
|
mockPrepared.getDimensions.andReturn([500, 500]);
|
||||||
|
mockPrepared.getLength.andReturn(3);
|
||||||
|
|
||||||
// Objects that will be drawn to in sub-plots
|
// Objects that will be drawn to in sub-plots
|
||||||
testDrawingObjects = [];
|
testDrawingObjects = [];
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
"elements/PlotPosition",
|
"elements/PlotPosition",
|
||||||
"elements/PlotPreparer",
|
"elements/PlotPreparer",
|
||||||
"elements/PlotTickGenerator",
|
"elements/PlotTickGenerator",
|
||||||
|
"elements/PlotUpdater",
|
||||||
"modes/PlotModeOptions",
|
"modes/PlotModeOptions",
|
||||||
"modes/PlotOverlayMode",
|
"modes/PlotOverlayMode",
|
||||||
"modes/PlotStackMode"
|
"modes/PlotStackMode"
|
||||||
|
71
platform/telemetry/src/TelemetryQueue.js
Normal file
71
platform/telemetry/src/TelemetryQueue.js
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
/*global define*/
|
||||||
|
|
||||||
|
define(
|
||||||
|
[],
|
||||||
|
function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supports TelemetrySubscription. Provides a simple data structure
|
||||||
|
* (with a pool-like interface) that aggregates key-value pairs into
|
||||||
|
* a queued series of large objects, ensuring that no value is
|
||||||
|
* overwritten (but consolidated non-overlapping keys into single
|
||||||
|
* objects.)
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
function TelemetryQueue() {
|
||||||
|
var queue = [];
|
||||||
|
|
||||||
|
// Look up an object in the queue that does not have a value
|
||||||
|
// assigned to this key (or, add a new one)
|
||||||
|
function getFreeObject(key) {
|
||||||
|
var index = 0, object;
|
||||||
|
|
||||||
|
// Look for an existing queue position where we can store
|
||||||
|
// a value to this key without overwriting an existing value.
|
||||||
|
for (index = 0; index < queue.length; index += 1) {
|
||||||
|
if (queue[index][key] === undefined) {
|
||||||
|
return queue[index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we made it through the loop, values have been assigned
|
||||||
|
// to that key in all queued containers, so we need to queue
|
||||||
|
// up a new container for key-value pairs.
|
||||||
|
object = {};
|
||||||
|
queue.push(object);
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* Check if any value groups remain in this pool.
|
||||||
|
* @return {boolean} true if value groups remain
|
||||||
|
*/
|
||||||
|
isEmpty: function () {
|
||||||
|
return queue.length < 1;
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Retrieve the next value group from this pool.
|
||||||
|
* This gives an object containing key-value pairs,
|
||||||
|
* where keys and values correspond to the arguments
|
||||||
|
* given to previous put functions.
|
||||||
|
* @return {object} key-value pairs
|
||||||
|
*/
|
||||||
|
poll: function () {
|
||||||
|
return queue.shift();
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Put a key-value pair into the pool.
|
||||||
|
* @param {string} key the key to store the value under
|
||||||
|
* @param {*} value the value to store
|
||||||
|
*/
|
||||||
|
put: function (key, value) {
|
||||||
|
getFreeObject(key)[key] = value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return TelemetryQueue;
|
||||||
|
}
|
||||||
|
);
|
@ -32,18 +32,23 @@ define(
|
|||||||
* associated telemetry data is of interest
|
* associated telemetry data is of interest
|
||||||
* @param {Function} callback a function to invoke
|
* @param {Function} callback a function to invoke
|
||||||
* when new data has become available.
|
* when new data has become available.
|
||||||
|
* @param {boolean} lossless flag to indicate whether the
|
||||||
|
* callback should be notified for all values
|
||||||
|
* (otherwise, multiple values in quick succession
|
||||||
|
* will call back with only the latest value.)
|
||||||
* @returns {TelemetrySubscription} the subscription,
|
* @returns {TelemetrySubscription} the subscription,
|
||||||
* which will provide access to latest values.
|
* which will provide access to latest values.
|
||||||
*
|
*
|
||||||
* @method
|
* @method
|
||||||
* @memberof TelemetrySubscriber
|
* @memberof TelemetrySubscriber
|
||||||
*/
|
*/
|
||||||
subscribe: function (domainObject, callback) {
|
subscribe: function (domainObject, callback, lossless) {
|
||||||
return new TelemetrySubscription(
|
return new TelemetrySubscription(
|
||||||
$q,
|
$q,
|
||||||
$timeout,
|
$timeout,
|
||||||
domainObject,
|
domainObject,
|
||||||
callback
|
callback,
|
||||||
|
lossless
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
/*global define*/
|
/*global define*/
|
||||||
|
|
||||||
define(
|
define(
|
||||||
[],
|
['./TelemetryQueue', './TelemetryTable'],
|
||||||
function () {
|
function (TelemetryQueue, TelemetryTable) {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
|
|
||||||
@ -25,11 +25,17 @@ define(
|
|||||||
* associated telemetry data is of interest
|
* associated telemetry data is of interest
|
||||||
* @param {Function} callback a function to invoke
|
* @param {Function} callback a function to invoke
|
||||||
* when new data has become available.
|
* when new data has become available.
|
||||||
|
* @param {boolean} lossless true if callback should be invoked
|
||||||
|
* once with every data point available; otherwise, multiple
|
||||||
|
* data events in a short period of time will only invoke
|
||||||
|
* the callback once, with access to the latest data
|
||||||
*/
|
*/
|
||||||
function TelemetrySubscription($q, $timeout, domainObject, callback) {
|
function TelemetrySubscription($q, $timeout, domainObject, callback, lossless) {
|
||||||
var unsubscribePromise,
|
var unsubscribePromise,
|
||||||
latestValues = {},
|
latestValues = {},
|
||||||
telemetryObjects = [],
|
telemetryObjects = [],
|
||||||
|
pool = lossless ? new TelemetryQueue() : new TelemetryTable(),
|
||||||
|
metadatas,
|
||||||
updatePending;
|
updatePending;
|
||||||
|
|
||||||
// Look up domain objects which have telemetry capabilities.
|
// Look up domain objects which have telemetry capabilities.
|
||||||
@ -55,10 +61,22 @@ define(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateValuesFromPool() {
|
||||||
|
var values = pool.poll();
|
||||||
|
Object.keys(values).forEach(function (k) {
|
||||||
|
latestValues[k] = values[k];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Invoke the observer callback to notify that new streaming
|
// Invoke the observer callback to notify that new streaming
|
||||||
// data has become available.
|
// data has become available.
|
||||||
function fireCallback() {
|
function fireCallback() {
|
||||||
callback();
|
// Play back from queue if we are lossless
|
||||||
|
while (!pool.isEmpty()) {
|
||||||
|
updateValuesFromPool();
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
|
||||||
// Clear the pending flag so that future updates will
|
// Clear the pending flag so that future updates will
|
||||||
// schedule this callback.
|
// schedule this callback.
|
||||||
updatePending = false;
|
updatePending = false;
|
||||||
@ -79,10 +97,10 @@ define(
|
|||||||
|
|
||||||
// Update the latest-value table
|
// Update the latest-value table
|
||||||
if (count > 0) {
|
if (count > 0) {
|
||||||
latestValues[domainObject.getId()] = {
|
pool.put(domainObject.getId(), {
|
||||||
domain: telemetry.getDomainValue(count - 1),
|
domain: telemetry.getDomainValue(count - 1),
|
||||||
range: telemetry.getRangeValue(count - 1)
|
range: telemetry.getRangeValue(count - 1)
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,6 +114,14 @@ define(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Look up metadata associated with an object's telemetry
|
||||||
|
function lookupMetadata(domainObject) {
|
||||||
|
var telemetryCapability =
|
||||||
|
domainObject.getCapability("telemetry");
|
||||||
|
return telemetryCapability &&
|
||||||
|
telemetryCapability.getMetadata();
|
||||||
|
}
|
||||||
|
|
||||||
// Prepare subscriptions to all relevant telemetry-providing
|
// Prepare subscriptions to all relevant telemetry-providing
|
||||||
// domain objects.
|
// domain objects.
|
||||||
function subscribeAll(domainObjects) {
|
function subscribeAll(domainObjects) {
|
||||||
@ -108,6 +134,7 @@ define(
|
|||||||
// to return a non-Promise to simplify usage elsewhere.
|
// to return a non-Promise to simplify usage elsewhere.
|
||||||
function cacheObjectReferences(objects) {
|
function cacheObjectReferences(objects) {
|
||||||
telemetryObjects = objects;
|
telemetryObjects = objects;
|
||||||
|
metadatas = objects.map(lookupMetadata);
|
||||||
return objects;
|
return objects;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -189,6 +216,21 @@ define(
|
|||||||
*/
|
*/
|
||||||
getTelemetryObjects: function () {
|
getTelemetryObjects: function () {
|
||||||
return telemetryObjects;
|
return telemetryObjects;
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Get all telemetry metadata associated with
|
||||||
|
* telemetry-providing domain objects managed by
|
||||||
|
* this controller.
|
||||||
|
*
|
||||||
|
* This will ordered in the
|
||||||
|
* same manner as `getTelemetryObjects()` or
|
||||||
|
* `getResponse()`; that is, the metadata at a
|
||||||
|
* given index will correspond to the telemetry-providing
|
||||||
|
* domain object at the same index.
|
||||||
|
* @returns {Array} an array of metadata objects
|
||||||
|
*/
|
||||||
|
getMetadata: function () {
|
||||||
|
return metadatas;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
53
platform/telemetry/src/TelemetryTable.js
Normal file
53
platform/telemetry/src/TelemetryTable.js
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
/*global define*/
|
||||||
|
|
||||||
|
define(
|
||||||
|
[],
|
||||||
|
function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supports TelemetrySubscription. Provides a simple data structure
|
||||||
|
* (with a pool-like interface) that aggregates key-value pairs into
|
||||||
|
* one large object, overwriting new values as necessary. Stands
|
||||||
|
* in contrast to the TelemetryQueue, which will avoid overwriting
|
||||||
|
* values.
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
function TelemetryTable() {
|
||||||
|
var table;
|
||||||
|
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* Check if any value groups remain in this pool.
|
||||||
|
* @return {boolean} true if value groups remain
|
||||||
|
*/
|
||||||
|
isEmpty: function () {
|
||||||
|
return !table;
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Retrieve the next value group from this pool.
|
||||||
|
* This gives an object containing key-value pairs,
|
||||||
|
* where keys and values correspond to the arguments
|
||||||
|
* given to previous put functions.
|
||||||
|
* @return {object} key-value pairs
|
||||||
|
*/
|
||||||
|
poll: function () {
|
||||||
|
var t = table;
|
||||||
|
table = undefined;
|
||||||
|
return t;
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Put a key-value pair into the pool.
|
||||||
|
* @param {string} key the key to store the value under
|
||||||
|
* @param {*} value the value to store
|
||||||
|
*/
|
||||||
|
put: function (key, value) {
|
||||||
|
table = table || {};
|
||||||
|
table[key] = value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return TelemetryTable;
|
||||||
|
}
|
||||||
|
);
|
55
platform/telemetry/test/TelemetryQueueSpec.js
Normal file
55
platform/telemetry/test/TelemetryQueueSpec.js
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
|
||||||
|
|
||||||
|
define(
|
||||||
|
["../src/TelemetryQueue"],
|
||||||
|
function (TelemetryQueue) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
describe("The telemetry queue", function () {
|
||||||
|
var queue;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
// put, isEmpty, dequeue
|
||||||
|
queue = new TelemetryQueue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("stores elements by key", function () {
|
||||||
|
queue.put("a", { someKey: "some value" });
|
||||||
|
expect(queue.poll())
|
||||||
|
.toEqual({ a: { someKey: "some value" }});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("merges non-overlapping keys", function () {
|
||||||
|
queue.put("a", { someKey: "some value" });
|
||||||
|
queue.put("b", 42);
|
||||||
|
expect(queue.poll())
|
||||||
|
.toEqual({ a: { someKey: "some value" }, b: 42 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("adds new objects for repeated keys", function () {
|
||||||
|
queue.put("a", { someKey: "some value" });
|
||||||
|
queue.put("a", { someKey: "some other value" });
|
||||||
|
queue.put("b", 42);
|
||||||
|
expect(queue.poll())
|
||||||
|
.toEqual({ a: { someKey: "some value" }, b: 42 });
|
||||||
|
expect(queue.poll())
|
||||||
|
.toEqual({ a: { someKey: "some other value" } });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reports emptiness", function () {
|
||||||
|
expect(queue.isEmpty()).toBeTruthy();
|
||||||
|
queue.put("a", { someKey: "some value" });
|
||||||
|
queue.put("a", { someKey: "some other value" });
|
||||||
|
queue.put("b", 42);
|
||||||
|
expect(queue.isEmpty()).toBeFalsy();
|
||||||
|
queue.poll();
|
||||||
|
expect(queue.isEmpty()).toBeFalsy();
|
||||||
|
queue.poll();
|
||||||
|
expect(queue.isEmpty()).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
);
|
@ -13,6 +13,7 @@ define(
|
|||||||
mockTelemetry,
|
mockTelemetry,
|
||||||
mockUnsubscribe,
|
mockUnsubscribe,
|
||||||
mockSeries,
|
mockSeries,
|
||||||
|
testMetadata,
|
||||||
subscription;
|
subscription;
|
||||||
|
|
||||||
function mockPromise(value) {
|
function mockPromise(value) {
|
||||||
@ -24,6 +25,8 @@ define(
|
|||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
|
testMetadata = { someKey: "some value" };
|
||||||
|
|
||||||
mockQ = jasmine.createSpyObj("$q", ["when", "all"]);
|
mockQ = jasmine.createSpyObj("$q", ["when", "all"]);
|
||||||
mockTimeout = jasmine.createSpy("$timeout");
|
mockTimeout = jasmine.createSpy("$timeout");
|
||||||
mockDomainObject = jasmine.createSpyObj(
|
mockDomainObject = jasmine.createSpyObj(
|
||||||
@ -33,7 +36,7 @@ define(
|
|||||||
mockCallback = jasmine.createSpy("callback");
|
mockCallback = jasmine.createSpy("callback");
|
||||||
mockTelemetry = jasmine.createSpyObj(
|
mockTelemetry = jasmine.createSpyObj(
|
||||||
"telemetry",
|
"telemetry",
|
||||||
["subscribe"]
|
["subscribe", "getMetadata"]
|
||||||
);
|
);
|
||||||
mockUnsubscribe = jasmine.createSpy("unsubscribe");
|
mockUnsubscribe = jasmine.createSpy("unsubscribe");
|
||||||
mockSeries = jasmine.createSpyObj(
|
mockSeries = jasmine.createSpyObj(
|
||||||
@ -48,6 +51,7 @@ define(
|
|||||||
mockDomainObject.getId.andReturn('test-id');
|
mockDomainObject.getId.andReturn('test-id');
|
||||||
|
|
||||||
mockTelemetry.subscribe.andReturn(mockUnsubscribe);
|
mockTelemetry.subscribe.andReturn(mockUnsubscribe);
|
||||||
|
mockTelemetry.getMetadata.andReturn(testMetadata);
|
||||||
|
|
||||||
mockSeries.getPointCount.andReturn(42);
|
mockSeries.getPointCount.andReturn(42);
|
||||||
mockSeries.getDomainValue.andReturn(123456);
|
mockSeries.getDomainValue.andReturn(123456);
|
||||||
@ -120,6 +124,52 @@ define(
|
|||||||
// Should have no objects
|
// Should have no objects
|
||||||
expect(subscription.getTelemetryObjects()).toEqual([]);
|
expect(subscription.getTelemetryObjects()).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// This test case corresponds to plot usage of
|
||||||
|
// telemetrySubscription, where failure to callback
|
||||||
|
// once-per-update results in loss of data, WTD-784
|
||||||
|
it("fires one event per update if requested", function () {
|
||||||
|
var i, domains = [], ranges = [], lastCall;
|
||||||
|
|
||||||
|
// Clear out the subscription from beforeEach
|
||||||
|
subscription.unsubscribe();
|
||||||
|
// Create a subscription which does not drop events
|
||||||
|
subscription = new TelemetrySubscription(
|
||||||
|
mockQ,
|
||||||
|
mockTimeout,
|
||||||
|
mockDomainObject,
|
||||||
|
mockCallback,
|
||||||
|
true // Don't drop updates!
|
||||||
|
);
|
||||||
|
|
||||||
|
// Snapshot getDomainValue, getRangeValue at time of callback
|
||||||
|
mockCallback.andCallFake(function () {
|
||||||
|
domains.push(subscription.getDomainValue(mockDomainObject));
|
||||||
|
ranges.push(subscription.getRangeValue(mockDomainObject));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send 100 updates
|
||||||
|
for (i = 0; i < 100; i += 1) {
|
||||||
|
// Return different values to verify later
|
||||||
|
mockSeries.getDomainValue.andReturn(i);
|
||||||
|
mockSeries.getRangeValue.andReturn(i * 2);
|
||||||
|
mockTelemetry.subscribe.mostRecentCall.args[0](mockSeries);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fire all timeouts that get scheduled
|
||||||
|
while (mockTimeout.mostRecentCall !== lastCall) {
|
||||||
|
lastCall = mockTimeout.mostRecentCall;
|
||||||
|
lastCall.args[0]();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should have only triggered the
|
||||||
|
expect(mockCallback.calls.length).toEqual(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("provides domain object metadata", function () {
|
||||||
|
expect(subscription.getMetadata()[0])
|
||||||
|
.toEqual(testMetadata);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
53
platform/telemetry/test/TelemetryTableSpec.js
Normal file
53
platform/telemetry/test/TelemetryTableSpec.js
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
|
||||||
|
|
||||||
|
define(
|
||||||
|
["../src/TelemetryTable"],
|
||||||
|
function (TelemetryTable) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
describe("The telemetry table", function () {
|
||||||
|
var queue;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
// put, isEmpty, dequeue
|
||||||
|
queue = new TelemetryTable();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("stores elements by key", function () {
|
||||||
|
queue.put("a", { someKey: "some value" });
|
||||||
|
expect(queue.poll())
|
||||||
|
.toEqual({ a: { someKey: "some value" }});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("merges non-overlapping keys", function () {
|
||||||
|
queue.put("a", { someKey: "some value" });
|
||||||
|
queue.put("b", 42);
|
||||||
|
expect(queue.poll())
|
||||||
|
.toEqual({ a: { someKey: "some value" }, b: 42 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("overwrites repeated keys", function () {
|
||||||
|
queue.put("a", { someKey: "some value" });
|
||||||
|
queue.put("a", { someKey: "some other value" });
|
||||||
|
queue.put("b", 42);
|
||||||
|
expect(queue.poll())
|
||||||
|
.toEqual({ a: { someKey: "some other value" }, b: 42 });
|
||||||
|
expect(queue.poll())
|
||||||
|
.toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reports emptiness", function () {
|
||||||
|
expect(queue.isEmpty()).toBeTruthy();
|
||||||
|
queue.put("a", { someKey: "some value" });
|
||||||
|
queue.put("a", { someKey: "some other value" });
|
||||||
|
queue.put("b", 42);
|
||||||
|
expect(queue.isEmpty()).toBeFalsy();
|
||||||
|
queue.poll();
|
||||||
|
expect(queue.isEmpty()).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
);
|
@ -3,6 +3,8 @@
|
|||||||
"TelemetryCapability",
|
"TelemetryCapability",
|
||||||
"TelemetryController",
|
"TelemetryController",
|
||||||
"TelemetryFormatter",
|
"TelemetryFormatter",
|
||||||
|
"TelemetryQueue",
|
||||||
"TelemetrySubscriber",
|
"TelemetrySubscriber",
|
||||||
"TelemetrySubscription"
|
"TelemetrySubscription",
|
||||||
|
"TelemetryTable"
|
||||||
]
|
]
|
Loading…
x
Reference in New Issue
Block a user