diff --git a/platform/telemetry/README.md b/platform/telemetry/README.md new file mode 100644 index 0000000000..664cabd7cb --- /dev/null +++ b/platform/telemetry/README.md @@ -0,0 +1,2 @@ +This bundle is responsible for introducing a reusable infrastructure +and set of APIs for using time-series data in Open MCT Web. diff --git a/platform/telemetry/bundle.json b/platform/telemetry/bundle.json new file mode 100644 index 0000000000..8ed450bc57 --- /dev/null +++ b/platform/telemetry/bundle.json @@ -0,0 +1,34 @@ +{ + "name": "Data bundle", + "description": "Interfaces and infrastructure for real-time and historical data.", + "extensions": { + "components": [ + { + "provides": "telemetryService", + "type": "aggregator", + "implementation": "TelemetryAggregator.js", + "depends": [ "$q" ] + } + ], + "controllers": [ + { + "key": "TelemetryController", + "implementation": "TelemetryController.js", + "depends": [ "$scope", "$q", "$timeout", "$log" ] + } + ], + "capabilities": [ + { + "key": "telemetry", + "implementation": "TelemetryCapability.js", + "depends": [ "telemetryService" ] + } + ], + "services": [ + { + "key": "telemetryHelper", + "implementation": "TelemetryHelper.js" + } + ] + } +} \ No newline at end of file diff --git a/platform/telemetry/src/Telemetry.js b/platform/telemetry/src/Telemetry.js new file mode 100644 index 0000000000..13c489863f --- /dev/null +++ b/platform/telemetry/src/Telemetry.js @@ -0,0 +1,38 @@ +/*global define,Promise*/ + +/** + * Module defining Telemetry. Created by vwoeltje on 11/12/14. + */ +define( + [], + function () { + "use strict"; + + /** + * + * @constructor + */ + function Telemetry(array, defaults) { + // Assume array-of-arrays if not otherwise specified, + // where first value is x, second is y + defaults = defaults || { + domain: 0, + range: 1 + }; + + return { + getPointCount: function () { + return array.length; + }, + getRangeValue: function (index, range) { + return array[index][range || defaults.range]; + }, + getDomainValue: function (index, domain) { + return array[index][domain || defaults.domain]; + } + }; + } + + return Telemetry; + } +); \ No newline at end of file diff --git a/platform/telemetry/src/TelemetryAggregator.js b/platform/telemetry/src/TelemetryAggregator.js new file mode 100644 index 0000000000..ca97440b4e --- /dev/null +++ b/platform/telemetry/src/TelemetryAggregator.js @@ -0,0 +1,43 @@ +/*global define,Promise*/ + +/** + * Module defining TelemetryProvider. Created by vwoeltje on 11/12/14. + */ +define( + [], + function () { + "use strict"; + + /** + * + * @constructor + */ + function TelemetryAggregator($q, telemetryProviders) { + + function mergeResults(results) { + var merged = {}; + + results.forEach(function (result) { + Object.keys(result).forEach(function (k) { + // Otherwise, just take the result + merged[k] = result[k]; + }); + }); + + return merged; + } + + function requestTelemetry(requests) { + return $q.all(telemetryProviders.map(function (provider) { + return provider.requestTelemetry(requests); + })).then(mergeResults); + } + + return { + requestTelemetry: requestTelemetry + }; + } + + return TelemetryAggregator; + } +); \ No newline at end of file diff --git a/platform/telemetry/src/TelemetryCapability.js b/platform/telemetry/src/TelemetryCapability.js new file mode 100644 index 0000000000..b0551399e0 --- /dev/null +++ b/platform/telemetry/src/TelemetryCapability.js @@ -0,0 +1,70 @@ +/*global define,Promise*/ + +/** + * Module defining TelemetryCapability. Created by vwoeltje on 11/12/14. + */ +define( + [], + function () { + "use strict"; + + /** + * + * @constructor + */ + function TelemetryCapability(telemetryService, domainObject) { + function buildRequest(request) { + var type = domainObject.getCapability("type"), + typeRequest = type.getDefinition().telemetry || {}, + modelTelemetry = domainObject.getModel().telemetry, + fullRequest = Object.create(typeRequest); + + Object.keys(modelTelemetry).forEach(function (k) { + fullRequest[k] = modelTelemetry[k]; + }); + + Object.keys(request).forEach(function (k) { + fullRequest[k] = request[k]; + }); + + // Include domain object ID, at minimum + if (!fullRequest.id) { + fullRequest.id = domainObject.getId(); + } + if (!fullRequest.key) { + fullRequest.key = domainObject.getId(); + } + + return fullRequest; + } + + function requestTelemetry(request) { + // Bring in any defaults from the object model + var fullRequest = buildRequest(request || {}), + source = fullRequest.source, + key = fullRequest.key; + + function getRelevantResponse(response) { + return (response[source] || {})[key] || {}; + } + + return telemetryService.requestTelemetry([fullRequest]) + .then(getRelevantResponse); + } + + return { + requestData: requestTelemetry, + getMetadata: function () { + return buildRequest({}); + } + //subscribe: subscribe + }; + } + + TelemetryCapability.appliesTo = function (model) { + return model.telemetry; + } + + return TelemetryCapability; + } +); \ No newline at end of file diff --git a/platform/telemetry/src/TelemetryController.js b/platform/telemetry/src/TelemetryController.js new file mode 100644 index 0000000000..cd7f4e2017 --- /dev/null +++ b/platform/telemetry/src/TelemetryController.js @@ -0,0 +1,209 @@ +/*global define,Promise*/ + +/** + * Module defining TelemetryController. Created by vwoeltje on 11/12/14. + */ +define( + [], + function () { + "use strict"; + + /** + * Serves as a reusable controller for views (or parts of views) + * which need to issue, use telemetry controls. + * + * @constructor + */ + function TelemetryController($scope, $q, $timeout, $log) { + /* + Want a notion of "the data set": All the latest data. + It can look like: + { + "source": { + "key": { + ...Telemetry object... + } + } + } + + Then, telemetry controller should provide: + { + // Element for which there is telemetry data available + elements: [ { <-- the objects to view + name: ...human-readable + metadata: ... + object: ...the domain object + data: ...telemetry data for that element + } ] + } + */ + var self = { + ids: [], + response: {}, + request: {}, + pending: 0, + metadatas: [], + interval: 1000, + refreshing: false, + broadcasting: false + }; + + + function doBroadcast() { + if (!self.broadcasting) { + self.broadcasting = true; + $timeout(function () { + self.broadcasting = false; + $scope.$broadcast("telemetryUpdate"); + }); + } + } + + function requestTelemetryForId(id, trackPending) { + var responseObject = self.response[id], + domainObject = responseObject.domainObject, + telemetry = domainObject.getCapability('telemetry'); + + function storeData(data) { + self.pending -= trackPending ? 1 : 0; + responseObject.data = data; + doBroadcast(); + } + + self.pending += trackPending ? 1 : 0; + + if (!telemetry) { + $log.warn([ + "Expected telemetry capability for ", + id, + " but found none. Cannot request data." + ].join("")); + return; + } + + return $q.when(telemetry.requestData(self.request)) + .then(storeData); + } + + function requestTelemetry(trackPending) { + return $q.all(self.ids.map(function (id) { + return requestTelemetryForId(id, trackPending); + })); + } + + function promiseRelevantDomainObjects() { + var domainObject = $scope.domainObject; + + if (!domainObject) { + return $q.when([]); + } + + return $q.when(domainObject.useCapability( + "delegation", + "telemetry" + )).then(function (result) { + var head = domainObject.hasCapability("telemetry") ? + [ domainObject ] : [], + tail = result || []; + return head.concat(tail); + }); + } + + function buildResponseContainer(domainObject) { + var telemetry = domainObject && + domainObject.getCapability("telemetry"), + metadata; + + if (telemetry) { + metadata = telemetry.getMetadata(); + + self.response[domainObject.getId()] = { + name: domainObject.getModel().name, + domainObject: domainObject, + metadata: metadata, + pending: 0, + data: {} + }; + } else { + $log.warn([ + "Expected telemetry capability for ", + domainObject.getId(), + " but none was found." + ].join("")); + } + } + + function buildResponseContainers(domainObjects) { + domainObjects.forEach(buildResponseContainer); + self.ids = domainObjects.map(function (obj) { + return obj.getId(); + }); + self.metadatas = self.ids.map(function (id) { + return self.response[id].metadata; + }); + + // Issue a request for the new objects, if we + // know what our request looks like + if (self.request) { + requestTelemetry(true); + } + } + + function getTelemetryObjects() { + promiseRelevantDomainObjects().then(buildResponseContainers); + } + + function startTimeout() { + if (!self.refreshing && self.interval !== undefined) { + self.refreshing = true; + $timeout(function () { + if (self.request) { + requestTelemetry(false); + } + + self.refreshing = false; + startTimeout(); + }, 1000); + } + } + + + $scope.$watch("domainObject", getTelemetryObjects); + startTimeout(); // Begin refreshing + + return { + getMetadata: function () { + return self.metadatas; + }, + getTelemetryObjects: function () { + return self.ids.map(function (id) { + return self.response[id].domainObject; + }); + }, + getResponse: function getResponse(arg) { + var id = arg && (typeof arg === 'string' ? + arg : arg.getId()); + + if (id) { + return (self.response[id] || {}).data; + } + + return (self.ids || []).map(getResponse); + }, + isRequestPending: function () { + return self.pending > 0; + }, + requestData: function (request) { + self.request = request || {}; + return requestTelemetry(true); + }, + setRefreshInterval: function (durationMillis) { + self.interval = durationMillis; + startTimeout(); + } + }; + } + + return TelemetryController; + } +); \ No newline at end of file diff --git a/platform/telemetry/src/TelemetryHelper.js b/platform/telemetry/src/TelemetryHelper.js new file mode 100644 index 0000000000..005f7832d4 --- /dev/null +++ b/platform/telemetry/src/TelemetryHelper.js @@ -0,0 +1,73 @@ +/*global define,Float32Array,Promise*/ + +/** + * Module defining TelemetryHelper. Created by vwoeltje on 11/14/14. + */ +define( + [], + function () { + "use strict"; + + /** + * Helps to interpret the contents of Telemetry objects. + * @constructor + */ + function TelemetryHelper() { + return { + getBufferedForm: function (telemetry, start, end, domain, range) { + var arr = [], + domainMin = Number.MAX_VALUE, + rangeMin = Number.MAX_VALUE, + domainMax = Number.MIN_VALUE, + rangeMax = Number.MIN_VALUE, + domainValue, + rangeValue, + count, + i; + + function trackBounds(domainValue, rangeValue) { + domainMin = Math.min(domainMin, domainValue); + domainMax = Math.max(domainMax, domainValue); + rangeMin = Math.min(rangeMin, rangeValue); + rangeMax = Math.max(rangeMax, rangeValue); + } + + function applyOffset() { + var j; + for (j = 0; j < arr.length; j += 2) { + arr[j] -= domainMin; + arr[j + 1] -= rangeMin; + } + } + + count = telemetry.getPointCount(); + + if (start === undefined) { + start = telemetry.getDomainValue(0, domain); + } + + if (end === undefined) { + end = telemetry.getDomainValue(count - 1, domain); + } + + for (i = 0; i < telemetry.getPointCount(); i += 1) { + // TODO: Binary search for start, end + domainValue = telemetry.getDomainValue(i, domain); + + if (domainValue >= start && domainValue <= end) { + rangeValue = telemetry.getRangeValue(i, range); + arr.push(domainValue); + arr.push(rangeValue); + trackBounds(domainValue, rangeValue); + } + } + + applyOffset(); + + } + }; + } + + return TelemetryHelper; + } +); \ No newline at end of file