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