From d4fdaf9cbc47bf3581ddef3412ef92a645e2055c Mon Sep 17 00:00:00 2001 From: Henry Date: Wed, 29 Mar 2017 12:18:11 -0700 Subject: [PATCH] [Fixed Position] Updated fixed position view to use new Telemetry API Added TelemetryCollection to handle bounds and buffering. Avoids loss of telemetry due to timing mismatch between telemetry providers and tick sources Fixed issues with removal and addition of objects --- .jshintrc | 3 +- platform/features/layout/bundle.js | 4 +- .../features/layout/src/FixedController.js | 339 ++++++++++----- .../layout/test/FixedControllerSpec.js | 396 ++++++++++++------ .../features/table/src/TelemetryCollection.js | 10 +- 5 files changed, 512 insertions(+), 240 deletions(-) diff --git a/.jshintrc b/.jshintrc index ec94c41acd..5b5f7236a3 100644 --- a/.jshintrc +++ b/.jshintrc @@ -15,7 +15,8 @@ "predef": [ "define", "Promise", - "WeakMap" + "WeakMap", + "Map" ], "shadow": "outer", "strict": "implied", diff --git a/platform/features/layout/bundle.js b/platform/features/layout/bundle.js index 421bbcc9c3..5b30106bd6 100644 --- a/platform/features/layout/bundle.js +++ b/platform/features/layout/bundle.js @@ -237,9 +237,7 @@ define([ "$scope", "$q", "dialogService", - "telemetryHandler", - "telemetryFormatter", - "throttle" + "openmct" ] } ], diff --git a/platform/features/layout/src/FixedController.js b/platform/features/layout/src/FixedController.js index e5345cb46a..fe512de870 100644 --- a/platform/features/layout/src/FixedController.js +++ b/platform/features/layout/src/FixedController.js @@ -21,8 +21,20 @@ *****************************************************************************/ define( - ['./FixedProxy', './elements/ElementProxies', './FixedDragHandle'], - function (FixedProxy, ElementProxies, FixedDragHandle) { + [ + 'lodash', + './FixedProxy', + './elements/ElementProxies', + './FixedDragHandle', + '../../../../src/api/objects/object-utils' + ], + function ( + _, + FixedProxy, + ElementProxies, + FixedDragHandle, + objectUtils + ) { var DEFAULT_DIMENSIONS = [2, 1]; @@ -35,13 +47,30 @@ define( * @constructor * @param {Scope} $scope the controller's Angular scope */ - function FixedController($scope, $q, dialogService, telemetryHandler, telemetryFormatter) { - var self = this, - handle, - names = {}, // Cache names by ID - values = {}, // Cache values by ID - elementProxiesById = {}, - maxDomainValue = Number.POSITIVE_INFINITY; + function FixedController($scope, $q, dialogService, openmct) { + this.names = {}; // Cache names by ID + this.values = {}; // Cache values by ID + this.elementProxiesById = {}; + + this.telemetryObjects = []; + this.subscriptions = []; + this.openmct = openmct; + this.$scope = $scope; + + this.gridSize = $scope.domainObject && $scope.domainObject.getModel().layoutGrid; + + var self = this; + [ + 'digest', + 'fetchHistoricalData', + 'getTelemetry', + 'setDisplayedValue', + 'subscribeToObjects', + 'unsubscribe', + 'updateView' + ].forEach(function (name) { + self[name] = self[name].bind(self); + }); // Convert from element x/y/width/height to an // appropriate ng-style argument, to position elements. @@ -79,55 +108,6 @@ define( return element.handles().map(generateDragHandle); } - // Update the value displayed in elements of this telemetry object - function setDisplayedValue(telemetryObject, value, alarm) { - var id = telemetryObject.getId(); - (elementProxiesById[id] || []).forEach(function (element) { - names[id] = telemetryObject.getModel().name; - values[id] = telemetryFormatter.formatRangeValue(value); - element.name = names[id]; - element.value = values[id]; - element.cssClass = alarm && alarm.cssClass; - }); - } - - // Update the displayed value for this object, from a specific - // telemetry series - function updateValueFromSeries(telemetryObject, telemetrySeries) { - var index = telemetrySeries.getPointCount() - 1, - limit = telemetryObject && - telemetryObject.getCapability('limit'), - datum = telemetryObject && handle.getDatum( - telemetryObject, - index - ); - - if (index >= 0) { - setDisplayedValue( - telemetryObject, - telemetrySeries.getRangeValue(index), - limit && datum && limit.evaluate(datum) - ); - } - } - - // Update the displayed value for this object - function updateValue(telemetryObject) { - var limit = telemetryObject && - telemetryObject.getCapability('limit'), - datum = telemetryObject && - handle.getDatum(telemetryObject); - - if (telemetryObject && - (handle.getDomainValue(telemetryObject) < maxDomainValue)) { - setDisplayedValue( - telemetryObject, - handle.getRangeValue(telemetryObject), - limit && datum && limit.evaluate(datum) - ); - } - } - // Update element positions when grid size changes function updateElementPositions(layoutGrid) { // Update grid size from model @@ -138,13 +118,6 @@ define( }); } - // Update telemetry values based on new data available - function updateValues() { - if (handle) { - handle.getTelemetryObjects().forEach(updateValue); - } - } - // Decorate an element for display function makeProxyElement(element, index, elements) { var ElementProxy = ElementProxies[element.type], @@ -186,64 +159,57 @@ define( // Finally, rebuild lists of elements by id to // facilitate faster update when new telemetry comes in. - elementProxiesById = {}; + self.elementProxiesById = {}; self.elementProxies.forEach(function (elementProxy) { var id = elementProxy.id; if (elementProxy.element.type === 'fixed.telemetry') { // Provide it a cached name/value to avoid flashing - elementProxy.name = names[id]; - elementProxy.value = values[id]; - elementProxiesById[id] = elementProxiesById[id] || []; - elementProxiesById[id].push(elementProxy); + elementProxy.name = self.names[id]; + elementProxy.value = self.values[id]; + self.elementProxiesById[id] = self.elementProxiesById[id] || []; + self.elementProxiesById[id].push(elementProxy); } }); - - // TODO: Ensure elements for all domain objects? } - // Free up subscription to telemetry - function releaseSubscription() { - if (handle) { - handle.unsubscribe(); - handle = undefined; - } - } + function removeObjects(ids) { + var configuration = self.$scope.configuration; - // Subscribe to telemetry updates for this domain object - function subscribe(domainObject) { - // Release existing subscription (if any) - if (handle) { - handle.unsubscribe(); + if (configuration && + configuration.elements) { + configuration.elements = configuration.elements.filter(function (proxy) { + return ids.indexOf(proxy.id) === -1; + }); + } + self.getTelemetry($scope.domainObject); + refreshElements(); + // Mark change as persistable + if (self.$scope.commit) { + self.$scope.commit("Objects removed."); } - - // Make a new subscription - handle = domainObject && telemetryHandler.handle( - domainObject, - updateValues - ); - // Request an initial historical telemetry value - handle.request( - { size: 1 }, // Only need a single data point - updateValueFromSeries - ); } // Handle changes in the object's composition - function updateComposition() { - // Populate panel positions - // TODO: Ensure defaults here + function updateComposition(composition, previousComposition) { + var removedIds = []; // Resubscribe - objects in view have changed - subscribe($scope.domainObject); + if (composition !== previousComposition) { + //remove any elements no longer in the composition + removedIds = _.difference(previousComposition, composition); + if (removedIds.length > 0) { + removeObjects(removedIds); + } + } } // Trigger a new query for telemetry data - function updateDisplayBounds(event, bounds) { - maxDomainValue = bounds.end; - if (handle) { - handle.request( - { size: 1 }, // Only need a single data point - updateValueFromSeries - ); + function updateDisplayBounds(bounds) { + if (!self.openmct.conductor.follow()) { + //Reset values + self.values = {}; + refreshElements(); + //Fetch new data + self.fetchHistoricalData(self.telemetryObjects); } } @@ -290,6 +256,9 @@ define( width: DEFAULT_DIMENSIONS[0], height: DEFAULT_DIMENSIONS[1] }); + + //Re-initialize objects, and subscribe to new object + self.getTelemetry($scope.domainObject); } this.elementProxies = []; @@ -311,25 +280,167 @@ define( // Detect changes to grid size $scope.$watch("model.layoutGrid", updateElementPositions); - // Refresh list of elements whenever model changes - $scope.$watch("model.modified", refreshElements); + // Position panes where they are dropped + $scope.$on("mctDrop", handleDrop); // Position panes when the model field changes $scope.$watch("model.composition", updateComposition); + // Refresh list of elements whenever model changes + $scope.$watch("model.modified", refreshElements); + // Subscribe to telemetry when an object is available - $scope.$watch("domainObject", subscribe); + $scope.$watch("domainObject", this.getTelemetry); // Free up subscription on destroy - $scope.$on("$destroy", releaseSubscription); - - // Position panes where they are dropped - $scope.$on("mctDrop", handleDrop); + $scope.$on("$destroy", function () { + self.unsubscribe(); + self.openmct.conductor.off("bounds", updateDisplayBounds); + }); // Respond to external bounds changes - $scope.$on("telemetry:display:bounds", updateDisplayBounds); + this.openmct.conductor.on("bounds", updateDisplayBounds); } + /** + * A rate-limited digest function. Caps digests at 60Hz + * @private + */ + FixedController.prototype.digest = function () { + var self = this; + + if (!this.digesting) { + this.digesting = true; + requestAnimationFrame(function () { + self.$scope.$digest(); + self.digesting = false; + }); + } + }; + + /** + * Unsubscribe all listeners + * @private + */ + FixedController.prototype.unsubscribe = function () { + this.subscriptions.forEach(function (unsubscribeFunc) { + unsubscribeFunc(); + }); + this.subscriptions = []; + this.telemetryObjects = []; + }; + + /** + * Subscribe to all given domain objects + * @private + * @param {object[]} objects Domain objects to subscribe to + * @returns {object[]} The provided objects, for chaining. + */ + FixedController.prototype.subscribeToObjects = function (objects) { + var self = this; + this.subscriptions = objects.map(function (object) { + return self.openmct.telemetry.subscribe(object, function (datum) { + if (self.openmct.conductor.follow()) { + self.updateView(object, datum); + } + }, {}); + }); + return objects; + }; + + /** + * Print the values from the given datum against the provided object in the view. + * @private + * @param {object} telemetryObject The domain object associated with the given telemetry data + * @param {object} datum The telemetry datum containing the values to print + */ + FixedController.prototype.updateView = function (telemetryObject, datum) { + var metadata = this.openmct.telemetry.getMetadata(telemetryObject); + var rangeMetadata = metadata.valuesForHints(['range'])[0]; + var rangeKey = rangeMetadata.source || rangeMetadata.key; + var valueMetadata = metadata.value(rangeKey); + var limitEvaluator = this.openmct.telemetry.limitEvaluator(telemetryObject); + var formatter = this.openmct.telemetry.getValueFormatter(valueMetadata); + var value = datum[valueMetadata.key]; + var alarm = limitEvaluator && limitEvaluator.evaluate(datum, rangeKey); + + this.setDisplayedValue( + telemetryObject, + formatter.format(value), + alarm && alarm.cssClass + ); + this.digest(); + }; + + /** + * Request the last historical data point for the given domain objects + * @param {object[]} objects + * @returns {object[]} the provided objects for chaining. + */ + FixedController.prototype.fetchHistoricalData = function (objects) { + var bounds = this.openmct.conductor.bounds(); + var self = this; + + objects.forEach(function (object) { + self.openmct.telemetry.request(object, {start: bounds.start, end: bounds.end, size: 1}) + .then(function (data) { + self.updateView(object, data[data.length - 1]); + }); + }); + return objects; + }; + + + /** + * Print a value to the onscreen element associated with a given telemetry object. + * @private + * @param {object} telemetryObject The telemetry object associated with the value + * @param {string | number} value The value to print to screen + * @param {string} [cssClass] an optional CSS class to apply to the onscreen element. + */ + FixedController.prototype.setDisplayedValue = function (telemetryObject, value, cssClass) { + var id = objectUtils.makeKeyString(telemetryObject.identifier); + var self = this; + + (self.elementProxiesById[id] || []).forEach(function (element) { + self.names[id] = telemetryObject.name; + self.values[id] = value; + element.name = self.names[id]; + element.value = self.values[id]; + element.cssClass = cssClass; + }); + }; + + FixedController.prototype.getTelemetry = function (domainObject) { + var newObject = domainObject.useCapability('adapter'); + var self = this; + + if (this.subscriptions.length > 0) { + this.unsubscribe(); + } + + function filterForTelemetryObjects(objects) { + return objects.filter(function (object) { + return self.openmct.telemetry.canProvideTelemetry(object); + }); + } + + function initializeDisplay(objects) { + self.telemetryObjects = objects; + objects.forEach(function (object) { + // Initialize values + self.setDisplayedValue(object, ""); + }); + return objects; + } + + return this.openmct.composition.get(newObject).load() + .then(filterForTelemetryObjects) + .then(initializeDisplay) + .then(this.fetchHistoricalData) + .then(this.subscribeToObjects); + }; + /** * Get the size of the grid, in pixels. The returned array * is in the form `[x, y]`. diff --git a/platform/features/layout/test/FixedControllerSpec.js b/platform/features/layout/test/FixedControllerSpec.js index 190658b4f9..b289f04628 100644 --- a/platform/features/layout/test/FixedControllerSpec.js +++ b/platform/features/layout/test/FixedControllerSpec.js @@ -37,6 +37,15 @@ define( testModel, testValues, testConfiguration, + mockOpenMCT, + mockTelemetryAPI, + mockCompositionAPI, + mockCompositionCollection, + mockChildren, + mockConductor, + mockMetadata, + mockTimeSystem, + mockLimitEvaluator, controller; // Utility function; find a watch for a given expression @@ -62,19 +71,18 @@ define( } function makeMockDomainObject(id) { - var mockObject = jasmine.createSpyObj( - 'domainObject-' + id, - ['getId', 'getModel', 'getCapability'] - ); - mockObject.getId.andReturn(id); - mockObject.getModel.andReturn({ name: "Point " + id}); - return mockObject; + return { + identifier: { + key: "domainObject-" + id + }, + name: "Point " + id + }; } beforeEach(function () { mockScope = jasmine.createSpyObj( '$scope', - ["$on", "$watch", "commit"] + ["$on", "$watch", "$digest", "commit"] ); mockHandler = jasmine.createSpyObj( 'telemetryHandler', @@ -87,12 +95,17 @@ define( ); mockFormatter = jasmine.createSpyObj( 'telemetryFormatter', - ['formatDomainValue', 'formatRangeValue'] + ['format'] ); + mockFormatter.format.andCallFake(function (value) { + return "Formatted " + value; + }); + mockDomainObject = jasmine.createSpyObj( 'domainObject', - ['getId', 'getModel', 'getCapability'] + ['getId', 'getModel', 'getCapability', 'useCapability'] ); + mockHandle = jasmine.createSpyObj( 'subscription', [ @@ -104,11 +117,39 @@ define( 'request' ] ); + mockConductor = jasmine.createSpyObj('conductor', [ + 'on', + 'off', + 'bounds', + 'timeSystem', + 'follow' + ]); + mockConductor.bounds.andReturn({}); + mockTimeSystem = { + metadata: { + key: 'key' + } + }; + mockConductor.timeSystem.andReturn(mockTimeSystem); + mockEvent = jasmine.createSpyObj( 'event', ['preventDefault'] ); + mockTelemetryAPI = jasmine.createSpyObj('telemetry', + [ + 'subscribe', + 'request', + 'canProvideTelemetry', + 'getMetadata', + 'limitEvaluator', + 'getValueFormatter' + ] + ); + mockTelemetryAPI.canProvideTelemetry.andReturn(true); + mockTelemetryAPI.request.andReturn(Promise.resolve([])); + testGrid = [123, 456]; testModel = { composition: ['a', 'b', 'c'], @@ -121,17 +162,23 @@ define( { type: "fixed.telemetry", id: 'c', x: 1, y: 1 } ]}; - mockHandler.handle.andReturn(mockHandle); - mockHandle.getTelemetryObjects.andReturn( - testModel.composition.map(makeMockDomainObject) + mockChildren = testModel.composition.map(makeMockDomainObject); + mockCompositionCollection = jasmine.createSpyObj('compositionCollection', + [ + 'load' + ] ); - mockHandle.getRangeValue.andCallFake(function (o) { - return testValues[o.getId()]; - }); - mockHandle.getDomainValue.andReturn(12321); - mockFormatter.formatRangeValue.andCallFake(function (v) { - return "Formatted " + v; - }); + mockCompositionAPI = jasmine.createSpyObj('composition', + [ + 'get' + ] + ); + mockCompositionAPI.get.andReturn(mockCompositionCollection); + mockCompositionCollection.load.andReturn( + Promise.resolve(mockChildren) + ); + + mockScope.model = testModel; mockScope.configuration = testConfiguration; mockScope.selection = jasmine.createSpyObj( @@ -139,12 +186,47 @@ define( ['select', 'get', 'selected', 'deselect', 'proxy'] ); + mockOpenMCT = { + conductor: mockConductor, + telemetry: mockTelemetryAPI, + composition: mockCompositionAPI + }; + + mockMetadata = jasmine.createSpyObj('mockMetadata', [ + 'valuesForHints', + 'value' + ]); + mockMetadata.value.andReturn({ + key: 'value' + }); + + mockMetadata.valuesForHints.andCallFake(function (hints) { + if (hints === ['domain']) { + return [{ + key: 'time' + }]; + } else { + return [{ + key: 'value' + }]; + } + }); + + mockLimitEvaluator = jasmine.createSpyObj('limitEvaluator', [ + 'evaluate' + ]); + + mockLimitEvaluator.evaluate.andReturn({}); + + mockTelemetryAPI.getMetadata.andReturn(mockMetadata); + mockTelemetryAPI.limitEvaluator.andReturn(mockLimitEvaluator); + mockTelemetryAPI.getValueFormatter.andReturn(mockFormatter); + controller = new FixedController( mockScope, mockQ, mockDialogService, - mockHandler, - mockFormatter + mockOpenMCT ); findWatch("model.layoutGrid")(testModel.layoutGrid); @@ -152,26 +234,61 @@ define( }); it("subscribes when a domain object is available", function () { + var dunzo = false; + mockScope.domainObject = mockDomainObject; - findWatch("domainObject")(mockDomainObject); - expect(mockHandler.handle).toHaveBeenCalledWith( - mockDomainObject, - jasmine.any(Function) - ); + findWatch("domainObject")(mockDomainObject).then(function () { + dunzo = true; + }); + + waitsFor(function () { + return dunzo; + }, "Telemetry fetched", 200); + + runs(function () { + mockChildren.forEach(function (child) { + expect(mockTelemetryAPI.subscribe).toHaveBeenCalledWith( + child, + jasmine.any(Function), + jasmine.any(Object) + ); + }); + }); }); it("releases subscriptions when domain objects change", function () { + var dunzo = false; + var unsubscribe = jasmine.createSpy('unsubscribe'); + + mockTelemetryAPI.subscribe.andReturn(unsubscribe); + mockScope.domainObject = mockDomainObject; + findWatch("domainObject")(mockDomainObject).then(function () { + dunzo = true; + }); - // First pass - should simply should subscribe - findWatch("domainObject")(mockDomainObject); - expect(mockHandle.unsubscribe).not.toHaveBeenCalled(); - expect(mockHandler.handle.calls.length).toEqual(1); + waitsFor(function () { + return dunzo; + }, "Telemetry fetched", 200); - // Object changes - should unsubscribe then resubscribe - findWatch("domainObject")(mockDomainObject); - expect(mockHandle.unsubscribe).toHaveBeenCalled(); - expect(mockHandler.handle.calls.length).toEqual(2); + runs(function () { + expect(unsubscribe).not.toHaveBeenCalled(); + + dunzo = false; + + findWatch("domainObject")(mockDomainObject).then(function () { + dunzo = true; + }); + + waitsFor(function () { + return dunzo; + }, "Telemetry fetched", 200); + + runs(function () { + expect(unsubscribe.calls.length).toBe(mockChildren.length); + }); + + }); }); it("exposes visible elements based on configuration", function () { @@ -254,25 +371,38 @@ define( expect(mockScope.selection.select.calls.length).toEqual(2); }); - it("provides values for telemetry elements", function () { + it("Displays received values for telemetry elements", function () { var elements; - // Initialize - mockScope.domainObject = mockDomainObject; - mockScope.model = testModel; - findWatch("domainObject")(mockDomainObject); - findWatch("model.modified")(1); - findWatch("model.composition")(mockScope.model.composition); + var mockTelemetry = { + time: 100, + value: 200 + }; + var testElement = {}; + var telemetryObject = { + identifier: { + key: '12345' + } + }; + controller.elementProxiesById = {}; + controller.elementProxiesById['12345'] = [testElement]; + controller.elementProxies = [testElement]; - // Invoke the subscription callback - mockHandler.handle.mostRecentCall.args[1](); + controller.subscribeToObjects([telemetryObject]); + mockConductor.follow.andReturn(true); + mockTelemetryAPI.subscribe.mostRecentCall.args[1](mockTelemetry); - // Get elements that controller is now exposing - elements = controller.getElements(); + waitsFor(function () { + return controller.digesting === false; + }, "digest to complete", 100); + + runs(function () { + // Get elements that controller is now exposing + elements = controller.getElements(); + + // Formatted values should be available + expect(elements[0].value).toEqual("Formatted 200"); + }); - // Formatted values should be available - expect(elements[0].value).toEqual("Formatted 10"); - expect(elements[1].value).toEqual("Formatted 42"); - expect(elements[2].value).toEqual("Formatted 31.42"); }); it("updates elements styles when grid size changes", function () { @@ -291,6 +421,9 @@ define( }); it("listens for drop events", function () { + mockScope.domainObject = mockDomainObject; + mockScope.model = testModel; + // Layout should position panels according to // where the user dropped them, so it needs to // listen for drop events. @@ -339,14 +472,29 @@ define( }); it("unsubscribes when destroyed", function () { - // Make an object available - findWatch('domainObject')(mockDomainObject); - // Also verify precondition - expect(mockHandle.unsubscribe).not.toHaveBeenCalled(); - // Destroy the scope - findOn('$destroy')(); - // Should have unsubscribed - expect(mockHandle.unsubscribe).toHaveBeenCalled(); + + var dunzo = false; + var unsubscribe = jasmine.createSpy('unsubscribe'); + + mockTelemetryAPI.subscribe.andReturn(unsubscribe); + + mockScope.domainObject = mockDomainObject; + findWatch("domainObject")(mockDomainObject).then(function () { + dunzo = true; + }); + + waitsFor(function () { + return dunzo; + }, "Telemetry fetched", 200); + + runs(function () { + expect(unsubscribe).not.toHaveBeenCalled(); + // Destroy the scope + findOn('$destroy')(); + + //Check that the same unsubscribe function returned by the + expect(unsubscribe.calls.length).toBe(mockChildren.length); + }); }); it("exposes its grid size", function () { @@ -427,92 +575,102 @@ define( describe("on display bounds changes", function () { var testBounds; + var boundsChangeCallback; + var objectOne; + var objectTwo; beforeEach(function () { testBounds = { start: 123, end: 321 }; - mockScope.domainObject = mockDomainObject; - mockScope.model = testModel; - findWatch("domainObject")(mockDomainObject); - findWatch("model.modified")(testModel.modified); - findWatch("model.composition")(mockScope.model.composition); - findOn('telemetry:display:bounds')({}, testBounds); + boundsChangeCallback = mockConductor.on.mostRecentCall.args[1]; + objectOne = {}; + objectTwo = {}; + controller.telemetryObjects = [ + objectOne, + objectTwo + ]; + spyOn(controller, "fetchHistoricalData"); + controller.fetchHistoricalData.andCallThrough(); }); - it("issues new requests", function () { - expect(mockHandle.request).toHaveBeenCalled(); + it("registers a bounds change listener", function () { + expect(mockConductor.on).toHaveBeenCalledWith("bounds", jasmine.any(Function)); }); it("requests only a single point", function () { - expect(mockHandle.request.mostRecentCall.args[0].size) - .toEqual(1); + mockConductor.follow.andReturn(false); + boundsChangeCallback(testBounds); + expect(mockTelemetryAPI.request.calls.length).toBe(2); + + mockTelemetryAPI.request.calls.forEach(function (call) { + expect(call.args[1].size).toBe(1); + }); }); - describe("and after data has been received", function () { - var mockSeries, - testValue; + it("Does not fetch historical data on tick", function () { + mockConductor.follow.andReturn(true); + boundsChangeCallback(testBounds); + expect(mockTelemetryAPI.request.calls.length).toBe(0); + }); + }); - beforeEach(function () { - testValue = 12321; + describe("on receipt of telemetry", function () { + var mockTelemetryObject; + var testValue; + var testElement; - mockSeries = jasmine.createSpyObj('series', [ - 'getPointCount', - 'getDomainValue', - 'getRangeValue' - ]); - mockSeries.getPointCount.andReturn(1); - mockSeries.getRangeValue.andReturn(testValue); + beforeEach(function () { + mockTelemetryObject = { + identifier: { + key: '12345' + } + }; + testValue = 30; + testElement = {}; - // Fire the callback associated with the request - mockHandle.request.mostRecentCall.args[1]( - mockHandle.getTelemetryObjects()[0], - mockSeries - ); + controller.elementProxiesById = {}; + controller.elementProxiesById['12345'] = [testElement]; + controller.elementProxies = [testElement]; + }); + + it("updates displayed values from historical telemetry", function () { + spyOn(controller, "updateView"); + controller.updateView.andCallThrough(); + + mockTelemetryAPI.request.andReturn(Promise.resolve([{ + time: 100, + value: testValue + }])); + + controller.fetchHistoricalData([mockTelemetryObject]); + + waitsFor(function () { + return controller.digesting === false; }); - it("updates displayed values", function () { + runs(function () { + expect(controller.updateView).toHaveBeenCalled(); expect(controller.getElements()[0].value) .toEqual("Formatted " + testValue); }); }); - }); + it("reflects limit status", function () { + mockLimitEvaluator.evaluate.andReturn({cssClass: "alarm-a"}); + controller.updateView(mockTelemetryObject, [{ + time: 100, + value: testValue + }]); - it("reflects limit status", function () { - var elements; - - mockHandle.getDatum.andReturn({}); - mockHandle.getTelemetryObjects().forEach(function (mockObject) { - var id = mockObject.getId(), - mockLimitCapability = - jasmine.createSpyObj('limit-' + id, ['evaluate']); - - mockObject.getCapability.andCallFake(function (key) { - return (key === 'limit') && mockLimitCapability; + waitsFor(function () { + return controller.digesting === false; }); - mockLimitCapability.evaluate - .andReturn({ cssClass: 'alarm-' + id }); + runs(function () { + // Limit-based CSS classes should be available + expect(controller.getElements()[0].cssClass).toEqual("alarm-a"); + }); }); - - // Initialize - mockScope.domainObject = mockDomainObject; - mockScope.model = testModel; - findWatch("domainObject")(mockDomainObject); - findWatch("model.modified")(1); - findWatch("model.composition")(mockScope.model.composition); - - // Invoke the subscription callback - mockHandler.handle.mostRecentCall.args[1](); - - // Get elements that controller is now exposing - elements = controller.getElements(); - - // Limit-based CSS classes should be available - expect(elements[0].cssClass).toEqual("alarm-a"); - expect(elements[1].cssClass).toEqual("alarm-b"); - expect(elements[2].cssClass).toEqual("alarm-c"); }); - }); } ); diff --git a/platform/features/table/src/TelemetryCollection.js b/platform/features/table/src/TelemetryCollection.js index 048ea7ae29..6b45bc2835 100644 --- a/platform/features/table/src/TelemetryCollection.js +++ b/platform/features/table/src/TelemetryCollection.js @@ -75,6 +75,7 @@ define( // If collection is not sorted by a time field, we cannot respond to // bounds events if (this.sortField === undefined) { + this.lastBounds = bounds; return; } @@ -94,7 +95,7 @@ define( if (discarded && discarded.length > 0) { /** - * A `discarded` event is thrown when telemetry data fall out of + * A `discarded` event is emitted when telemetry data fall out of * bounds due to a bounds change event * @type {object[]} discarded the telemetry data * discarded as a result of the bounds change @@ -103,7 +104,7 @@ define( } if (added && added.length > 0) { /** - * An `added` event is thrown when a bounds change results in + * An `added` event is emitted when a bounds change results in * received telemetry falling within the new bounds. * @type {object[]} added the telemetry data that is now within bounds */ @@ -194,11 +195,14 @@ define( */ TelemetryCollection.prototype.clear = function () { this.telemetry = []; + this.highBuffer = []; }; /** * Sorts the telemetry collection based on the provided sort field - * specifier. + * specifier. Subsequent inserts are sorted to maintain specified sport + * order. + * * @example * // First build some mock telemetry for the purpose of an example * let now = Date.now();