Merge branch 'open1077' into 1435-integration
This commit is contained in:
@@ -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;
|
||||
}
|
||||
);
|
||||
@@ -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;
|
||||
}
|
||||
);
|
||||
@@ -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;
|
||||
}
|
||||
);
|
||||
@@ -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 ||
|
||||
|
||||
255
platform/features/table/src/TelemetryCollection.js
Normal file
255
platform/features/table/src/TelemetryCollection.js
Normal file
@@ -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;
|
||||
}
|
||||
);
|
||||
@@ -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;
|
||||
}
|
||||
);
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
);
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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: "=?"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user