[Table] Added ticking to combined historical/real-time table

Don't add duplicate telemetry data
This commit is contained in:
Henry 2017-01-17 14:44:09 -08:00
parent 50f303bbdc
commit 0c3ff82cfe
7 changed files with 195 additions and 59 deletions

View File

@ -25,7 +25,7 @@ define([
'../../core/src/timeSystems/LocalClock' '../../core/src/timeSystems/LocalClock'
], function (TimeSystem, LocalClock) { ], function (TimeSystem, LocalClock) {
var FIFTEEN_MINUTES = 15 * 60 * 1000, var FIFTEEN_MINUTES = 15 * 60 * 1000,
DEFAULT_PERIOD = 1000; DEFAULT_PERIOD = 100;
/** /**
* This time system supports UTC dates and provides a ticking clock source. * This time system supports UTC dates and provides a ticking clock source.

View File

@ -5,7 +5,7 @@
headers="headers" headers="headers"
rows="rows" rows="rows"
time-columns="tableController.timeColumns" time-columns="tableController.timeColumns"
on-show-cell="" format-cell="formatCell"
enableFilter="true" enableFilter="true"
enableSort="true" enableSort="true"
auto-scroll="autoScroll" auto-scroll="autoScroll"

View File

@ -66,7 +66,8 @@ define(
return { return {
cssClass: alarm && alarm.cssClass, cssClass: alarm && alarm.cssClass,
text: formatter ? formatter.format(telemetryDatum[metadatum.key]) text: formatter ? formatter.format(telemetryDatum[metadatum.key])
: telemetryDatum[metadatum.key] : telemetryDatum[metadatum.key],
value: telemetryDatum[metadatum.key]
} }
} }
}); });

View File

@ -0,0 +1,114 @@
/*****************************************************************************
* 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'],
function (_) {
function TelemetryCollection() {
this.telemetry = [];
this.sortField = undefined;
this.lastBounds = {};
_.bindAll(this,[
'iteratee'
]);
}
TelemetryCollection.prototype.iteratee = function (element) {
return _.get(element, this.sortField);
};
TelemetryCollection.prototype.bounds = function (bounds) {
var startChanged = this.lastBounds.start !== bounds.start;
var endChanged = this.lastBounds.end !== bounds.end;
var fromStart = 0;
var fromEnd = 0;
var discarded = [];
if (startChanged){
var testValue = _.set({}, this.sortField, bounds.start);
fromStart = _.sortedIndex(this.telemetry, testValue, this.sortField);
discarded = this.telemetry.splice(0, fromStart);
}
if (endChanged) {
var testValue = _.set({}, this.sortField, bounds.end);
fromEnd = _.sortedLastIndex(this.telemetry, testValue, this.sortField);
discarded = discarded.concat(this.telemetry.splice(fromEnd, this.telemetry.length - fromEnd));
}
this.lastBounds = bounds;
return discarded;
};
TelemetryCollection.prototype.isValid = function (element) {
var noBoundsDefined = !this.lastBounds || (!this.lastBounds.start && !this.lastBounds.end);
var withinBounds = _.get(element, this.sortField) >= this.lastBounds.start &&
_.get(element, this.sortField) <= this.lastBounds.end;
return noBoundsDefined || withinBounds;
};
TelemetryCollection.prototype.add = function (element) {
//console.log('data: ' + element.Time.value);
if (this.isValid(element)){
// Going to check for duplicates. Bound the search problem to
// elements 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 isDuplicate = false;
var startIx = _.sortedIndex(this.telemetry, element, this.sortField);
if (startIx !== this.telemetry.length) {
var endIx = _.sortedLastIndex(this.telemetry, element, this.sortField);
// Create an array of potential dupes, based on having the
// same time stamp
var potentialDupes = this.telemetry.slice(startIx, endIx + 1);
// Search potential dupes for exact dupe
isDuplicate = _.findIndex(potentialDupes, _.isEqual.bind(undefined, element)) > -1;
}
if (!isDuplicate) {
this.telemetry.splice(startIx, 0, element);
return true;
} else {
return false;
}
} else {
return false;
}
};
TelemetryCollection.prototype.clear = function () {
this.telemetry = undefined;
};
TelemetryCollection.prototype.sort = function (sortField){
this.sortField = sortField;
this.telemetry = _.sortBy(this.telemetry, this.iteratee);
};
return TelemetryCollection;
}
);

View File

@ -35,7 +35,7 @@ define(
'changeTimeSystem', 'changeTimeSystem',
'scrollToBottom', 'scrollToBottom',
'addRow', 'addRow',
'removeRow', 'removeRows',
'onScroll', 'onScroll',
'firstVisible', 'firstVisible',
'lastVisible', 'lastVisible',
@ -126,7 +126,7 @@ define(
* Listen for rows added individually (eg. for real-time tables) * Listen for rows added individually (eg. for real-time tables)
*/ */
$scope.$on('add:row', this.addRow); $scope.$on('add:row', this.addRow);
$scope.$on('remove:row', this.removeRow); $scope.$on('remove:rows', this.removeRows);
/** /**
* Populated from the default-sort attribute on MctTable * Populated from the default-sort attribute on MctTable
@ -229,24 +229,33 @@ define(
* `remove:row` broadcast event. * `remove:row` broadcast event.
* @private * @private
*/ */
MCTTableController.prototype.removeRow = function (event, rowIndex) { MCTTableController.prototype.removeRows = function (event, rows) {
var row = this.$scope.rows[rowIndex], var indexInDisplayRows;
rows.forEach(function (row){
// Do a sequential search here. Only way of finding row is by // Do a sequential search here. Only way of finding row is by
// object equality, so array is in effect unsorted. // object equality, so array is in effect unsorted.
indexInDisplayRows = this.$scope.displayRows.indexOf(row); indexInDisplayRows = this.$scope.displayRows.indexOf(row);
if (indexInDisplayRows !== -1) { if (indexInDisplayRows !== -1) {
this.$scope.displayRows.splice(indexInDisplayRows, 1); this.$scope.displayRows.splice(indexInDisplayRows, 1);
this.setVisibleRows();
} }
}, 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 * @private
*/ */
MCTTableController.prototype.onScroll = function (event) { MCTTableController.prototype.onScroll = function (event) {
if (!this.scrolling) {
this.scrolling = true;
requestAnimationFrame(function () { requestAnimationFrame(function () {
this.setVisibleRows(); this.setVisibleRows();
this.digest(); this.digest();
@ -261,7 +270,6 @@ define(
} }
this.scrolling = false; this.scrolling = false;
}.bind(this)); }.bind(this));
}
}; };
/** /**
@ -339,13 +347,19 @@ define(
this.$scope.visibleRows[0].rowIndex === start && this.$scope.visibleRows[0].rowIndex === start &&
this.$scope.visibleRows[this.$scope.visibleRows.length - 1] this.$scope.visibleRows[this.$scope.visibleRows.length - 1]
.rowIndex === end) { .rowIndex === end) {
return this.digest();
return Promise.resolve(); // don't update if no changes are required. //return Promise.resolve(); // don't update if no changes are required.
} }
} }
//Set visible rows from display rows, based on calculated offset. //Set visible rows from display rows, based on calculated offset.
this.$scope.visibleRows = this.$scope.displayRows.slice(start, end) this.$scope.visibleRows = this.$scope.displayRows.slice(start, end)
.map(function (row, i) { .map(function (row, i) {
/* var formattedRow = JSON.parse(JSON.stringify(row));
if (self.$scope.formatCell) {
Object.keys(formattedRow).forEach(function (header) {
formattedRow[header].text = self.$scope.formatCell(header, row[header].text);
});
} */
return { return {
rowIndex: start + i, rowIndex: start + i,
offsetY: ((start + i) * self.$scope.rowHeight) + offsetY: ((start + i) * self.$scope.rowHeight) +

View File

@ -27,10 +27,11 @@
define( define(
[ [
'../TableConfiguration', '../TableConfiguration',
'../../../../../src/api/objects/object-utils' '../../../../../src/api/objects/object-utils',
'../TelemetryCollection'
], ],
function (TableConfiguration, objectUtils) { function (TableConfiguration, objectUtils, TelemetryCollection) {
/** /**
* The TableController is responsible for getting data onto the page * The TableController is responsible for getting data onto the page
@ -62,6 +63,7 @@ define(
openmct); openmct);
this.lastBounds = this.openmct.conductor.bounds(); this.lastBounds = this.openmct.conductor.bounds();
this.requestTime = 0; this.requestTime = 0;
this.telemetry = new TelemetryCollection();
/* /*
* Create a new format object from legacy object, and replace it * Create a new format object from legacy object, and replace it
@ -136,18 +138,8 @@ define(
this.openmct.conductor.on('bounds', this.changeBounds); this.openmct.conductor.on('bounds', this.changeBounds);
}; };
TelemetryTableController.prototype.tick = function (bounds) {
// Can't do ticking until we change how data is handled
// Pass raw values to table, with format function
/*if (this.$scope.defaultSort) {
this.$scope.rows.filter(function (row){
return row[]
})
}*/
};
TelemetryTableController.prototype.changeBounds = function (bounds) { TelemetryTableController.prototype.changeBounds = function (bounds) {
//console.log('bounds.end: ' + bounds.end);
var follow = this.openmct.conductor.follow(); var follow = this.openmct.conductor.follow();
var isTick = follow && var isTick = follow &&
bounds.start !== this.lastBounds.start && bounds.start !== this.lastBounds.start &&
@ -157,10 +149,16 @@ define(
(bounds.start !== this.lastBounds.start || (bounds.start !== this.lastBounds.start ||
bounds.end !== this.lastBounds.end); bounds.end !== this.lastBounds.end);
var discarded = this.telemetry.bounds(bounds);
if (discarded.length > 0){
this.$scope.$broadcast('remove:rows', discarded);
}
if (isTick){ if (isTick){
// Treat it as a realtime tick // Treat it as a realtime tick
// Drop old data that falls outside of bounds // Drop old data that falls outside of bounds
this.tick(bounds); //this.tick(bounds);
} else if (isDeltaChange){ } else if (isDeltaChange){
// No idea... // No idea...
// Historical query for bounds, then tick on // Historical query for bounds, then tick on
@ -214,11 +212,13 @@ define(
var allColumns = telemetryApi.commonValuesForHints(metadatas, []); var allColumns = telemetryApi.commonValuesForHints(metadatas, []);
this.table.populateColumns(allColumns); this.table.populateColumns(allColumns);
this.timeColumns = telemetryApi.commonValuesForHints(metadatas, ['x']).map(function (metadatum) { this.timeColumns = telemetryApi.commonValuesForHints(metadatas, ['x']).map(function (metadatum) {
return metadatum.name; return metadatum.name;
}); });
// For now, use first time field for time conductor
this.telemetry.sort(this.timeColumns[0] + '.value');
this.filterColumns(); this.filterColumns();
var timeSystem = this.openmct.conductor.timeSystem(); var timeSystem = this.openmct.conductor.timeSystem();
@ -241,12 +241,13 @@ define(
var rowData = []; var rowData = [];
var processedObjects = 0; var processedObjects = 0;
var requestTime = this.lastRequestTime = Date.now(); var requestTime = this.lastRequestTime = Date.now();
var telemetryCollection = this.telemetry;
return new Promise(function (resolve, reject){ return new Promise(function (resolve, reject){
function finishProcessing(tableRows){ function finishProcessing(){
scope.rows = tableRows.concat(scope.rows); scope.rows = telemetryCollection.telemetry;
scope.loading = false; scope.loading = false;
resolve(tableRows); resolve(scope.rows);
} }
function processData(historicalData, index, limitEvaluator){ function processData(historicalData, index, limitEvaluator){
@ -254,13 +255,14 @@ define(
processedObjects++; processedObjects++;
if (processedObjects === objects.length) { if (processedObjects === objects.length) {
finishProcessing(rowData); finishProcessing();
} }
} else { } else {
rowData = rowData.concat( historicalData.slice(index, index + this.batchSize)
historicalData.slice(index, index + this.batchSize).map( .forEach(function (datum) {
this.table.getRowValues.bind(this.table, limitEvaluator)) telemetryCollection.add(this.table.getRowValues(
); limitEvaluator, datum));
}.bind(this));
this.timeoutHandle = this.$timeout(processData.bind( this.timeoutHandle = this.$timeout(processData.bind(
this, this,
historicalData, historicalData,
@ -305,8 +307,12 @@ define(
*/ */
TelemetryTableController.prototype.subscribeToNewData = function (objects) { TelemetryTableController.prototype.subscribeToNewData = function (objects) {
var telemetryApi = this.openmct.telemetry; var telemetryApi = this.openmct.telemetry;
var telemetryCollection = this.telemetry;
//Set table max length to avoid unbounded growth. //Set table max length to avoid unbounded growth.
var maxRows = 100000; //var maxRows = 100000;
var maxRows = Number.MAX_VALUE;
var limitEvaluator;
var added = false;
this.subscriptions.forEach(function (subscription) { this.subscriptions.forEach(function (subscription) {
subscription(); subscription();
@ -314,15 +320,15 @@ define(
this.subscriptions = []; this.subscriptions = [];
function newData(domainObject, datum) { function newData(domainObject, datum) {
this.$scope.rows.push(this.table.getRowValues( limitEvaluator = telemetryApi.limitEvaluator(domainObject);
telemetryApi.limitEvaluator(domainObject), datum)); added = telemetryCollection.add(this.table.getRowValues(limitEvaluator, datum));
//Inform table that a new row has been added //Inform table that a new row has been added
if (this.$scope.rows.length > maxRows) { if (this.$scope.rows.length > maxRows) {
this.$scope.$broadcast('remove:row', 0); this.$scope.$broadcast('remove:rows', this.$scope.rows[0]);
this.$scope.rows.shift(); this.$scope.rows.shift();
} }
if (!this.$scope.loading) { if (!this.$scope.loading && added) {
this.$scope.$broadcast('add:row', this.$scope.$broadcast('add:row',
this.$scope.rows.length - 1); this.$scope.rows.length - 1);
} }

View File

@ -94,6 +94,7 @@ define(
scope: { scope: {
headers: "=", headers: "=",
rows: "=", rows: "=",
formatCell: "=?",
enableFilter: "=?", enableFilter: "=?",
enableSort: "=?", enableSort: "=?",
autoScroll: "=?", autoScroll: "=?",