diff --git a/example/kerbal/bundle.js b/example/kerbal/bundle.js new file mode 100644 index 00000000000..70b14470a4b --- /dev/null +++ b/example/kerbal/bundle.js @@ -0,0 +1,82 @@ +define([ + 'legacyRegistry' +], function (legacyRegistry) { + legacyRegistry.register("example/kerbal", { + "name": "Kerbal Telemetry Adapter", + "extensions": { + "types": [ + { + "name": "Spacecraft", + "key": "kerbal.spacecraft", + "glyph": "o" + }, + { + "name": "Subsystem", + "key": "kerbal.subsystem", + "glyph": "o", + "model": {"composition": []} + }, + { + "name": "Measurement", + "key": "kerbal.measurement", + "glyph": "T", + "model": {"telemetry": {}}, + "telemetry": { + "source": "kerbal.source", + "domains": [ + { + "name": "Time", + "key": "timestamp" + } + ] + } + } + ], + "roots": [ + { + "id": "kerbal:sc", + "priority": "preferred", + "model": { + "type": "kerbal.spacecraft", + "name": "My Spacecraft", + "composition": [] + } + } + ], + "services": [ + { + "key": "kerbal.adapter", + "implementation": "KerbalTelemetryServerAdapter.js", + "depends": ["$q", "$http", "$interval", "KERBAL_HTTP_API_URL"] + } + ], + "constants": [ + { + "key": "KERBAL_HTTP_API_URL", + "priority": "fallback", + "value": "http://localhost:8085/telemachus/datalink" + } + ], + "runs": [ + { + "implementation": "KerbalTelemetryInitializer.js", + "depends": ["kerbal.adapter", "objectService"] + } + ], + "components": [ + { + "provides": "modelService", + "type": "provider", + "implementation": "KerbalTelemetryModelProvider.js", + "depends": ["kerbal.adapter", "$q"] + }, + { + "provides": "telemetryService", + "type": "provider", + "implementation": "KerbalTelemetryProvider.js", + "depends": ["kerbal.adapter", "$q"] + } + ] + } + }); +}); \ No newline at end of file diff --git a/example/kerbal/res/dictionary.json b/example/kerbal/res/dictionary.json new file mode 100644 index 00000000000..e1a77daa798 --- /dev/null +++ b/example/kerbal/res/dictionary.json @@ -0,0 +1,636 @@ +{ + "name": "Kerbal Spacecraft", + "identifier": "sc", + "subsystems": [ + { + "name": "Flight", + "identifier": "flight_plotables", + "measurements": [ + { + "name": "f.throttle", + "identifier": "f.throttle", + "units": "%", + "type": "float" + }, + { + "name": "v.rcsValue", + "identifier": "v.rcsValue", + "units": "None", + "type": "string" + }, + { + "name": "v.sasValue", + "identifier": "v.sasValue", + "units": "None", + "type": "string" + }, + { + "name": "v.lightValue", + "identifier": "v.lightValue", + "units": "None", + "type": "string" + }, + { + "name": "v.brakeValue", + "identifier": "v.brakeValue", + "units": "None", + "type": "string" + }, + { + "name": "v.gearValue", + "identifier": "v.gearValue", + "units": "None", + "type": "string" + } + ] + }, + { + "name": "Target", + "identifier": "target_plotables", + "measurements": [ + { + "name": "tar.o.sma", + "identifier": "tar.o.sma", + "units": "None", + "type": "string" + }, + { + "name": "tar.o.lan", + "identifier": "tar.o.lan", + "units": "None", + "type": "string" + }, + { + "name": "tar.o.maae", + "identifier": "tar.o.maae", + "units": "None", + "type": "string" + }, + { + "name": "tar.name", + "identifier": "tar.name", + "units": "None", + "type": "string" + }, + { + "name": "tar.type", + "identifier": "tar.type", + "units": "None", + "type": "string" + }, + { + "name": "tar.distance", + "identifier": "tar.distance", + "units": "None", + "type": "string" + }, + { + "name": "tar.o.velocity", + "identifier": "tar.o.velocity", + "units": "None", + "type": "string" + }, + { + "name": "tar.o.PeA", + "identifier": "tar.o.PeA", + "units": "None", + "type": "string" + }, + { + "name": "tar.o.ApA", + "identifier": "tar.o.ApA", + "units": "None", + "type": "string" + }, + { + "name": "tar.o.timeToAp", + "identifier": "tar.o.timeToAp", + "units": "None", + "type": "string" + }, + { + "name": "tar.o.timeToPe", + "identifier": "tar.o.timeToPe", + "units": "None", + "type": "string" + }, + { + "name": "tar.o.inclination", + "identifier": "tar.o.inclination", + "units": "None", + "type": "string" + }, + { + "name": "tar.o.eccentricity", + "identifier": "tar.o.eccentricity", + "units": "None", + "type": "string" + }, + { + "name": "tar.o.period", + "identifier": "tar.o.period", + "units": "None", + "type": "string" + }, + { + "name": "tar.o.relativeVelocity", + "identifier": "tar.o.relativeVelocity", + "units": "None", + "type": "string" + }, + { + "name": "tar.o.orbitingBody", + "identifier": "tar.o.orbitingBody", + "units": "None", + "type": "string" + }, + { + "name": "tar.o.argumentOfPeriapsis", + "identifier": "tar.o.argumentOfPeriapsis", + "units": "None", + "type": "string" + }, + { + "name": "tar.o.timeToTransition1", + "identifier": "tar.o.timeToTransition1", + "units": "None", + "type": "string" + }, + { + "name": "tar.o.timeToTransition2", + "identifier": "tar.o.timeToTransition2", + "units": "None", + "type": "string" + }, + { + "name": "tar.o.timeOfPeriapsisPassage", + "identifier": "tar.o.timeOfPeriapsisPassage", + "units": "None", + "type": "string" + } + ] + }, + { + "name": "Docking", + "identifier": "docking_plotables", + "measurements": [ + { + "name": "dock.ax", + "identifier": "dock.ax", + "units": "None", + "type": "string" + }, + { + "name": "dock.ay", + "identifier": "dock.ay", + "units": "None", + "type": "string" + }, + { + "name": "dock.az", + "identifier": "dock.az", + "units": "None", + "type": "string" + }, + { + "name": "dock.x", + "identifier": "dock.x", + "units": "None", + "type": "string" + }, + { + "name": "dock.y", + "identifier": "dock.y", + "units": "None", + "type": "string" + } + ] + }, + { + "name": "Navball", + "identifier": "navball_plotables", + "measurements": [ + { + "name": "n.heading", + "identifier": "n.heading", + "units": "None", + "type": "string" + }, + { + "name": "n.pitch", + "identifier": "n.pitch", + "units": "None", + "type": "string" + }, + { + "name": "n.roll", + "identifier": "n.roll", + "units": "None", + "type": "string" + }, + { + "name": "n.rawheading", + "identifier": "n.rawheading", + "units": "None", + "type": "string" + }, + { + "name": "n.rawpitch", + "identifier": "n.rawpitch", + "units": "None", + "type": "string" + }, + { + "name": "n.rawroll", + "identifier": "n.rawroll", + "units": "None", + "type": "string" + } + ] + }, + { + "name": "Vessel", + "identifier": "vessel_plotables", + "measurements": [ + { + "name": "v.altitude", + "identifier": "v.altitude", + "units": "meters", + "type": "float" + }, + { + "name": "v.heightFromTerrain", + "identifier": "v.heightFromTerrain", + "units": "meters", + "type": "float" + }, + { + "name": "v.terrainHeight", + "identifier": "v.terrainHeight", + "units": "meters", + "type": "float" + }, + { + "name": "v.missionTime", + "identifier": "v.missionTime", + "units": "seconds", + "type": "float" + }, + { + "name": "v.surfaceVelocity", + "identifier": "v.surfaceVelocity", + "units": "m/s", + "type": "float" + }, + { + "name": "v.surfaceVelocityx", + "identifier": "v.surfaceVelocityx", + "units": "m/s", + "type": "float" + }, + { + "name": "v.surfaceVelocityy", + "identifier": "v.surfaceVelocityy", + "units": "m/s", + "type": "float" + }, + { + "name": "v.surfaceVelocityz", + "identifier": "v.surfaceVelocityz", + "units": "m/s", + "type": "float" + }, + { + "name": "v.angularVelocity", + "identifier": "v.angularVelocity", + "units": "m/s", + "type": "float" + }, + { + "name": "v.orbitalVelocity", + "identifier": "v.orbitalVelocity", + "units": "m/s", + "type": "float" + }, + { + "name": "v.surfaceSpeed", + "identifier": "v.surfaceSpeed", + "units": "m/s", + "type": "float" + }, + { + "name": "v.verticalSpeed", + "identifier": "v.verticalSpeed", + "units": "m/s", + "type": "float" + }, + { + "name": "v.geeForce", + "identifier": "v.geeForce", + "units": "G", + "type": "float" + }, + { + "name": "v.atmosphericDensity", + "identifier": "v.atmosphericDensity", + "units": "None", + "type": "float" + }, + { + "name": "v.long", + "identifier": "v.long", + "units": "None", + "type": "string" + }, + { + "name": "v.lat", + "identifier": "v.lat", + "units": "None", + "type": "string" + }, + { + "name": "v.dynamicPressure", + "identifier": "v.dynamicPressure", + "units": "None", + "type": "float" + }, + { + "name": "v.name", + "identifier": "v.name", + "units": "None", + "type": "string" + }, + { + "name": "v.body", + "identifier": "v.body", + "units": "None", + "type": "string" + }, + { + "name": "v.angleToPrograde", + "identifier": "v.angleToPrograde", + "units": "degrees", + "type": "float" + } + ] + }, + { + "name": "Orbit", + "identifier": "orbit_plotables", + "measurements": [ + { + "name": "o.relativeVelocity", + "identifier": "o.relativeVelocity", + "units": "m/s", + "type": "float" + }, + { + "name": "o.PeA", + "identifier": "o.PeA", + "units": "None", + "type": "float" + }, + { + "name": "o.ApA", + "identifier": "o.ApA", + "units": "None", + "type": "float" + }, + { + "name": "o.timeToAp", + "identifier": "o.timeToAp", + "units": "seconds", + "type": "float" + }, + { + "name": "o.timeToPe", + "identifier": "o.timeToPe", + "units": "None", + "type": "string" + }, + { + "name": "o.inclination", + "identifier": "o.inclination", + "units": "seconds", + "type": "float" + }, + { + "name": "o.eccentricity", + "identifier": "o.eccentricity", + "units": "None", + "type": "float" + }, + { + "name": "o.epoch", + "identifier": "o.epoch", + "units": "None", + "type": "string" + }, + { + "name": "o.period", + "identifier": "o.period", + "units": "None", + "type": "string" + }, + { + "name": "o.argumentOfPeriapsis", + "identifier": "o.argumentOfPeriapsis", + "units": "None", + "type": "string" + }, + { + "name": "o.timeToTransition1", + "identifier": "o.timeToTransition1", + "units": "seconds", + "type": "float" + }, + { + "name": "o.timeToTransition2", + "identifier": "o.timeToTransition2", + "units": "seconds", + "type": "float" + }, + { + "name": "o.sma", + "identifier": "o.sma", + "units": "None", + "type": "string" + }, + { + "name": "o.lan", + "identifier": "o.lan", + "units": "None", + "type": "string" + }, + { + "name": "o.maae", + "identifier": "o.maae", + "units": "None", + "type": "string" + }, + { + "name": "o.timeOfPeriapsisPassage", + "identifier": "o.timeOfPeriapsisPassage", + "units": "seconds", + "type": "float" + }, + { + "name": "o.trueAnomaly", + "identifier": "o.trueAnomaly", + "units": "None", + "type": "string" + } + ] + }, + { + "name": "Sensor", + "identifier": "sensor_plotables", + "measurements": [ + { + "name": "s.sensor.temp", + "identifier": "s.sensor.temp", + "units": "centigrade", + "type": "float" + }, + { + "name": "s.sensor.pres", + "identifier": "s.sensor.pres", + "units": "None", + "type": "float" + }, + { + "name": "s.sensor.grav", + "identifier": "s.sensor.grav", + "units": "None", + "type": "float" + }, + { + "name": "s.sensor.acc", + "identifier": "s.sensor.acc", + "units": "None", + "type": "float" + } + ] + }, + { + "name": "Time Warp", + "identifier": "time_warp_plotables", + "measurements": [ + { + "name": "t.universalTime", + "identifier": "t.universalTime", + "units": "seconds", + "type": "float" + } + ] + }, + { + "name": "Resource", + "identifier": "resource_plotables", + "measurements": [ + { + "name": "r.resourceMax[ElectricCharge]", + "identifier": "r.resourceMax[ElectricCharge]", + "units": "None", + "type": "float" + }, + { + "name": "r.resourceCurrent[ElectricCharge]", + "identifier": "r.resourceCurrent[ElectricCharge]", + "units": "None", + "type": "float" + }, + { + "name": "r.resource[ElectricCharge]", + "identifier": "r.resource[ElectricCharge]", + "units": "None", + "type": "float" + }, + { + "name": "r.resourceMax[LiquidFuel]", + "identifier": "r.resourceMax[LiquidFuel]", + "units": "None", + "type": "float" + }, + { + "name": "r.resourceCurrent[LiquidFuel]", + "identifier": "r.resourceCurrent[LiquidFuel]", + "units": "None", + "type": "float" + }, + { + "name": "r.resource[LiquidFuel]", + "identifier": "r.resource[LiquidFuel]", + "units": "None", + "type": "float" + }, + { + "name": "r.resourceMax[Oxidizer]", + "identifier": "r.resourceMax[Oxidizer]", + "units": "None", + "type": "float" + }, + { + "name": "r.resourceCurrent[Oxidizer]", + "identifier": "r.resourceCurrent[Oxidizer]", + "units": "None", + "type": "float" + }, + { + "name": "r.resource[Oxidizer]", + "identifier": "r.resource[Oxidizer]", + "units": "None", + "type": "float" + }, + { + "name": "r.resourceCurrent[MonoPropellant]", + "identifier": "r.resourceCurrent[MonoPropellant]", + "units": "None", + "type": "float" + }, + { + "name": "r.resource[MonoPropellant]", + "identifier": "r.resource[MonoPropellant]", + "units": "None", + "type": "float" + }, + { + "name": "r.resourceMax[XenonGas]", + "identifier": "r.resourceMax[XenonGas]", + "units": "None", + "type": "float" + }, + { + "name": "r.resourceCurrent[XenonGas]", + "identifier": "r.resourceCurrent[XenonGas]", + "units": "None", + "type": "float" + }, + { + "name": "r.resource[XenonGas]", + "identifier": "r.resource[XenonGas]", + "units": "None", + "type": "float" + }, + { + "name": "r.resourceMax[IntakeAir]", + "identifier": "r.resourceMax[IntakeAir]", + "units": "None", + "type": "float" + }, + { + "name": "r.resourceCurrent[IntakeAir]", + "identifier": "r.resourceCurrent[IntakeAir]", + "units": "None", + "type": "float" + }, + { + "name": "r.resource[IntakeAir", + "identifier": "r.resource[IntakeAir", + "units": "None", + "type": "float" + } + ] + } + ] +} diff --git a/example/kerbal/src/KerbalTelemetryInitializer.js b/example/kerbal/src/KerbalTelemetryInitializer.js new file mode 100644 index 00000000000..650c1a495df --- /dev/null +++ b/example/kerbal/src/KerbalTelemetryInitializer.js @@ -0,0 +1,49 @@ +/*global define*/ + +define( + function () { + "use strict"; + + var TAXONOMY_ID = "kerbal:sc", + PREFIX = "kerbal_tlm:"; + + function KerbalTelemetryInitializer(adapter, objectService) { + // Generate a domain object identifier for a dictionary element + function makeId(element) { + return PREFIX + element.identifier; + } + + // When the dictionary is available, add all subsystems + // to the composition of My Spacecraft + function initializeTaxonomy(dictionary) { + // Get the top-level container for dictionary objects + // from a group of domain objects. + function getTaxonomyObject(domainObjects) { + return domainObjects[TAXONOMY_ID]; + } + + // Populate + function populateModel(taxonomyObject) { + return taxonomyObject.useCapability( + "mutation", + function (model) { + model.name = + dictionary.name; + model.composition = + dictionary.subsystems.map(makeId); + } + ); + } + + // Look up My Spacecraft, and populate it accordingly. + objectService.getObjects([TAXONOMY_ID]) + .then(getTaxonomyObject) + .then(populateModel); + } + + adapter.dictionary().then(initializeTaxonomy); + } + + return KerbalTelemetryInitializer; + } +); \ No newline at end of file diff --git a/example/kerbal/src/KerbalTelemetryModelProvider.js b/example/kerbal/src/KerbalTelemetryModelProvider.js new file mode 100644 index 00000000000..3d1733f6e17 --- /dev/null +++ b/example/kerbal/src/KerbalTelemetryModelProvider.js @@ -0,0 +1,80 @@ +/*global define*/ + +define( + function () { + "use strict"; + + var PREFIX = "kerbal_tlm:", + FORMAT_MAPPINGS = { + float: "number", + integer: "number", + string: "string" + }; + + function KerbalTelemetryModelProvider(adapter, $q) { + var modelPromise, empty = $q.when({}); + + // Check if this model is in our dictionary (by prefix) + function isRelevant(id) { + return id.indexOf(PREFIX) === 0; + } + + // Build a domain object identifier by adding a prefix + function makeId(element) { + return PREFIX + element.identifier; + } + + // Create domain object models from this dictionary + function buildTaxonomy(dictionary) { + var models = {}; + + // Create & store a domain object model for a measurement + function addMeasurement(measurement) { + var format = FORMAT_MAPPINGS[measurement.type]; + models[makeId(measurement)] = { + type: "kerbal.measurement", + name: measurement.name, + telemetry: { + key: measurement.identifier, + ranges: [{ + key: "value", + name: "Value", + units: measurement.units, + format: format + }] + } + }; + } + + // Create & store a domain object model for a subsystem + function addSubsystem(subsystem) { + var measurements = + (subsystem.measurements || []); + models[makeId(subsystem)] = { + type: "kerbal.subsystem", + name: subsystem.name, + composition: measurements.map(makeId) + }; + measurements.forEach(addMeasurement); + } + + (dictionary.subsystems || []).forEach(addSubsystem); + + return models; + } + + // Begin generating models once the dictionary is available + modelPromise = adapter.dictionary().then(buildTaxonomy); + + return { + getModels: function (ids) { + // Return models for the dictionary only when they + // are relevant to the request. + return ids.some(isRelevant) ? modelPromise : empty; + } + }; + } + + return KerbalTelemetryModelProvider; + } +); \ No newline at end of file diff --git a/example/kerbal/src/KerbalTelemetryProvider.js b/example/kerbal/src/KerbalTelemetryProvider.js new file mode 100644 index 00000000000..a2a471210a7 --- /dev/null +++ b/example/kerbal/src/KerbalTelemetryProvider.js @@ -0,0 +1,86 @@ +/*global define*/ + +define( + ['./KerbalTelemetrySeries'], + function (KerbalTelemetrySeries) { + "use strict"; + + var SOURCE = "kerbal.source"; + + function KerbalTelemetryProvider(adapter, $q) { + var subscribers = {}; + + // Used to filter out requests for telemetry + // from some other source + function matchesSource(request) { + return (request.source === SOURCE); + } + + // Listen for data, notify subscribers + adapter.listen(function (message) { + var packaged = {}; + packaged[SOURCE] = {}; + packaged[SOURCE][message.id] = + new KerbalTelemetrySeries([message.value]); + (subscribers[message.id] || []).forEach(function (cb) { + cb(packaged); + }); + }); + + return { + requestTelemetry: function (requests) { + var packaged = {}, + relevantReqs = requests.filter(matchesSource); + + // Package historical telemetry that has been received + function addToPackage(history) { + packaged[SOURCE][history.id] = + new KerbalTelemetrySeries(history.value); + } + + // Retrieve telemetry for a specific measurement + function handleRequest(request) { + var key = request.key; + return adapter.history(key).then(addToPackage); + } + + packaged[SOURCE] = {}; + return $q.all(relevantReqs.map(handleRequest)) + .then(function () { + return packaged; + }); + }, + subscribe: function (callback, requests) { + var keys = requests.filter(matchesSource) + .map(function (req) { + return req.key; + }); + + function notCallback(cb) { + return cb !== callback; + } + + function unsubscribe(key) { + subscribers[key] = + (subscribers[key] || []).filter(notCallback); + if (subscribers[key].length < 1) { + adapter.unsubscribe(key); + } + } + + keys.forEach(function (key) { + subscribers[key] = subscribers[key] || []; + adapter.subscribe(key); + subscribers[key].push(callback); + }); + + return function () { + keys.forEach(unsubscribe); + }; + } + }; + } + + return KerbalTelemetryProvider; + } +); \ No newline at end of file diff --git a/example/kerbal/src/KerbalTelemetrySeries.js b/example/kerbal/src/KerbalTelemetrySeries.js new file mode 100644 index 00000000000..0134159243d --- /dev/null +++ b/example/kerbal/src/KerbalTelemetrySeries.js @@ -0,0 +1,23 @@ +/*global define*/ + +define( + function () { + "use strict"; + + function KerbalTelemetrySeries(data) { + return { + getPointCount: function () { + return data.length; + }, + getDomainValue: function (index) { + return (data[index] || {}).timestamp; + }, + getRangeValue: function (index) { + return (data[index] || {}).value; + } + }; + } + + return KerbalTelemetrySeries; + } +); \ No newline at end of file diff --git a/example/kerbal/src/KerbalTelemetryServerAdapter.js b/example/kerbal/src/KerbalTelemetryServerAdapter.js new file mode 100644 index 00000000000..9934b339540 --- /dev/null +++ b/example/kerbal/src/KerbalTelemetryServerAdapter.js @@ -0,0 +1,90 @@ +/*global define,WebSocket*/ + +define( + [], + function () { + "use strict"; + + function KerbalTelemetryServerAdapter($q, $http, $interval, apiUrl) { + var listeners = [], + histories = {}, + params = [], + dictionary = $q.defer(); + + function pollApi () { + $http({ + method: 'GET', + url: apiUrl, + params: params.reduce(function(result, param) { + result[param] = param; + return result; + }, {}) + }).then(function(message) { + for (var id in message.data) { + // Store telemetry data in the history + histories[id] = histories[id] || { + id: id, + type: 'history', + value: [] + }; + + histories[id]['value'].push({ + timestamp: Date.now(), + value: message.data[id] + }); + + // Push telemetry data to listeners + listeners.forEach(function (listener) { + listener({ + id: id, + value: { + timestamp: Date.now(), + value: message.data[id] + } + }); + }); + } + }); + } + + // Retrieve dictionary + $http({ + method: 'GET', + url: '/example/kerbal/res/dictionary.json' + }).then(function(result) { + dictionary.resolve(result.data); + + // Retrieve API parameters from the dictionary + for (var s in result.data.subsystems) { + for (var m in result.data.subsystems[s]['measurements']) { + params.push(result.data.subsystems[s]['measurements'][m]['identifier']); + } + } + + $interval(pollApi, 1000); + }); + + return { + dictionary: function () { + return dictionary.promise; + }, + history: function (id) { + var defer = $q.defer(); + defer.resolve(histories[id] || {}); + return defer.promise; + }, + subscribe: function (id) { + params[id] = id; + }, + unsubscribe: function (id) { + delete params[id]; + }, + listen: function (callback) { + listeners.push(callback); + } + }; + } + + return KerbalTelemetryServerAdapter; + } +); \ No newline at end of file