openmct/platform/features/autoflow/src/AutoflowTabularController.js

325 lines
13 KiB
JavaScript
Executable File

define(
['moment'],
function (moment) {
var ROW_HEIGHT = 16,
SLIDER_HEIGHT = 10,
INITIAL_COLUMN_WIDTH = 225,
MAX_COLUMN_WIDTH = 525,
COLUMN_WIDTH_STEP = 25,
DEBOUNCE_INTERVAL = 100,
DATE_FORMAT = "YYYY-DDD HH:mm:ss.SSS\\Z",
NOT_UPDATED = "No updates",
EMPTY_ARRAY = [];
/**
* Responsible for supporting the autoflow tabular view.
* Implements the all-over logic which drives that view,
* mediating between template-provided areas, the included
* `mct-autoflow-table` directive, and the underlying
* domain object model.
* @constructor
*/
function AutflowTabularController(
$scope,
$timeout,
telemetrySubscriber
) {
var filterValue = "",
filterValueLowercase = "",
subscription,
filteredObjects = [],
lastUpdated = {},
updateText = NOT_UPDATED,
rangeValues = {},
classes = {},
limits = {},
updatePending = false,
lastBounce = Number.NEGATIVE_INFINITY,
columnWidth = INITIAL_COLUMN_WIDTH,
rows = 1,
counter = 0;
// Trigger an update of the displayed table by incrementing
// the counter that it watches.
function triggerDisplayUpdate() {
counter += 1;
}
// Check whether or not an object's name matches the
// user-entered filter value.
function filterObject(domainObject) {
return (domainObject.getModel().name || "")
.toLowerCase()
.indexOf(filterValueLowercase) !== -1;
}
// Comparator for sorting points back into packet order
function compareObject(objectA, objectB) {
var indexA = objectA.getModel().index || 0,
indexB = objectB.getModel().index || 0;
return indexA - indexB;
}
// Update the list of currently-displayed objects; these
// will be the subset of currently subscribed-to objects
// which match a user-entered filter.
function doUpdateFilteredObjects() {
// Generate the list
filteredObjects = (
subscription ?
subscription.getTelemetryObjects() :
[]
).filter(filterObject).sort(compareObject);
// Clear the pending flag
updatePending = false;
// Track when this occurred, so that we can wait
// a whole before updating again.
lastBounce = Date.now();
triggerDisplayUpdate();
}
// Request an update to the list of current objects; this may
// run on a timeout to avoid excessive calls, e.g. while the user
// is typing a filter.
function updateFilteredObjects() {
// Don't do anything if an update is already scheduled
if (!updatePending) {
if (Date.now() > lastBounce + DEBOUNCE_INTERVAL) {
// Update immediately if it's been long enough
doUpdateFilteredObjects();
} else {
// Otherwise, update later, and track that we have
// an update pending so that subsequent calls can
// be ignored.
updatePending = true;
$timeout(doUpdateFilteredObjects, DEBOUNCE_INTERVAL);
}
}
}
// Track the latest data values for this domain object
function recordData(telemetryObject) {
// Get latest domain/range values for this object.
var id = telemetryObject.getId(),
domainValue = subscription.getDomainValue(telemetryObject),
rangeValue = subscription.getRangeValue(telemetryObject);
// Track the most recent timestamp change observed...
if (domainValue !== undefined && domainValue !== lastUpdated[id]) {
lastUpdated[id] = domainValue;
// ... and update the displayable text for that timestamp
updateText = isNaN(domainValue) ? "" :
moment.utc(domainValue).format(DATE_FORMAT);
}
// Store data values into the rangeValues structure, which
// will be used to populate the table itself.
// Note that we want full precision here.
rangeValues[id] = rangeValue;
// Update limit states as well
classes[id] = limits[id] && (limits[id].evaluate({
// This relies on external knowledge that the
// range value of a telemetry point is encoded
// in its datum as "value."
value: rangeValue
}) || {}).cssClass;
}
// Look at telemetry objects from the subscription; this is watched
// to detect changes from the subscription.
function subscribedTelemetry() {
return subscription ?
subscription.getTelemetryObjects() : EMPTY_ARRAY;
}
// Update the data values which will be used to populate the table
function updateValues() {
subscribedTelemetry().forEach(recordData);
triggerDisplayUpdate();
}
// Getter-setter function for user-entered filter text.
function filter(value) {
// If value was specified, we're a setter
if (value !== undefined) {
// Store the new value
filterValue = value;
filterValueLowercase = value.toLowerCase();
// Change which objects appear in the table
updateFilteredObjects();
}
// Always act as a getter
return filterValue;
}
// Update the bounds (width and height) of this view;
// called from the mct-resize directive. Recalculates how
// many rows should appear in the contained table.
function setBounds(bounds) {
var availableSpace = bounds.height - SLIDER_HEIGHT;
rows = Math.max(1, Math.floor(availableSpace / ROW_HEIGHT));
}
// Increment the current column width, up to the defined maximum.
// When the max is hit, roll back to the default.
function increaseColumnWidth() {
columnWidth += COLUMN_WIDTH_STEP;
// Cycle down to the initial width instead of exceeding max
columnWidth = columnWidth > MAX_COLUMN_WIDTH ?
INITIAL_COLUMN_WIDTH : columnWidth;
}
// Get displayable text for last-updated value
function updated() {
return updateText;
}
// Unsubscribe, if a subscription is active.
function releaseSubscription() {
if (subscription) {
subscription.unsubscribe();
subscription = undefined;
}
}
// Update set of telemetry objects managed by this view
function updateTelemetryObjects(telemetryObjects) {
updateFilteredObjects();
limits = {};
telemetryObjects.forEach(function (telemetryObject) {
var id = telemetryObject.getId();
limits[id] = telemetryObject.getCapability('limit');
});
}
// Create a subscription for the represented domain object.
// This will resolve capability delegation as necessary.
function makeSubscription(domainObject) {
// Unsubscribe, if there is an existing subscription
releaseSubscription();
// Clear updated timestamp
lastUpdated = {};
updateText = NOT_UPDATED;
// Create a new subscription; telemetrySubscriber gets
// to do the meaningful work here.
subscription = domainObject && telemetrySubscriber.subscribe(
domainObject,
updateValues
);
// Our set of in-view telemetry objects may have changed,
// so update the set that is being passed down to the table.
updateFilteredObjects();
}
// Watch for changes to the set of objects which have telemetry
$scope.$watch(subscribedTelemetry, updateTelemetryObjects);
// Watch for the represented domainObject (this field will
// be populated by mct-representation)
$scope.$watch("domainObject", makeSubscription);
// Make sure we unsubscribe when this view is destroyed.
$scope.$on("$destroy", releaseSubscription);
return {
/**
* Get the number of rows which should be shown in this table.
* @return {number} the number of rows to show
*/
getRows: function () {
return rows;
},
/**
* Get the objects which should currently be displayed in
* this table. This will be watched, so the return value
* should be stable when this list is unchanging. Only
* objects which match the user-entered filter value should
* be returned here.
* @return {DomainObject[]} the domain objects to include in
* this table.
*/
getTelemetryObjects: function () {
return filteredObjects;
},
/**
* Set the bounds (width/height) of this autoflow tabular view.
* The template must ensure that these bounds are tracked on
* the table area only.
* @param bounds the bounds; and object with `width` and
* `height` properties, both as numbers, in pixels.
*/
setBounds: setBounds,
/**
* Increments the width of the autoflow column.
* Setting does not yet persist.
*/
increaseColumnWidth: increaseColumnWidth,
/**
* Get-or-set the user-supplied filter value.
* @param {string} [value] the new filter value; omit to use
* as a getter
* @returns {string} the user-supplied filter value
*/
filter: filter,
/**
* Get all range values for use in this table. These will be
* returned as an object of key-value pairs, where keys are
* domain object IDs, and values are the most recently observed
* data values associated with those objects, formatted for
* display.
* @returns {object.<string,string>} most recent values
*/
rangeValues: function () {
return rangeValues;
},
/**
* Get CSS classes to apply to specific rows, representing limit
* states and/or stale states. These are returned as key-value
* pairs where keys are domain object IDs, and values are CSS
* classes to display for domain objects with those IDs.
* @returns {object.<string,string>} CSS classes
*/
classes: function () {
return classes;
},
/**
* Get the "last updated" text for this view; this will be
* the most recent timestamp observed for any telemetry-
* providing object, formatted for display.
* @returns {string} the time of the most recent update
*/
updated: updated,
/**
* Get the current column width, in pixels.
* @returns {number} column width
*/
columnWidth: function () {
return columnWidth;
},
/**
* Keep a counter and increment this whenever the display
* should be updated; this will be watched by the
* `mct-autoflow-table`.
* @returns {number} a counter value
*/
counter: function () {
return counter;
}
};
}
return AutflowTabularController;
}
);