+ ng-click="table.onRowClick($event, visibleRow.rowIndex)">
|
-
-
-
\ No newline at end of file
diff --git a/platform/features/table/res/templates/historical-table.html b/platform/features/table/res/templates/telemetry-table.html
similarity index 62%
rename from platform/features/table/res/templates/historical-table.html
rename to platform/features/table/res/templates/telemetry-table.html
index c2abbf5708..5bda288f1c 100644
--- a/platform/features/table/res/templates/historical-table.html
+++ b/platform/features/table/res/templates/telemetry-table.html
@@ -1,12 +1,14 @@
-
\ No newline at end of file
diff --git a/platform/features/table/src/DomainColumn.js b/platform/features/table/src/DomainColumn.js
deleted file mode 100644
index 9e5c4d7813..0000000000
--- a/platform/features/table/src/DomainColumn.js
+++ /dev/null
@@ -1,62 +0,0 @@
-/*****************************************************************************
- * Open MCT, Copyright (c) 2014-2016, United States Government
- * as represented by the Administrator of the National Aeronautics and Space
- * Administration. All rights reserved.
- *
- * Open MCT is licensed under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- * http://www.apache.org/licenses/LICENSE-2.0.
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations
- * under the License.
- *
- * Open MCT includes source code licensed under additional open source
- * licenses. See the Open Source Licenses file (LICENSES.md) included with
- * this source code distribution or the Licensing information page available
- * at runtime from the About dialog for additional information.
- *****************************************************************************/
-
-/**
- * Module defining DomainColumn.
- */
-define(
- [],
- function () {
-
- /**
- * A column which will report telemetry domain values
- * (typically, timestamps.) Used by the ScrollingListController.
- *
- * @memberof platform/features/table
- * @constructor
- * @param domainMetadata an object with the machine- and human-
- * readable names for this domain (in `key` and `name`
- * fields, respectively.)
- * @param {TelemetryFormatter} telemetryFormatter the telemetry
- * formatting service, for making values human-readable.
- */
- function DomainColumn(domainMetadata, telemetryFormatter) {
- this.domainMetadata = domainMetadata;
- this.telemetryFormatter = telemetryFormatter;
- }
-
- DomainColumn.prototype.getTitle = function () {
- return this.domainMetadata.name;
- };
-
- DomainColumn.prototype.getValue = function (domainObject, datum) {
- return {
- text: this.telemetryFormatter.formatDomainValue(
- datum[this.domainMetadata.key],
- this.domainMetadata.format
- )
- };
- };
-
- return DomainColumn;
- }
-);
diff --git a/platform/features/table/src/NameColumn.js b/platform/features/table/src/NameColumn.js
deleted file mode 100644
index 2499ed505b..0000000000
--- a/platform/features/table/src/NameColumn.js
+++ /dev/null
@@ -1,52 +0,0 @@
-/*****************************************************************************
- * Open MCT, Copyright (c) 2014-2016, United States Government
- * as represented by the Administrator of the National Aeronautics and Space
- * Administration. All rights reserved.
- *
- * Open MCT is licensed under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- * http://www.apache.org/licenses/LICENSE-2.0.
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations
- * under the License.
- *
- * Open MCT includes source code licensed under additional open source
- * licenses. See the Open Source Licenses file (LICENSES.md) included with
- * this source code distribution or the Licensing information page available
- * at runtime from the About dialog for additional information.
- *****************************************************************************/
-
-/**
- * Module defining NameColumn. Created by vwoeltje on 11/18/14.
- */
-define(
- [],
- function () {
-
- /**
- * A column which will report the name of the domain object
- * which exposed specific telemetry values.
- *
- * @memberof platform/features/table
- * @constructor
- */
- function NameColumn() {
- }
-
- NameColumn.prototype.getTitle = function () {
- return "Name";
- };
-
- NameColumn.prototype.getValue = function (domainObject) {
- return {
- text: domainObject.getModel().name
- };
- };
-
- return NameColumn;
- }
-);
diff --git a/platform/features/table/src/RangeColumn.js b/platform/features/table/src/RangeColumn.js
deleted file mode 100644
index 1b4c64b8a9..0000000000
--- a/platform/features/table/src/RangeColumn.js
+++ /dev/null
@@ -1,65 +0,0 @@
-/*****************************************************************************
- * Open MCT, Copyright (c) 2014-2016, United States Government
- * as represented by the Administrator of the National Aeronautics and Space
- * Administration. All rights reserved.
- *
- * Open MCT is licensed under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- * http://www.apache.org/licenses/LICENSE-2.0.
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations
- * under the License.
- *
- * Open MCT includes source code licensed under additional open source
- * licenses. See the Open Source Licenses file (LICENSES.md) included with
- * this source code distribution or the Licensing information page available
- * at runtime from the About dialog for additional information.
- *****************************************************************************/
-
-/**
- * Module defining DomainColumn. Created by vwoeltje on 11/18/14.
- */
-define(
- [],
- function () {
-
- /**
- * A column which will report telemetry range values
- * (typically, measurements.) Used by the ScrollingListController.
- *
- * @memberof platform/features/table
- * @constructor
- * @param rangeMetadata an object with the machine- and human-
- * readable names for this range (in `key` and `name`
- * fields, respectively.)
- * @param {TelemetryFormatter} telemetryFormatter the telemetry
- * formatting service, for making values human-readable.
- */
- function RangeColumn(rangeMetadata, telemetryFormatter) {
- this.rangeMetadata = rangeMetadata;
- this.telemetryFormatter = telemetryFormatter;
- }
-
- RangeColumn.prototype.getTitle = function () {
- return this.rangeMetadata.name;
- };
-
- RangeColumn.prototype.getValue = function (domainObject, datum) {
- var range = this.rangeMetadata.key,
- limit = domainObject.getCapability('limit'),
- value = isNaN(datum[range]) ? datum[range] : parseFloat(datum[range]),
- alarm = limit && limit.evaluate(datum, range);
-
- return {
- cssClass: alarm && alarm.cssClass,
- text: typeof (value) === 'undefined' ? undefined : this.telemetryFormatter.formatRangeValue(value)
- };
- };
-
- return RangeColumn;
- }
-);
diff --git a/platform/features/table/src/TableConfiguration.js b/platform/features/table/src/TableConfiguration.js
index fee22d47bc..a59ffa457f 100644
--- a/platform/features/table/src/TableConfiguration.js
+++ b/platform/features/table/src/TableConfiguration.js
@@ -21,12 +21,8 @@
*****************************************************************************/
define(
- [
- './DomainColumn',
- './RangeColumn',
- './NameColumn'
- ],
- function (DomainColumn, RangeColumn, NameColumn) {
+ [],
+ function () {
/**
* Class that manages table metadata, state, and contents.
@@ -34,10 +30,10 @@ define(
* @param domainObject
* @constructor
*/
- function TableConfiguration(domainObject, telemetryFormatter) {
+ function TableConfiguration(domainObject, openmct) {
this.domainObject = domainObject;
this.columns = [];
- this.telemetryFormatter = telemetryFormatter;
+ this.openmct = openmct;
}
/**
@@ -47,61 +43,51 @@ define(
*/
TableConfiguration.prototype.populateColumns = function (metadata) {
var self = this;
+ var telemetryApi = this.openmct.telemetry;
this.columns = [];
if (metadata) {
metadata.forEach(function (metadatum) {
- //Push domains first
- (metadatum.domains || []).forEach(function (domainMetadata) {
- self.addColumn(new DomainColumn(domainMetadata,
- self.telemetryFormatter));
- });
- (metadatum.ranges || []).forEach(function (rangeMetadata) {
- self.addColumn(new RangeColumn(rangeMetadata,
- self.telemetryFormatter));
+ var formatter = telemetryApi.getValueFormatter(metadatum);
+
+ self.columns.push({
+ getKey: function () {
+ return metadatum.key;
+ },
+ getTitle: function () {
+ return metadatum.name;
+ },
+ getValue: function (telemetryDatum, limitEvaluator) {
+ var isValueColumn = !!(metadatum.hints.y || metadatum.hints.range);
+ var alarm = isValueColumn &&
+ limitEvaluator &&
+ limitEvaluator.evaluate(telemetryDatum, metadatum);
+ var value = {
+ text: formatter ? formatter.format(telemetryDatum[metadatum.key])
+ : telemetryDatum[metadatum.key],
+ value: telemetryDatum[metadatum.key]
+ };
+
+ if (alarm) {
+ value.cssClass = alarm.cssClass;
+ }
+ return value;
+ }
});
});
-
- if (this.columns.length > 0) {
- self.addColumn(new NameColumn(), 0);
- }
}
return this;
};
- /**
- * Add a column definition to this Table
- * @param {RangeColumn | DomainColumn | NameColumn} column
- * @param {Number} [index] Where the column should appear (will be
- * affected by column filtering)
- */
- TableConfiguration.prototype.addColumn = function (column, index) {
- if (typeof index === 'undefined') {
- this.columns.push(column);
- } else {
- this.columns.splice(index, 0, column);
- }
- };
-
- /**
- * @private
- * @param column
- * @returns {*|string}
- */
- TableConfiguration.prototype.getColumnTitle = function (column) {
- return column.getTitle();
- };
-
/**
* Get a simple list of column titles
* @returns {Array} The titles of the columns
*/
TableConfiguration.prototype.getHeaders = function () {
- var self = this;
return this.columns.map(function (column, i) {
- return self.getColumnTitle(column) || 'Column ' + (i + 1);
+ return column.getTitle() || 'Column ' + (i + 1);
});
};
@@ -113,17 +99,16 @@ define(
* @returns {Object} Key value pairs where the key is the column
* title, and the value is the formatted value from the provided datum.
*/
- TableConfiguration.prototype.getRowValues = function (telemetryObject, datum) {
- var self = this;
+ TableConfiguration.prototype.getRowValues = function (limitEvaluator, datum) {
return this.columns.reduce(function (rowObject, column, i) {
- var columnTitle = self.getColumnTitle(column) || 'Column ' + (i + 1),
- columnValue = column.getValue(telemetryObject, datum);
+ var columnTitle = column.getTitle() || 'Column ' + (i + 1),
+ columnValue = column.getValue(datum, limitEvaluator);
if (columnValue !== undefined && columnValue.text === undefined) {
columnValue.text = '';
}
// Don't replace something with nothing.
- // This occurs when there are multiple columns with the
+ // This occurs when there are multiple columns with the same
// column title
if (rowObject[columnTitle] === undefined ||
rowObject[columnTitle].text === undefined ||
diff --git a/platform/features/table/src/TelemetryCollection.js b/platform/features/table/src/TelemetryCollection.js
new file mode 100644
index 0000000000..ddd91377d3
--- /dev/null
+++ b/platform/features/table/src/TelemetryCollection.js
@@ -0,0 +1,255 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2016, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+
+define(
+ [
+ 'lodash',
+ 'EventEmitter'
+ ],
+ function (_, EventEmitter) {
+
+ /**
+ * @constructor
+ */
+ function TelemetryCollection() {
+ EventEmitter.call(this, arguments);
+ this.telemetry = [];
+ this.highBuffer = [];
+ this.sortField = undefined;
+ this.lastBounds = {};
+
+ _.bindAll(this, [
+ 'addOne',
+ 'iteratee'
+ ]);
+ }
+
+ TelemetryCollection.prototype = Object.create(EventEmitter.prototype);
+
+ TelemetryCollection.prototype.iteratee = function (item) {
+ return _.get(item, this.sortField);
+ };
+
+ /**
+ * This function is optimized for ticking - it assumes that start and end
+ * bounds will only increase and as such this cannot be used for decreasing
+ * bounds changes.
+ *
+ * An implication of this is that data will not be discarded that exceeds
+ * the given end bounds. For arbitrary bounds changes, it's assumed that
+ * a telemetry requery is performed anyway, and the collection is cleared
+ * and repopulated.
+ *
+ * @fires TelemetryCollection#added
+ * @fires TelemetryCollection#discarded
+ * @param bounds
+ */
+ TelemetryCollection.prototype.bounds = function (bounds) {
+ var startChanged = this.lastBounds.start !== bounds.start;
+ var endChanged = this.lastBounds.end !== bounds.end;
+ var startIndex = 0;
+ var endIndex = 0;
+ var discarded;
+ var added;
+ var testValue;
+
+ // If collection is not sorted by a time field, we cannot respond to
+ // bounds events
+ if (this.sortField === undefined) {
+ return;
+ }
+
+ if (startChanged) {
+ testValue = _.set({}, this.sortField, bounds.start);
+ // Calculate the new index of the first item within the bounds
+ startIndex = _.sortedIndex(this.telemetry, testValue, this.sortField);
+ discarded = this.telemetry.splice(0, startIndex);
+ }
+ if (endChanged) {
+ testValue = _.set({}, this.sortField, bounds.end);
+ // Calculate the new index of the last item in bounds
+ endIndex = _.sortedLastIndex(this.highBuffer, testValue, this.sortField);
+ added = this.highBuffer.splice(0, endIndex);
+ this.telemetry = this.telemetry.concat(added);
+ }
+
+ if (discarded && discarded.length > 0) {
+ /**
+ * A `discarded` event is thrown 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
+ */
+ this.emit('discarded', discarded);
+ }
+ if (added && added.length > 0) {
+ /**
+ * An `added` event is thrown when a bounds change results in
+ * received telemetry falling within the new bounds.
+ * @type {object[]} added the telemetry data that is now within bounds
+ */
+ this.emit('added', added);
+ }
+ this.lastBounds = bounds;
+ };
+
+ /**
+ * Determines is a given telemetry datum is within the bounds currently
+ * defined for this telemetry collection.
+ * @private
+ * @param datum
+ * @returns {boolean}
+ */
+ TelemetryCollection.prototype.inBounds = function (datum) {
+ var noBoundsDefined = !this.lastBounds || (this.lastBounds.start === undefined && this.lastBounds.end === undefined);
+ var withinBounds =
+ _.get(datum, this.sortField) >= this.lastBounds.start &&
+ _.get(datum, this.sortField) <= this.lastBounds.end;
+
+ return noBoundsDefined || withinBounds;
+ };
+
+ /**
+ * Adds an individual item to the collection. Used internally only
+ * @private
+ * @param item
+ */
+ TelemetryCollection.prototype.addOne = function (item) {
+ var isDuplicate = false;
+ var boundsDefined = this.lastBounds &&
+ (this.lastBounds.start !== undefined && this.lastBounds.end !== undefined);
+ var array;
+ var boundsLow;
+ var boundsHigh;
+
+ // If collection is not sorted by a time field, we cannot respond to
+ // bounds events, so no bounds checking necessary
+ if (this.sortField === undefined) {
+ this.telemetry.push(item);
+ return true;
+ }
+
+ // Insert into either in-bounds array, or the out of bounds high buffer.
+ // Data in the high buffer will be re-evaluated for possible insertion on next tick
+
+ if (boundsDefined) {
+ boundsHigh = _.get(item, this.sortField) > this.lastBounds.end;
+ boundsLow = _.get(item, this.sortField) < this.lastBounds.start;
+
+ if (!boundsHigh && !boundsLow) {
+ array = this.telemetry;
+ } else if (boundsHigh) {
+ array = this.highBuffer;
+ }
+ } else {
+ array = this.telemetry;
+ }
+
+ // If out of bounds low, disregard data
+ if (!boundsLow) {
+
+ // Going to check for duplicates. Bound the search problem to
+ // items around the given time. Use sortedIndex because it
+ // employs a binary search which is O(log n). Can use binary search
+ // based on time stamp because the array is guaranteed ordered due
+ // to sorted insertion.
+ var startIx = _.sortedIndex(array, item, this.sortField);
+
+ if (startIx !== array.length) {
+ var endIx = _.sortedLastIndex(array, item, this.sortField);
+
+ // Create an array of potential dupes, based on having the
+ // same time stamp
+ var potentialDupes = array.slice(startIx, endIx + 1);
+ // Search potential dupes for exact dupe
+ isDuplicate = _.findIndex(potentialDupes, _.isEqual.bind(undefined, item)) > -1;
+ }
+
+ if (!isDuplicate) {
+ array.splice(startIx, 0, item);
+
+ //Return true if it was added and in bounds
+ return array === this.telemetry;
+ }
+ }
+ return false;
+ };
+
+ /**
+ * Add an array of objects to this telemetry collection
+ * @fires TelemetryCollection#added
+ * @param {object[]} items
+ */
+ TelemetryCollection.prototype.add = function (items) {
+ var added = items.filter(this.addOne);
+ this.emit('added', added);
+ };
+
+ /**
+ * Clears the contents of the telemetry collection
+ */
+ TelemetryCollection.prototype.clear = function () {
+ this.telemetry = [];
+ };
+
+ /**
+ * Sorts the telemetry collection based on the provided sort field
+ * specifier.
+ * @example
+ * // First build some mock telemetry for the purpose of an example
+ * let now = Date.now();
+ * let telemetry = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(function (value) {
+ * return {
+ * // define an object property to demonstrate nested paths
+ * timestamp: {
+ * ms: now - value * 1000,
+ * text:
+ * },
+ * value: value
+ * }
+ * });
+ * let collection = new TelemetryCollection();
+ *
+ * collection.add(telemetry);
+ *
+ * // Sort by telemetry value
+ * collection.sort("value");
+ *
+ * // Sort by ms since epoch
+ * collection.sort("timestamp.ms");
+ *
+ * // Sort by formatted date text
+ * collection.sort("timestamp.text");
+ *
+ *
+ * @param {string} sortField An object property path.
+ */
+ TelemetryCollection.prototype.sort = function (sortField) {
+ this.sortField = sortField;
+ if (sortField !== undefined) {
+ this.telemetry = _.sortBy(this.telemetry, this.iteratee);
+ }
+ };
+
+ return TelemetryCollection;
+ }
+);
diff --git a/platform/features/table/src/controllers/HistoricalTableController.js b/platform/features/table/src/controllers/HistoricalTableController.js
deleted file mode 100644
index 0f56f6b4ee..0000000000
--- a/platform/features/table/src/controllers/HistoricalTableController.js
+++ /dev/null
@@ -1,141 +0,0 @@
-/*****************************************************************************
- * Open MCT, Copyright (c) 2014-2016, United States Government
- * as represented by the Administrator of the National Aeronautics and Space
- * Administration. All rights reserved.
- *
- * Open MCT is licensed under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- * http://www.apache.org/licenses/LICENSE-2.0.
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations
- * under the License.
- *
- * Open MCT includes source code licensed under additional open source
- * licenses. See the Open Source Licenses file (LICENSES.md) included with
- * this source code distribution or the Licensing information page available
- * at runtime from the About dialog for additional information.
- *****************************************************************************/
-
-define(
- [
- './TelemetryTableController'
- ],
- function (TableController) {
- var BATCH_SIZE = 1000;
-
- /**
- * Extends TelemetryTableController and adds real-time streaming
- * support.
- * @memberof platform/features/table
- * @param $scope
- * @param telemetryHandler
- * @param telemetryFormatter
- * @constructor
- */
- function HistoricalTableController($scope, telemetryHandler, telemetryFormatter, $timeout, openmct) {
- var self = this;
-
- this.$timeout = $timeout;
- this.timeoutHandle = undefined;
- this.batchSize = BATCH_SIZE;
-
- $scope.$on("$destroy", function () {
- if (self.timeoutHandle) {
- self.$timeout.cancel(self.timeoutHandle);
- }
- });
-
- TableController.call(this, $scope, telemetryHandler, telemetryFormatter, openmct);
- }
-
- HistoricalTableController.prototype = Object.create(TableController.prototype);
-
- /**
- * Set provided row data on scope, and cancel loading spinner
- * @private
- */
- HistoricalTableController.prototype.doneProcessing = function (rowData) {
- this.$scope.rows = rowData;
- this.$scope.loading = false;
- };
-
- /**
- * @private
- */
- HistoricalTableController.prototype.registerChangeListeners = function () {
- TableController.prototype.registerChangeListeners.call(this);
- //Change of bounds in time conductor
- this.changeListeners.push(this.$scope.$on('telemetry:display:bounds',
- this.boundsChange.bind(this))
- );
- };
-
- /**
- * @private
- */
- HistoricalTableController.prototype.boundsChange = function (event, bounds, follow) {
- // If in follow mode, don't bother re-subscribing, data will be
- // received from existing subscription.
- if (follow !== true) {
- this.subscribe();
- }
- };
-
- /**
- * Processes an array of objects, formatting the telemetry available
- * for them and setting it on scope when done
- * @private
- */
- HistoricalTableController.prototype.processTelemetryObjects = function (objects, offset, start, rowData) {
- var telemetryObject = objects[offset],
- series,
- i = start,
- pointCount,
- end;
-
- //No more objects to process
- if (!telemetryObject) {
- return this.doneProcessing(rowData);
- }
-
- series = this.handle.getSeries(telemetryObject);
-
- pointCount = series.getPointCount();
- end = Math.min(start + this.batchSize, pointCount);
-
- //Process rows in a batch with size not exceeding a maximum length
- for (; i < end; i++) {
- rowData.push(this.table.getRowValues(telemetryObject,
- this.handle.makeDatum(telemetryObject, series, i)));
- }
-
- //Done processing all rows for this object.
- if (end >= pointCount) {
- offset++;
- end = 0;
- }
-
- // Done processing either a batch or an object, yield process
- // before continuing processing
- this.timeoutHandle = this.$timeout(this.processTelemetryObjects.bind(this, objects, offset, end, rowData));
- };
-
- /**
- * Populates historical data on scope when it becomes available from
- * the telemetry API
- */
- HistoricalTableController.prototype.addHistoricalData = function () {
- if (this.timeoutHandle) {
- this.$timeout.cancel(this.timeoutHandle);
- }
-
- this.timeoutHandle = this.$timeout(this.processTelemetryObjects.bind(this, this.handle.getTelemetryObjects(), 0, 0, []));
- };
-
- return HistoricalTableController;
- }
-);
diff --git a/platform/features/table/src/controllers/MCTTableController.js b/platform/features/table/src/controllers/MCTTableController.js
index 3ab1887c29..d9f3a9f680 100644
--- a/platform/features/table/src/controllers/MCTTableController.js
+++ b/platform/features/table/src/controllers/MCTTableController.js
@@ -1,7 +1,10 @@
define(
- ['zepto'],
- function ($) {
+ [
+ 'zepto',
+ 'lodash'
+ ],
+ function ($, _) {
/**
* A controller for the MCTTable directive. Populates scope with
@@ -12,13 +15,13 @@ define(
* @param element
* @constructor
*/
- function MCTTableController($scope, $timeout, element, exportService, formatService, openmct) {
+ function MCTTableController($scope, $window, element, exportService, formatService, openmct) {
var self = this;
this.$scope = $scope;
this.element = $(element[0]);
- this.$timeout = $timeout;
- this.maxDisplayRows = 50;
+ this.$window = $window;
+ this.maxDisplayRows = 100;
this.scrollable = this.element.find('.l-view-section.scrolling').first();
this.resultsHeader = this.element.find('.mct-table>thead').first();
@@ -27,15 +30,39 @@ define(
this.conductor = openmct.conductor;
this.toiFormatter = undefined;
this.formatService = formatService;
+ this.callbacks = {};
//Bind all class functions to 'this'
- Object.keys(MCTTableController.prototype).filter(function (key) {
- return typeof MCTTableController.prototype[key] === 'function';
- }).forEach(function (key) {
- this[key] = MCTTableController.prototype[key].bind(this);
- }.bind(this));
+ _.bindAll(this, [
+ 'addRows',
+ 'binarySearch',
+ 'buildLargestRow',
+ 'changeBounds',
+ 'changeTimeOfInterest',
+ 'changeTimeSystem',
+ 'destroyConductorListeners',
+ 'digest',
+ 'filterAndSort',
+ 'filterRows',
+ 'firstVisible',
+ 'insertSorted',
+ 'lastVisible',
+ 'onRowClick',
+ 'onScroll',
+ 'removeRows',
+ 'resize',
+ 'scrollToBottom',
+ 'scrollToRow',
+ 'setElementSizes',
+ 'setHeaders',
+ 'setRows',
+ 'setTimeOfInterestRow',
+ 'setVisibleRows',
+ 'sortComparator',
+ 'sortRows'
+ ]);
- this.scrollable.on('scroll', this.onScroll.bind(this));
+ this.scrollable.on('scroll', this.onScroll);
$scope.visibleRows = [];
@@ -86,7 +113,7 @@ define(
$scope.sortDirection = 'asc';
}
self.setRows($scope.rows);
- self.setTimeOfInterest(self.conductor.timeOfInterest());
+ self.setTimeOfInterestRow(self.conductor.timeOfInterest());
};
/*
@@ -95,20 +122,28 @@ define(
$scope.$watchCollection('filters', function () {
self.setRows($scope.rows);
});
- $scope.$watch('headers', this.setHeaders);
+ $scope.$watch('headers', function (newHeaders, oldHeaders) {
+ if (newHeaders !== oldHeaders) {
+ this.setHeaders(newHeaders);
+ }
+ }.bind(this));
$scope.$watch('rows', this.setRows);
/*
* Listen for rows added individually (eg. for real-time tables)
*/
- $scope.$on('add:row', this.addRow);
- $scope.$on('remove:row', this.removeRow);
+ $scope.$on('add:rows', this.addRows);
+ $scope.$on('remove:rows', this.removeRows);
/**
* Populated from the default-sort attribute on MctTable
* directive tag.
*/
- $scope.$watch('sortColumn', $scope.toggleSort);
+ $scope.$watch('defaultSort', function (newColumn, oldColumn) {
+ if (newColumn !== oldColumn) {
+ $scope.toggleSort(newColumn);
+ }
+ });
/*
* Listen for resize events to trigger recalculation of table width
@@ -125,7 +160,7 @@ define(
this.destroyConductorListeners();
this.conductor.on('timeSystem', this.changeTimeSystem);
- this.conductor.on('timeOfInterest', this.setTimeOfInterest);
+ this.conductor.on('timeOfInterest', this.changeTimeOfInterest);
this.conductor.on('bounds', this.changeBounds);
// If time system defined, set initially
@@ -135,12 +170,20 @@ define(
}
}.bind(this));
- $scope.$on('$destroy', this.destroyConductorListeners);
+ $scope.$on('$destroy', function () {
+ this.scrollable.off('scroll', this.onScroll);
+ this.destroyConductorListeners();
+
+ // In case for some reason this controller instance lingers around,
+ // destroy scope as it can be extremely large for large tables.
+ delete this.$scope;
+
+ }.bind(this));
}
MCTTableController.prototype.destroyConductorListeners = function () {
this.conductor.off('timeSystem', this.changeTimeSystem);
- this.conductor.off('timeOfInterest', this.setTimeOfInterest);
+ this.conductor.off('timeOfInterest', this.changeTimeOfInterest);
this.conductor.off('bounds', this.changeBounds);
};
@@ -155,15 +198,7 @@ define(
* @private
*/
MCTTableController.prototype.scrollToBottom = function () {
- var self = this;
-
- //Use timeout to defer execution until next digest when any
- // pending UI changes have completed, eg. a new row in the table.
- if (this.$scope.autoScroll) {
- this.$timeout(function () {
- self.scrollable[0].scrollTop = self.scrollable[0].scrollHeight;
- });
- }
+ this.scrollable[0].scrollTop = this.scrollable[0].scrollHeight;
};
/**
@@ -171,18 +206,24 @@ define(
* `add:row` broadcast event.
* @private
*/
- MCTTableController.prototype.addRow = function (event, rowIndex) {
- var row = this.$scope.rows[rowIndex];
-
+ MCTTableController.prototype.addRows = function (event, rows) {
//Does the row pass the current filter?
- if (this.filterRows([row]).length === 1) {
- //Insert the row into the correct position in the array
- this.insertSorted(this.$scope.displayRows, row);
+ if (this.filterRows(rows).length > 0) {
+ rows.forEach(this.insertSorted.bind(this, this.$scope.displayRows));
//Resize the columns , then update the rows visible in the table
- this.resize([this.$scope.sizingRow, row])
- .then(this.setVisibleRows.bind(this))
- .then(this.scrollToBottom.bind(this));
+ this.resize([this.$scope.sizingRow].concat(rows))
+ .then(this.setVisibleRows)
+ .then(function () {
+ if (this.$scope.autoScroll) {
+ this.scrollToBottom();
+ }
+ }.bind(this));
+
+ var toi = this.conductor.timeOfInterest();
+ if (toi !== -1) {
+ this.setTimeOfInterestRow(toi);
+ }
}
};
@@ -191,31 +232,47 @@ define(
* `remove:row` broadcast event.
* @private
*/
- MCTTableController.prototype.removeRow = function (event, rowIndex) {
- var row = this.$scope.rows[rowIndex],
+ MCTTableController.prototype.removeRows = function (event, rows) {
+ var indexInDisplayRows;
+ rows.forEach(function (row) {
// Do a sequential search here. Only way of finding row is by
// object equality, so array is in effect unsorted.
indexInDisplayRows = this.$scope.displayRows.indexOf(row);
- if (indexInDisplayRows !== -1) {
- this.$scope.displayRows.splice(indexInDisplayRows, 1);
- this.setVisibleRows();
- }
+ if (indexInDisplayRows !== -1) {
+ this.$scope.displayRows.splice(indexInDisplayRows, 1);
+ }
+ }, this);
+
+ this.$scope.sizingRow = this.buildLargestRow([this.$scope.sizingRow].concat(rows));
+
+ this.setElementSizes();
+ this.setVisibleRows()
+ .then(function () {
+ if (this.$scope.autoScroll) {
+ this.scrollToBottom();
+ }
+ }.bind(this));
+
};
/**
* @private
*/
MCTTableController.prototype.onScroll = function (event) {
- //If user scrolls away from bottom, disable auto-scroll.
- // Auto-scroll will be re-enabled if user scrolls to bottom again.
- if (this.scrollable[0].scrollTop <
- (this.scrollable[0].scrollHeight - this.scrollable[0].offsetHeight)) {
- this.$scope.autoScroll = false;
- } else {
- this.$scope.autoScroll = true;
- }
- this.setVisibleRows();
- this.$scope.$digest();
+ this.$window.requestAnimationFrame(function () {
+ this.setVisibleRows();
+ this.digest();
+
+ // If user scrolls away from bottom, disable auto-scroll.
+ // Auto-scroll will be re-enabled if user scrolls to bottom again.
+ if (this.scrollable[0].scrollTop <
+ (this.scrollable[0].scrollHeight - this.scrollable[0].offsetHeight) - 20) {
+ this.$scope.autoScroll = false;
+ } else {
+ this.$scope.autoScroll = true;
+ }
+ this.scrolling = false;
+ }.bind(this));
};
/**
@@ -293,8 +350,7 @@ define(
this.$scope.visibleRows[0].rowIndex === start &&
this.$scope.visibleRows[this.$scope.visibleRows.length - 1]
.rowIndex === end) {
-
- return; // don't update if no changes are required.
+ return this.digest();
}
}
//Set visible rows from display rows, based on calculated offset.
@@ -307,6 +363,7 @@ define(
contents: row
};
});
+ return this.digest();
};
/**
@@ -522,6 +579,28 @@ define(
return largestRow;
};
+ // Will effectively cap digests at 60Hz
+ // Also turns digest into a promise allowing code to force digest, then
+ // schedule something to happen afterwards
+ MCTTableController.prototype.digest = function () {
+ var scope = this.$scope;
+ var self = this;
+ var raf = this.$window.requestAnimationFrame;
+ var promise = this.digestPromise;
+
+ if (!promise) {
+ self.digestPromise = promise = new Promise(function (resolve) {
+ raf(function () {
+ scope.$digest();
+ self.digestPromise = undefined;
+ resolve();
+ });
+ });
+ }
+
+ return promise;
+ };
+
/**
* Calculates the widest row in the table, and if necessary, resizes
* the table accordingly
@@ -533,7 +612,7 @@ define(
*/
MCTTableController.prototype.resize = function (rows) {
this.$scope.sizingRow = this.buildLargestRow(rows);
- return this.$timeout(this.setElementSizes.bind(this));
+ return this.digest().then(this.setElementSizes);
};
/**
@@ -562,19 +641,20 @@ define(
}
this.$scope.displayRows = this.filterAndSort(newRows || []);
- this.resize(newRows)
- .then(this.setVisibleRows)
+ return this.resize(newRows)
+ .then(function (rows) {
+ return this.setVisibleRows(rows);
+ }.bind(this))
//Timeout following setVisibleRows to allow digest to
// perform DOM changes, otherwise scrollTo won't work.
- .then(this.$timeout)
.then(function () {
//If TOI specified, scroll to it
var timeOfInterest = this.conductor.timeOfInterest();
if (timeOfInterest) {
- this.setTimeOfInterest(timeOfInterest);
+ this.setTimeOfInterestRow(timeOfInterest);
+ this.scrollToRow(this.$scope.toiRowIndex);
}
}.bind(this));
-
};
/**
@@ -615,6 +695,7 @@ define(
};
/**
+ * Scroll the view to a given row index
* @param displayRowIndex {number} The index in the displayed rows
* to scroll to.
*/
@@ -635,7 +716,7 @@ define(
* Update rows with new data. If filtering is enabled, rows
* will be sorted before display.
*/
- MCTTableController.prototype.setTimeOfInterest = function (newTOI) {
+ MCTTableController.prototype.setTimeOfInterestRow = function (newTOI) {
var isSortedByTime =
this.$scope.timeColumns &&
this.$scope.timeColumns.indexOf(this.$scope.sortColumn) !== -1;
@@ -652,17 +733,24 @@ define(
if (rowIndex > 0 && rowIndex < this.$scope.displayRows.length) {
this.$scope.toiRowIndex = rowIndex;
- this.scrollToRow(this.$scope.toiRowIndex);
}
}
};
+ MCTTableController.prototype.changeTimeOfInterest = function (newTOI) {
+ this.setTimeOfInterestRow(newTOI);
+ this.scrollToRow(this.$scope.toiRowIndex);
+ };
+
/**
* On zoom, pan, etc. reset TOI
* @param bounds
*/
MCTTableController.prototype.changeBounds = function (bounds) {
- this.setTimeOfInterest(this.conductor.timeOfInterest());
+ this.setTimeOfInterestRow(this.conductor.timeOfInterest());
+ if (this.$scope.toiRowIndex !== -1) {
+ this.scrollToRow(this.$scope.toiRowIndex);
+ }
};
/**
diff --git a/platform/features/table/src/controllers/RealtimeTableController.js b/platform/features/table/src/controllers/RealtimeTableController.js
deleted file mode 100644
index c6ff7b8aee..0000000000
--- a/platform/features/table/src/controllers/RealtimeTableController.js
+++ /dev/null
@@ -1,76 +0,0 @@
-/*****************************************************************************
- * Open MCT, Copyright (c) 2014-2016, United States Government
- * as represented by the Administrator of the National Aeronautics and Space
- * Administration. All rights reserved.
- *
- * Open MCT is licensed under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- * http://www.apache.org/licenses/LICENSE-2.0.
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations
- * under the License.
- *
- * Open MCT includes source code licensed under additional open source
- * licenses. See the Open Source Licenses file (LICENSES.md) included with
- * this source code distribution or the Licensing information page available
- * at runtime from the About dialog for additional information.
- *****************************************************************************/
-
-define(
- [
- './TelemetryTableController'
- ],
- function (TableController) {
-
- /**
- * Extends TelemetryTableController and adds real-time streaming
- * support.
- * @memberof platform/features/table
- * @param $scope
- * @param telemetryHandler
- * @param telemetryFormatter
- * @constructor
- */
- function RealtimeTableController($scope, telemetryHandler, telemetryFormatter, openmct) {
- TableController.call(this, $scope, telemetryHandler, telemetryFormatter, openmct);
-
- this.maxRows = 100000;
- }
-
- RealtimeTableController.prototype = Object.create(TableController.prototype);
-
- /**
- * Overrides method on TelemetryTableController providing handling
- * for realtime data.
- */
- RealtimeTableController.prototype.addRealtimeData = function () {
- var self = this,
- datum,
- row;
- this.handle.getTelemetryObjects().forEach(function (telemetryObject) {
- datum = self.handle.getDatum(telemetryObject);
- if (datum) {
- //Populate row values from telemetry datum
- row = self.table.getRowValues(telemetryObject, datum);
- self.$scope.rows.push(row);
-
- //Inform table that a new row has been added
- if (self.$scope.rows.length > self.maxRows) {
- self.$scope.$broadcast('remove:row', 0);
- self.$scope.rows.shift();
- }
-
- self.$scope.$broadcast('add:row',
- self.$scope.rows.length - 1);
- }
- });
- this.$scope.loading = false;
- };
-
- return RealtimeTableController;
- }
-);
diff --git a/platform/features/table/src/controllers/TelemetryTableController.js b/platform/features/table/src/controllers/TelemetryTableController.js
index 7d6cbc2bec..daf042893e 100644
--- a/platform/features/table/src/controllers/TelemetryTableController.js
+++ b/platform/features/table/src/controllers/TelemetryTableController.js
@@ -19,6 +19,7 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
+/* global console*/
/**
* This bundle adds a table view for displaying telemetry data.
@@ -26,9 +27,13 @@
*/
define(
[
- '../TableConfiguration'
+ '../TableConfiguration',
+ '../../../../../src/api/objects/object-utils',
+ '../TelemetryCollection',
+ 'lodash'
+
],
- function (TableConfiguration) {
+ function (TableConfiguration, objectUtils, TelemetryCollection, _) {
/**
* The TableController is responsible for getting data onto the page
@@ -36,183 +41,412 @@ define(
* configuration, and telemetry subscriptions.
* @memberof platform/features/table
* @param $scope
- * @param telemetryHandler
- * @param telemetryFormatter
* @constructor
*/
function TelemetryTableController(
$scope,
- telemetryHandler,
- telemetryFormatter,
+ $timeout,
openmct
) {
- var self = this;
this.$scope = $scope;
+ this.$timeout = $timeout;
+ this.openmct = openmct;
+ this.batchSize = 1000;
+
+ /*
+ * Initialization block
+ */
this.columns = {}; //Range and Domain columns
- this.handle = undefined;
- this.telemetryHandler = telemetryHandler;
- this.table = new TableConfiguration($scope.domainObject,
- telemetryFormatter);
- this.changeListeners = [];
- this.conductor = openmct.conductor;
-
- $scope.rows = [];
-
- // Subscribe to telemetry when a domain object becomes available
- this.$scope.$watch('domainObject', function () {
- self.subscribe();
- self.registerChangeListeners();
- });
-
- this.destroy = this.destroy.bind(this);
-
- // Unsubscribe when the plot is destroyed
- this.$scope.$on("$destroy", this.destroy);
+ this.unobserveObject = undefined;
+ this.subscriptions = [];
this.timeColumns = [];
+ $scope.rows = [];
+ this.table = new TableConfiguration($scope.domainObject,
+ openmct);
+ this.lastBounds = this.openmct.conductor.bounds();
+ this.lastRequestTime = 0;
+ this.telemetry = new TelemetryCollection();
+ /*
+ * Create a new format object from legacy object, and replace it
+ * when it changes
+ */
+ this.newObject = objectUtils.toNewFormat($scope.domainObject.getModel(),
+ $scope.domainObject.getId());
- this.sortByTimeSystem = this.sortByTimeSystem.bind(this);
- this.conductor.on('timeSystem', this.sortByTimeSystem);
- this.conductor.off('timeSystem', this.sortByTimeSystem);
+ _.bindAll(this, [
+ 'destroy',
+ 'sortByTimeSystem',
+ 'loadColumns',
+ 'getHistoricalData',
+ 'subscribeToNewData',
+ 'changeBounds',
+ 'setScroll',
+ 'addRowsToTable',
+ 'removeRowsFromTable'
+ ]);
+
+ // Retrieve data when domain object is available.
+ // Also deferring telemetry request makes testing easier as controller
+ // construction has no unintended consequences.
+ $scope.$watch("domainObject", function () {
+ this.getData();
+ this.registerChangeListeners();
+ }.bind(this));
+
+ this.setScroll(this.openmct.conductor.follow());
+
+ this.$scope.$on("$destroy", this.destroy);
}
+ /**
+ * @private
+ * @param {boolean} scroll
+ */
+ TelemetryTableController.prototype.setScroll = function (scroll) {
+ this.$scope.autoScroll = scroll;
+ };
+
/**
* Based on the selected time system, find a matching domain column
* to sort by. By default will just match on key.
- * @param timeSystem
+ *
+ * @private
+ * @param {TimeSystem} timeSystem
*/
TelemetryTableController.prototype.sortByTimeSystem = function (timeSystem) {
var scope = this.$scope;
+ var sortColumn;
scope.defaultSort = undefined;
+
if (timeSystem) {
this.table.columns.forEach(function (column) {
- if (column.domainMetadata && column.domainMetadata.key === timeSystem.metadata.key) {
- scope.defaultSort = column.getTitle();
+ if (column.getKey() === timeSystem.metadata.key) {
+ sortColumn = column;
}
});
+ if (sortColumn) {
+ scope.defaultSort = sortColumn.getTitle();
+ this.telemetry.sort(sortColumn.getTitle() + '.value');
+ }
}
};
- TelemetryTableController.prototype.unregisterChangeListeners = function () {
- this.changeListeners.forEach(function (listener) {
- return listener && listener();
- });
- this.changeListeners = [];
- };
-
/**
- * Defer registration of change listeners until domain object is
- * available in order to avoid race conditions
+ * Attaches listeners that respond to state change in domain object,
+ * conductor, and receipt of telemetry
+ *
* @private
*/
TelemetryTableController.prototype.registerChangeListeners = function () {
- var self = this;
- this.unregisterChangeListeners();
-
- // When composition changes, re-subscribe to the various
- // telemetry subscriptions
- this.changeListeners.push(this.$scope.$watchCollection(
- 'domainObject.getModel().composition',
- function (newVal, oldVal) {
- if (newVal !== oldVal) {
- self.subscribe();
- }
- })
- );
- };
-
- /**
- * Release the current subscription (called when scope is destroyed)
- */
- TelemetryTableController.prototype.destroy = function () {
- if (this.handle) {
- this.handle.unsubscribe();
- this.handle = undefined;
+ if (this.unobserveObject) {
+ this.unobserveObject();
}
- };
- /**
- * Function for handling realtime data when it is available. This
- * will be called by the telemetry framework when new data is
- * available.
- *
- * Method should be overridden by specializing class.
- */
- TelemetryTableController.prototype.addRealtimeData = function () {
- };
-
- /**
- * Function for handling historical data. Will be called by
- * telemetry framework when requested historical data is available.
- * Should be overridden by specializing class.
- */
- TelemetryTableController.prototype.addHistoricalData = function () {
- };
-
- /**
- Create a new subscription. This can be overridden by children to
- change default behaviour (which is to retrieve historical telemetry
- only).
- */
- TelemetryTableController.prototype.subscribe = function () {
- if (this.handle) {
- this.handle.unsubscribe();
- }
- this.$scope.loading = true;
-
- this.handle = this.$scope.domainObject && this.telemetryHandler.handle(
- this.$scope.domainObject,
- this.addRealtimeData.bind(this),
- true // Lossless
+ this.unobserveObject = this.openmct.objects.observe(this.newObject, "*",
+ function (domainObject) {
+ this.newObject = domainObject;
+ this.getData();
+ }.bind(this)
);
- this.handle.request({}).then(this.addHistoricalData.bind(this));
+ this.openmct.conductor.on('timeSystem', this.sortByTimeSystem);
+ this.openmct.conductor.on('bounds', this.changeBounds);
+ this.openmct.conductor.on('follow', this.setScroll);
- this.setup();
- };
-
- TelemetryTableController.prototype.populateColumns = function (telemetryMetadata) {
- this.table.populateColumns(telemetryMetadata);
-
- //Identify time columns
- telemetryMetadata.forEach(function (metadatum) {
- //Push domains first
- (metadatum.domains || []).forEach(function (domainMetadata) {
- this.timeColumns.push(domainMetadata.name);
- }.bind(this));
- }.bind(this));
-
- var timeSystem = this.conductor.timeSystem();
- if (timeSystem) {
- this.sortByTimeSystem(timeSystem);
- }
+ this.telemetry.on('added', this.addRowsToTable);
+ this.telemetry.on('discarded', this.removeRowsFromTable);
};
/**
- * Setup table columns based on domain object metadata
+ * On receipt of new telemetry, informs mct-table directive that new rows
+ * are available and passes populated rows to it
+ *
+ * @private
+ * @param rows
*/
- TelemetryTableController.prototype.setup = function () {
- var handle = this.handle,
- self = this;
+ TelemetryTableController.prototype.addRowsToTable = function (rows) {
+ this.$scope.$broadcast('add:rows', rows);
+ };
- if (handle) {
- this.timeColumns = [];
- handle.promiseTelemetryObjects().then(function () {
- self.$scope.headers = [];
- self.$scope.rows = [];
+ /**
+ * When rows are to be removed, informs mct-table directive. Row removal
+ * happens when rows call outside the bounds of the time conductor
+ *
+ * @private
+ * @param rows
+ */
+ TelemetryTableController.prototype.removeRowsFromTable = function (rows) {
+ this.$scope.$broadcast('remove:rows', rows);
+ };
- self.populateColumns(handle.getMetadata());
- self.filterColumns();
+ /**
+ * On Time Conductor bounds change, update displayed telemetry. In the
+ * case of a tick, previously visible telemetry that is now out of band
+ * will be removed from the table.
+ * @param {openmct.TimeConductorBounds~TimeConductorBounds} bounds
+ */
+ TelemetryTableController.prototype.changeBounds = function (bounds) {
+ var follow = this.openmct.conductor.follow();
+ var isTick = follow &&
+ bounds.start !== this.lastBounds.start &&
+ bounds.end !== this.lastBounds.end;
- // When table column configuration changes, (due to being
- // selected or deselected), filter columns appropriately.
- self.changeListeners.push(self.$scope.$watchCollection(
- 'domainObject.getModel().configuration.table.columns',
- self.filterColumns.bind(self)
- ));
- });
+ if (isTick) {
+ this.telemetry.bounds(bounds);
+ } else {
+ // Is fixed bounds change
+ this.getData();
}
+ this.lastBounds = bounds;
+ };
+
+ /**
+ * Clean controller, deregistering listeners etc.
+ */
+ TelemetryTableController.prototype.destroy = function () {
+
+ this.openmct.conductor.off('timeSystem', this.sortByTimeSystem);
+ this.openmct.conductor.off('bounds', this.changeBounds);
+ this.openmct.conductor.off('follow', this.setScroll);
+
+ this.subscriptions.forEach(function (subscription) {
+ subscription();
+ });
+
+ if (this.unobserveObject) {
+ this.unobserveObject();
+ }
+ this.subscriptions = [];
+
+ if (this.timeoutHandle) {
+ this.$timeout.cancel(this.timeoutHandle);
+ }
+
+ // In case controller instance lingers around (currently there is a
+ // temporary memory leak with PlotController), clean up scope as it
+ // can be extremely large.
+ this.$scope = null;
+ this.table = null;
+ };
+
+ /**
+ * For given objects, populate column metadata and table headers.
+ * @private
+ * @param {module:openmct.DomainObject[]} objects the domain objects for
+ * which columns should be populated
+ */
+ TelemetryTableController.prototype.loadColumns = function (objects) {
+ var telemetryApi = this.openmct.telemetry;
+
+ this.$scope.headers = [];
+
+ if (objects.length > 0) {
+ var metadatas = objects.map(telemetryApi.getMetadata.bind(telemetryApi));
+ var allColumns = telemetryApi.commonValuesForHints(metadatas, []);
+
+ this.table.populateColumns(allColumns);
+
+ var domainColumns = telemetryApi.commonValuesForHints(metadatas, ['x']);
+ this.timeColumns = domainColumns.map(function (metadatum) {
+ return metadatum.name;
+ });
+
+ this.filterColumns();
+
+ // Default to no sort on underlying telemetry collection. Sorting
+ // is necessary to do bounds filtering, but this is only possible
+ // if data matches selected time system
+ this.telemetry.sort(undefined);
+
+ var timeSystem = this.openmct.conductor.timeSystem();
+ if (timeSystem) {
+ this.sortByTimeSystem(timeSystem);
+ }
+
+ }
+ return objects;
+ };
+
+ /**
+ * Request telemetry data from an historical store for given objects.
+ * @private
+ * @param {object[]} The domain objects to request telemetry for
+ * @returns {Promise} resolved when historical data is available
+ */
+ TelemetryTableController.prototype.getHistoricalData = function (objects) {
+ var self = this;
+ var openmct = this.openmct;
+ var bounds = openmct.conductor.bounds();
+ var scope = this.$scope;
+ var rowData = [];
+ var processedObjects = 0;
+ var requestTime = this.lastRequestTime = Date.now();
+ var telemetryCollection = this.telemetry;
+
+ var promise = new Promise(function (resolve, reject) {
+ /*
+ * On completion of batched processing, set the rows on scope
+ */
+ function finishProcessing() {
+ telemetryCollection.add(rowData);
+ scope.rows = telemetryCollection.telemetry;
+ scope.loading = false;
+
+ resolve(scope.rows);
+ }
+
+ /*
+ * Process a batch of historical data
+ */
+ function processData(historicalData, index, limitEvaluator) {
+ if (index >= historicalData.length) {
+ processedObjects++;
+
+ if (processedObjects === objects.length) {
+ finishProcessing();
+ }
+ } else {
+ rowData = rowData.concat(historicalData.slice(index, index + self.batchSize)
+ .map(self.table.getRowValues.bind(self.table, limitEvaluator)));
+
+ /*
+ Use timeout to yield process to other UI activities. On
+ return, process next batch
+ */
+ self.timeoutHandle = self.$timeout(function () {
+ processData(historicalData, index + self.batchSize, limitEvaluator);
+ });
+ }
+ }
+
+ function makeTableRows(object, historicalData) {
+ // Only process the most recent request
+ if (requestTime === self.lastRequestTime) {
+ var limitEvaluator = openmct.telemetry.limitEvaluator(object);
+ processData(historicalData, 0, limitEvaluator);
+ } else {
+ resolve(rowData);
+ }
+ }
+
+ /*
+ Use the telemetry API to request telemetry for a given object
+ */
+ function requestData(object) {
+ return openmct.telemetry.request(object, {
+ start: bounds.start,
+ end: bounds.end
+ }).then(makeTableRows.bind(undefined, object))
+ .catch(reject);
+ }
+ this.$timeout.cancel(this.timeoutHandle);
+
+ if (objects.length > 0) {
+ objects.forEach(requestData);
+ } else {
+ scope.loading = false;
+ resolve([]);
+ }
+ }.bind(this));
+
+ return promise;
+ };
+
+
+ /**
+ * Subscribe to real-time data for the given objects.
+ * @private
+ * @param {object[]} objects The objects to subscribe to.
+ */
+ TelemetryTableController.prototype.subscribeToNewData = function (objects) {
+ var telemetryApi = this.openmct.telemetry;
+ var telemetryCollection = this.telemetry;
+ //Set table max length to avoid unbounded growth.
+ //var maxRows = 100000;
+ var maxRows = Number.MAX_VALUE;
+ var limitEvaluator;
+ var added = false;
+ var scope = this.$scope;
+ var table = this.table;
+
+ this.subscriptions.forEach(function (subscription) {
+ subscription();
+ });
+ this.subscriptions = [];
+
+ function newData(domainObject, datum) {
+ limitEvaluator = telemetryApi.limitEvaluator(domainObject);
+ added = telemetryCollection.add([table.getRowValues(limitEvaluator, datum)]);
+
+ //Inform table that a new row has been added
+ if (scope.rows.length > maxRows) {
+ scope.$broadcast('remove:rows', scope.rows[0]);
+ scope.rows.shift();
+ }
+ if (!scope.loading && added) {
+ scope.$broadcast('add:row',
+ scope.rows.length - 1);
+ }
+ }
+
+ objects.forEach(function (object) {
+ this.subscriptions.push(
+ telemetryApi.subscribe(object, newData.bind(this, object), {}));
+ }.bind(this));
+
+ return objects;
+ };
+
+ /**
+ * Request historical data, and subscribe to for real-time data.
+ * @private
+ * @returns {Promise} A promise that is resolved once subscription is
+ * established, and historical telemetry is received and processed.
+ */
+ TelemetryTableController.prototype.getData = function () {
+ var telemetryApi = this.openmct.telemetry;
+ var compositionApi = this.openmct.composition;
+ var scope = this.$scope;
+ var newObject = this.newObject;
+
+ this.telemetry.clear();
+ this.telemetry.bounds(this.openmct.conductor.bounds());
+
+ this.$scope.loading = true;
+
+ function error(e) {
+ scope.loading = false;
+ console.error(e.stack);
+ }
+
+ function filterForTelemetry(objects) {
+ return objects.filter(telemetryApi.canProvideTelemetry.bind(telemetryApi));
+ }
+
+ function getDomainObjects() {
+ var objects = [newObject];
+ var composition = compositionApi.get(newObject);
+
+ if (composition) {
+ return composition
+ .load()
+ .then(function (children) {
+ return objects.concat(children);
+ });
+ } else {
+ return Promise.resolve(objects);
+ }
+ }
+ scope.rows = [];
+
+ return getDomainObjects()
+ .then(filterForTelemetry)
+ .then(this.loadColumns)
+ .then(this.subscribeToNewData)
+ .then(this.getHistoricalData)
+ .catch(error);
};
/**
diff --git a/platform/features/table/src/directives/MCTTable.js b/platform/features/table/src/directives/MCTTable.js
index dad23c2eb5..70a2b6665c 100644
--- a/platform/features/table/src/directives/MCTTable.js
+++ b/platform/features/table/src/directives/MCTTable.js
@@ -77,13 +77,13 @@ define(
*
* @constructor
*/
- function MCTTable($timeout) {
+ function MCTTable() {
return {
restrict: "E",
template: TableTemplate,
controller: [
'$scope',
- '$timeout',
+ '$window',
'$element',
'exportService',
'formatService',
@@ -94,6 +94,7 @@ define(
scope: {
headers: "=",
rows: "=",
+ formatCell: "=?",
enableFilter: "=?",
enableSort: "=?",
autoScroll: "=?",
@@ -104,7 +105,7 @@ define(
timeColumns: "=?",
// Indicate a column to sort on. Allows control of sort
// via configuration (eg. for default sort column).
- sortColumn: "=?"
+ defaultSort: "=?"
}
};
}
diff --git a/platform/features/table/test/DomainColumnSpec.js b/platform/features/table/test/DomainColumnSpec.js
deleted file mode 100644
index 3c144b8427..0000000000
--- a/platform/features/table/test/DomainColumnSpec.js
+++ /dev/null
@@ -1,80 +0,0 @@
-/*****************************************************************************
- * Open MCT, Copyright (c) 2014-2016, United States Government
- * as represented by the Administrator of the National Aeronautics and Space
- * Administration. All rights reserved.
- *
- * Open MCT is licensed under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- * http://www.apache.org/licenses/LICENSE-2.0.
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations
- * under the License.
- *
- * Open MCT includes source code licensed under additional open source
- * licenses. See the Open Source Licenses file (LICENSES.md) included with
- * this source code distribution or the Licensing information page available
- * at runtime from the About dialog for additional information.
- *****************************************************************************/
-
-/**
- * MergeModelsSpec. Created by vwoeltje on 11/6/14.
- */
-define(
- ["../src/DomainColumn"],
- function (DomainColumn) {
-
- var TEST_DOMAIN_VALUE = "some formatted domain value";
-
- describe("A domain column", function () {
- var mockDatum,
- testMetadata,
- mockFormatter,
- column;
-
- beforeEach(function () {
-
- mockFormatter = jasmine.createSpyObj(
- "formatter",
- ["formatDomainValue", "formatRangeValue"]
- );
- testMetadata = {
- key: "testKey",
- name: "Test Name",
- format: "Test Format"
- };
- mockFormatter.formatDomainValue.andReturn(TEST_DOMAIN_VALUE);
-
- column = new DomainColumn(testMetadata, mockFormatter);
- });
-
- it("reports a column header from domain metadata", function () {
- expect(column.getTitle()).toEqual("Test Name");
- });
-
- describe("when given a datum", function () {
- beforeEach(function () {
- mockDatum = {
- testKey: "testKeyValue"
- };
- });
-
- it("looks up data from the given datum", function () {
- expect(column.getValue(undefined, mockDatum))
- .toEqual({ text: TEST_DOMAIN_VALUE });
- });
-
- it("uses formatter to format domain values as requested", function () {
- column.getValue(undefined, mockDatum);
- expect(mockFormatter.formatDomainValue)
- .toHaveBeenCalledWith("testKeyValue", "Test Format");
- });
-
- });
-
- });
- }
-);
diff --git a/platform/features/table/test/NameColumnSpec.js b/platform/features/table/test/NameColumnSpec.js
deleted file mode 100644
index 13e858c2ed..0000000000
--- a/platform/features/table/test/NameColumnSpec.js
+++ /dev/null
@@ -1,56 +0,0 @@
-/*****************************************************************************
- * Open MCT, Copyright (c) 2014-2016, United States Government
- * as represented by the Administrator of the National Aeronautics and Space
- * Administration. All rights reserved.
- *
- * Open MCT is licensed under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- * http://www.apache.org/licenses/LICENSE-2.0.
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations
- * under the License.
- *
- * Open MCT includes source code licensed under additional open source
- * licenses. See the Open Source Licenses file (LICENSES.md) included with
- * this source code distribution or the Licensing information page available
- * at runtime from the About dialog for additional information.
- *****************************************************************************/
-
-/**
- * MergeModelsSpec. Created by vwoeltje on 11/6/14.
- */
-define(
- ["../src/NameColumn"],
- function (NameColumn) {
-
- describe("A name column", function () {
- var mockDomainObject,
- column;
-
- beforeEach(function () {
- mockDomainObject = jasmine.createSpyObj(
- "domainObject",
- ["getModel"]
- );
- mockDomainObject.getModel.andReturn({
- name: "Test object name"
- });
- column = new NameColumn();
- });
-
- it("reports a column header", function () {
- expect(column.getTitle()).toEqual("Name");
- });
-
- it("looks up name from an object's model", function () {
- expect(column.getValue(mockDomainObject).text)
- .toEqual("Test object name");
- });
-
- });
- }
-);
diff --git a/platform/features/table/test/RangeColumnSpec.js b/platform/features/table/test/RangeColumnSpec.js
deleted file mode 100644
index 473f26ae56..0000000000
--- a/platform/features/table/test/RangeColumnSpec.js
+++ /dev/null
@@ -1,74 +0,0 @@
-/*****************************************************************************
- * Open MCT, Copyright (c) 2014-2016, United States Government
- * as represented by the Administrator of the National Aeronautics and Space
- * Administration. All rights reserved.
- *
- * Open MCT is licensed under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- * http://www.apache.org/licenses/LICENSE-2.0.
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations
- * under the License.
- *
- * Open MCT includes source code licensed under additional open source
- * licenses. See the Open Source Licenses file (LICENSES.md) included with
- * this source code distribution or the Licensing information page available
- * at runtime from the About dialog for additional information.
- *****************************************************************************/
-
-/**
- * MergeModelsSpec. Created by vwoeltje on 11/6/14.
- */
-define(
- ["../src/RangeColumn"],
- function (RangeColumn) {
-
- var TEST_RANGE_VALUE = "some formatted range value";
-
- describe("A range column", function () {
- var testDatum,
- testMetadata,
- mockFormatter,
- mockDomainObject,
- column;
-
- beforeEach(function () {
- testDatum = { testKey: 123, otherKey: 456 };
- mockFormatter = jasmine.createSpyObj(
- "formatter",
- ["formatDomainValue", "formatRangeValue"]
- );
- testMetadata = {
- key: "testKey",
- name: "Test Name"
- };
- mockDomainObject = jasmine.createSpyObj(
- "domainObject",
- ["getModel", "getCapability"]
- );
- mockFormatter.formatRangeValue.andReturn(TEST_RANGE_VALUE);
-
- column = new RangeColumn(testMetadata, mockFormatter);
- });
-
- it("reports a column header from range metadata", function () {
- expect(column.getTitle()).toEqual("Test Name");
- });
-
- it("formats range values as numbers", function () {
- expect(column.getValue(mockDomainObject, testDatum).text)
- .toEqual(TEST_RANGE_VALUE);
-
- // Make sure that service interactions were as expected
- expect(mockFormatter.formatRangeValue)
- .toHaveBeenCalledWith(testDatum.testKey);
- expect(mockFormatter.formatDomainValue)
- .not.toHaveBeenCalled();
- });
- });
- }
-);
diff --git a/platform/features/table/test/TableConfigurationSpec.js b/platform/features/table/test/TableConfigurationSpec.js
index dcac2a9876..9538335a86 100644
--- a/platform/features/table/test/TableConfigurationSpec.js
+++ b/platform/features/table/test/TableConfigurationSpec.js
@@ -22,13 +22,14 @@
define(
[
- "../src/TableConfiguration",
- "../src/DomainColumn"
+ "../src/TableConfiguration"
],
- function (Table, DomainColumn) {
+ function (Table) {
describe("A table", function () {
var mockDomainObject,
+ mockAPI,
+ mockTelemetryAPI,
mockTelemetryFormatter,
table,
mockModel;
@@ -49,90 +50,63 @@ define(
mockTelemetryFormatter = jasmine.createSpyObj('telemetryFormatter',
[
- 'formatDomainValue',
- 'formatRangeValue'
+ 'format'
]);
- mockTelemetryFormatter.formatDomainValue.andCallFake(function (valueIn) {
- return valueIn;
- });
- mockTelemetryFormatter.formatRangeValue.andCallFake(function (valueIn) {
+ mockTelemetryFormatter.format.andCallFake(function (valueIn) {
return valueIn;
});
- table = new Table(mockDomainObject, mockTelemetryFormatter);
- });
+ mockTelemetryAPI = jasmine.createSpyObj('telemetryAPI', [
+ 'getValueFormatter'
+ ]);
+ mockAPI = {
+ telemetry: mockTelemetryAPI
+ };
+ mockTelemetryAPI.getValueFormatter.andReturn(mockTelemetryFormatter);
- it("Add column with no index adds new column to the end", function () {
- var firstColumn = {title: 'First Column'},
- secondColumn = {title: 'Second Column'},
- thirdColumn = {title: 'Third Column'};
-
- table.addColumn(firstColumn);
- table.addColumn(secondColumn);
- table.addColumn(thirdColumn);
-
- expect(table.columns).toBeDefined();
- expect(table.columns.length).toBe(3);
- expect(table.columns[0]).toBe(firstColumn);
- expect(table.columns[1]).toBe(secondColumn);
- expect(table.columns[2]).toBe(thirdColumn);
- });
-
- it("Add column with index adds new column at the specified" +
- " position", function () {
- var firstColumn = {title: 'First Column'},
- secondColumn = {title: 'Second Column'},
- thirdColumn = {title: 'Third Column'};
-
- table.addColumn(firstColumn);
- table.addColumn(thirdColumn);
- table.addColumn(secondColumn, 1);
-
- expect(table.columns).toBeDefined();
- expect(table.columns.length).toBe(3);
- expect(table.columns[0]).toBe(firstColumn);
- expect(table.columns[1]).toBe(secondColumn);
- expect(table.columns[2]).toBe(thirdColumn);
+ table = new Table(mockDomainObject, mockAPI);
});
describe("Building columns from telemetry metadata", function () {
- var metadata = [{
- ranges: [
- {
- name: 'Range 1',
- key: 'range1'
- },
- {
- name: 'Range 2',
- key: 'range2'
+ var metadata = [
+ {
+ name: 'Range 1',
+ key: 'range1',
+ hints: {
+ y: 1
}
- ],
- domains: [
- {
- name: 'Domain 1',
- key: 'domain1',
- format: 'utc'
- },
- {
- name: 'Domain 2',
- key: 'domain2',
- format: 'utc'
+ },
+ {
+ name: 'Range 2',
+ key: 'range2',
+ hints: {
+ y: 2
}
- ]
- }];
+ },
+ {
+ name: 'Domain 1',
+ key: 'domain1',
+ format: 'utc',
+ hints: {
+ x: 1
+ }
+ },
+ {
+ name: 'Domain 2',
+ key: 'domain2',
+ format: 'utc',
+ hints: {
+ x: 2
+ }
+ }
+ ];
beforeEach(function () {
table.populateColumns(metadata);
});
it("populates columns", function () {
- expect(table.columns.length).toBe(5);
- });
-
- it("Build columns populates columns with domains to the left", function () {
- expect(table.columns[1] instanceof DomainColumn).toBeTruthy();
- expect(table.columns[2] instanceof DomainColumn).toBeTruthy();
- expect(table.columns[3] instanceof DomainColumn).toBeFalsy();
+ expect(table.columns.length).toBe(4);
});
it("Produces headers for each column based on title", function () {
@@ -141,7 +115,7 @@ define(
spyOn(firstColumn, 'getTitle');
headers = table.getHeaders();
- expect(headers.length).toBe(5);
+ expect(headers.length).toBe(4);
expect(firstColumn.getTitle).toHaveBeenCalled();
});
@@ -178,23 +152,33 @@ define(
beforeEach(function () {
datum = {
- 'range1': 'range 1 value',
- 'range2': 'range 2 value',
+ 'range1': 10,
+ 'range2': 20,
'domain1': 0,
'domain2': 1
};
- rowValues = table.getRowValues(mockDomainObject, datum);
+ var limitEvaluator = {
+ evaluate: function () {
+ return {
+ "cssClass": "alarm-class"
+ };
+ }
+ };
+ rowValues = table.getRowValues(limitEvaluator, datum);
});
it("Returns a value for every column", function () {
expect(rowValues['Range 1'].text).toBeDefined();
- expect(rowValues['Range 1'].text).toEqual('range 1' +
- ' value');
+ expect(rowValues['Range 1'].text).toEqual(10);
});
- it("Uses the telemetry formatter to appropriately format" +
+ it("Applies appropriate css class if limit violated.", function () {
+ expect(rowValues['Range 1'].cssClass).toEqual("alarm-class");
+ });
+
+ it("Uses telemetry formatter to appropriately format" +
" telemetry values", function () {
- expect(mockTelemetryFormatter.formatRangeValue).toHaveBeenCalled();
+ expect(mockTelemetryFormatter.format).toHaveBeenCalled();
});
});
});
diff --git a/platform/features/table/test/TelemetryCollectionSpec.js b/platform/features/table/test/TelemetryCollectionSpec.js
new file mode 100644
index 0000000000..014e5c684e
--- /dev/null
+++ b/platform/features/table/test/TelemetryCollectionSpec.js
@@ -0,0 +1,191 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2016, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+
+define(
+ [
+ "../src/TelemetryCollection"
+ ],
+ function (TelemetryCollection) {
+
+ describe("A telemetry collection", function () {
+
+ var collection;
+ var telemetryObjects;
+ var ms;
+ var integerTextMap = ["ZERO", "ONE", "TWO", "THREE", "FOUR", "FIVE",
+ "SIX", "SEVEN", "EIGHT", "NINE", "TEN", "ELEVEN"];
+
+ beforeEach(function () {
+ telemetryObjects = [0,9,2,4,7,8,5,1,3,6].map(function (number) {
+ ms = number * 1000;
+ return {
+ timestamp: ms,
+ value: {
+ integer: number,
+ text: integerTextMap[number]
+ }
+ };
+ });
+ collection = new TelemetryCollection();
+ });
+
+ it("Sorts inserted telemetry by specified field",
+ function () {
+ collection.sort('value.integer');
+ collection.add(telemetryObjects);
+ expect(collection.telemetry[0].value.integer).toBe(0);
+ expect(collection.telemetry[1].value.integer).toBe(1);
+ expect(collection.telemetry[2].value.integer).toBe(2);
+ expect(collection.telemetry[3].value.integer).toBe(3);
+
+ collection.sort('value.text');
+ expect(collection.telemetry[0].value.text).toBe("EIGHT");
+ expect(collection.telemetry[1].value.text).toBe("FIVE");
+ expect(collection.telemetry[2].value.text).toBe("FOUR");
+ expect(collection.telemetry[3].value.text).toBe("NINE");
+ }
+ );
+
+ describe("on bounds change", function () {
+ var discardedCallback;
+
+ beforeEach(function () {
+ discardedCallback = jasmine.createSpy("discarded");
+ collection.on("discarded", discardedCallback);
+ collection.sort("timestamp");
+ collection.add(telemetryObjects);
+ collection.bounds({start: 5000, end: 8000});
+ });
+
+
+ it("emits an event indicating that telemetry has " +
+ "been discarded", function () {
+ expect(discardedCallback).toHaveBeenCalled();
+ });
+
+ it("discards telemetry data with a time stamp " +
+ "before specified start bound", function () {
+ var discarded = discardedCallback.mostRecentCall.args[0];
+
+ // Expect 5 because as an optimization, the TelemetryCollection
+ // will not consider telemetry values that exceed the upper
+ // bounds. Arbitrary bounds changes in which the end bound is
+ // decreased is assumed to require a new historical query, and
+ // hence re-population of the collection anyway
+ expect(discarded.length).toBe(5);
+ expect(discarded[0].value.integer).toBe(0);
+ expect(discarded[1].value.integer).toBe(1);
+ expect(discarded[4].value.integer).toBe(4);
+ });
+ });
+
+ describe("when adding telemetry to a collection", function () {
+ var addedCallback;
+ beforeEach(function () {
+ collection.sort("timestamp");
+ collection.add(telemetryObjects);
+ addedCallback = jasmine.createSpy("added");
+ collection.on("added", addedCallback);
+ });
+
+ it("emits an event",
+ function () {
+ var addedObject = {
+ timestamp: 10000,
+ value: {
+ integer: 10,
+ text: integerTextMap[10]
+ }
+ };
+ collection.add([addedObject]);
+ expect(addedCallback).toHaveBeenCalledWith([addedObject]);
+ }
+ );
+ it("inserts in the correct order",
+ function () {
+ var addedObjectA = {
+ timestamp: 10000,
+ value: {
+ integer: 10,
+ text: integerTextMap[10]
+ }
+ };
+ var addedObjectB = {
+ timestamp: 11000,
+ value: {
+ integer: 11,
+ text: integerTextMap[11]
+ }
+ };
+ collection.add([addedObjectB, addedObjectA]);
+
+ expect(collection.telemetry[11]).toBe(addedObjectB);
+ }
+ );
+ });
+
+ describe("buffers telemetry", function () {
+ var addedObjectA;
+ var addedObjectB;
+
+ beforeEach(function () {
+ collection.sort("timestamp");
+ collection.add(telemetryObjects);
+
+ addedObjectA = {
+ timestamp: 10000,
+ value: {
+ integer: 10,
+ text: integerTextMap[10]
+ }
+ };
+ addedObjectB = {
+ timestamp: 11000,
+ value: {
+ integer: 11,
+ text: integerTextMap[11]
+ }
+ };
+
+ collection.bounds({start: 0, end: 10000});
+ collection.add([addedObjectA, addedObjectB]);
+ });
+ it("when it falls outside of bounds", function () {
+ expect(collection.highBuffer).toBeDefined();
+ expect(collection.highBuffer.length).toBe(1);
+ expect(collection.highBuffer[0]).toBe(addedObjectB);
+ });
+ it("and adds it to collection when it falls within bounds", function () {
+ expect(collection.telemetry.length).toBe(11);
+ collection.bounds({start: 0, end: 11000});
+ expect(collection.telemetry.length).toBe(12);
+ expect(collection.telemetry[11]).toBe(addedObjectB);
+ });
+ it("and removes it from the buffer when it falls within bounds", function () {
+ expect(collection.highBuffer.length).toBe(1);
+ collection.bounds({start: 0, end: 11000});
+ expect(collection.highBuffer.length).toBe(0);
+ });
+ });
+ });
+ }
+);
diff --git a/platform/features/table/test/controllers/HistoricalTableControllerSpec.js b/platform/features/table/test/controllers/HistoricalTableControllerSpec.js
deleted file mode 100644
index 39f7d1a8f5..0000000000
--- a/platform/features/table/test/controllers/HistoricalTableControllerSpec.js
+++ /dev/null
@@ -1,380 +0,0 @@
-/*****************************************************************************
- * Open MCT, Copyright (c) 2014-2016, United States Government
- * as represented by the Administrator of the National Aeronautics and Space
- * Administration. All rights reserved.
- *
- * Open MCT is licensed under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- * http://www.apache.org/licenses/LICENSE-2.0.
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations
- * under the License.
- *
- * Open MCT includes source code licensed under additional open source
- * licenses. See the Open Source Licenses file (LICENSES.md) included with
- * this source code distribution or the Licensing information page available
- * at runtime from the About dialog for additional information.
- *****************************************************************************/
-
-define(
- [
- "../../src/controllers/HistoricalTableController"
- ],
- function (TableController) {
-
- describe('The Table Controller', function () {
- var mockScope,
- mockTelemetryHandler,
- mockTelemetryHandle,
- mockTelemetryFormatter,
- mockDomainObject,
- mockTable,
- mockConfiguration,
- mockAngularTimeout,
- mockTimeoutHandle,
- watches,
- mockConductor,
- controller;
-
- function promise(value) {
- return {
- then: function (callback) {
- return promise(callback(value));
- }
- };
- }
-
- function getCallback(target, event) {
- return target.calls.filter(function (call) {
- return call.args[0] === event;
- })[0].args[1];
- }
-
- beforeEach(function () {
- watches = {};
- mockScope = jasmine.createSpyObj('scope', [
- '$on',
- '$watch',
- '$watchCollection'
- ]);
-
- mockScope.$on.andCallFake(function (expression, callback) {
- watches[expression] = callback;
- });
- mockScope.$watch.andCallFake(function (expression, callback) {
- watches[expression] = callback;
- });
- mockScope.$watchCollection.andCallFake(function (expression, callback) {
- watches[expression] = callback;
- });
-
- mockTimeoutHandle = jasmine.createSpy("timeoutHandle");
- mockAngularTimeout = jasmine.createSpy("$timeout");
- mockAngularTimeout.andReturn(mockTimeoutHandle);
- mockAngularTimeout.cancel = jasmine.createSpy("cancelTimeout");
-
- mockConfiguration = {
- 'range1': true,
- 'range2': true,
- 'domain1': true
- };
-
- mockTable = jasmine.createSpyObj('table',
- [
- 'populateColumns',
- 'buildColumnConfiguration',
- 'getRowValues',
- 'saveColumnConfiguration'
- ]
- );
- mockTable.columns = [];
- mockTable.buildColumnConfiguration.andReturn(mockConfiguration);
-
- mockDomainObject = jasmine.createSpyObj('domainObject', [
- 'getCapability',
- 'useCapability',
- 'getModel'
- ]);
- mockDomainObject.getModel.andReturn({});
-
- mockScope.domainObject = mockDomainObject;
-
- mockTelemetryHandle = jasmine.createSpyObj('telemetryHandle', [
- 'request',
- 'promiseTelemetryObjects',
- 'getTelemetryObjects',
- 'getMetadata',
- 'getSeries',
- 'unsubscribe',
- 'makeDatum'
- ]);
- mockTelemetryHandle.promiseTelemetryObjects.andReturn(promise(undefined));
- mockTelemetryHandle.request.andReturn(promise(undefined));
- mockTelemetryHandle.getTelemetryObjects.andReturn([]);
- mockTelemetryHandle.getMetadata.andReturn([]);
-
- mockTelemetryHandler = jasmine.createSpyObj('telemetryHandler', [
- 'handle'
- ]);
- mockTelemetryHandler.handle.andReturn(mockTelemetryHandle);
-
- mockConductor = jasmine.createSpyObj("conductor", [
- "timeSystem",
- "on",
- "off"
- ]);
-
- controller = new TableController(mockScope, mockTelemetryHandler,
- mockTelemetryFormatter, mockAngularTimeout, {conductor: mockConductor});
-
- controller.table = mockTable;
- controller.handle = mockTelemetryHandle;
- });
-
- it('subscribes to telemetry handler for telemetry updates', function () {
- controller.subscribe();
- expect(mockTelemetryHandler.handle).toHaveBeenCalled();
- expect(mockTelemetryHandle.request).toHaveBeenCalled();
- });
-
- it('Unsubscribes from telemetry when scope is destroyed', function () {
- controller.handle = mockTelemetryHandle;
- watches.$destroy();
- expect(mockTelemetryHandle.unsubscribe).toHaveBeenCalled();
- });
-
- describe('makes use of the table', function () {
-
- it('to create column definitions from telemetry' +
- ' metadata', function () {
- controller.setup();
- expect(mockTable.populateColumns).toHaveBeenCalled();
- });
-
- it('to create column configuration, which is written to the' +
- ' object model', function () {
- controller.setup();
- expect(mockTable.buildColumnConfiguration).toHaveBeenCalled();
- });
- });
-
- it('updates the rows on scope when historical telemetry is received', function () {
- var mockSeries = {
- getPointCount: function () {
- return 5;
- },
- getDomainValue: function () {
- return 'Domain Value';
- },
- getRangeValue: function () {
- return 'Range Value';
- }
- },
- mockRow = {'domain': 'Domain Value', 'range': 'Range' +
- ' Value'};
-
- mockTelemetryHandle.makeDatum.andCallFake(function () {
- return mockRow;
- });
- mockTable.getRowValues.andReturn(mockRow);
- mockTelemetryHandle.getTelemetryObjects.andReturn([mockDomainObject]);
- mockTelemetryHandle.getSeries.andReturn(mockSeries);
-
- controller.addHistoricalData(mockDomainObject, mockSeries);
-
- // Angular timeout is called a minumum of twice, regardless
- // of batch size used.
- expect(mockAngularTimeout).toHaveBeenCalled();
- mockAngularTimeout.mostRecentCall.args[0]();
- expect(mockAngularTimeout.calls.length).toEqual(2);
- mockAngularTimeout.mostRecentCall.args[0]();
-
- expect(controller.$scope.rows.length).toBe(5);
- expect(controller.$scope.rows[0]).toBe(mockRow);
- });
-
- it('filters the visible columns based on configuration', function () {
- controller.filterColumns();
- expect(controller.$scope.headers.length).toBe(3);
- expect(controller.$scope.headers[2]).toEqual('domain1');
-
- mockConfiguration.domain1 = false;
- controller.filterColumns();
- expect(controller.$scope.headers.length).toBe(2);
- expect(controller.$scope.headers[2]).toBeUndefined();
- });
-
- describe('creates event listeners', function () {
- beforeEach(function () {
- spyOn(controller, 'subscribe');
- spyOn(controller, 'filterColumns');
- });
-
- it('triggers telemetry subscription update when domain' +
- ' object changes', function () {
- controller.registerChangeListeners();
- //'watches' object is populated by fake scope watch and
- // watchCollection functions defined above
- expect(watches.domainObject).toBeDefined();
- watches.domainObject(mockDomainObject);
- expect(controller.subscribe).toHaveBeenCalled();
- });
-
- it('triggers telemetry subscription update when domain' +
- ' object composition changes', function () {
- controller.registerChangeListeners();
- expect(watches['domainObject.getModel().composition']).toBeDefined();
- watches['domainObject.getModel().composition']([], []);
- expect(controller.subscribe).toHaveBeenCalled();
- });
-
- it('triggers telemetry subscription update when time' +
- ' conductor bounds change', function () {
- controller.registerChangeListeners();
- expect(watches['telemetry:display:bounds']).toBeDefined();
- watches['telemetry:display:bounds']();
- expect(controller.subscribe).toHaveBeenCalled();
- });
-
- it('triggers refiltering of the columns when configuration' +
- ' changes', function () {
- controller.setup();
- expect(watches['domainObject.getModel().configuration.table.columns']).toBeDefined();
- watches['domainObject.getModel().configuration.table.columns']();
- expect(controller.filterColumns).toHaveBeenCalled();
- });
-
- });
- describe('After populating columns', function () {
- var metadata;
- beforeEach(function () {
- metadata = [{domains: [{name: 'time domain 1'}, {name: 'time domain 2'}]}, {domains: [{name: 'time domain 3'}, {name: 'time domain 4'}]}];
- controller.populateColumns(metadata);
- });
-
- it('Automatically identifies time columns', function () {
- expect(controller.timeColumns.length).toBe(4);
- expect(controller.timeColumns[0]).toBe('time domain 1');
- });
-
- it('Automatically sorts by time column that matches current' +
- ' time system', function () {
- var key = 'time_domain_1',
- name = 'time domain 1',
- mockTimeSystem = {
- metadata: {
- key: key
- }
- };
-
- mockTable.columns = [
- {
- domainMetadata: {
- key: key
- },
- getTitle: function () {
- return name;
- }
- },
- {
- domainMetadata: {
- key: 'anotherColumn'
- },
- getTitle: function () {
- return 'some other column';
- }
- },
- {
- domainMetadata: {
- key: 'thirdColumn'
- },
- getTitle: function () {
- return 'a third column';
- }
- }
- ];
-
- expect(mockConductor.on).toHaveBeenCalledWith('timeSystem', jasmine.any(Function));
- getCallback(mockConductor.on, 'timeSystem')(mockTimeSystem);
- expect(controller.$scope.defaultSort).toBe(name);
- });
- });
- describe('Yields thread', function () {
- var mockSeries,
- mockRow;
-
- beforeEach(function () {
- mockSeries = {
- getPointCount: function () {
- return 5;
- },
- getDomainValue: function () {
- return 'Domain Value';
- },
- getRangeValue: function () {
- return 'Range Value';
- }
- };
- mockRow = {'domain': 'Domain Value', 'range': 'Range Value'};
-
- mockTelemetryHandle.makeDatum.andCallFake(function () {
- return mockRow;
- });
- mockTable.getRowValues.andReturn(mockRow);
- mockTelemetryHandle.getTelemetryObjects.andReturn([mockDomainObject]);
- mockTelemetryHandle.getSeries.andReturn(mockSeries);
- });
- it('when row count exceeds batch size', function () {
- controller.batchSize = 3;
- controller.addHistoricalData(mockDomainObject, mockSeries);
-
- //Timeout is called a minimum of two times
- expect(mockAngularTimeout).toHaveBeenCalled();
- mockAngularTimeout.mostRecentCall.args[0]();
-
- expect(mockAngularTimeout.calls.length).toEqual(2);
- mockAngularTimeout.mostRecentCall.args[0]();
-
- //Because it yields, timeout will have been called a
- // third time for the batch.
- expect(mockAngularTimeout.calls.length).toEqual(3);
- mockAngularTimeout.mostRecentCall.args[0]();
-
- expect(controller.$scope.rows.length).toBe(5);
- expect(controller.$scope.rows[0]).toBe(mockRow);
- });
- it('cancelling any outstanding timeouts', function () {
- controller.batchSize = 3;
- controller.addHistoricalData(mockDomainObject, mockSeries);
-
- expect(mockAngularTimeout).toHaveBeenCalled();
- mockAngularTimeout.mostRecentCall.args[0]();
-
- controller.addHistoricalData(mockDomainObject, mockSeries);
-
- expect(mockAngularTimeout.cancel).toHaveBeenCalledWith(mockTimeoutHandle);
- });
- it('cancels timeout on scope destruction', function () {
- controller.batchSize = 3;
- controller.addHistoricalData(mockDomainObject, mockSeries);
-
- //Destroy is used by parent class as well, so multiple
- // calls are made to scope.$on
- var destroyCalls = mockScope.$on.calls.filter(function (call) {
- return call.args[0] === '$destroy';
- });
- //Call destroy function
- expect(destroyCalls.length).toEqual(2);
-
- destroyCalls[0].args[1]();
- expect(mockAngularTimeout.cancel).toHaveBeenCalledWith(mockTimeoutHandle);
-
- });
- });
- });
- }
-);
diff --git a/platform/features/table/test/controllers/MCTTableControllerSpec.js b/platform/features/table/test/controllers/MCTTableControllerSpec.js
index 61e4a2eece..f57b981c50 100644
--- a/platform/features/table/test/controllers/MCTTableControllerSpec.js
+++ b/platform/features/table/test/controllers/MCTTableControllerSpec.js
@@ -39,21 +39,13 @@ define(
var controller,
mockScope,
watches,
- mockTimeout,
+ mockWindow,
mockElement,
mockExportService,
mockConductor,
mockFormatService,
mockFormat;
- function promise(value) {
- return {
- then: function (callback) {
- return promise(callback(value));
- }
- };
- }
-
function getCallback(target, event) {
return target.calls.filter(function (call) {
return call.args[0] === event;
@@ -66,7 +58,8 @@ define(
mockScope = jasmine.createSpyObj('scope', [
'$watch',
'$on',
- '$watchCollection'
+ '$watchCollection',
+ '$digest'
]);
mockScope.$watchCollection.andCallFake(function (event, callback) {
watches[event] = callback;
@@ -86,8 +79,11 @@ define(
]);
mockScope.displayHeaders = true;
- mockTimeout = jasmine.createSpy('$timeout');
- mockTimeout.andReturn(promise(undefined));
+ mockWindow = jasmine.createSpyObj('$window', ['requestAnimationFrame']);
+ mockWindow.requestAnimationFrame.andCallFake(function (f) {
+ return f();
+ });
+
mockFormat = jasmine.createSpyObj('formatter', [
'parse',
'format'
@@ -99,7 +95,7 @@ define(
controller = new MCTTableController(
mockScope,
- mockTimeout,
+ mockWindow,
mockElement,
mockExportService,
mockFormatService,
@@ -114,12 +110,12 @@ define(
expect(mockScope.$watch).toHaveBeenCalledWith('rows', jasmine.any(Function));
});
- it('destroys listeners on destruction', function () {
- expect(mockScope.$on).toHaveBeenCalledWith('$destroy', controller.destroyConductorListeners);
+ it('unregisters listeners on destruction', function () {
+ expect(mockScope.$on).toHaveBeenCalledWith('$destroy', jasmine.any(Function));
getCallback(mockScope.$on, '$destroy')();
expect(mockConductor.off).toHaveBeenCalledWith('timeSystem', controller.changeTimeSystem);
- expect(mockConductor.off).toHaveBeenCalledWith('timeOfInterest', controller.setTimeOfInterest);
+ expect(mockConductor.off).toHaveBeenCalledWith('timeOfInterest', controller.changeTimeOfInterest);
expect(mockConductor.off).toHaveBeenCalledWith('bounds', controller.changeBounds);
});
@@ -233,9 +229,20 @@ define(
//Mock setting the rows on scope
var rowsCallback = getCallback(mockScope.$watch, 'rows');
- rowsCallback(rowsAsc);
+ var setRowsPromise = rowsCallback(rowsAsc);
+ var promiseResolved = false;
+ setRowsPromise.then(function () {
+ promiseResolved = true;
+ });
+
+ waitsFor(function () {
+ return promiseResolved;
+ }, "promise to resolve", 100);
+
+ runs(function () {
+ expect(mockScope.toiRowIndex).toBe(2);
+ });
- expect(mockScope.toiRowIndex).toBe(2);
});
});
@@ -287,7 +294,7 @@ define(
});
it('Supports adding rows individually', function () {
- var addRowFunc = getCallback(mockScope.$on, 'add:row'),
+ var addRowFunc = getCallback(mockScope.$on, 'add:rows'),
row4 = {
'col1': {'text': 'row3 col1'},
'col2': {'text': 'ghi'},
@@ -296,15 +303,15 @@ define(
controller.setRows(testRows);
expect(mockScope.displayRows.length).toBe(3);
testRows.push(row4);
- addRowFunc(undefined, 3);
+ addRowFunc(undefined, [row4]);
expect(mockScope.displayRows.length).toBe(4);
});
it('Supports removing rows individually', function () {
- var removeRowFunc = getCallback(mockScope.$on, 'remove:row');
+ var removeRowFunc = getCallback(mockScope.$on, 'remove:rows');
controller.setRows(testRows);
expect(mockScope.displayRows.length).toBe(3);
- removeRowFunc(undefined, 2);
+ removeRowFunc(undefined, [testRows[2]]);
expect(mockScope.displayRows.length).toBe(2);
expect(controller.setVisibleRows).toHaveBeenCalled();
});
@@ -366,7 +373,7 @@ define(
it('Allows sort column to be changed externally by ' +
'setting or changing sortBy attribute', function () {
mockScope.displayRows = testRows;
- var sortByCB = getCallback(mockScope.$watch, 'sortColumn');
+ var sortByCB = getCallback(mockScope.$watch, 'defaultSort');
sortByCB('col2');
expect(mockScope.sortDirection).toEqual('asc');
@@ -381,10 +388,21 @@ define(
it('updates visible rows in scope', function () {
var oldRows;
mockScope.rows = testRows;
- controller.setRows(testRows);
+ var setRowsPromise = controller.setRows(testRows);
+ var promiseResolved = false;
+ setRowsPromise.then(function () {
+ promiseResolved = true;
+ });
oldRows = mockScope.visibleRows;
mockScope.toggleSort('col2');
- expect(mockScope.visibleRows).not.toEqual(oldRows);
+
+ waitsFor(function () {
+ return promiseResolved;
+ }, "promise to resolve", 100);
+
+ runs(function () {
+ expect(mockScope.visibleRows).not.toEqual(oldRows);
+ });
});
it('correctly sorts rows of differing types', function () {
@@ -464,21 +482,10 @@ define(
mockScope.displayRows = controller.sortRows(testRows.slice(0));
- mockScope.rows.push(row4);
- controller.addRow(undefined, mockScope.rows.length - 1);
+ controller.addRows(undefined, [row4, row5, row6, row6]);
expect(mockScope.displayRows[0].col2.text).toEqual('xyz');
-
- mockScope.rows.push(row5);
- controller.addRow(undefined, mockScope.rows.length - 1);
- expect(mockScope.displayRows[4].col2.text).toEqual('aaa');
-
- mockScope.rows.push(row6);
- controller.addRow(undefined, mockScope.rows.length - 1);
- expect(mockScope.displayRows[2].col2.text).toEqual('ggg');
-
- //Add a duplicate row
- mockScope.rows.push(row6);
- controller.addRow(undefined, mockScope.rows.length - 1);
+ expect(mockScope.displayRows[6].col2.text).toEqual('aaa');
+ //Added a duplicate row
expect(mockScope.displayRows[2].col2.text).toEqual('ggg');
expect(mockScope.displayRows[3].col2.text).toEqual('ggg');
});
@@ -493,13 +500,11 @@ define(
mockScope.displayRows = controller.sortRows(testRows.slice(0));
mockScope.displayRows = controller.filterRows(testRows);
- mockScope.rows.push(row5);
- controller.addRow(undefined, mockScope.rows.length - 1);
+ controller.addRows(undefined, [row5]);
expect(mockScope.displayRows.length).toBe(2);
expect(mockScope.displayRows[1].col2.text).toEqual('aaa');
- mockScope.rows.push(row6);
- controller.addRow(undefined, mockScope.rows.length - 1);
+ controller.addRows(undefined, [row6]);
expect(mockScope.displayRows.length).toBe(2);
//Row was not added because does not match filter
});
@@ -512,12 +517,10 @@ define(
mockScope.displayRows = testRows.slice(0);
- mockScope.rows.push(row5);
- controller.addRow(undefined, mockScope.rows.length - 1);
+ controller.addRows(undefined, [row5]);
expect(mockScope.displayRows[3].col2.text).toEqual('aaa');
- mockScope.rows.push(row6);
- controller.addRow(undefined, mockScope.rows.length - 1);
+ controller.addRows(undefined, [row6]);
expect(mockScope.displayRows[4].col2.text).toEqual('ggg');
});
@@ -535,8 +538,7 @@ define(
mockScope.displayRows = testRows.slice(0);
- mockScope.rows.push(row7);
- controller.addRow(undefined, mockScope.rows.length - 1);
+ controller.addRows(undefined, [row7]);
expect(controller.$scope.sizingRow.col2).toEqual({text: 'some longer string'});
});
diff --git a/platform/features/table/test/controllers/RealtimeTableControllerSpec.js b/platform/features/table/test/controllers/RealtimeTableControllerSpec.js
deleted file mode 100644
index bf29c3d7bd..0000000000
--- a/platform/features/table/test/controllers/RealtimeTableControllerSpec.js
+++ /dev/null
@@ -1,171 +0,0 @@
-/*****************************************************************************
- * Open MCT, Copyright (c) 2014-2016, United States Government
- * as represented by the Administrator of the National Aeronautics and Space
- * Administration. All rights reserved.
- *
- * Open MCT is licensed under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- * http://www.apache.org/licenses/LICENSE-2.0.
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations
- * under the License.
- *
- * Open MCT includes source code licensed under additional open source
- * licenses. See the Open Source Licenses file (LICENSES.md) included with
- * this source code distribution or the Licensing information page available
- * at runtime from the About dialog for additional information.
- *****************************************************************************/
-
-define(
- [
- "../../src/controllers/RealtimeTableController"
- ],
- function (TableController) {
-
- describe('The real-time table controller', function () {
- var mockScope,
- mockTelemetryHandler,
- mockTelemetryHandle,
- mockTelemetryFormatter,
- mockDomainObject,
- mockTable,
- mockConfiguration,
- watches,
- mockTableRow,
- mockConductor,
- controller;
-
- function promise(value) {
- return {
- then: function (callback) {
- return promise(callback(value));
- }
- };
- }
-
- beforeEach(function () {
- watches = {};
- mockTableRow = {'col1': 'val1', 'col2': 'row2'};
-
- mockScope = jasmine.createSpyObj('scope', [
- '$on',
- '$watch',
- '$watchCollection',
- '$digest',
- '$broadcast'
- ]);
- mockScope.$on.andCallFake(function (expression, callback) {
- watches[expression] = callback;
- });
- mockScope.$watch.andCallFake(function (expression, callback) {
- watches[expression] = callback;
- });
- mockScope.$watchCollection.andCallFake(function (expression, callback) {
- watches[expression] = callback;
- });
-
- mockConfiguration = {
- 'range1': true,
- 'range2': true,
- 'domain1': true
- };
-
- mockTable = jasmine.createSpyObj('table',
- [
- 'populateColumns',
- 'buildColumnConfiguration',
- 'getRowValues',
- 'saveColumnConfiguration'
- ]
- );
- mockTable.columns = [];
- mockTable.buildColumnConfiguration.andReturn(mockConfiguration);
- mockTable.getRowValues.andReturn(mockTableRow);
-
- mockDomainObject = jasmine.createSpyObj('domainObject', [
- 'getCapability',
- 'useCapability',
- 'getModel'
- ]);
- mockDomainObject.getModel.andReturn({});
- mockDomainObject.getCapability.andReturn(
- {
- getMetadata: function () {
- return {ranges: [{format: 'string'}]};
- }
- });
-
- mockScope.domainObject = mockDomainObject;
-
- mockTelemetryHandle = jasmine.createSpyObj('telemetryHandle', [
- 'getMetadata',
- 'unsubscribe',
- 'getDatum',
- 'promiseTelemetryObjects',
- 'getTelemetryObjects',
- 'request',
- 'getMetadata'
- ]);
-
- // Arbitrary array with non-zero length, contents are not
- // used by mocks
- mockTelemetryHandle.getTelemetryObjects.andReturn([{}]);
- mockTelemetryHandle.promiseTelemetryObjects.andReturn(promise(undefined));
- mockTelemetryHandle.getDatum.andReturn({});
- mockTelemetryHandle.request.andReturn(promise(undefined));
- mockTelemetryHandle.getMetadata.andReturn([]);
-
- mockTelemetryHandler = jasmine.createSpyObj('telemetryHandler', [
- 'handle'
- ]);
- mockTelemetryHandler.handle.andReturn(mockTelemetryHandle);
-
- mockConductor = jasmine.createSpyObj('conductor', [
- 'on',
- 'off',
- 'bounds',
- 'timeSystem',
- 'timeOfInterest'
- ]);
-
- controller = new TableController(mockScope, mockTelemetryHandler, mockTelemetryFormatter, {conductor: mockConductor});
- controller.table = mockTable;
- controller.handle = mockTelemetryHandle;
- });
-
- it('registers for streaming telemetry', function () {
- controller.subscribe();
- expect(mockTelemetryHandler.handle).toHaveBeenCalledWith(jasmine.any(Object), jasmine.any(Function), true);
- });
-
- describe('receives new telemetry', function () {
- beforeEach(function () {
- controller.subscribe();
- mockScope.rows = [];
- });
-
- it('updates table with new streaming telemetry', function () {
- mockTelemetryHandler.handle.mostRecentCall.args[1]();
- expect(mockScope.$broadcast).toHaveBeenCalledWith('add:row', 0);
- });
- it('observes the row limit', function () {
- var i = 0;
- controller.maxRows = 10;
-
- //Fill rows array with elements
- for (; i < 10; i++) {
- mockScope.rows.push({row: i});
- }
- mockTelemetryHandler.handle.mostRecentCall.args[1]();
- expect(mockScope.rows.length).toBe(controller.maxRows);
- expect(mockScope.rows[mockScope.rows.length - 1]).toBe(mockTableRow);
- expect(mockScope.rows[0].row).toBe(1);
- });
- });
- });
- }
-);
diff --git a/platform/features/table/test/controllers/TelemetryTableControllerSpec.js b/platform/features/table/test/controllers/TelemetryTableControllerSpec.js
new file mode 100644
index 0000000000..4f403edc32
--- /dev/null
+++ b/platform/features/table/test/controllers/TelemetryTableControllerSpec.js
@@ -0,0 +1,364 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2016, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+
+define(
+ [
+ '../../src/controllers/TelemetryTableController',
+ '../../../../../src/api/objects/object-utils',
+ 'lodash'
+ ],
+ function (TelemetryTableController, objectUtils, _) {
+
+ describe('The TelemetryTableController', function () {
+
+ var controller,
+ mockScope,
+ mockTimeout,
+ mockConductor,
+ mockAPI,
+ mockDomainObject,
+ mockTelemetryAPI,
+ mockObjectAPI,
+ mockCompositionAPI,
+ unobserve,
+ mockBounds;
+
+ function getCallback(target, event) {
+ return target.calls.filter(function (call) {
+ return call.args[0] === event;
+ })[0].args[1];
+ }
+
+ beforeEach(function () {
+ mockBounds = {
+ start: 0,
+ end: 10
+ };
+ mockConductor = jasmine.createSpyObj("conductor", [
+ "bounds",
+ "follow",
+ "on",
+ "off",
+ "timeSystem"
+ ]);
+ mockConductor.bounds.andReturn(mockBounds);
+ mockConductor.follow.andReturn(false);
+
+ mockDomainObject = jasmine.createSpyObj("domainObject", [
+ "getModel",
+ "getId",
+ "useCapability"
+ ]);
+ mockDomainObject.getModel.andReturn({});
+ mockDomainObject.getId.andReturn("mockId");
+ mockDomainObject.useCapability.andReturn(true);
+
+ mockCompositionAPI = jasmine.createSpyObj("compositionAPI", [
+ "get"
+ ]);
+
+ mockObjectAPI = jasmine.createSpyObj("objectAPI", [
+ "observe"
+ ]);
+ unobserve = jasmine.createSpy("unobserve");
+ mockObjectAPI.observe.andReturn(unobserve);
+
+ mockScope = jasmine.createSpyObj("scope", [
+ "$on",
+ "$watch",
+ "$broadcast"
+ ]);
+ mockScope.domainObject = mockDomainObject;
+
+ mockTelemetryAPI = jasmine.createSpyObj("telemetryAPI", [
+ "canProvideTelemetry",
+ "subscribe",
+ "getMetadata",
+ "commonValuesForHints",
+ "request",
+ "limitEvaluator",
+ "getValueFormatter"
+ ]);
+ mockTelemetryAPI.commonValuesForHints.andReturn([]);
+ mockTelemetryAPI.request.andReturn(Promise.resolve([]));
+
+
+ mockTelemetryAPI.canProvideTelemetry.andReturn(false);
+
+ mockTimeout = jasmine.createSpy("timeout");
+ mockTimeout.andReturn(1); // Return something
+ mockTimeout.cancel = jasmine.createSpy("cancel");
+
+ mockAPI = {
+ conductor: mockConductor,
+ objects: mockObjectAPI,
+ telemetry: mockTelemetryAPI,
+ composition: mockCompositionAPI
+ };
+ controller = new TelemetryTableController(mockScope, mockTimeout, mockAPI);
+ });
+
+ describe('listens for', function () {
+ beforeEach(function () {
+ controller.registerChangeListeners();
+ });
+ it('object mutation', function () {
+ var calledObject = mockObjectAPI.observe.mostRecentCall.args[0];
+
+ expect(mockObjectAPI.observe).toHaveBeenCalled();
+ expect(calledObject.identifier.key).toEqual(mockDomainObject.getId());
+ });
+ it('conductor changes', function () {
+ expect(mockConductor.on).toHaveBeenCalledWith("timeSystem", jasmine.any(Function));
+ expect(mockConductor.on).toHaveBeenCalledWith("bounds", jasmine.any(Function));
+ expect(mockConductor.on).toHaveBeenCalledWith("follow", jasmine.any(Function));
+ });
+ });
+
+ describe('deregisters all listeners on scope destruction', function () {
+ var timeSystemListener,
+ boundsListener,
+ followListener;
+
+ beforeEach(function () {
+ controller.registerChangeListeners();
+
+ timeSystemListener = getCallback(mockConductor.on, "timeSystem");
+ boundsListener = getCallback(mockConductor.on, "bounds");
+ followListener = getCallback(mockConductor.on, "follow");
+
+ var destroy = getCallback(mockScope.$on, "$destroy");
+ destroy();
+ });
+
+ it('object mutation', function () {
+ expect(unobserve).toHaveBeenCalled();
+ });
+ it('conductor changes', function () {
+ expect(mockConductor.off).toHaveBeenCalledWith("timeSystem", timeSystemListener);
+ expect(mockConductor.off).toHaveBeenCalledWith("bounds", boundsListener);
+ expect(mockConductor.off).toHaveBeenCalledWith("follow", followListener);
+ });
+ });
+
+ describe ('Subscribes to new data', function () {
+ var mockComposition,
+ mockTelemetryObject,
+ mockChildren,
+ unsubscribe,
+ done;
+
+ beforeEach(function () {
+ mockComposition = jasmine.createSpyObj("composition", [
+ "load"
+ ]);
+
+ mockTelemetryObject = jasmine.createSpyObj("mockTelemetryObject", [
+ "something"
+ ]);
+ mockTelemetryObject.identifier = {
+ key: "mockTelemetryObject"
+ };
+
+ unsubscribe = jasmine.createSpy("unsubscribe");
+ mockTelemetryAPI.subscribe.andReturn(unsubscribe);
+
+ mockChildren = [mockTelemetryObject];
+ mockComposition.load.andReturn(Promise.resolve(mockChildren));
+ mockCompositionAPI.get.andReturn(mockComposition);
+
+ mockTelemetryAPI.canProvideTelemetry.andCallFake(function (obj) {
+ return obj.identifier.key === mockTelemetryObject.identifier.key;
+ });
+
+ done = false;
+ controller.getData().then(function () {
+ done = true;
+ });
+ });
+
+ it('fetches historical data', function () {
+ waitsFor(function () {
+ return done;
+ }, "getData to return", 100);
+
+ runs(function () {
+ expect(mockTelemetryAPI.request).toHaveBeenCalledWith(mockTelemetryObject, jasmine.any(Object));
+ });
+ });
+
+ it('fetches historical data for the time period specified by the conductor bounds', function () {
+ waitsFor(function () {
+ return done;
+ }, "getData to return", 100);
+
+ runs(function () {
+ expect(mockTelemetryAPI.request).toHaveBeenCalledWith(mockTelemetryObject, mockBounds);
+ });
+ });
+
+ it('subscribes to new data', function () {
+ waitsFor(function () {
+ return done;
+ }, "getData to return", 100);
+
+ runs(function () {
+ expect(mockTelemetryAPI.subscribe).toHaveBeenCalledWith(mockTelemetryObject, jasmine.any(Function), {});
+ });
+
+ });
+ it('and unsubscribes on view destruction', function () {
+ waitsFor(function () {
+ return done;
+ }, "getData to return", 100);
+
+ runs(function () {
+ var destroy = getCallback(mockScope.$on, "$destroy");
+ destroy();
+
+ expect(unsubscribe).toHaveBeenCalled();
+ });
+ });
+ });
+
+ it('When in real-time mode, enables auto-scroll', function () {
+ controller.registerChangeListeners();
+
+ var followCallback = getCallback(mockConductor.on, "follow");
+ //Confirm pre-condition
+ expect(mockScope.autoScroll).toBeFalsy();
+
+ //Mock setting the conductor to 'follow' mode
+ followCallback(true);
+ expect(mockScope.autoScroll).toBe(true);
+ });
+
+ describe('populates table columns', function () {
+ var domainMetadata;
+ var allMetadata;
+ var mockTimeSystem;
+
+ beforeEach(function () {
+ domainMetadata = [{
+ key: "column1",
+ name: "Column 1",
+ hints: {}
+ }];
+
+ allMetadata = [{
+ key: "column1",
+ name: "Column 1",
+ hints: {}
+ }, {
+ key: "column2",
+ name: "Column 2",
+ hints: {}
+ }, {
+ key: "column3",
+ name: "Column 3",
+ hints: {}
+ }];
+
+ mockTimeSystem = {
+ metadata: {
+ key: "column1"
+ }
+ };
+
+ mockTelemetryAPI.commonValuesForHints.andCallFake(function (metadata, hints) {
+ if (_.eq(hints, ["x"])) {
+ return domainMetadata;
+ } else if (_.eq(hints, [])) {
+ return allMetadata;
+ }
+ });
+
+ controller.loadColumns([mockDomainObject]);
+ });
+
+ it('based on metadata for given objects', function () {
+ expect(mockScope.headers).toBeDefined();
+ expect(mockScope.headers.length).toBeGreaterThan(0);
+ expect(mockScope.headers.indexOf(allMetadata[0].name)).not.toBe(-1);
+ expect(mockScope.headers.indexOf(allMetadata[1].name)).not.toBe(-1);
+ expect(mockScope.headers.indexOf(allMetadata[2].name)).not.toBe(-1);
+ });
+
+ it('and sorts by column matching time system', function () {
+ expect(mockScope.defaultSort).not.toEqual("Column 1");
+ controller.sortByTimeSystem(mockTimeSystem);
+ expect(mockScope.defaultSort).toEqual("Column 1");
+ });
+
+ it('batches processing of rows for performance when receiving historical telemetry', function () {
+ var mockHistoricalData = [
+ {
+ "column1": 1,
+ "column2": 2,
+ "column3": 3
+ },{
+ "column1": 4,
+ "column2": 5,
+ "column3": 6
+ }, {
+ "column1": 7,
+ "column2": 8,
+ "column3": 9
+ }
+ ];
+ controller.batchSize = 2;
+ mockTelemetryAPI.request.andReturn(Promise.resolve(mockHistoricalData));
+ controller.getHistoricalData([mockDomainObject]);
+
+ waitsFor(function () {
+ return !!controller.timeoutHandle;
+ }, "first batch to be processed", 100);
+
+ runs(function () {
+ //Verify that timeout is being used to yield process
+ expect(mockTimeout).toHaveBeenCalled();
+ mockTimeout.mostRecentCall.args[0]();
+ expect(mockTimeout.calls.length).toBe(2);
+ mockTimeout.mostRecentCall.args[0]();
+ expect(mockScope.rows.length).toBe(3);
+ });
+ });
+ });
+
+ it('Removes telemetry rows from table when they fall out of bounds', function () {
+ var discardedRows = [
+ {"column1": "value 1"},
+ {"column2": "value 2"},
+ {"column3": "value 3"}
+ ];
+
+ spyOn(controller.telemetry, "on").andCallThrough();
+
+ controller.registerChangeListeners();
+ expect(controller.telemetry.on).toHaveBeenCalledWith("discarded", jasmine.any(Function));
+ var onDiscard = getCallback(controller.telemetry.on, "discarded");
+ onDiscard(discardedRows);
+ expect(mockScope.$broadcast).toHaveBeenCalledWith("remove:rows", discardedRows);
+ });
+
+ });
+ });
|