diff --git a/.jshintrc b/.jshintrc index dc2b733533..ec94c41acd 100644 --- a/.jshintrc +++ b/.jshintrc @@ -14,7 +14,8 @@ "nonew": true, "predef": [ "define", - "Promise" + "Promise", + "WeakMap" ], "shadow": "outer", "strict": "implied", diff --git a/src/MCT.js b/src/MCT.js index 3bfe5417a1..32380baf88 100644 --- a/src/MCT.js +++ b/src/MCT.js @@ -192,7 +192,7 @@ define([ * @memberof module:openmct.MCT# * @name telemetry */ - this.telemetry = new api.TelemetryAPI(); + this.telemetry = new api.TelemetryAPI(this); this.TimeConductor = this.conductor; // compatibility for prototype this.on('navigation', this.selection.clear.bind(this.selection)); diff --git a/src/api/telemetry/LegacyTelemetryProvider.js b/src/api/telemetry/LegacyTelemetryProvider.js new file mode 100644 index 0000000000..672fb9c83f --- /dev/null +++ b/src/api/telemetry/LegacyTelemetryProvider.js @@ -0,0 +1,148 @@ +/***************************************************************************** + * 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([ + '../objects/object-utils' +], function ( + utils +) { + /** + * @implements module:openmct.TelemetryAPI~TelemetryProvider + * @constructor + */ + function LegacyTelemetryProvider(instantiate) { + this.instantiate = instantiate; + } + + /** + * Can provide telemetry for all objects that have the "telemetry" + * capability + * + * @see module:openmct.TelemetryAPI~TelemetryProvider#canProvideTelemetry + */ + LegacyTelemetryProvider.prototype.canProvideTelemetry = function (domainObject) { + return this.instantiate(utils.toOldFormat(domainObject), + utils.makeKeyString(domainObject.identifier)).hasCapability("telemetry"); + }; + + function createDatum(domainObject, metadata, legacySeries, i) { + var datum = {}; + + metadata.domains.reduce(function (d, domain) { + d[domain.key] = legacySeries.getDomainValue(i, domain.key); + return d; + }, datum); + + metadata.ranges.reduce(function (d, range) { + d[range.key] = legacySeries.getRangeValue(i, range.key); + return d; + }, datum); + + datum.name = domainObject.name; + + return datum; + } + + function adaptSeries(domainObject, metadata, legacySeries) { + var series = []; + + for (var i = 0; i < legacySeries.getPointCount(); i++) { + series.push(createDatum(domainObject, metadata, legacySeries, i)); + } + + return series; + } + + /** + * @typedef {object} ConvertedTelemetryObject + * Telemetry data objects are converted from TelemetrySeries. Metadata is used + * to populate the returned object with attributes corresponding to the keys + * of domains and ranges. The attribute values are those returned by calls to + * [TelemetrySeries.getDomainValue()]{@link TelemetrySeries#getDomainValue} + * and [TelemetrySeries.getRangeValue()]{@link TelemetrySeries#getRangeValue}. + */ + + /** + * @see module:openmct.TelemetryAPI~TelemetryProvider#request + * @param {module:openmct.DomainObject} + * @param {module:openmct.TelemetryAPI~TelemetryRequest} options + * options for this request. Passed straight through to legacy provider + * @returns {Promise.} a promise for an array of + * telemetry data. + */ + LegacyTelemetryProvider.prototype.request = function (domainObject, request) { + var oldObject = this.instantiate(utils.toOldFormat(domainObject), utils.makeKeyString(domainObject.identifier)); + var capability = oldObject.getCapability("telemetry"); + + return capability.requestData(request).then(function (telemetrySeries) { + return Promise.resolve(adaptSeries(domainObject, capability.getMetadata(), telemetrySeries)); + }).catch(function (error) { + return Promise.reject(error); + }); + }; + + /** + * @callback LegacyTelemetryProvider~SubscribeCallback + * @param {ConvertedTelemetryObject} + */ + + /** + * @see module:openmct.TelemetryAPI~TelemetryProvider#request + * @param {module:openmct.DomainObject} + * @param {LegacyTelemetryProvider~SubscribeCallback} callback will be called with a single datum when + * new data is available. + * @param {module:openmct.TelemetryAPI~TelemetryRequest} options + * options for this request. Passed straight through to legacy provider + * @returns {platform|telemetry.TelemetrySubscription|*} + */ + LegacyTelemetryProvider.prototype.subscribe = function (domainObject, callback, request) { + var oldObject = this.instantiate(utils.toOldFormat(domainObject), utils.makeKeyString(domainObject.identifier)); + var capability = oldObject.getCapability("telemetry"); + + function callbackWrapper(series) { + callback(createDatum(domainObject, capability.getMetadata(), series, series.getPointCount() - 1)); + } + + return capability.subscribe(callbackWrapper, request); + }; + + LegacyTelemetryProvider.prototype.limitEvaluator = function (domainObject) { + var oldObject = this.instantiate( + utils.toOldFormat(domainObject), + utils.makeKeyString(domainObject.identifier)); + var limitEvaluator = oldObject.getCapability("limit"); + + return { + evaluate: function (datum, property) { + return limitEvaluator.evaluate(datum, property.key); + } + }; + }; + + return function (openmct, instantiate) { + // Push onto the start of the default providers array so that it's + // always the last resort + openmct.telemetry.defaultProviders.unshift( + new LegacyTelemetryProvider(instantiate)); + }; + +}); diff --git a/src/api/telemetry/TelemetryAPI.js b/src/api/telemetry/TelemetryAPI.js index 6e310b1aa8..183432eb95 100644 --- a/src/api/telemetry/TelemetryAPI.js +++ b/src/api/telemetry/TelemetryAPI.js @@ -21,9 +21,13 @@ *****************************************************************************/ define([ + './TelemetryMetadataManager', + './TelemetryValueFormatter', 'lodash', 'EventEmitter' ], function ( + TelemetryMetadataManager, + TelemetryValueFormatter, _, EventEmitter ) { @@ -155,9 +159,13 @@ define([ * @augments module:openmct.TelemetryAPI~TelemetryProvider * @memberof module:openmct */ - function TelemetryAPI() { + function TelemetryAPI(MCT) { + this.MCT = MCT; this.providersByStrategy = {}; this.defaultProviders = []; + this.metadataCache = new WeakMap(); + this.formatMapCache = new WeakMap(); + this.valueFormatterCache = new WeakMap(); } /** @@ -240,6 +248,85 @@ define([ Promise.reject([]); }; + /** + * Get telemetry metadata for a given domain object. Returns a telemetry + * metadata manager which provides methods for interrogating telemetry + * metadata. + * + * @returns {TelemetryMetadataManager} + */ + TelemetryAPI.prototype.getMetadata = function (domainObject) { + if (!this.metadataCache.has(domainObject)) { + if (!this.typeService) { + this.typeService = this.MCT.$injector.get('typeService'); + } + this.metadataCache.set( + domainObject, + new TelemetryMetadataManager(domainObject, this.typeService) + ); + } + return this.metadataCache.get(domainObject); + }; + + /** + * Return an array of valueMetadatas that are common to all supplied + * telemetry objects and match the requested hints. + * + */ + TelemetryAPI.prototype.commonValuesForHints = function (metadatas, hints) { + var options = metadatas.map(function (metadata) { + var values = metadata.valuesForHints(hints); + return _.indexBy(values, 'key'); + }).reduce(function (a, b) { + var results = {}; + Object.keys(a).forEach(function (key) { + if (b.hasOwnProperty(key)) { + results[key] = a[key]; + } + }); + return results; + }); + var sortKeys = hints.map(function (h) { + return 'hints.' + h; + }); + return _.sortByAll(options, sortKeys); + }; + + /** + * Get a value formatter for a given valueMetadata. + * + * @returns {TelemetryValueFormatter} + */ + TelemetryAPI.prototype.getValueFormatter = function (valueMetadata) { + if (!this.valueFormatterCache.has(valueMetadata)) { + if (!this.formatService) { + this.formatService = this.MCT.$injector.get('formatService'); + } + this.valueFormatterCache.set( + valueMetadata, + new TelemetryValueFormatter(valueMetadata, this.formatService) + ); + } + return this.valueFormatterCache.get(valueMetadata); + }; + + /** + * Get a format map of all value formatters for a given piece of telemetry + * metadata. + * + * @returns {Object} + */ + TelemetryAPI.prototype.getFormatMap = function (metadata) { + if (!this.formatMapCache.has(metadata)) { + var formatMap = metadata.values().reduce(function (map, valueMetadata) { + map[valueMetadata.key] = this.getValueFormatter(valueMetadata); + return map; + }.bind(this), {}); + this.formatMapCache.set(metadata, formatMap); + } + return this.formatMapCache.get(metadata); + }; + /** * Subscribe to realtime telemetry for a specific domain object. * The callback will be called whenever data is received from a diff --git a/src/api/telemetry/TelemetryMetadataManager.js b/src/api/telemetry/TelemetryMetadataManager.js new file mode 100644 index 0000000000..db0e6d3b0a --- /dev/null +++ b/src/api/telemetry/TelemetryMetadataManager.js @@ -0,0 +1,152 @@ +/***************************************************************************** + * 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 valueMetadatasFromOldFormat(metadata) { + var valueMetadatas = []; + + valueMetadatas.push({ + key: 'name', + name: 'Name' + }); + + metadata.domains.forEach(function (domain, index) { + var valueMetadata = _.clone(domain); + valueMetadata.hints = { + x: index + 1, + domain: index + 1 + }; + valueMetadatas.push(valueMetadata); + }); + + metadata.ranges.forEach(function (range, index) { + var valueMetadata = _.clone(range); + valueMetadata.hints = { + y: index, + range: index, + priority: index + metadata.domains.length + 1 + }; + + if (valueMetadata.type === 'enum') { + valueMetadata.key = 'enum'; + valueMetadata.hints.y -= 10; + valueMetadata.hints.range -= 10; + valueMetadata.enumerations = + _.sortBy(valueMetadata.enumerations.map(function (e) { + return { + string: e.string, + value: +e.value + }; + }), 'e.value'); + valueMetadata.values = _.pluck(valueMetadata.enumerations, 'value'); + valueMetadata.max = _.max(valueMetadata.values); + valueMetadata.min = _.min(valueMetadata.values); + } + + valueMetadatas.push(valueMetadata); + }); + + return valueMetadatas; + } + + function applyReasonableDefaults(valueMetadata, index) { + valueMetadata.source = valueMetadata.source || valueMetadata.key; + valueMetadata.hints = valueMetadata.hints || {}; + + if (!valueMetadata.hints.hasOwnProperty('priority')) { + valueMetadata.hints.priority = index; + } + return valueMetadata; + } + + /** + * Utility class for handling telemetry metadata for a domain object. + * Wraps old format metadata to new format metadata. + * Provides methods for interrogating telemetry metadata. + */ + function TelemetryMetadataManager(domainObject, typeService) { + this.metadata = domainObject.telemetry || {}; + + if (this.metadata.values) { + this.valueMetadatas = this.metadata.values; + } else { + var typeMetadata = typeService + .getType(domainObject.type).typeDef.telemetry; + + _.extend(this.metadata, typeMetadata); + this.valueMetadatas = valueMetadatasFromOldFormat(this.metadata); + } + + this.valueMetadatas = this.valueMetadatas.map(applyReasonableDefaults); + } + + + /** + * Get value metadata for a single key. + */ + TelemetryMetadataManager.prototype.value = function (key) { + return this.valueMetadatas.filter(function (metadata) { + return metadata.key === key; + })[0]; + }; + + /** + * Returns all value metadatas, sorted by priority. + */ + TelemetryMetadataManager.prototype.values = function () { + return this.valuesForHints(['priority']); + }; + + /** + * Get an array of valueMetadatas that posess all hints requested. + * Array is sorted based on hint priority. + * + */ + TelemetryMetadataManager.prototype.valuesForHints = function ( + hints + ) { + function hasHint(hint) { + /*jshint validthis: true */ + return this.hints.hasOwnProperty(hint); + } + function hasHints(metadata) { + return hints.every(hasHint, metadata); + } + var matchingMetadata = this.valueMetadatas.filter(hasHints); + var sortedMetadata = _.sortBy(matchingMetadata, function (metadata) { + return hints.map(function (hint) { + return metadata.hints[hint]; + }); + }); + return sortedMetadata; + }; + + + return TelemetryMetadataManager; + +}); diff --git a/src/api/telemetry/TelemetryValueFormatter.js b/src/api/telemetry/TelemetryValueFormatter.js new file mode 100644 index 0000000000..801aee1176 --- /dev/null +++ b/src/api/telemetry/TelemetryValueFormatter.js @@ -0,0 +1,98 @@ +/***************************************************************************** + * 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 ( + _ +) { + + // TODO: needs reference to formatService; + function TelemetryValueFormatter(valueMetadata, formatService) { + this.valueMetadata = valueMetadata; + this.parseCache = new WeakMap(); + this.formatCache = new WeakMap(); + try { + this.formatter = formatService + .getFormat(valueMetadata.format, valueMetadata); + } catch (e) { + // TODO: Better formatting + this.formatter = { + parse: function (x) { + return Number(x); + }, + format: function (x) { + return x; + }, + validate: function (x) { + return true; + } + }; + } + + if (valueMetadata.type === 'enum') { + this.formatter = {}; + this.enumerations = valueMetadata.enumerations.reduce(function (vm, e) { + vm.byValue[e.value] = e.string; + vm.byString[e.string] = e.value; + return vm; + }, {byValue: {}, byString: {}}); + this.formatter.format = function (value) { + return this.enumerations.byValue[value]; + }.bind(this); + this.formatter.parse = function (string) { + if (typeof string === "string" && this.enumerations.hasOwnProperty(string)) { + return this.enumerations.byString[string]; + } + return Number(string); + }.bind(this); + } + } + + TelemetryValueFormatter.prototype.parse = function (datum) { + if (_.isObject(datum)) { + if (!this.parseCache.has(datum)) { + this.parseCache.set( + datum, + this.formatter.parse(datum[this.valueMetadata.source]) + ); + } + return this.parseCache.get(datum); + } + return this.formatter.parse(datum); + }; + + TelemetryValueFormatter.prototype.format = function (datum) { + if (_.isObject(datum)) { + if (!this.formatCache.has(datum)) { + this.formatCache.set( + datum, + this.formatter.format(datum[this.valueMetadata.source]) + ); + } + return this.formatCache.get(datum); + } + return this.formatter.format(datum); + }; + + return TelemetryValueFormatter; +}); diff --git a/src/api/telemetry/bundle.js b/src/api/telemetry/bundle.js index a389ebf804..d12fcd518e 100644 --- a/src/api/telemetry/bundle.js +++ b/src/api/telemetry/bundle.js @@ -22,12 +22,14 @@ define([ './TelemetryAPI', + './LegacyTelemetryProvider', 'legacyRegistry' ], function ( TelemetryAPI, + LegacyTelemetryProvider, legacyRegistry ) { - legacyRegistry.register('api/telemetry-api', { + legacyRegistry.register('src/api/telemetry', { name: 'Telemetry API', description: 'The public Telemetry API', extensions: { @@ -38,6 +40,14 @@ define([ depends: [ 'formatService' ] + }, + { + key: "LegacyTelemetryAdapter", + implementation: LegacyTelemetryProvider, + depends: [ + "openmct", + "instantiate" + ] } ] } diff --git a/src/defaultRegistry.js b/src/defaultRegistry.js index cba485c8a5..cf9e5e5e72 100644 --- a/src/defaultRegistry.js +++ b/src/defaultRegistry.js @@ -25,6 +25,7 @@ define([ '../src/adapter/bundle', '../src/api/objects/bundle', + '../src/api/telemetry/bundle', '../example/builtins/bundle', '../example/composite/bundle', @@ -96,6 +97,7 @@ define([ var DEFAULTS = [ 'src/adapter', 'src/api/objects', + 'src/api/telemetry', 'platform/framework', 'platform/core', 'platform/representation',