diff --git a/API.md b/API.md new file mode 100644 index 0000000000..24083e1cf6 --- /dev/null +++ b/API.md @@ -0,0 +1,285 @@ +# Open MCT API + +The Open MCT framework public api can be utilized by building the application +(`gulp install`) and then copying the file from `dist/main.js` to your +directory of choice. + +Open MCT supports AMD, CommonJS, and loading via a script tag; it's easy to use +in your project. The [`openmct`]{@link module:openmct} module is exported +via AMD and CommonJS, and is also exposed as `openmct` in the global scope +if loaded via a script tag. + +## Overview + +Open MCT's goal is to allow you to browse, create, edit, and visualize all of +the domain knowledge you need on a daily basis. + +To do this, the main building block provided by Open MCT is the _domain object_. +The temperature sensor on the starboard solar panel, +an overlay plot comparing the results of all temperature sensor, +the command dictionary for a spacecraft, +the individual commands in that dictionary, your "my documents" folder: +All of these things are domain objects. + +Domain objects have Types, so a specific instrument temperature sensor is a +"Telemetry Point," and turning on a drill for a certain duration of time is +an "Activity". Types allow you to form an ontology of knowledge and provide +an abstraction for grouping, visualizing, and interpreting data. + +And then we have Views. Views allow you to visualize domain objects. Views can +apply to specific domain objects; they may also apply to certain types of +domain objects, or they may apply to everything. Views are simply a method +of visualizing domain objects. + +Regions allow you to specify what views are displayed for specific types of +domain objects in response to different user actions. For instance, you may +want to display a different view while editing, or you may want to update the +toolbar display when objects are selected. Regions allow you to map views to +specific user actions. + +Domain objects can be mutated and persisted, developers can create custom +actions and apply them to domain objects, and many more things can be done. +For more information, read on! + +## Running Open MCT + +Once the [`openmct`](@link module:openmct) module has been loaded, you can +simply invoke [`start`]{@link module:openmct.MCT#start} to run Open MCT: + + +``` +openmct.start(); +``` + +Generally, however, you will want to configure Open MCT by adding plugins +before starting it. It is important to install plugins and configure Open MCT +_before_ calling [`start`]{@link module:openmct.MCT#start}; Open MCT is not +designed to be reconfigured once started. + +## Configuring Open MCT + +The [`openmct`]{@link module:openmct} module (more specifically, the +[`MCT`]{@link module:openmct.MCT} class, of which `openmct` is an instance) +exposes a variety of methods to allow the application to be configured, +extended, and customized before running. + +Short examples follow; see the linked documentation for further details. + +### Adding Domain Object Types + +Custom types may be registered via +[`openmct.types`]{@link module:openmct.MCT#types}: + +``` +openmct.types.addType('my-type', new openmct.Type({ + label: "My Type", + description: "This is a type that I added!" +}); +``` + +### Adding Views + +Custom views may be registered based on the region in the application +where they should appear: + +* [`openmct.mainViews`]{@link module:openmct.MCT#mainViews} is a registry + of views of domain objects which should appear in the main viewing area. +* [`openmct.inspectors`]{@link module:openmct.MCT#inspectors} is a registry + of views of domain objects and/or active selections, which should appear in + the Inspector. +* [`openmct.toolbars`]{@link module:openmct.MCT#toolbars} is a registry + of views of domain objects and/or active selections, which should appear in + the toolbar area while editing. +* [`openmct.indicators`]{@link module:openmct.MCT#inspectors} is a registry + of views which should appear in the status area of the application. + +Example: + +``` +openmct.mainViews.addProvider({ + canView: function (domainObject) { + return domainObject.type === 'my-type'; + }, + view: function (domainObject) { + return new MyView(domainObject); + } +}); +``` + +### Adding a Root-level Object + +In many cases, you'd like a certain object (or a certain hierarchy of +objects) to be accessible from the top level of the application (the +tree on the left-hand side of Open MCT.) It is typical to expose a telemetry +dictionary as a hierarchy of telemetry-providing domain objects in this +fashion. + +To do so, use the [`addRoot`]{@link module:openmct.ObjectAPI#addRoot} method +of the [object API]{@link module:openmct.ObjectAPI}: + +``` +openmct.objects.addRoot({ + identifier: { key: "my-key", namespace: "my-namespace" } + name: "My Root-level Object", + type: "my-type" +}); +``` + +You can also remove this root-level object via its identifier: + +``` +openmct.objects.removeRoot({ key: "my-key", namespace: "my-namespace" }); +``` + +### Adding Composition Providers + +The "composition" of a domain object is the list of objects it contains, +as shown (for example) in the tree for browsing. Open MCT provides a +default solution for composition, but there may be cases where you want +to provide the composition of a certain object (or type of object) dynamically. +For instance, you may want to populate a hierarchy under a custom root-level +object based on the contents of a telemetry dictionary. +To do this, you can add a new CompositionProvider: + +``` +openmct.composition.addProvider({ + appliesTo: function (domainObject) { + return domainObject.type === 'my-type'; + }, + load: function (domainObject) { + return Promise.resolve(myDomainObjects); + } +}); +``` + +### Adding Telemetry Providers + +When connecting to a new telemetry source, you will want to register a new +[telemetry provider]{@link module:openmct.TelemetryAPI~TelemetryProvider} +with the [telemetry API]{@link module:openmct.TelemetryAPI#addProvider}: + +``` +openmct.telemetry.addProvider({ + canProvideTelemetry: function (domainObject) { + return domainObject.type === 'my-type'; + }, + properties: function (domainObject) { + return [ + { key: 'value', name: "Temperature", units: "degC" }, + { key: 'time', name: "UTC" } + ]; + }, + request: function (domainObject, options) { + var telemetryId = domainObject.myTelemetryId; + return myAdapter.request(telemetryId, options.start, options.end); + }, + subscribe: function (domainObject, callback) { + var telemetryId = domainObject.myTelemetryId; + myAdapter.subscribe(telemetryId, callback); + return myAdapter.unsubscribe.bind(myAdapter, telemetryId, callback); + } +}); +``` + +The implementations for `request` and `subscribe` can vary depending on the +nature of the endpoint which will provide telemetry. In the example above, +it is assumed that `myAdapter` contains the specific implementations +(HTTP requests, WebSocket connections, etc.) associated with some telemetry +source. + +## Using Open MCT + +When implementing new features, it is useful and sometimes necessary to +utilize functionality exposed by Open MCT. + +### Retrieving Composition + +To limit which objects are loaded at any given time, the composition of +a domain object must be requested asynchronously: + +``` +openmct.composition(myObject).load().then(function (childObjects) { + childObjects.forEach(doSomething); +}); +``` + +### Support Common Gestures + +Custom views may also want to support common gestures using the +[gesture API]{@link module:openmct.GestureAPI}. For instance, to make +a view (or part of a view) selectable: + +``` +openmct.gestures.selectable(myHtmlElement, myDomainObject); +``` + +### Working with Domain Objects + +The [object API]{@link module:openmct.ObjectAPI} provides useful methods +for working with domain objects. + +To make changes to a domain object, use the +[`mutate`]{@link module:openmct.ObjectAPI#mutate} method: + +``` +openmct.objects.mutate(myDomainObject, "name", "New name!"); +``` + +Making modifications in this fashion allows other usages of the domain +object to remain up to date using the +[`observe`]{@link module:openmct.ObjectAPI#observe} method: + +``` +openmct.objects.observe(myDomainObject, "name", function (newName) { + myLabel.textContent = newName; +}); +``` + +### Using Telemetry + +Very often in Open MCT, you wish to work with telemetry data (for instance, +to display it in a custom visualization.) + + +### Synchronizing with the Time Conductor + +Views which wish to remain synchronized with the state of Open MCT's +time conductor should utilize +[`openmct.conductor`]{@link module:openmct.TimeConductor}: + +``` +openmct.conductor.on('bounds', function (newBounds) { + requestTelemetry(newBounds.start, newBounds.end).then(displayTelemetry); +}); +``` + +## Plugins + +While you can register new features with Open MCT directly, it is generally +more useful to package these as a plugin. A plugin is a function that takes +[`openmct`]{@link module:openmct} as an argument, and performs configuration +upon `openmct` when invoked. + +### Installing Plugins + +To install plugins, use the [`install`]{@link module:openmct.MCT#install} +method: + +``` +openmct.install(myPlugin); +``` + +The plugin will be invoked to configure Open MCT before it is started. + +### Writing Plugins + +Plugins configure Open MCT, and should utilize the +[`openmct`]{@link module:openmct} module to do so, as summarized above in +"Configuring Open MCT" and "Using Open MCT" above. + +### Distributing Plugins + +Hosting or downloading plugins is outside of the scope of this documentation. +We recommend distributing plugins as UMD modules which export a single +function. + diff --git a/LICENSES.md b/LICENSES.md index 483a6e4072..94a7fecf13 100644 --- a/LICENSES.md +++ b/LICENSES.md @@ -560,3 +560,132 @@ The above copyright notice and this permission notice shall be included in all c THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +--- + +### Almond + +* Link: https://github.com/requirejs/almond + +* Version: 0.3.3 + +* Author: jQuery Foundation + +* Description: Lightweight RequireJS replacement for builds + +#### License + +Copyright jQuery Foundation and other contributors, https://jquery.org/ + +This software consists of voluntary contributions made by many +individuals. For exact contribution history, see the revision history +available at https://github.com/requirejs/almond + +The following license applies to all parts of this software except as +documented below: + +==== + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +==== + +Copyright and related rights for sample code are waived via CC0. Sample +code is defined as all source code displayed within the prose of the +documentation. + +CC0: http://creativecommons.org/publicdomain/zero/1.0/ + +==== + +Files located in the node_modules directory, and certain utilities used +to build or test the software in the test and dist directories, are +externally maintained libraries used by this software which have their own +licenses; we recommend you read them, as their terms may differ from the +terms above. + + +### Lodash + +* Link: https://lodash.com + +* Version: 3.10.1 + +* Author: Dojo Foundation + +* Description: Utility functions + +#### License + +Copyright 2012-2015 The Dojo Foundation +Based on Underscore.js, copyright 2009-2015 Jeremy Ashkenas, +DocumentCloud and Investigative Reporters & Editors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +### EventEmitter3 + +* Link: https://github.com/primus/eventemitter3 + +* Version: 1.2.0 + +* Author: Arnout Kazemier + +* Description: Event-driven programming support + +#### License + +The MIT License (MIT) + +Copyright (c) 2014 Arnout Kazemier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index b9486dad3c..8761314a0b 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,24 @@ Try Open MCT now with our [live demo](https://openmct-demo.herokuapp.com/). ![Demo](https://nasa.github.io/openmct/static/res/images/Open-MCT.Browse.Layout.Mars-Weather-1.jpg) ## New API -A new API is currently under development that will deprecate a lot of the documentation currently in the docs directory, however Open MCT will remain compatible with the currently documented API. An updated set of tutorials is being developed with the new API, and progress on this task can be followed in the [associated pull request](https://github.com/nasa/openmct/pull/999). Any code in this branch should be considered experimental, and we welcome any feedback. -Differences between the two APIs include a move away from a declarative system of JSON configuration files towards an imperative system based on function calls. Developers will be able to extend and build on Open MCT by making direct function calls to a public API. Open MCT is also being refactored to minimize the dependencies that using Open MCT imposes on developers, such as the current requirement to use Angular JS. +A simpler, [easier-to-use API](https://nasa.github.io/openmct/docs/api/) +has been added to Open MCT. Changes in this +API include a move away from a declarative system of JSON configuration files +towards an imperative system based on function calls. Developers will be able +to extend and build on Open MCT by making direct function calls to a public +API. Open MCT is also being refactored to minimize the dependencies that using +Open MCT imposes on developers, such as the current requirement to use +AngularJS. + +This new API has not yet been heavily used and is likely to contain defects. +You can help by trying it out, and reporting any issues you encounter +using our GitHub issue tracker. Such issues may include bugs, suggestions, +missing documentation, or even just requests for help if you're having +trouble. + +We want Open MCT to be as easy to use, install, run, and develop for as +possible, and your feedback will help us get there! ## Building and Running Open MCT Locally diff --git a/bower.json b/bower.json index 419871fe57..161ee04186 100644 --- a/bower.json +++ b/bower.json @@ -19,6 +19,10 @@ "comma-separated-values": "^3.6.4", "FileSaver.js": "^0.0.2", "zepto": "^1.1.6", + "eventemitter3": "^1.2.0", + "lodash": "3.10.1", + "almond": "~0.3.2", + "d3": "~4.1.0", "html2canvas": "^0.4.1" } } diff --git a/build-docs.sh b/build-docs.sh index 29841f921e..22e87949bf 100755 --- a/build-docs.sh +++ b/build-docs.sh @@ -24,7 +24,7 @@ # Script to build and deploy docs. -OUTPUT_DIRECTORY="target/docs" +OUTPUT_DIRECTORY="dist/docs" # Docs, once built, are pushed to the private website repo REPOSITORY_URL="git@github.com:nasa/openmct-website.git" WEBSITE_DIRECTORY="website" diff --git a/docs/src/index.md b/docs/src/index.md index 3b4d767106..1d523657f4 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -9,26 +9,29 @@ Open MCT provides functionality out of the box, but it's also a platform for building rich mission operations applications based on modern web technology. - The platform is configured declaratively, and defines conventions for - building on the provided capabilities by creating modular 'bundles' that - extend the platform at a variety of extension points. The details of how to + The platform is configured by plugins which extend the platform at a variety + of extension points. The details of how to extend the platform are provided in the following documentation. ## Sections - - * The [Architecture Overview](architecture/) describes the concepts used - throughout Open MCT, and gives a high level overview of the platform's design. - - * The [Developer's Guide](guide/) goes into more detail about how to use the - platform and the functionality that it provides. - - * The [Tutorials](tutorials/) give examples of extending the platform to add - functionality, - and integrate with data sources. * The [API](api/) document is generated from inline documentation using [JSDoc](http://usejsdoc.org/), and describes the JavaScript objects and functions that make up the software platform. - * Finally, the [Development Process](process/) document describes the + * The [Development Process](process/) document describes the Open MCT software development cycle. + +## Legacy Documentation + +As we transition to a new API, the following documentation for the old API +(which is supported during the transtion) may be useful as well: + + * The [Architecture Overview](architecture/) describes the concepts used + throughout Open MCT, and gives a high level overview of the platform's design. + + * The [Developer's Guide](guide/) goes into more detail about how to use the + platform and the functionality that it provides. + + * The [Tutorials](tutorials/) give examples of extending the platform to add + functionality, and integrate with data sources. diff --git a/example/localTimeSystem/bundle.js b/example/localTimeSystem/bundle.js new file mode 100644 index 0000000000..1d1ff2cf39 --- /dev/null +++ b/example/localTimeSystem/bundle.js @@ -0,0 +1,48 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web 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 Web 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([ + "./src/LocalTimeSystem", + "./src/LocalTimeFormat", + 'legacyRegistry' +], function ( + LocalTimeSystem, + LocalTimeFormat, + legacyRegistry +) { + legacyRegistry.register("example/localTimeSystem", { + "extensions": { + "formats": [ + { + "key": "local-format", + "implementation": LocalTimeFormat + } + ], + "timeSystems": [ + { + "implementation": LocalTimeSystem, + "depends": ["$timeout"] + } + ] + } + }); +}); diff --git a/example/localTimeSystem/src/LADTickSource.js b/example/localTimeSystem/src/LADTickSource.js new file mode 100644 index 0000000000..35fff32f54 --- /dev/null +++ b/example/localTimeSystem/src/LADTickSource.js @@ -0,0 +1,43 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web 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 Web 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(['../../../platform/features/conductor-v2/conductor/src/timeSystems/LocalClock'], function (LocalClock) { + /** + * @implements TickSource + * @constructor + */ + function LADTickSource ($timeout, period) { + LocalClock.call(this, $timeout, period); + + this.metadata = { + key: 'test-lad', + mode: 'lad', + cssclass: 'icon-clock', + label: 'Latest Available Data', + name: 'Latest available data', + description: 'Monitor real-time streaming data as it comes in. The Time Conductor and displays will automatically advance themselves based on a UTC clock.' + }; + } + LADTickSource.prototype = Object.create(LocalClock.prototype); + + return LADTickSource; +}); diff --git a/example/localTimeSystem/src/LocalTimeFormat.js b/example/localTimeSystem/src/LocalTimeFormat.js new file mode 100644 index 0000000000..a9b26ed4ef --- /dev/null +++ b/example/localTimeSystem/src/LocalTimeFormat.js @@ -0,0 +1,112 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web 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 Web 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([ + 'moment' +], function ( + moment +) { + + var DATE_FORMAT = "YYYY-MM-DD h:mm:ss.SSS a", + DATE_FORMATS = [ + DATE_FORMAT, + "YYYY-MM-DD h:mm:ss a", + "YYYY-MM-DD h:mm a", + "YYYY-MM-DD" + ]; + + /** + * @typedef Scale + * @property {number} min the minimum scale value, in ms + * @property {number} max the maximum scale value, in ms + */ + + /** + * Formatter for UTC timestamps. Interprets numeric values as + * milliseconds since the start of 1970. + * + * @implements {Format} + * @constructor + * @memberof platform/commonUI/formats + */ + function LocalTimeFormat() { + } + + /** + * Returns an appropriate time format based on the provided value and + * the threshold required. + * @private + */ + function getScaledFormat (d) { + var m = moment.utc(d); + /** + * Uses logic from d3 Time-Scales, v3 of the API. See + * https://github.com/d3/d3-3.x-api-reference/blob/master/Time-Scales.md + * + * Licensed + */ + return [ + [".SSS", function(m) { return m.milliseconds(); }], + [":ss", function(m) { return m.seconds(); }], + ["hh:mma", function(m) { return m.minutes(); }], + ["hha", function(m) { return m.hours(); }], + ["ddd DD", function(m) { + return m.days() && + m.date() != 1; + }], + ["MMM DD", function(m) { return m.date() != 1; }], + ["MMMM", function(m) { + return m.month(); + }], + ["YYYY", function() { return true; }] + ].filter(function (row){ + return row[1](m); + })[0][0]; + }; + + /** + * + * @param value + * @param {Scale} [scale] Optionally provides context to the + * format request, allowing for scale-appropriate formatting. + * @returns {string} the formatted date + */ + LocalTimeFormat.prototype.format = function (value, scale) { + if (scale !== undefined){ + var scaledFormat = getScaledFormat(value, scale); + if (scaledFormat) { + return moment.utc(value).format(scaledFormat); + } + } + return moment(value).format(DATE_FORMAT); + }; + + LocalTimeFormat.prototype.parse = function (text) { + return moment(text, DATE_FORMATS).valueOf(); + }; + + LocalTimeFormat.prototype.validate = function (text) { + return moment(text, DATE_FORMATS).isValid(); + }; + + return LocalTimeFormat; +}); diff --git a/example/localTimeSystem/src/LocalTimeSystem.js b/example/localTimeSystem/src/LocalTimeSystem.js new file mode 100644 index 0000000000..95485b7e69 --- /dev/null +++ b/example/localTimeSystem/src/LocalTimeSystem.js @@ -0,0 +1,79 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web 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 Web 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([ + '../../../platform/features/conductor-v2/conductor/src/timeSystems/TimeSystem', + '../../../platform/features/conductor-v2/conductor/src/timeSystems/LocalClock', + './LADTickSource' +], function (TimeSystem, LocalClock, LADTickSource) { + var THIRTY_MINUTES = 30 * 60 * 1000, + DEFAULT_PERIOD = 1000; + + /** + * This time system supports UTC dates and provides a ticking clock source. + * @implements TimeSystem + * @constructor + */ + function LocalTimeSystem ($timeout) { + TimeSystem.call(this); + + /** + * Some metadata, which will be used to identify the time system in + * the UI + * @type {{key: string, name: string, glyph: string}} + */ + this.metadata = { + 'key': 'local', + 'name': 'Local', + 'glyph': '\u0043' + }; + + this.fmts = ['local-format']; + this.sources = [new LocalClock($timeout, DEFAULT_PERIOD), new LADTickSource($timeout, DEFAULT_PERIOD)]; + } + + LocalTimeSystem.prototype = Object.create(TimeSystem.prototype); + + LocalTimeSystem.prototype.formats = function () { + return this.fmts; + }; + + LocalTimeSystem.prototype.deltaFormat = function () { + return 'duration'; + }; + + LocalTimeSystem.prototype.tickSources = function () { + return this.sources; + }; + + LocalTimeSystem.prototype.defaults = function (key) { + var now = Math.ceil(Date.now() / 1000) * 1000; + return { + key: 'local-default', + name: 'Local 12 hour time system defaults', + deltas: {start: THIRTY_MINUTES, end: 0}, + bounds: {start: now - THIRTY_MINUTES, end: now} + }; + }; + + return LocalTimeSystem; +}); diff --git a/example/msl/bundle.js b/example/msl/bundle.js index aefc3ffc16..fbd0843935 100644 --- a/example/msl/bundle.js +++ b/example/msl/bundle.js @@ -36,7 +36,7 @@ define([ legacyRegistry ) { "use strict"; - legacyRegistry.register("example/notifications", { + legacyRegistry.register("example/msl-adapter", { "name" : "Mars Science Laboratory Data Adapter", "extensions" : { "types": [ diff --git a/gulpfile.js b/gulpfile.js index ff994c6a2c..e9a5d0fcf0 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -21,41 +21,34 @@ *****************************************************************************/ /*global require,__dirname*/ + var gulp = require('gulp'), - requirejsOptimize = require('gulp-requirejs-optimize'), sourcemaps = require('gulp-sourcemaps'), - rename = require('gulp-rename'), - sass = require('gulp-sass'), - bourbon = require('node-bourbon'), - jshint = require('gulp-jshint'), - jscs = require('gulp-jscs'), - replace = require('gulp-replace-task'), - karma = require('karma'), path = require('path'), fs = require('fs'), git = require('git-rev-sync'), moment = require('moment'), - merge = require('merge-stream'), project = require('./package.json'), _ = require('lodash'), paths = { - main: 'main.js', + main: 'openmct.js', dist: 'dist', - assets: 'dist/assets', reports: 'dist/reports', scss: ['./platform/**/*.scss', './example/**/*.scss'], - scripts: [ 'main.js', 'platform/**/*.js', 'src/**/*.js' ], + assets: [ + './{example,platform}/**/*.{css,css.map,png,svg,ico,woff,eot,ttf}' + ], + scripts: [ 'openmct.js', 'platform/**/*.js', 'src/**/*.js' ], specs: [ 'platform/**/*Spec.js', 'src/**/*Spec.js' ], - static: [ - 'index.html', - 'platform/**/*', - 'example/**/*', - 'bower_components/**/*' - ] }, options = { requirejsOptimize: { - name: paths.main.replace(/\.js$/, ''), + name: 'bower_components/almond/almond.js', + include: paths.main.replace('.js', ''), + wrap: { + startFile: "src/start.frag", + endFile: "src/end.frag" + }, mainConfigFile: paths.main, wrapShim: true }, @@ -64,7 +57,6 @@ var gulp = require('gulp'), singleRun: true }, sass: { - includePaths: bourbon.includePaths, sourceComments: true }, replace: { @@ -78,6 +70,8 @@ var gulp = require('gulp'), }; gulp.task('scripts', function () { + var requirejsOptimize = require('gulp-requirejs-optimize'); + var replace = require('gulp-replace-task'); return gulp.src(paths.main) .pipe(sourcemaps.init()) .pipe(requirejsOptimize(options.requirejsOptimize)) @@ -87,10 +81,16 @@ gulp.task('scripts', function () { }); gulp.task('test', function (done) { + var karma = require('karma'); new karma.Server(options.karma, done).start(); }); gulp.task('stylesheets', function () { + var sass = require('gulp-sass'); + var rename = require('gulp-rename'); + var bourbon = require('node-bourbon'); + options.sass.includePaths = bourbon.includePaths; + return gulp.src(paths.scss, {base: '.'}) .pipe(sourcemaps.init()) .pipe(sass(options.sass).on('error', sass.logError)) @@ -104,6 +104,9 @@ gulp.task('stylesheets', function () { }); gulp.task('lint', function () { + var jshint = require('gulp-jshint'); + var merge = require('merge-stream'); + var nonspecs = paths.specs.map(function (glob) { return "!" + glob; }), @@ -122,6 +125,8 @@ gulp.task('lint', function () { }); gulp.task('checkstyle', function () { + var jscs = require('gulp-jscs'); + return gulp.src(paths.scripts) .pipe(jscs()) .pipe(jscs.reporter()) @@ -129,18 +134,20 @@ gulp.task('checkstyle', function () { }); gulp.task('fixstyle', function () { + var jscs = require('gulp-jscs'); + return gulp.src(paths.scripts, { base: '.' }) .pipe(jscs({ fix: true })) .pipe(gulp.dest('.')); }); -gulp.task('static', ['stylesheets'], function () { - return gulp.src(paths.static, { base: '.' }) +gulp.task('assets', ['stylesheets'], function () { + return gulp.src(paths.assets) .pipe(gulp.dest(paths.dist)); }); gulp.task('watch', function () { - gulp.watch(paths.scss, ['stylesheets']); + return gulp.watch(paths.scss, ['stylesheets', 'assets']); }); gulp.task('serve', function () { @@ -148,9 +155,9 @@ gulp.task('serve', function () { var app = require('./app.js'); }); -gulp.task('develop', ['serve', 'stylesheets', 'watch']); +gulp.task('develop', ['serve', 'install', 'watch']); -gulp.task('install', [ 'static', 'scripts' ]); +gulp.task('install', [ 'assets', 'scripts' ]); gulp.task('verify', [ 'lint', 'test', 'checkstyle' ]); diff --git a/index.html b/index.html index fcd37cfb0b..aa5d79cfdc 100644 --- a/index.html +++ b/index.html @@ -28,12 +28,15 @@ @@ -47,7 +50,5 @@
- -
diff --git a/jsdoc.json b/jsdoc.json index f913b650d1..ac485a5efa 100644 --- a/jsdoc.json +++ b/jsdoc.json @@ -1,9 +1,9 @@ { "source": { "include": [ - "platform/" + "src/" ], - "includePattern": "platform/.+\\.js$", + "includePattern": "src/.+\\.js$", "excludePattern": ".+\\Spec\\.js$|lib/.+" }, "plugins": [ diff --git a/karma.conf.js b/karma.conf.js index e682ec6d86..535c27b2a5 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -37,9 +37,11 @@ module.exports = function(config) { {pattern: 'bower_components/**/*.js', included: false}, {pattern: 'src/**/*.js', included: false}, {pattern: 'example/**/*.js', included: false}, + {pattern: 'example/**/*.json', included: false}, {pattern: 'platform/**/*.js', included: false}, {pattern: 'warp/**/*.js', included: false}, {pattern: 'platform/**/*.html', included: false}, + {pattern: 'src/**/*.html', included: false}, 'test-main.js' ], diff --git a/main.js b/openmct.js similarity index 60% rename from main.js rename to openmct.js index 6968d8ff26..68151008cd 100644 --- a/main.js +++ b/openmct.js @@ -27,6 +27,7 @@ requirejs.config({ "angular": "bower_components/angular/angular.min", "angular-route": "bower_components/angular-route/angular-route.min", "csv": "bower_components/comma-separated-values/csv.min", + "EventEmitter": "bower_components/eventemitter3/index", "es6-promise": "bower_components/es6-promise/es6-promise.min", "html2canvas": "bower_components/html2canvas/build/html2canvas.min", "moment": "bower_components/moment/moment", @@ -35,7 +36,9 @@ requirejs.config({ "screenfull": "bower_components/screenfull/dist/screenfull.min", "text": "bower_components/text/text", "uuid": "bower_components/node-uuid/uuid", - "zepto": "bower_components/zepto/zepto.min" + "zepto": "bower_components/zepto/zepto.min", + "lodash": "bower_components/lodash/lodash", + "d3": "bower_components/d3/d3.min" }, "shim": { "angular": { @@ -44,6 +47,9 @@ requirejs.config({ "angular-route": { "deps": ["angular"] }, + "EventEmitter": { + "exports": "EventEmitter" + }, "html2canvas": { "exports": "html2canvas" }, @@ -55,54 +61,28 @@ requirejs.config({ }, "zepto": { "exports": "Zepto" + }, + "lodash": { + "exports": "lodash" + }, + "d3": { + "exports": "d3" } } }); define([ './platform/framework/src/Main', - 'legacyRegistry', + './src/defaultRegistry', + './src/MCT' +], function (Main, defaultRegistry, MCT) { + var openmct = new MCT(); - './platform/framework/bundle', - './platform/core/bundle', - './platform/representation/bundle', - './platform/commonUI/about/bundle', - './platform/commonUI/browse/bundle', - './platform/commonUI/edit/bundle', - './platform/commonUI/dialog/bundle', - './platform/commonUI/formats/bundle', - './platform/commonUI/general/bundle', - './platform/commonUI/inspect/bundle', - './platform/commonUI/mobile/bundle', - './platform/commonUI/themes/espresso/bundle', - './platform/commonUI/notification/bundle', - './platform/containment/bundle', - './platform/execution/bundle', - './platform/exporters/bundle', - './platform/telemetry/bundle', - './platform/features/clock/bundle', - './platform/features/fixed/bundle', - './platform/features/imagery/bundle', - './platform/features/layout/bundle', - './platform/features/pages/bundle', - './platform/features/plot/bundle', - './platform/features/timeline/bundle', - './platform/features/table/bundle', - './platform/forms/bundle', - './platform/identity/bundle', - './platform/persistence/aggregator/bundle', - './platform/persistence/local/bundle', - './platform/persistence/queue/bundle', - './platform/policy/bundle', - './platform/entanglement/bundle', - './platform/search/bundle', - './platform/status/bundle', - './platform/commonUI/regions/bundle' -], function (Main, legacyRegistry) { - return { - legacyRegistry: legacyRegistry, - run: function () { - return new Main().run(legacyRegistry); - } - }; + openmct.legacyRegistry = defaultRegistry; + + openmct.on('start', function () { + return new Main().run(defaultRegistry); + }); + + return openmct; }); diff --git a/package.json b/package.json index e47a64e50e..9ac04c5189 100644 --- a/package.json +++ b/package.json @@ -48,8 +48,8 @@ "test": "karma start --single-run", "jshint": "jshint platform example", "watch": "karma start", - "jsdoc": "jsdoc -c jsdoc.json -r -d target/docs/api", - "otherdoc": "node docs/gendocs.js --in docs/src --out target/docs --suppress-toc 'docs/src/index.md|docs/src/process/index.md'", + "jsdoc": "jsdoc -c jsdoc.json -R API.md -r -d dist/docs/api", + "otherdoc": "node docs/gendocs.js --in docs/src --out dist/docs --suppress-toc 'docs/src/index.md|docs/src/process/index.md'", "docs": "npm run jsdoc ; npm run otherdoc", "prepublish": "node ./node_modules/bower/bin/bower install && node ./node_modules/gulp/bin/gulp.js install" }, diff --git a/platform/commonUI/browse/bundle.js b/platform/commonUI/browse/bundle.js index 5ff13d789e..b0aa7e4277 100644 --- a/platform/commonUI/browse/bundle.js +++ b/platform/commonUI/browse/bundle.js @@ -41,6 +41,7 @@ define([ "text!./res/templates/items/items.html", "text!./res/templates/browse/object-properties.html", "text!./res/templates/browse/inspector-region.html", + "text!./res/templates/view-object.html", 'legacyRegistry' ], function ( BrowseController, @@ -63,6 +64,7 @@ define([ itemsTemplate, objectPropertiesTemplate, inspectorRegionTemplate, + viewObjectTemplate, legacyRegistry ) { @@ -142,7 +144,7 @@ define([ "representations": [ { "key": "view-object", - "templateUrl": "templates/view-object.html" + "template": viewObjectTemplate }, { "key": "browse-object", diff --git a/platform/commonUI/browse/res/templates/browse-object.html b/platform/commonUI/browse/res/templates/browse-object.html index eb32a15648..f431003e47 100644 --- a/platform/commonUI/browse/res/templates/browse-object.html +++ b/platform/commonUI/browse/res/templates/browse-object.html @@ -43,7 +43,7 @@ -
+
@@ -59,4 +59,5 @@
+
diff --git a/platform/commonUI/browse/res/templates/browse/object-header.html b/platform/commonUI/browse/res/templates/browse/object-header.html index 48a64796da..62391caac5 100644 --- a/platform/commonUI/browse/res/templates/browse/object-header.html +++ b/platform/commonUI/browse/res/templates/browse/object-header.html @@ -22,7 +22,8 @@ {{parameters.mode}} - {{model.name}} + {{model.name}} + @@ -42,11 +42,12 @@ - - {{currentAction.getMetadata().name}} - + + \ No newline at end of file diff --git a/platform/commonUI/edit/res/templates/edit-object.html b/platform/commonUI/edit/res/templates/edit-object.html index 04e978c0f5..f93f3cd60f 100644 --- a/platform/commonUI/edit/res/templates/edit-object.html +++ b/platform/commonUI/edit/res/templates/edit-object.html @@ -66,4 +66,9 @@
+ + + diff --git a/platform/commonUI/edit/src/actions/EditAndComposeAction.js b/platform/commonUI/edit/src/actions/EditAndComposeAction.js index a6dc32b0d9..f1dc5e47d3 100644 --- a/platform/commonUI/edit/src/actions/EditAndComposeAction.js +++ b/platform/commonUI/edit/src/actions/EditAndComposeAction.js @@ -40,19 +40,11 @@ define( var self = this, editAction = this.domainObject.getCapability('action').getActions("edit")[0]; - // Persist changes to the domain object - function doPersist() { - var persistence = - self.domainObject.getCapability('persistence'); - return persistence.persist(); - } - // Link these objects function doLink() { var composition = self.domainObject && self.domainObject.getCapability('composition'); - return composition && composition.add(self.selectedObject) - .then(doPersist); + return composition && composition.add(self.selectedObject); } if (editAction) { diff --git a/platform/commonUI/edit/src/actions/PropertiesAction.js b/platform/commonUI/edit/src/actions/PropertiesAction.js index 43ebe215ad..e17e0053f1 100644 --- a/platform/commonUI/edit/src/actions/PropertiesAction.js +++ b/platform/commonUI/edit/src/actions/PropertiesAction.js @@ -50,12 +50,6 @@ define( domainObject = this.domainObject, dialogService = this.dialogService; - // Persist modifications to this domain object - function doPersist() { - var persistence = domainObject.getCapability('persistence'); - return persistence && persistence.persist(); - } - // Update the domain object model based on user input function updateModel(userInput, dialog) { return domainObject.useCapability('mutation', function (model) { @@ -73,11 +67,9 @@ define( dialog.getFormStructure(), dialog.getInitialFormValue() ).then(function (userInput) { - // Update the model, if user input was provided - return userInput && updateModel(userInput, dialog); - }).then(function (result) { - return result && doPersist(); - }); + // Update the model, if user input was provided + return userInput && updateModel(userInput, dialog); + }); } return type && showDialog(type); @@ -94,9 +86,7 @@ define( creatable = type && type.hasFeature('creation'); // Only allow creatable types to be edited - return domainObject && - domainObject.hasCapability("persistence") && - creatable; + return domainObject && creatable; }; return PropertiesAction; diff --git a/platform/commonUI/edit/src/actions/RemoveAction.js b/platform/commonUI/edit/src/actions/RemoveAction.js index ab460f522d..604d4f0f0b 100644 --- a/platform/commonUI/edit/src/actions/RemoveAction.js +++ b/platform/commonUI/edit/src/actions/RemoveAction.js @@ -39,9 +39,8 @@ define( * @constructor * @implements {Action} */ - function RemoveAction($q, navigationService, context) { + function RemoveAction(navigationService, context) { this.domainObject = (context || {}).domainObject; - this.$q = $q; this.navigationService = navigationService; } @@ -51,8 +50,7 @@ define( * fulfilled when the action has completed. */ RemoveAction.prototype.perform = function () { - var $q = this.$q, - navigationService = this.navigationService, + var navigationService = this.navigationService, domainObject = this.domainObject; /* * Check whether an object ID matches the ID of the object being @@ -71,15 +69,6 @@ define( model.composition = model.composition.filter(isNotObject); } - /* - * Invoke persistence on a domain object. This will be called upon - * the removed object's parent (as its composition will have changed.) - */ - function doPersist(domainObj) { - var persistence = domainObj.getCapability('persistence'); - return persistence && persistence.persist(); - } - /* * Checks current object and ascendants of current * object with object being removed, if the current @@ -119,15 +108,10 @@ define( // navigates to existing object up tree checkObjectNavigation(object, parent); - return $q.when( - parent.useCapability('mutation', doMutate) - ).then(function () { - return doPersist(parent); - }); + return parent.useCapability('mutation', doMutate); } - return $q.when(domainObject) - .then(removeFromContext); + return removeFromContext(domainObject); }; // Object needs to have a parent for Remove to be applicable diff --git a/platform/commonUI/edit/src/capabilities/TransactionalPersistenceCapability.js b/platform/commonUI/edit/src/capabilities/TransactionalPersistenceCapability.js index bcda009c56..f9467326c0 100644 --- a/platform/commonUI/edit/src/capabilities/TransactionalPersistenceCapability.js +++ b/platform/commonUI/edit/src/capabilities/TransactionalPersistenceCapability.js @@ -81,6 +81,10 @@ define( return this.persistenceCapability.getSpace(); }; + TransactionalPersistenceCapability.prototype.persisted = function () { + return this.persistenceCapability.persisted(); + }; + return TransactionalPersistenceCapability; } ); diff --git a/platform/commonUI/edit/src/controllers/EditActionController.js b/platform/commonUI/edit/src/controllers/EditActionController.js index 7c33799ebb..ba91b63e9d 100644 --- a/platform/commonUI/edit/src/controllers/EditActionController.js +++ b/platform/commonUI/edit/src/controllers/EditActionController.js @@ -61,6 +61,12 @@ define( $scope.otherEditActions = $scope.action ? $scope.action.getActions(OTHERS_ACTION_CONTEXT) : []; + + // Required because Angular does not allow 'bind' + // in expressions. + $scope.actionPerformer = function (action) { + return action.perform.bind(action); + }; } // Update set of actions whenever the action capability diff --git a/platform/commonUI/edit/src/representers/EditRepresenter.js b/platform/commonUI/edit/src/representers/EditRepresenter.js index 33613f8a17..81252977b1 100644 --- a/platform/commonUI/edit/src/representers/EditRepresenter.js +++ b/platform/commonUI/edit/src/representers/EditRepresenter.js @@ -50,17 +50,13 @@ define( this.listenHandle = undefined; // Mutate and persist a new version of a domain object's model. - function doPersist(model) { + function doMutate(model) { var domainObject = self.domainObject; // First, mutate; then, persist. return $q.when(domainObject.useCapability("mutation", function () { return model; - })).then(function (result) { - // Only persist when mutation was successful - return result && - domainObject.getCapability("persistence").persist(); - }); + })); } // Handle changes to model and/or view configuration @@ -80,14 +76,14 @@ define( ].join(" ")); // Update the configuration stored in the model, and persist. - if (domainObject && domainObject.hasCapability("persistence")) { + if (domainObject) { // Configurations for specific views are stored by // key in the "configuration" field of the model. if (self.key && configuration) { model.configuration = model.configuration || {}; model.configuration[self.key] = configuration; } - doPersist(model); + doMutate(model); } } diff --git a/platform/commonUI/edit/src/services/NestedTransaction.js b/platform/commonUI/edit/src/services/NestedTransaction.js new file mode 100644 index 0000000000..c7fcee4d5e --- /dev/null +++ b/platform/commonUI/edit/src/services/NestedTransaction.js @@ -0,0 +1,48 @@ +/***************************************************************************** + * 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(['./Transaction'], function (Transaction) { + /** + * A nested transaction is a transaction which takes place in the context + * of a larger parent transaction. It becomes part of the parent + * transaction when (and only when) committed. + * @param parent + * @constructor + * @extends {platform/commonUI/edit/services.Transaction} + * @memberof platform/commonUI/edit/services + */ + function NestedTransaction(parent) { + this.parent = parent; + Transaction.call(this, parent.$log); + } + + NestedTransaction.prototype = Object.create(Transaction.prototype); + + NestedTransaction.prototype.commit = function () { + this.parent.add( + Transaction.prototype.commit.bind(this), + Transaction.prototype.cancel.bind(this) + ); + return Promise.resolve(true); + }; + + return NestedTransaction; +}); diff --git a/platform/commonUI/edit/src/services/Transaction.js b/platform/commonUI/edit/src/services/Transaction.js new file mode 100644 index 0000000000..803536be4d --- /dev/null +++ b/platform/commonUI/edit/src/services/Transaction.js @@ -0,0 +1,96 @@ +/***************************************************************************** + * 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([], function () { + /** + * A Transaction represents a set of changes that are intended to + * be kept or discarded as a unit. + * @param $log Angular's `$log` service, for logging messages + * @constructor + * @memberof platform/commonUI/edit/services + */ + function Transaction($log) { + this.$log = $log; + this.callbacks = []; + } + + /** + * Add a change to the current transaction, as expressed by functions + * to either keep or discard the change. + * @param {Function} commit called when the transaction is committed + * @param {Function} cancel called when the transaction is cancelled + * @returns {Function) a function which may be called to remove this + * pair of callbacks from the transaction + */ + Transaction.prototype.add = function (commit, cancel) { + var callback = { commit: commit, cancel: cancel }; + this.callbacks.push(callback); + return function () { + this.callbacks = this.callbacks.filter(function (c) { + return c !== callback; + }); + }.bind(this); + }; + + /** + * Get the number of changes in the current transaction. + * @returns {number} the size of the current transaction + */ + Transaction.prototype.size = function () { + return this.callbacks.length; + }; + + /** + * Keep all changes associated with this transaction. + * @method {platform/commonUI/edit/services.Transaction#commit} + * @returns {Promise} a promise which will resolve when all callbacks + * have been handled. + */ + + /** + * Discard all changes associated with this transaction. + * @method {platform/commonUI/edit/services.Transaction#cancel} + * @returns {Promise} a promise which will resolve when all callbacks + * have been handled. + */ + + ['commit', 'cancel'].forEach(function (method) { + Transaction.prototype[method] = function () { + var promises = []; + var callback; + + while (this.callbacks.length > 0) { + callback = this.callbacks.shift(); + try { + promises.push(callback[method]()); + } catch (e) { + this.$log + .error("Error trying to " + method + " transaction."); + } + } + + return Promise.all(promises); + }; + }); + + + return Transaction; +}); diff --git a/platform/commonUI/edit/src/services/TransactionService.js b/platform/commonUI/edit/src/services/TransactionService.js index df0a051f0d..3c234ca882 100644 --- a/platform/commonUI/edit/src/services/TransactionService.js +++ b/platform/commonUI/edit/src/services/TransactionService.js @@ -21,8 +21,8 @@ *****************************************************************************/ /*global define*/ define( - [], - function () { + ['./Transaction', './NestedTransaction'], + function (Transaction, NestedTransaction) { /** * Implements an application-wide transaction state. Once a * transaction is started, calls to @@ -37,10 +37,7 @@ define( function TransactionService($q, $log) { this.$q = $q; this.$log = $log; - this.transaction = false; - - this.onCommits = []; - this.onCancels = []; + this.transactions = []; } /** @@ -50,18 +47,18 @@ define( * #cancel} are called */ TransactionService.prototype.startTransaction = function () { - if (this.transaction) { - //Log error because this is a programming error if it occurs. - this.$log.error("Transaction already in progress"); - } - this.transaction = true; + var transaction = this.isActive() ? + new NestedTransaction(this.transactions[0]) : + new Transaction(this.$log); + + this.transactions.push(transaction); }; /** * @returns {boolean} If true, indicates that a transaction is in progress */ TransactionService.prototype.isActive = function () { - return this.transaction; + return this.transactions.length > 0; }; /** @@ -72,24 +69,20 @@ define( * @param onCancel A function to call on cancel */ TransactionService.prototype.addToTransaction = function (onCommit, onCancel) { - if (this.transaction) { - this.onCommits.push(onCommit); - if (onCancel) { - this.onCancels.push(onCancel); - } + if (this.isActive()) { + return this.activeTransaction().add(onCommit, onCancel); } else { //Log error because this is a programming error if it occurs. this.$log.error("No transaction in progress"); } + }; - return function () { - this.onCommits = this.onCommits.filter(function (callback) { - return callback !== onCommit; - }); - this.onCancels = this.onCancels.filter(function (callback) { - return callback !== onCancel; - }); - }.bind(this); + /** + * Get the transaction at the top of the stack. + * @private + */ + TransactionService.prototype.activeTransaction = function () { + return this.transactions[this.transactions.length - 1]; }; /** @@ -100,24 +93,8 @@ define( * completed. Will reject if any commit operations fail */ TransactionService.prototype.commit = function () { - var self = this, - promises = [], - onCommit; - - while (this.onCommits.length > 0) { // ...using a while in case some onCommit adds to transaction - onCommit = this.onCommits.pop(); - try { // ...also don't want to fail mid-loop... - promises.push(onCommit()); - } catch (e) { - this.$log.error("Error committing transaction."); - } - } - return this.$q.all(promises).then(function () { - self.transaction = false; - - self.onCommits = []; - self.onCancels = []; - }); + var transaction = this.transactions.pop(); + return transaction ? transaction.commit() : Promise.reject(); }; /** @@ -129,28 +106,17 @@ define( * @returns {*} */ TransactionService.prototype.cancel = function () { - var self = this, - results = [], - onCancel; - - while (this.onCancels.length > 0) { - onCancel = this.onCancels.pop(); - try { - results.push(onCancel()); - } catch (error) { - this.$log.error("Error cancelling transaction."); - } - } - return this.$q.all(results).then(function () { - self.transaction = false; - - self.onCommits = []; - self.onCancels = []; - }); + var transaction = this.transactions.pop(); + return transaction ? transaction.cancel() : Promise.reject(); }; + /** + * Get the size (the number of commit/cancel callbacks) of + * the active transaction. + * @returns {number} size of the active transaction + */ TransactionService.prototype.size = function () { - return this.onCommits.length; + return this.isActive() ? this.activeTransaction().size() : 0; }; return TransactionService; diff --git a/platform/commonUI/edit/test/actions/EditAndComposeActionSpec.js b/platform/commonUI/edit/test/actions/EditAndComposeActionSpec.js index 25ec4567a6..0010a92735 100644 --- a/platform/commonUI/edit/test/actions/EditAndComposeActionSpec.js +++ b/platform/commonUI/edit/test/actions/EditAndComposeActionSpec.js @@ -30,7 +30,6 @@ define( mockParent, mockContext, mockComposition, - mockPersistence, mockActionCapability, mockEditAction, mockType, @@ -68,7 +67,6 @@ define( }; mockContext = jasmine.createSpyObj("context", ["getParent"]); mockComposition = jasmine.createSpyObj("composition", ["invoke", "add"]); - mockPersistence = jasmine.createSpyObj("persistence", ["persist"]); mockType = jasmine.createSpyObj("type", ["hasFeature", "getKey"]); mockActionCapability = jasmine.createSpyObj("actionCapability", ["getActions"]); mockEditAction = jasmine.createSpyObj("editAction", ["perform"]); @@ -84,7 +82,6 @@ define( capabilities = { composition: mockComposition, - persistence: mockPersistence, action: mockActionCapability, type: mockType }; @@ -107,11 +104,6 @@ define( .toHaveBeenCalledWith(mockDomainObject); }); - it("persists changes afterward", function () { - action.perform(); - expect(mockPersistence.persist).toHaveBeenCalled(); - }); - it("enables edit mode for objects that have an edit action", function () { mockActionCapability.getActions.andReturn([mockEditAction]); action.perform(); diff --git a/platform/commonUI/edit/test/actions/PropertiesActionSpec.js b/platform/commonUI/edit/test/actions/PropertiesActionSpec.js index 8123a506f4..20e32ab015 100644 --- a/platform/commonUI/edit/test/actions/PropertiesActionSpec.js +++ b/platform/commonUI/edit/test/actions/PropertiesActionSpec.js @@ -43,7 +43,6 @@ define( }, hasFeature: jasmine.createSpy('hasFeature') }, - persistence: jasmine.createSpyObj("persistence", ["persist"]), mutation: jasmine.createSpy("mutation") }; model = {}; @@ -78,25 +77,18 @@ define( action = new PropertiesAction(dialogService, context); }); - it("persists when an action is performed", function () { - action.perform(); - expect(capabilities.persistence.persist) - .toHaveBeenCalled(); - }); - - it("does not persist any changes upon cancel", function () { - input = undefined; - action.perform(); - expect(capabilities.persistence.persist) - .not.toHaveBeenCalled(); - }); - it("mutates an object when performed", function () { action.perform(); expect(capabilities.mutation).toHaveBeenCalled(); capabilities.mutation.mostRecentCall.args[0]({}); }); + it("does not muate object upon cancel", function () { + input = undefined; + action.perform(); + expect(capabilities.mutation).not.toHaveBeenCalled(); + }); + it("is only applicable when a domain object is in context", function () { expect(PropertiesAction.appliesTo(context)).toBeTruthy(); expect(PropertiesAction.appliesTo({})).toBeFalsy(); diff --git a/platform/commonUI/edit/test/actions/RemoveActionSpec.js b/platform/commonUI/edit/test/actions/RemoveActionSpec.js index 7830ca0293..c5755b3636 100644 --- a/platform/commonUI/edit/test/actions/RemoveActionSpec.js +++ b/platform/commonUI/edit/test/actions/RemoveActionSpec.js @@ -37,7 +37,6 @@ define( mockGrandchildContext, mockRootContext, mockMutation, - mockPersistence, mockType, actionContext, model, @@ -53,8 +52,6 @@ define( } beforeEach(function () { - - mockDomainObject = jasmine.createSpyObj( "domainObject", ["getId", "getCapability"] @@ -88,7 +85,6 @@ define( mockGrandchildContext = jasmine.createSpyObj("context", ["getParent"]); mockRootContext = jasmine.createSpyObj("context", ["getParent"]); mockMutation = jasmine.createSpyObj("mutation", ["invoke"]); - mockPersistence = jasmine.createSpyObj("persistence", ["persist"]); mockType = jasmine.createSpyObj("type", ["hasFeature"]); mockNavigationService = jasmine.createSpyObj( "navigationService", @@ -109,7 +105,6 @@ define( capabilities = { mutation: mockMutation, - persistence: mockPersistence, type: mockType }; model = { @@ -118,7 +113,7 @@ define( actionContext = { domainObject: mockDomainObject }; - action = new RemoveAction(mockQ, mockNavigationService, actionContext); + action = new RemoveAction(mockNavigationService, actionContext); }); it("only applies to objects with parents", function () { @@ -154,9 +149,6 @@ define( // Should have removed "test" - that was our // mock domain object's id. expect(result.composition).toEqual(["a", "b"]); - - // Finally, should have persisted - expect(mockPersistence.persist).toHaveBeenCalled(); }); it("removes parent of object currently navigated to", function () { diff --git a/platform/commonUI/edit/test/representers/EditRepresenterSpec.js b/platform/commonUI/edit/test/representers/EditRepresenterSpec.js index 7af3740fd6..cb766f95a7 100644 --- a/platform/commonUI/edit/test/representers/EditRepresenterSpec.js +++ b/platform/commonUI/edit/test/representers/EditRepresenterSpec.js @@ -30,7 +30,6 @@ define( mockScope, testRepresentation, mockDomainObject, - mockPersistence, mockStatusCapability, mockEditorCapability, mockCapabilities, @@ -56,15 +55,12 @@ define( "useCapability", "hasCapability" ]); - mockPersistence = - jasmine.createSpyObj("persistence", ["persist"]); mockStatusCapability = jasmine.createSpyObj("statusCapability", ["listen"]); mockEditorCapability = jasmine.createSpyObj("editorCapability", ["isEditContextRoot"]); mockCapabilities = { - 'persistence': mockPersistence, 'status': mockStatusCapability, 'editor': mockEditorCapability }; @@ -96,7 +92,7 @@ define( expect(representer.listenHandle).toHaveBeenCalled(); }); - it("mutates and persists upon observed changes", function () { + it("mutates upon observed changes", function () { mockScope.model = { someKey: "some value" }; mockScope.configuration = { someConfiguration: "something" }; @@ -108,9 +104,6 @@ define( jasmine.any(Function) ); - // ... and should have persisted the mutation - expect(mockPersistence.persist).toHaveBeenCalled(); - // Finally, check that the provided mutation function // includes both model and configuration expect( diff --git a/platform/commonUI/edit/test/services/NestedTransactionSpec.js b/platform/commonUI/edit/test/services/NestedTransactionSpec.js new file mode 100644 index 0000000000..df82c12a78 --- /dev/null +++ b/platform/commonUI/edit/test/services/NestedTransactionSpec.js @@ -0,0 +1,78 @@ +/***************************************************************************** + * 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. + *****************************************************************************/ +/*global define,describe,it,expect,beforeEach,jasmine*/ + +define(["../../src/services/NestedTransaction"], function (NestedTransaction) { + var TRANSACTION_METHODS = ['add', 'commit', 'cancel', 'size']; + + describe("A NestedTransaction", function () { + var mockTransaction, + nestedTransaction; + + beforeEach(function () { + mockTransaction = + jasmine.createSpyObj('transaction', TRANSACTION_METHODS); + nestedTransaction = new NestedTransaction(mockTransaction); + }); + + it("exposes a Transaction's interface", function () { + TRANSACTION_METHODS.forEach(function (method) { + expect(nestedTransaction[method]) + .toEqual(jasmine.any(Function)); + }); + }); + + describe("when callbacks are added", function () { + var mockCommit, + mockCancel, + remove; + + beforeEach(function () { + mockCommit = jasmine.createSpy('commit'); + mockCancel = jasmine.createSpy('cancel'); + remove = nestedTransaction.add(mockCommit, mockCancel); + }); + + it("does not interact with its parent transaction", function () { + TRANSACTION_METHODS.forEach(function (method) { + expect(mockTransaction[method]) + .not.toHaveBeenCalled(); + }); + }); + + describe("and the transaction is committed", function () { + beforeEach(function () { + nestedTransaction.commit(); + }); + + it("adds to its parent transaction", function () { + expect(mockTransaction.add).toHaveBeenCalledWith( + jasmine.any(Function), + jasmine.any(Function) + ); + }); + }); + }); + }); +}); + + diff --git a/platform/commonUI/edit/test/services/TransactionServiceSpec.js b/platform/commonUI/edit/test/services/TransactionServiceSpec.js index 8c4d635a6f..f05fb9df3d 100644 --- a/platform/commonUI/edit/test/services/TransactionServiceSpec.js +++ b/platform/commonUI/edit/test/services/TransactionServiceSpec.js @@ -57,8 +57,7 @@ define( transactionService.startTransaction(); transactionService.addToTransaction(onCommit, onCancel); - expect(transactionService.onCommits.length).toBe(1); - expect(transactionService.onCancels.length).toBe(1); + expect(transactionService.size()).toBe(1); }); it("size function returns size of commit and cancel queues", function () { @@ -85,7 +84,7 @@ define( }); it("commit calls all queued commit functions", function () { - expect(transactionService.onCommits.length).toBe(3); + expect(transactionService.size()).toBe(3); transactionService.commit(); onCommits.forEach(function (spy) { expect(spy).toHaveBeenCalled(); @@ -95,8 +94,8 @@ define( it("commit resets active state and clears queues", function () { transactionService.commit(); expect(transactionService.isActive()).toBe(false); - expect(transactionService.onCommits.length).toBe(0); - expect(transactionService.onCancels.length).toBe(0); + expect(transactionService.size()).toBe(0); + expect(transactionService.size()).toBe(0); }); }); @@ -116,7 +115,7 @@ define( }); it("cancel calls all queued cancel functions", function () { - expect(transactionService.onCancels.length).toBe(3); + expect(transactionService.size()).toBe(3); transactionService.cancel(); onCancels.forEach(function (spy) { expect(spy).toHaveBeenCalled(); @@ -126,8 +125,7 @@ define( it("cancel resets active state and clears queues", function () { transactionService.cancel(); expect(transactionService.isActive()).toBe(false); - expect(transactionService.onCommits.length).toBe(0); - expect(transactionService.onCancels.length).toBe(0); + expect(transactionService.size()).toBe(0); }); }); diff --git a/platform/commonUI/edit/test/services/TransactionSpec.js b/platform/commonUI/edit/test/services/TransactionSpec.js new file mode 100644 index 0000000000..3b555f3d05 --- /dev/null +++ b/platform/commonUI/edit/test/services/TransactionSpec.js @@ -0,0 +1,110 @@ +/***************************************************************************** + * 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. + *****************************************************************************/ +/*global define,describe,it,expect,beforeEach,jasmine*/ + +define( + ["../../src/services/Transaction"], + function (Transaction) { + + describe("A Transaction", function () { + var mockLog, + transaction; + + beforeEach(function () { + mockLog = jasmine.createSpyObj( + '$log', + ['warn', 'info', 'error', 'debug'] + ); + transaction = new Transaction(mockLog); + }); + + it("initially has a size of zero", function () { + expect(transaction.size()).toEqual(0); + }); + + describe("when callbacks are added", function () { + var mockCommit, + mockCancel, + remove; + + beforeEach(function () { + mockCommit = jasmine.createSpy('commit'); + mockCancel = jasmine.createSpy('cancel'); + remove = transaction.add(mockCommit, mockCancel); + }); + + it("reports a new size", function () { + expect(transaction.size()).toEqual(1); + }); + + it("returns a function to remove those callbacks", function () { + expect(remove).toEqual(jasmine.any(Function)); + remove(); + expect(transaction.size()).toEqual(0); + }); + + describe("and the transaction is committed", function () { + beforeEach(function () { + transaction.commit(); + }); + + it("triggers the commit callback", function () { + expect(mockCommit).toHaveBeenCalled(); + }); + + it("does not trigger the cancel callback", function () { + expect(mockCancel).not.toHaveBeenCalled(); + }); + }); + + describe("and the transaction is cancelled", function () { + beforeEach(function () { + transaction.cancel(); + }); + + it("triggers the cancel callback", function () { + expect(mockCancel).toHaveBeenCalled(); + }); + + it("does not trigger the commit callback", function () { + expect(mockCommit).not.toHaveBeenCalled(); + }); + }); + + describe("and an exception is encountered during commit", function () { + beforeEach(function () { + mockCommit.andCallFake(function () { + throw new Error("test error"); + }); + transaction.commit(); + }); + + it("logs an error", function () { + expect(mockLog.error).toHaveBeenCalled(); + }); + }); + }); + + }); + } +); + diff --git a/platform/commonUI/formats/bundle.js b/platform/commonUI/formats/bundle.js index 040c7f57fb..c685151149 100644 --- a/platform/commonUI/formats/bundle.js +++ b/platform/commonUI/formats/bundle.js @@ -23,10 +23,12 @@ define([ "./src/FormatProvider", "./src/UTCTimeFormat", + "./src/DurationFormat", 'legacyRegistry' ], function ( FormatProvider, UTCTimeFormat, + DurationFormat, legacyRegistry ) { @@ -48,6 +50,10 @@ define([ { "key": "utc", "implementation": UTCTimeFormat + }, + { + "key": "duration", + "implementation": DurationFormat } ], "constants": [ @@ -55,6 +61,17 @@ define([ "key": "DEFAULT_TIME_FORMAT", "value": "utc" } + ], + "licenses": [ + { + "name": "d3", + "version": "3.0.0", + "description": "Incorporates modified code from d3 Time Scales", + "author": "Mike Bostock", + "copyright": "Copyright 2010-2016 Mike Bostock. " + + "All rights reserved.", + "link": "https://github.com/d3/d3/blob/master/LICENSE" + } ] } }); diff --git a/platform/commonUI/formats/src/DurationFormat.js b/platform/commonUI/formats/src/DurationFormat.js new file mode 100644 index 0000000000..12ce3b3c6b --- /dev/null +++ b/platform/commonUI/formats/src/DurationFormat.js @@ -0,0 +1,62 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web 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 Web 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([ + 'moment' +], function ( + moment +) { + + var DATE_FORMAT = "HH:mm:ss", + DATE_FORMATS = [ + DATE_FORMAT + ]; + + + /** + * Formatter for duration. Uses moment to produce a date from a given + * value, but output is formatted to display only time. Can be used for + * specifying a time duration. For specifying duration, it's best to + * specify a date of January 1, 1970, as the ms offset will equal the + * duration represented by the time. + * + * @implements {Format} + * @constructor + * @memberof platform/commonUI/formats + */ + function DurationFormat() { + } + + DurationFormat.prototype.format = function (value) { + return moment.utc(value).format(DATE_FORMAT); + }; + + DurationFormat.prototype.parse = function (text) { + return moment.duration(text).asMilliseconds(); + }; + + DurationFormat.prototype.validate = function (text) { + return moment.utc(text, DATE_FORMATS).isValid(); + }; + + return DurationFormat; +}); diff --git a/platform/commonUI/formats/src/FormatProvider.js b/platform/commonUI/formats/src/FormatProvider.js index f8f126001d..4700260fd8 100644 --- a/platform/commonUI/formats/src/FormatProvider.js +++ b/platform/commonUI/formats/src/FormatProvider.js @@ -58,6 +58,10 @@ define([ * @method format * @memberof Format# * @param {number} value the numeric value to format + * @param {number} [threshold] Optionally provides context to the + * format request, allowing for scale-appropriate formatting. This value + * should be the minimum unit to be represented by this format, in ms. For + * example, to display seconds, a threshold of 1 * 1000 should be provided. * @returns {string} the text representation of the value */ diff --git a/platform/commonUI/formats/src/UTCTimeFormat.js b/platform/commonUI/formats/src/UTCTimeFormat.js index 8c4277f9b8..3faab7a620 100644 --- a/platform/commonUI/formats/src/UTCTimeFormat.js +++ b/platform/commonUI/formats/src/UTCTimeFormat.js @@ -34,6 +34,11 @@ define([ "YYYY-MM-DD" ]; + /** + * @typedef Scale + * @property {number} min the minimum scale value, in ms + * @property {number} max the maximum scale value, in ms + */ /** * Formatter for UTC timestamps. Interprets numeric values as @@ -46,7 +51,64 @@ define([ function UTCTimeFormat() { } - UTCTimeFormat.prototype.format = function (value) { + /** + * Returns an appropriate time format based on the provided value and + * the threshold required. + * @private + */ + function getScaledFormat(d) { + var momentified = moment.utc(d); + /** + * Uses logic from d3 Time-Scales, v3 of the API. See + * https://github.com/d3/d3-3.x-api-reference/blob/master/Time-Scales.md + * + * Licensed + */ + return [ + [".SSS", function (m) { + return m.milliseconds(); + }], + [":ss", function (m) { + return m.seconds(); + }], + ["HH:mm", function (m) { + return m.minutes(); + }], + ["HH", function (m) { + return m.hours(); + }], + ["ddd DD", function (m) { + return m.days() && + m.date() !== 1; + }], + ["MMM DD", function (m) { + return m.date() !== 1; + }], + ["MMMM", function (m) { + return m.month(); + }], + ["YYYY", function () { + return true; + }] + ].filter(function (row) { + return row[1](momentified); + })[0][0]; + } + + /** + * + * @param value + * @param {Scale} [scale] Optionally provides context to the + * format request, allowing for scale-appropriate formatting. + * @returns {string} the formatted date + */ + UTCTimeFormat.prototype.format = function (value, scale) { + if (scale !== undefined) { + var scaledFormat = getScaledFormat(value, scale); + if (scaledFormat) { + return moment.utc(value).format(scaledFormat); + } + } return moment.utc(value).format(DATE_FORMAT) + "Z"; }; diff --git a/platform/commonUI/formats/src/UTCTimeFormatSpec.js b/platform/commonUI/formats/src/UTCTimeFormatSpec.js new file mode 100644 index 0000000000..c4111709a3 --- /dev/null +++ b/platform/commonUI/formats/src/UTCTimeFormatSpec.js @@ -0,0 +1,62 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web 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 Web 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([ + "./UTCTimeFormat", + "moment" +], function ( + UTCTimeFormat, + moment +) { + describe("The UTCTimeFormat class", function () { + var format; + var scale; + + beforeEach(function () { + format = new UTCTimeFormat(); + scale = {min: 0, max: 0}; + }); + + it("Provides an appropriately scaled time format based on the input" + + " time", function () { + var TWO_HUNDRED_MS = 200; + var THREE_SECONDS = 3000; + var FIVE_MINUTES = 5 * 60 * 1000; + var ONE_HOUR_TWENTY_MINS = (1 * 60 * 60 * 1000) + (20 * 60 * 1000); + var TEN_HOURS = (10 * 60 * 60 * 1000); + + var JUNE_THIRD = moment.utc("2016-06-03", "YYYY-MM-DD"); + var APRIL = moment.utc("2016-04", "YYYY-MM"); + var TWENTY_SIXTEEN = moment.utc("2016", "YYYY"); + + expect(format.format(TWO_HUNDRED_MS, scale)).toBe(".200"); + expect(format.format(THREE_SECONDS, scale)).toBe(":03"); + expect(format.format(FIVE_MINUTES, scale)).toBe("00:05"); + expect(format.format(ONE_HOUR_TWENTY_MINS, scale)).toBe("01:20"); + expect(format.format(TEN_HOURS, scale)).toBe("10"); + + expect(format.format(JUNE_THIRD, scale)).toBe("Fri 03"); + expect(format.format(APRIL, scale)).toBe("April"); + expect(format.format(TWENTY_SIXTEEN, scale)).toBe("2016"); + }); + }); +}); diff --git a/platform/commonUI/general/bundle.js b/platform/commonUI/general/bundle.js index 15ea3076a6..66d9b3de95 100644 --- a/platform/commonUI/general/bundle.js +++ b/platform/commonUI/general/bundle.js @@ -48,6 +48,7 @@ define([ "./src/directives/MCTSplitPane", "./src/directives/MCTSplitter", "./src/directives/MCTTree", + "./src/filters/ReverseFilter", "text!./res/templates/bottombar.html", "text!./res/templates/controls/action-button.html", "text!./res/templates/controls/input-filter.html", @@ -96,6 +97,7 @@ define([ MCTSplitPane, MCTSplitter, MCTTree, + ReverseFilter, bottombarTemplate, actionButtonTemplate, inputFilterTemplate, @@ -146,7 +148,8 @@ define([ "depends": [ "stylesheets[]", "$document", - "THEME" + "THEME", + "ASSETS_PATH" ] }, { @@ -158,7 +161,7 @@ define([ ], "filters": [ { - "implementation": "filters/ReverseFilter.js", + "implementation": ReverseFilter, "key": "reverse" } ], @@ -405,6 +408,11 @@ define([ "key": "THEME", "value": "unspecified", "priority": "fallback" + }, + { + "key": "ASSETS_PATH", + "value": ".", + "priority": "fallback" } ], "containers": [ diff --git a/platform/commonUI/general/res/sass/_animations.scss b/platform/commonUI/general/res/sass/_animations.scss new file mode 100644 index 0000000000..fb1c0f1bfb --- /dev/null +++ b/platform/commonUI/general/res/sass/_animations.scss @@ -0,0 +1,91 @@ +@include keyframes(rotation) { + 100% { @include transform(rotate(360deg)); } +} + +@include keyframes(rotation-centered) { + 0% { @include transform(translate(-50%, -50%) rotate(0deg)); } + 100% { @include transform(translate(-50%, -50%) rotate(360deg)); } +} + +@include keyframes(clock-hands) { + 0% { @include transform(translate(-50%, -50%) rotate(0deg)); } + 100% { @include transform(translate(-50%, -50%) rotate(360deg)); } +} + +@include keyframes(clock-hands-sticky) { + 0% { + @include transform(translate(-50%, -50%) rotate(0deg)); + } + 7% { + @include transform(translate(-50%, -50%) rotate(0deg)); + } + 8% { + @include transform(translate(-50%, -50%) rotate(30deg)); + } + 15% { + @include transform(translate(-50%, -50%) rotate(30deg)); + } + 16% { + @include transform(translate(-50%, -50%) rotate(60deg)); + } + 24% { + @include transform(translate(-50%, -50%) rotate(60deg)); + } + 25% { + @include transform(translate(-50%, -50%) rotate(90deg)); + } + 32% { + @include transform(translate(-50%, -50%) rotate(90deg)); + } + 33% { + @include transform(translate(-50%, -50%) rotate(120deg)); + } + 40% { + @include transform(translate(-50%, -50%) rotate(120deg)); + } + 41% { + @include transform(translate(-50%, -50%) rotate(150deg)); + } + 49% { + @include transform(translate(-50%, -50%) rotate(150deg)); + } + 50% { + @include transform(translate(-50%, -50%) rotate(180deg)); + } + 57% { + @include transform(translate(-50%, -50%) rotate(180deg)); + } + 58% { + @include transform(translate(-50%, -50%) rotate(210deg)); + } + 65% { + @include transform(translate(-50%, -50%) rotate(210deg)); + } + 66% { + @include transform(translate(-50%, -50%) rotate(240deg)); + } + 74% { + @include transform(translate(-50%, -50%) rotate(240deg)); + } + 75% { + @include transform(translate(-50%, -50%) rotate(270deg)); + } + 82% { + @include transform(translate(-50%, -50%) rotate(270deg)); + } + 83% { + @include transform(translate(-50%, -50%) rotate(300deg)); + } + 90% { + @include transform(translate(-50%, -50%) rotate(300deg)); + } + 91% { + @include transform(translate(-50%, -50%) rotate(330deg)); + } + 99% { + @include transform(translate(-50%, -50%) rotate(330deg)); + } + 100% { + @include transform(translate(-50%, -50%) rotate(360deg)); + } +} \ No newline at end of file diff --git a/platform/commonUI/general/res/sass/_archetypes.scss b/platform/commonUI/general/res/sass/_archetypes.scss index d51944e2f4..206f126fde 100644 --- a/platform/commonUI/general/res/sass/_archetypes.scss +++ b/platform/commonUI/general/res/sass/_archetypes.scss @@ -108,6 +108,9 @@ &.grows { @include flex(1 1 auto); } + &.contents-align-right { + text-align: right; + } } .flex-container { // Apply to wrapping elements, mct-includes, etc. diff --git a/platform/commonUI/general/res/sass/_constants.scss b/platform/commonUI/general/res/sass/_constants.scss index 1e609cc038..efa87c6a08 100644 --- a/platform/commonUI/general/res/sass/_constants.scss +++ b/platform/commonUI/general/res/sass/_constants.scss @@ -49,7 +49,6 @@ $uePaneMiniTabFontSize: 8px; $uePaneMiniTabCollapsedW: 18px; $ueEditLeftPaneW: 75%; $treeSearchInputBarH: 25px; -$ueTimeControlH: (33px, 18px, 20px); /*************** Panes */ $ueBrowseLeftPaneTreeMinW: 150px; $ueBrowseLeftPaneTreeMaxW: 35%; @@ -112,6 +111,7 @@ $bubbleMaxW: 300px; $reqSymbolW: 15px; $reqSymbolM: $interiorMargin * 2; $reqSymbolFontSize: 0.75em; +$inputTextP: 3px 5px; /*************** Wait Spinner Defaults */ $waitSpinnerD: 32px; $waitSpinnerTreeD: 20px; diff --git a/platform/commonUI/general/res/sass/_data-status.scss b/platform/commonUI/general/res/sass/_data-status.scss index 24a15db794..d058885cf1 100644 --- a/platform/commonUI/general/res/sass/_data-status.scss +++ b/platform/commonUI/general/res/sass/_data-status.scss @@ -4,4 +4,3 @@ @include s-stale(); } } - diff --git a/platform/commonUI/general/res/sass/_effects.scss b/platform/commonUI/general/res/sass/_effects.scss index 22a5443294..acdda74b55 100644 --- a/platform/commonUI/general/res/sass/_effects.scss +++ b/platform/commonUI/general/res/sass/_effects.scss @@ -39,20 +39,20 @@ @include pulse($animName: pulse-subtle, $dur: 500ms, $opacity0: 0.7); } -@mixin animTo($animName, $propName, $propValStart, $propValEnd, $dur: 500ms, $delay: 0) { +@mixin animTo($animName, $propName, $propValStart, $propValEnd, $dur: 500ms, $delay: 0, $dir: normal, $count: 1) { @include keyframes($animName) { from { #{propName}: $propValStart; } to { #{$propName}: $propValEnd; } } - @include animToParams($animName, $dur: 500ms, $delay: 0) + @include animToParams($animName, $dur: $dur, $delay: $delay, $dir: $dir, $count: $count) } -@mixin animToParams($animName, $dur: 500ms, $delay: 0) { +@mixin animToParams($animName, $dur: 500ms, $delay: 0, $dir: normal, $count: 1) { @include animation-name($animName); @include animation-duration($dur); @include animation-delay($delay); @include animation-fill-mode(both); - @include animation-direction(normal); - @include animation-iteration-count(1); + @include animation-direction($dir); + @include animation-iteration-count($count); @include animation-timing-function(ease-in-out); } \ No newline at end of file diff --git a/platform/commonUI/general/res/sass/_global.scss b/platform/commonUI/general/res/sass/_global.scss index 9d3627f3aa..964d609c4f 100644 --- a/platform/commonUI/general/res/sass/_global.scss +++ b/platform/commonUI/general/res/sass/_global.scss @@ -82,7 +82,7 @@ input, textarea { input[type="text"], input[type="search"] { vertical-align: baseline; - padding: 3px 5px; + padding: $inputTextP; } h1, h2, h3 { diff --git a/platform/commonUI/general/res/sass/_icons.scss b/platform/commonUI/general/res/sass/_icons.scss index a8990d4fe8..d15ec1d362 100644 --- a/platform/commonUI/general/res/sass/_icons.scss +++ b/platform/commonUI/general/res/sass/_icons.scss @@ -30,6 +30,7 @@ .ui-symbol { font-family: 'symbolsfont'; + -webkit-font-smoothing: antialiased; } .ui-symbol.icon { @@ -70,10 +71,21 @@ line-height: inherit; position: relative; &.l-icon-link { - .t-item-icon-glyph { + &:after { + color: $colorIconLink; + content: $glyph-icon-link; + height: auto; width: auto; + position: absolute; + left: 0; top: 0; right: 0; bottom: 20%; + @include transform-origin(bottom left); + @include transform(scale(0.3)); + z-index: 2; + } + +/* .t-item-icon-glyph { &:after { color: $colorIconLink; - content: $glyph-icon-link; + content: '\e921'; //$glyph-icon-link; height: auto; width: auto; position: absolute; left: 0; top: 0; right: 0; bottom: 20%; @@ -81,6 +93,6 @@ @include transform(scale(0.3)); z-index: 2; } - } + }*/ } } diff --git a/platform/commonUI/general/res/sass/_main.scss b/platform/commonUI/general/res/sass/_main.scss index e092f1474a..3ecb2304ab 100644 --- a/platform/commonUI/general/res/sass/_main.scss +++ b/platform/commonUI/general/res/sass/_main.scss @@ -22,6 +22,7 @@ @import "effects"; @import "global"; @import "glyphs"; +@import "animations"; @import "archetypes"; @import "about"; @import "text"; @@ -41,7 +42,6 @@ @import "controls/lists"; @import "controls/menus"; @import "controls/messages"; -@import "controls/time-controller"; @import "mobile/controls/menus"; /********************************* FORMS */ diff --git a/platform/commonUI/general/res/sass/_mixins.scss b/platform/commonUI/general/res/sass/_mixins.scss index 318bfff495..91720348b9 100644 --- a/platform/commonUI/general/res/sass/_mixins.scss +++ b/platform/commonUI/general/res/sass/_mixins.scss @@ -185,21 +185,15 @@ } @mixin sliderTrack($bg: $scrollbarTrackColorBg) { - //$b: 1px solid lighten($bg, 30%); border-radius: 2px; box-sizing: border-box; @include boxIncised(0.7); background-color: $bg; - //border-bottom: $b; - //border-right: $b; } @mixin controlGrippy($b, $direction: horizontal, $w: 1px, $style: dotted) { - //&:before { - //@include trans-prop-nice("border-color", 25ms); content: ''; display: block; - //height: auto; pointer-events: none; position: absolute; z-index: 2; @@ -274,16 +268,6 @@ text-shadow: rgba(black, $sVal) 0 3px 7px; } -@function pullForward($c, $p: 20%) { - // For dark interfaces, lighter things come forward - @return lighten($c, $p); -} - -@function pushBack($c, $p: 20%) { - // For dark interfaces, darker things move back - @return darken($c, $p); -} - @function percentToDecimal($p) { @return $p / 100%; } @@ -304,7 +288,6 @@ border-radius: $controlCr; box-sizing: border-box; color: $fg; - //display: inline-block; } @mixin btnBase($bg: $colorBtnBg, $bgHov: $colorBtnBgHov, $fg: $colorBtnFg, $fgHov: $colorBtnFgHov, $ic: $colorBtnIcon, $icHov: $colorBtnIconHov) { diff --git a/platform/commonUI/general/res/sass/controls/_controls.scss b/platform/commonUI/general/res/sass/controls/_controls.scss index d1935d8ce0..ad32ea0515 100644 --- a/platform/commonUI/general/res/sass/controls/_controls.scss +++ b/platform/commonUI/general/res/sass/controls/_controls.scss @@ -296,8 +296,6 @@ input[type="search"] { .title-label { color: $colorObjHdrTxt; @include ellipsize(); - @include webkitProp(flex, '0 1 auto'); - padding-right: 0.35em; // For context arrow. Done with em's so pad is relative to the scale of the text. } .context-available-w { @@ -308,6 +306,10 @@ input[type="search"] { font-size: 0.7em; @include flex(0 0 1); } + + .t-object-alert { + display: none; + } } /******************************************************** PROGRESS BAR */ @@ -441,6 +443,63 @@ input[type="search"] { } } +@mixin sliderKnob() { + $h: 16px; + cursor: pointer; + width: floor($h/1.75); + height: $h; + margin-top: 1 + floor($h/2) * -1; + @include btnSubtle(pullForward($colorBtnBg, 10%)); + //border-radius: 50% !important; +} + +@mixin sliderKnobRound() { + $h: 12px; + cursor: pointer; + width: $h; + height: $h; + margin-top: 1 + floor($h/2) * -1; + @include btnSubtle(pullForward($colorBtnBg, 10%)); + border-radius: 50% !important; +} + +input[type="range"] { + // HTML5 range inputs + + -webkit-appearance: none; /* Hides the slider so that custom slider can be made */ + background: transparent; /* Otherwise white in Chrome */ + &:focus { + outline: none; /* Removes the blue border. */ + } + + // Thumb + &::-webkit-slider-thumb { + -webkit-appearance: none; + @include sliderKnobRound(); + } + &::-moz-range-thumb { + border: none; + @include sliderKnobRound(); + } + &::-ms-thumb { + border: none; + @include sliderKnobRound(); + } + + // Track + &::-webkit-slider-runnable-track { + width: 100%; + height: 3px; + @include sliderTrack(); + } + + &::-moz-range-track { + width: 100%; + height: 3px; + @include sliderTrack(); + } +} + /******************************************************** DATETIME PICKER */ .l-datetime-picker { $r1H: 15px; diff --git a/platform/commonUI/general/res/sass/controls/_menus.scss b/platform/commonUI/general/res/sass/controls/_menus.scss index 61e87b5b2b..def9c3d3bb 100644 --- a/platform/commonUI/general/res/sass/controls/_menus.scss +++ b/platform/commonUI/general/res/sass/controls/_menus.scss @@ -178,7 +178,7 @@ } .pane { box-sizing: border-box; - &.left { + &.menu-items { border-right: 1px solid pullForward($colorMenuBg, 10%); left: 0; padding-right: $interiorMargin; @@ -194,38 +194,53 @@ } } } - &.right { + &.menu-item-description { left: auto; right: 0; padding: $interiorMargin * 5; width: $prw; + .desc-area { + &.icon { + color: $colorCreateMenuLgIcon; + font-size: 8em; + margin-bottom: $interiorMargin * 3; + position: relative; + text-align: center; + } + &.title { + color: $colorCreateMenuText; + font-size: 1.2em; + margin-bottom: $interiorMargin * 2; + } + &.description { + color: pushBack($colorCreateMenuText, 20%); + font-size: 0.8em; + line-height: 1.5em; + } + } } } - .menu-item-description { - .desc-area { - &.icon { - $h: 150px; - color: $colorCreateMenuLgIcon; - position: relative; - font-size: 8em; - left: 0; - height: $h; - line-height: $h; - margin-bottom: $interiorMargin * 5; - text-align: center; - } - &.title { - color: $colorCreateMenuText; - font-size: 1.2em; - margin-bottom: 0.5em; - } - &.description { - color: $colorCreateMenuText; - font-size: 0.8em; - line-height: 1.5em; - } - } - } + + &.mini { + width: 400px; + height: 300px; + .pane { + &.menu-items { + font-size: 0.8em; + } + &.menu-item-description { + padding: $interiorMargin * 3; + .desc-area { + &.icon { + font-size: 4em; + } + &.title { + font-size: 1em; + } + } + } + } + } } .context-menu { font-size: 0.80rem; @@ -262,3 +277,7 @@ right: 0; width: auto; } + +.menus-up .menu { + bottom: $btnStdH; top: auto; +} diff --git a/platform/commonUI/general/res/sass/controls/_messages.scss b/platform/commonUI/general/res/sass/controls/_messages.scss index 2604d498bd..485202c252 100644 --- a/platform/commonUI/general/res/sass/controls/_messages.scss +++ b/platform/commonUI/general/res/sass/controls/_messages.scss @@ -345,3 +345,29 @@ body.desktop .t-message-single { body.desktop .t-message-list { .message-contents .l-message { margin-right: $interiorMarginLg; } } + +// Alert elements in views +.s-unsynced { + $c: $colorPausedBg; + border: 1px solid $c; + @include animTo($animName: pulsePaused, $propName: border-color, $propValStart: rgba($c, 0.8), $propValEnd: rgba($c, 0.5), $dur: $animPausedPulseDur, $dir: alternate, $count: infinite); +} + +.s-status-timeconductor-unsynced { + // Plot areas + .gl-plot .gl-plot-display-area { + @extend .s-unsynced; + } + + // Object headers + .object-header { + .t-object-alert { + display: inline; + &.t-alert-unsynced { + @extend .icon-alert-triangle; + color: $colorPausedBg; + } + } + } +} + diff --git a/platform/commonUI/general/res/sass/controls/_time-controller.scss b/platform/commonUI/general/res/sass/controls/_time-controller.scss deleted file mode 100644 index ba2cf5b3ee..0000000000 --- a/platform/commonUI/general/res/sass/controls/_time-controller.scss +++ /dev/null @@ -1,266 +0,0 @@ -@mixin toiLineHovEffects() { - &:before, - &:after { - background-color: $timeControllerToiLineColorHov; - } -} - -.l-time-controller { - $minW: 500px; - $knobHOffset: 0px; - $knobM: ($sliderKnobW + $knobHOffset) * -1; - $rangeValPad: $interiorMargin; - $rangeValOffset: $sliderKnobW + $interiorMargin; - $timeRangeSliderLROffset: 150px + ($sliderKnobW * 2); - $r1H: nth($ueTimeControlH,1); // Not currently used - $r2H: nth($ueTimeControlH,2); - $r3H: nth($ueTimeControlH,3); - - min-width: $minW; - font-size: 0.8rem; - - .l-time-range-inputs-holder, - .l-time-range-slider-holder, - .l-time-range-ticks-holder - { - box-sizing: border-box; - position: relative; - &:not(:first-child) { - margin-top: $interiorMargin; - } - } - .l-time-range-slider, - .l-time-range-ticks { - @include absPosDefault(0, visible); - left: $timeRangeSliderLROffset; right: $timeRangeSliderLROffset; - } - - .l-time-range-inputs-holder { - border-top: 1px solid $colorInteriorBorder; - padding-top: $interiorMargin; - &.l-flex-row, - .l-flex-row { - @include align-items(center); - .flex-elem { - height: auto; - line-height: normal; - } - } - .type-icon { - font-size: 120%; - vertical-align: middle; - } - .l-time-range-input-w, - .l-time-range-inputs-elem { - margin-right: $interiorMargin; - .lbl { - color: $colorPlotLabelFg; - } - .ui-symbol.icon { - font-size: 11px; - } - } - .l-time-range-input-w { - // Wraps a datetime text input field - position: relative; - input[type="text"] { - width: 200px; - &.picker-icon { - padding-right: 20px; - } - } - .icon-calendar { - position: absolute; - right: 5px; - top: 5px; - } - } - } - - .l-time-range-slider-holder { - height: $r2H; - .range-holder { - box-shadow: none; - background: none; - border: none; - .range { - .toi-line { - $myC: $timeControllerToiLineColor; - $myW: 8px; - @include transform(translateX(50%)); - position: absolute; - top: 0; right: 0; bottom: 0px; left: auto; - width: $myW; - height: auto; - z-index: 2; - &:before { - // Vert line - background-color: $myC; - position: absolute; - content: ""; - top: 0; right: auto; bottom: -10px; left: floor($myW/2) - 1; - width: 1px; - } - } - &:hover .toi-line { - @include toiLineHovEffects; - } - } - } - &:not(:active) { - .knob, - .range { - @include transition-property(left, right); - @include transition-duration(500ms); - @include transition-timing-function(ease-in-out); - } - } - } - - .l-time-range-ticks-holder { - height: $r3H; - .l-time-range-ticks { - border-top: 1px solid $colorTick; - .tick { - background-color: $colorTick; - border:none; - height: 5px; - width: 1px; - margin-left: -1px; - position: absolute; - &:first-child { - margin-left: 0; - } - .l-time-range-tick-label { - @include webkitProp(transform, translateX(-50%)); - color: $colorPlotLabelFg; - display: inline-block; - font-size: 0.7rem; - position: absolute; - top: 5px; - white-space: nowrap; - z-index: 2; - } - } - } - } - - .knob { - z-index: 2; - &:before { - $mTB: 2px; - $grippyW: 3px; - $mLR: ($sliderKnobW - $grippyW)/2; - @include bgStripes($c: pullForward($sliderColorKnob, 20%), $a: 1, $bgsize: 4px, $angle: 0deg); - content: ''; - display: block; - position: absolute; - top: $mTB; right: $mLR; bottom: $mTB; left: $mLR; - } - .range-value { - @include trans-prop-nice-fade(.25s); - font-size: 0.7rem; - position: absolute; - height: $r2H; - line-height: $r2H; - white-space: nowrap; - z-index: 1; - } - &:hover { - .range-value { - color: $sliderColorKnobHov; - } - } - &.knob-l { - margin-left: $knobM; - .range-value { - text-align: right; - right: $rangeValOffset; - } - } - &.knob-r { - margin-right: $knobM; - .range-value { - left: $rangeValOffset; - } - &:hover + .range-holder .range .toi-line { - @include toiLineHovEffects; - } - } - } - - .l-time-domain-selector { - position: absolute; - right: 0px; - top: $interiorMargin; - } - -} - -.s-time-range-val { - border-radius: $controlCr; - background-color: $colorInputBg; - padding: 1px 1px 0 $interiorMargin; -} - -/******************************************************************** MOBILE */ - -@include phoneandtablet { - .l-time-controller { - min-width: 0; - .l-time-range-slider-holder, - .l-time-range-ticks-holder { - display: none; - } - } -} - -@include phone { - .l-time-controller { - .l-time-range-inputs-holder { - &.l-flex-row, - .l-flex-row { - @include align-items(flex-start); - } - .l-time-range-inputs-elem { - &.type-icon { - margin-top: 3px; - } - } - .t-inputs-w { - @include flex-direction(column); - .l-time-range-input-w:not(:first-child) { - &:not(:first-child) { - margin-top: $interiorMargin; - } - margin-right: 0; - } - .l-time-range-inputs-elem { - &.lbl { display: none; } - } - } - } - } -} - -@include phonePortrait { - .l-time-controller { - .l-time-range-inputs-holder { - .t-inputs-w { - @include flex(1 1 auto); - padding-top: 25px; // Make room for the ever lovin' Time Domain Selector - .flex-elem { - @include flex(1 1 auto); - width: 100%; - } - input[type="text"] { - width: 100%; - } - } - } - } - .l-time-domain-selector { - right: auto; - left: 20px; - } -} diff --git a/platform/commonUI/general/res/sass/features/_imagery.scss b/platform/commonUI/general/res/sass/features/_imagery.scss index cd04568e02..e33970b0e9 100644 --- a/platform/commonUI/general/res/sass/features/_imagery.scss +++ b/platform/commonUI/general/res/sass/features/_imagery.scss @@ -74,7 +74,7 @@ .s-image-main { border: 1px solid transparent; &.paused { - border-color: $colorPausedBg; + @extend .s-unsynced; } } diff --git a/platform/commonUI/general/res/sass/helpers/_wait-spinner.scss b/platform/commonUI/general/res/sass/helpers/_wait-spinner.scss index aecf42e02f..93c4fa093f 100644 --- a/platform/commonUI/general/res/sass/helpers/_wait-spinner.scss +++ b/platform/commonUI/general/res/sass/helpers/_wait-spinner.scss @@ -19,15 +19,6 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ -@include keyframes(rotation) { - 100% { @include transform(rotate(360deg)); } -} - -@include keyframes(rotation-centered) { - 0% { @include transform(translate(-50%, -50%) rotate(0deg)); } - 100% { @include transform(translate(-50%, -50%) rotate(360deg)); } -} - @mixin spinner($b: 5px, $c: $colorKey) { @include transform-origin(center); @include animation-name(rotation-centered); diff --git a/platform/commonUI/general/res/sass/user-environ/_layout.scss b/platform/commonUI/general/res/sass/user-environ/_layout.scss index 9740171ff0..d9ef67bb0a 100644 --- a/platform/commonUI/general/res/sass/user-environ/_layout.scss +++ b/platform/commonUI/general/res/sass/user-environ/_layout.scss @@ -128,7 +128,7 @@ line-height: $ueTopBarH; } - .primary-pane { + .t-object.primary-pane { // Need to lift up this pane to ensure that 'collapsed' panes don't block user interactions z-index: 4; } @@ -212,6 +212,8 @@ body.desktop .pane .mini-tab-icon.toggle-pane { .holder-object { top: $bodyMargin; bottom: $interiorMargin; + // Clip element that have min-widths + overflow: hidden; } .holder-inspector { top: $bodyMargin; diff --git a/platform/commonUI/general/res/templates/controls/datetime-field.html b/platform/commonUI/general/res/templates/controls/datetime-field.html index 1cb5e76f6d..684ac0b684 100644 --- a/platform/commonUI/general/res/templates/controls/datetime-field.html +++ b/platform/commonUI/general/res/templates/controls/datetime-field.html @@ -23,6 +23,8 @@ .l-row-elem { + // First order row elements + box-sizing: border-box; + width: 100%; + position: relative; + } + + .mode-selector .s-menu-button, + .time-delta { + &:before { + @extend .ui-symbol; + } + } + + .time-delta { + &:before { + color: $colorTimeCondKeyBg; + } + } + + .l-time-conductor-inputs-holder, + .l-time-conductor-inputs-and-ticks, + .l-time-conductor-zoom-w { + font-size: 0.8rem; + } + + .l-time-conductor-inputs-holder { + $ticksBlockerFadeW: 50px; + $iconCalendarW: 16px; + $wBgColor: $colorBodyBg; + + height: $r1H; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1; + .l-time-range-w { + // Wraps a datetime text input field + height: 100%; + position: absolute; + .title { + display: inline-block; + margin-right: $interiorMarginSm; + } + &.start-w { + @include background-image(linear-gradient(270deg, transparent, $wBgColor $ticksBlockerFadeW)); + padding-right: $ticksBlockerFadeW; + .title:before { + content: 'Start'; + } + } + &.end-w { + @include background-image(linear-gradient(90deg, transparent, $wBgColor $ticksBlockerFadeW)); + padding-left: $ticksBlockerFadeW; + right: 0; + .title:before { + content: 'End'; + } + } + input[type="text"] { + @include trans-prop-nice(padding, 250ms); + } + .time-range-input input[type="text"] { + width: $timeCondInputTimeSysDefW; + } + .hrs-min-input input[type="text"] { + width: $timeCondInputDeltaDefW; + } + .icon-calendar { + margin-top: 4px; + } + } + } + + .l-time-conductor-inputs-and-ticks { + $c: $colorTimeCondTicks; //$colorTick; + height: $r1H; + mct-conductor-axis { + display: block; + position: relative; + width: 100%; + } + .l-axis-holder { + height: $r1H; + position: relative; + width: 100%; + svg { + text-rendering: geometricPrecision; + width: 100%; + height: 100%; + > g { + font-size: 0.9em; + } + path { + // Line beneath ticks + display: none; + } + line { + // Tick marks + stroke: $c; + } + text { + // Tick labels + fill: $c; + } + } + } + } + .l-data-visualization { + background: $colorTimeCondDataVisBg; + height: $r2H; + } + + .l-time-conductor-controls { + align-items: center; + margin-top: $interiorMargin; + .l-time-conductor-zoom-w { + @include justify-content(flex-end); + .time-conductor-zoom { + display: none; // TEMP per request from Andrew 8/1/16 + height: $r3H; + min-width: 100px; + width: 20%; + } + .time-conductor-zoom-current-range { + display: none; // TEMP per request from Andrew 8/1/16 + color: $colorTick; + } + } + } + + // Real-time, latest modes + &.realtime-mode, + &.lad-mode { + .time-conductor-icon { + &:before { color: $colorTimeCondKeyBg; } + div[class*="hand"] { + @include animation-name(clock-hands); + &:before { + background: $colorTimeCondKeyBg; + } + } + } + + .l-time-conductor-inputs-holder { + .l-time-range-input-w { + input[type="text"]:not(.error) { + background: transparent; + box-shadow: none; + border-radius: 0; + padding-left: 0; + padding-right: 0; + &:hover, + &:focus { + @include nice-input(); + padding: $inputTextP; + } + } + .icon-calendar { + display: none; + } + &.start-date { + display: none; + } + &.end-date { + pointer-events: none; + input[type="text"] { + color: pullForward($colorTimeCondKeyBg, 5%); + margin-right: $interiorMargin; + tab-index: -1; + } + } + } + } + + .l-data-visualization { + background: $colorTimeCondDataVisRtBg !important + } + + .mode-selector .s-menu-button { + $fg: $colorTimeCondKeyFg; + @include btnSubtle($bg: $colorTimeCondKeyBg, $bgHov: pullForward($colorTimeCondKeyBg, $ltGamma), $fg: $colorTimeCondKeyFg); + &:before { color: $fg !important; }; + color: $fg !important; + } + } + + // Fixed mode + &.fixed-mode { + $i: $glyph-icon-calendar; + .time-conductor-icon div[class*="hand"] { + &.hand-little { + @include transform(rotate(120deg)); + } + } + .mode-selector .s-menu-button:before { + content: $i; + } + } + + // Realtime mode + &.realtime-mode { + $i: $glyph-icon-clock; + .time-conductor-icon div[class*="hand"] { + @include animation-name(clock-hands); + } + .time-delta:before { + content: $i; + } + .l-time-conductor-inputs-holder .l-time-range-w.end-w .title:before { + content: 'Now'; + } + .mode-selector .s-menu-button:before { + content: $i; + } + } + + // LAD mode + &.lad-mode { + $i: $glyph-icon-database; + .time-conductor-icon div[class*="hand"] { + @include animation-name(clock-hands-sticky); + &.hand-big { + @include animation-duration(5s); + } + &.hand-little { + @include animation-duration(60s); + } + } + .time-delta:before { + content: $i; + } + .l-time-conductor-inputs-holder .l-time-range-w.end-w .title:before { + content: 'LAD'; + } + .mode-selector .s-menu-button:before { + content: $i; + } + } +} + +/******************************************************************** MOBILE */ + +@include phoneandtablet { + .l-time-conductor-holder { min-width: 0 !important; } + .super-menu.mini { + width: 200px; + height: 100px; + .pane.menu-item-description { + display: none; + } + } +} + +@include phone { + .l-time-conductor { + min-width: 0; + .l-time-conductor-inputs-and-ticks { + .l-time-conductor-inputs-holder { + .l-time-range-w { + background-image: none !important; + } + } + mct-conductor-axis { + display: none; + } + } + } +} + +@include phonePortrait { + .l-time-conductor { + .l-data-visualization, + .l-time-conductor-zoom-w, + .time-delta { + display: none; + } + + .l-time-conductor-inputs-and-ticks { + height: auto !important; + .l-time-conductor-inputs-holder { + position: relative; + height: auto !important; + + .l-time-range-w { + background-image: none !important; + display: block; + height: auto !important; + padding: 0 !important; + position: relative; + text-align: left; + &:not(:first-child) { + margin-top: $interiorMargin; + } + } + } + } + + // Fixed mode + &.fixed-mode { + .l-time-conductor-inputs-and-ticks { + .l-time-range-w { + .title { + width: 30px; + } + } + } + } + + // Real-time, latest modes + &.realtime-mode, + &.lad-mode { + .l-time-conductor-inputs-and-ticks { + .l-time-range-w { + &.start-w { + display: none; + } + &.end-w { + margin-top: 0; + .end-date input[type="text"] { + margin: 0; + text-align: left; + } + } + } + } + } + } +} diff --git a/platform/features/conductor-v2/conductor/res/sass/time-conductor-espresso.scss b/platform/features/conductor-v2/conductor/res/sass/time-conductor-espresso.scss new file mode 100644 index 0000000000..a47f4c07a7 --- /dev/null +++ b/platform/features/conductor-v2/conductor/res/sass/time-conductor-espresso.scss @@ -0,0 +1,39 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2015, 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. + *****************************************************************************/ +@import "bourbon"; +@import "../../../../../commonUI/general/res/sass/constants"; +@import "../../../../../commonUI/general/res/sass/mixins"; +@import "../../../../../commonUI/general/res/sass/mobile/constants"; +@import "../../../../../commonUI/general/res/sass/mobile/mixins"; +@import "../../../../../commonUI/themes/espresso/res/sass/constants"; +@import "../../../../../commonUI/themes/espresso/res/sass/mixins"; +@import "../../../../../commonUI/general/res/sass/glyphs"; +@import "../../../../../commonUI/general/res/sass/icons"; +@import "constants"; + +// Thematic constants +$colorTimeCondTicks: pullForward($colorBodyBg, 30%); +$colorTimeCondKeyBg: #4e70dc; +$colorTimeCondKeyFg: #fff; +$colorTimeCondDataVisBg: pullForward($colorBodyBg, 10%); +$colorTimeCondDataVisRtBg: pushBack($colorTimeCondKeyBg, 10%); +@import "time-conductor-base"; \ No newline at end of file diff --git a/platform/features/conductor-v2/conductor/res/sass/time-conductor-snow.scss b/platform/features/conductor-v2/conductor/res/sass/time-conductor-snow.scss new file mode 100644 index 0000000000..626ceb0e51 --- /dev/null +++ b/platform/features/conductor-v2/conductor/res/sass/time-conductor-snow.scss @@ -0,0 +1,39 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2015, 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. + *****************************************************************************/ +@import "bourbon"; +@import "../../../../../commonUI/general/res/sass/constants"; +@import "../../../../../commonUI/general/res/sass/mixins"; +@import "../../../../../commonUI/general/res/sass/mobile/constants"; +@import "../../../../../commonUI/general/res/sass/mobile/mixins"; +@import "../../../../../commonUI/themes/snow/res/sass/constants"; +@import "../../../../../commonUI/themes/snow/res/sass/mixins"; +@import "../../../../../commonUI/general/res/sass/glyphs"; +@import "../../../../../commonUI/general/res/sass/icons"; +@import "constants"; + +// Thematic constants +$colorTimeCondTicks: pullForward($colorBodyBg, 30%); +$colorTimeCondKeyBg: #6178dc; +$colorTimeCondKeyFg: #fff; +$colorTimeCondDataVisBg: pullForward($colorBodyBg, 10%); +$colorTimeCondDataVisRtBg: pushBack($colorTimeCondKeyBg, 30%); +@import "time-conductor-base"; \ No newline at end of file diff --git a/platform/features/conductor-v2/conductor/res/templates/mode-selector/mode-menu.html b/platform/features/conductor-v2/conductor/res/templates/mode-selector/mode-menu.html new file mode 100644 index 0000000000..990c6de482 --- /dev/null +++ b/platform/features/conductor-v2/conductor/res/templates/mode-selector/mode-menu.html @@ -0,0 +1,45 @@ + +
+ + +
diff --git a/platform/features/conductor-v2/conductor/res/templates/mode-selector/mode-selector.html b/platform/features/conductor-v2/conductor/res/templates/mode-selector/mode-selector.html new file mode 100644 index 0000000000..1f346f2bef --- /dev/null +++ b/platform/features/conductor-v2/conductor/res/templates/mode-selector/mode-selector.html @@ -0,0 +1,34 @@ + + +
+ {{ngModel.options[ngModel.selectedKey] + .label}} +
+ +
\ No newline at end of file diff --git a/platform/features/conductor-v2/conductor/res/templates/time-conductor.html b/platform/features/conductor-v2/conductor/res/templates/time-conductor.html new file mode 100644 index 0000000000..19c86b2b74 --- /dev/null +++ b/platform/features/conductor-v2/conductor/res/templates/time-conductor.html @@ -0,0 +1,108 @@ + +
+ +
+
+
+
+ +
+ +
+
+ + + + + + + + - + + + + + + + + + + + + + + + + + + + +
+ +
+ + +
+ + +
+ + + + + +
+ + +
+
+ +
+
diff --git a/platform/features/conductor-v2/conductor/src/TimeConductor.js b/platform/features/conductor-v2/conductor/src/TimeConductor.js new file mode 100644 index 0000000000..2c2194b5a0 --- /dev/null +++ b/platform/features/conductor-v2/conductor/src/TimeConductor.js @@ -0,0 +1,179 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web 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 Web 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(['EventEmitter'], function (EventEmitter) { + + /** + * The public API for setting and querying time conductor state. The + * time conductor is the means by which the temporal bounds of a view + * are controlled. Time-sensitive views will typically respond to + * changes to bounds or other properties of the time conductor and + * update the data displayed based on the time conductor state. + * + * The TimeConductor extends the EventEmitter class. A number of events are + * fired when properties of the time conductor change, which are + * documented below. + * @constructor + */ + function TimeConductor() { + EventEmitter.call(this); + + //The Time System + this.system = undefined; + //The Time Of Interest + this.toi = undefined; + + this.boundsVal = { + start: undefined, + end: undefined + }; + + //Default to fixed mode + this.followMode = false; + } + + TimeConductor.prototype = Object.create(EventEmitter.prototype); + + /** + * Validate the given bounds. This can be used for pre-validation of + * bounds, for example by views validating user inputs. + * @param bounds The start and end time of the conductor. + * @returns {string | true} A validation error, or true if valid + */ + TimeConductor.prototype.validateBounds = function (bounds) { + if ((bounds.start === undefined) || + (bounds.end === undefined) || + isNaN(bounds.start) || + isNaN(bounds.end) + ) { + return "Start and end must be specified as integer values"; + } else if (bounds.start > bounds.end) { + return "Specified start date exceeds end bound"; + } + return true; + }; + + /** + * Get or set the follow mode of the time conductor. In follow mode the + * time conductor ticks, regularly updating the bounds from a timing + * source appropriate to the selected time system and mode of the time + * conductor. + * @fires TimeConductor#follow + * @param {boolean} followMode + * @returns {boolean} + */ + TimeConductor.prototype.follow = function (followMode) { + if (arguments.length > 0) { + this.followMode = followMode; + /** + * @event TimeConductor#follow The TimeConductor has toggled + * into or out of follow mode. + * @property {boolean} followMode true if follow mode is + * enabled, otherwise false. + */ + this.emit('follow', this.followMode); + } + return this.followMode; + }; + + /** + * @typedef {Object} TimeConductorBounds + * @property {number} start The start time displayed by the time conductor in ms since epoch. Epoch determined by current time system + * @property {number} end The end time displayed by the time conductor in ms since epoch. + */ + /** + * Get or set the start and end time of the time conductor. Basic validation + * of bounds is performed. + * + * @param {TimeConductorBounds} newBounds + * @throws {Error} Validation error + * @fires TimeConductor#bounds + * @returns {TimeConductorBounds} + */ + TimeConductor.prototype.bounds = function (newBounds) { + if (arguments.length > 0) { + var validationResult = this.validateBounds(newBounds); + if (validationResult !== true) { + throw new Error(validationResult); + } + //Create a copy to avoid direct mutation of conductor bounds + this.boundsVal = JSON.parse(JSON.stringify(newBounds)); + /** + * @event TimeConductor#bounds The start time, end time, or + * both have been updated + * @property {TimeConductorBounds} bounds + */ + this.emit('bounds', this.boundsVal); + } + //Return a copy to prevent direct mutation of time conductor bounds. + return JSON.parse(JSON.stringify(this.boundsVal)); + }; + + /** + * Get or set the time system of the TimeConductor. Time systems determine + * units, epoch, and other aspects of time representation. When changing + * the time system in use, new valid bounds must also be provided. + * @param {TimeSystem} newTimeSystem + * @param {TimeConductorBounds} bounds + * @fires TimeConductor#timeSystem + * @returns {TimeSystem} The currently applied time system + */ + TimeConductor.prototype.timeSystem = function (newTimeSystem, bounds) { + if (arguments.length >= 2) { + this.system = newTimeSystem; + /** + * @event TimeConductor#timeSystem The time system used by the time + * conductor has changed. A change in Time System will always be + * followed by a bounds event specifying new query bounds + * @property {TimeSystem} The value of the currently applied + * Time System + * */ + this.emit('timeSystem', this.system); + this.bounds(bounds); + } else if (arguments.length === 1) { + throw new Error('Must set bounds when changing time system'); + } + return this.system; + }; + + /** + * Get or set the Time of Interest. The Time of Interest is the temporal + * focus of the current view. It can be manipulated by the user from the + * time conductor or from other views. + * @fires TimeConductor#timeOfInterest + * @param newTOI + * @returns {number} the current time of interest + */ + TimeConductor.prototype.timeOfInterest = function (newTOI) { + if (arguments.length > 0) { + this.toi = newTOI; + /** + * @event TimeConductor#timeOfInterest The Time of Interest has moved. + * @property {number} Current time of interest + */ + this.emit('timeOfInterest', this.toi); + } + return this.toi; + }; + + return TimeConductor; +}); diff --git a/platform/features/conductor-v2/conductor/src/TimeConductorSpec.js b/platform/features/conductor-v2/conductor/src/TimeConductorSpec.js new file mode 100644 index 0000000000..7701f5e9b4 --- /dev/null +++ b/platform/features/conductor-v2/conductor/src/TimeConductorSpec.js @@ -0,0 +1,110 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web 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 Web 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(['./TimeConductor'], function (TimeConductor) { + describe("The Time Conductor", function () { + var tc, + timeSystem, + bounds, + eventListener, + toi, + follow; + + beforeEach(function () { + tc = new TimeConductor(); + timeSystem = {}; + bounds = {start: 0, end: 0}; + eventListener = jasmine.createSpy("eventListener"); + toi = 111; + follow = true; + }); + + it("Supports setting and querying of time of interest and and follow mode", function () { + expect(tc.timeOfInterest()).not.toBe(toi); + tc.timeOfInterest(toi); + expect(tc.timeOfInterest()).toBe(toi); + + expect(tc.follow()).not.toBe(follow); + tc.follow(follow); + expect(tc.follow()).toBe(follow); + }); + + it("Allows setting of valid bounds", function () { + bounds = {start: 0, end: 1}; + expect(tc.bounds()).not.toEqual(bounds); + expect(tc.bounds.bind(tc, bounds)).not.toThrow(); + expect(tc.bounds()).toEqual(bounds); + }); + + it("Disallows setting of invalid bounds", function () { + bounds = {start: 1, end: 0}; + expect(tc.bounds()).not.toEqual(bounds); + expect(tc.bounds.bind(tc, bounds)).toThrow(); + expect(tc.bounds()).not.toEqual(bounds); + + bounds = {start: 1}; + expect(tc.bounds()).not.toEqual(bounds); + expect(tc.bounds.bind(tc, bounds)).toThrow(); + expect(tc.bounds()).not.toEqual(bounds); + }); + + it("Allows setting of time system with bounds", function () { + expect(tc.timeSystem()).not.toBe(timeSystem); + expect(tc.timeSystem.bind(tc, timeSystem, bounds)).not.toThrow(); + expect(tc.timeSystem()).toBe(timeSystem); + }); + + it("Disallows setting of time system without bounds", function () { + expect(tc.timeSystem()).not.toBe(timeSystem); + expect(tc.timeSystem.bind(tc, timeSystem)).toThrow(); + expect(tc.timeSystem()).not.toBe(timeSystem); + }); + + it("Emits an event when time system changes", function () { + expect(eventListener).not.toHaveBeenCalled(); + tc.on("timeSystem", eventListener); + tc.timeSystem(timeSystem, bounds); + expect(eventListener).toHaveBeenCalledWith(timeSystem); + }); + + it("Emits an event when time of interest changes", function () { + expect(eventListener).not.toHaveBeenCalled(); + tc.on("timeOfInterest", eventListener); + tc.timeOfInterest(toi); + expect(eventListener).toHaveBeenCalledWith(toi); + }); + + it("Emits an event when bounds change", function () { + expect(eventListener).not.toHaveBeenCalled(); + tc.on("bounds", eventListener); + tc.bounds(bounds); + expect(eventListener).toHaveBeenCalledWith(bounds); + }); + + it("Emits an event when follow mode changes", function () { + expect(eventListener).not.toHaveBeenCalled(); + tc.on("follow", eventListener); + tc.follow(follow); + expect(eventListener).toHaveBeenCalledWith(follow); + }); + }); +}); diff --git a/platform/features/conductor-v2/conductor/src/timeSystems/LocalClock.js b/platform/features/conductor-v2/conductor/src/timeSystems/LocalClock.js new file mode 100644 index 0000000000..09ddd35612 --- /dev/null +++ b/platform/features/conductor-v2/conductor/src/timeSystems/LocalClock.js @@ -0,0 +1,89 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web 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 Web 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(['./TickSource'], function (TickSource) { + /** + * @implements TickSource + * @constructor + */ + function LocalClock($timeout, period) { + TickSource.call(this); + + this.metadata = { + key: 'local', + mode: 'realtime', + cssclass: 'icon-clock', + label: 'Real-time', + name: 'Real-time Mode', + description: 'Monitor real-time streaming data as it comes in. The Time Conductor and displays will automatically advance themselves based on a UTC clock.' + }; + + this.period = period; + this.$timeout = $timeout; + this.timeoutHandle = undefined; + } + + LocalClock.prototype = Object.create(TickSource.prototype); + + LocalClock.prototype.start = function () { + this.timeoutHandle = this.$timeout(this.tick.bind(this), this.period); + }; + + LocalClock.prototype.stop = function () { + if (this.timeoutHandle) { + this.$timeout.cancel(this.timeoutHandle); + } + }; + + LocalClock.prototype.tick = function () { + var now = Date.now(); + this.listeners.forEach(function (listener) { + listener(now); + }); + this.timeoutHandle = this.$timeout(this.tick.bind(this), this.period); + }; + + /** + * Register a listener for the local clock. When it ticks, the local + * clock will provide the current local system time + * + * @param listener + * @returns {function} a function for deregistering the provided listener + */ + LocalClock.prototype.listen = function (listener) { + var listeners = this.listeners; + listeners.push(listener); + + if (listeners.length === 1) { + this.start(); + } + + return function () { + listeners.splice(listeners.indexOf(listener)); + if (listeners.length === 0) { + this.stop(); + } + }.bind(this); + }; + + return LocalClock; +}); diff --git a/platform/features/conductor-v2/conductor/src/timeSystems/LocalClockSpec.js b/platform/features/conductor-v2/conductor/src/timeSystems/LocalClockSpec.js new file mode 100644 index 0000000000..b34b625f43 --- /dev/null +++ b/platform/features/conductor-v2/conductor/src/timeSystems/LocalClockSpec.js @@ -0,0 +1,50 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web 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 Web 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(["./LocalClock"], function (LocalClock) { + describe("The LocalClock class", function () { + var clock, + mockTimeout, + timeoutHandle = {}; + + beforeEach(function () { + mockTimeout = jasmine.createSpy("timeout"); + mockTimeout.andReturn(timeoutHandle); + mockTimeout.cancel = jasmine.createSpy("cancel"); + + clock = new LocalClock(mockTimeout, 0); + clock.start(); + }); + + it("calls listeners on tick with current time", function () { + var mockListener = jasmine.createSpy("listener"); + clock.listen(mockListener); + clock.tick(); + expect(mockListener).toHaveBeenCalledWith(jasmine.any(Number)); + }); + + it("stops ticking when stop is called", function () { + clock.stop(); + expect(mockTimeout.cancel).toHaveBeenCalledWith(timeoutHandle); + }); + }); +}); diff --git a/platform/features/conductor-v2/conductor/src/timeSystems/TickSource.js b/platform/features/conductor-v2/conductor/src/timeSystems/TickSource.js new file mode 100644 index 0000000000..8cb9fd308d --- /dev/null +++ b/platform/features/conductor-v2/conductor/src/timeSystems/TickSource.js @@ -0,0 +1,47 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web 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 Web 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([], function () { + /** + * A tick source is an event generator such as a timing signal, or + * indicator of data availability, which can be used to advance the Time + * Conductor. Usage is simple, a listener registers a callback which is + * invoked when this source 'ticks'. + * + * @interface + * @constructor + */ + function TickSource() { + this.listeners = []; + } + + /** + * @param callback Function to be called when this tick source ticks. + * @returns an 'unlisten' function that will remove the callback from + * the registered listeners + */ + TickSource.prototype.listen = function (callback) { + throw new Error('Not implemented'); + }; + + return TickSource; +}); diff --git a/platform/features/conductor-v2/conductor/src/timeSystems/TimeSystem.js b/platform/features/conductor-v2/conductor/src/timeSystems/TimeSystem.js new file mode 100644 index 0000000000..652ea9ed0f --- /dev/null +++ b/platform/features/conductor-v2/conductor/src/timeSystems/TimeSystem.js @@ -0,0 +1,93 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web 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 Web 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([], function () { + /** + * @interface + * @constructor + */ + function TimeSystem() { + /** + * @typedef TimeSystemMetadata + * @property {string} key + * @property {string} name + * @property {string} description + * + * @type {TimeSystemMetadata} + */ + this.metadata = undefined; + } + + /** + * Time formats are defined as extensions. Time systems that implement + * this interface should provide an array of format keys supported by them. + * + * @returns {string[]} An array of time format keys + */ + TimeSystem.prototype.formats = function () { + throw new Error('Not implemented'); + }; + + /** + * @typedef DeltaFormat + * @property {string} type the type of MctControl used to represent this + * field. Typically 'datetime-field' for UTC based dates, or 'textfield' + * otherwise + * @property {string} [format] An optional field specifying the + * Format to use for delta fields in this time system. + */ + /** + * Specifies a format for deltas in this time system. + * + * @returns {DeltaFormat} a delta format specifier + */ + TimeSystem.prototype.deltaFormat = function () { + throw new Error('Not implemented'); + }; + + /** + * Returns the tick sources supported by this time system. Tick sources + * are event generators that can be used to advance the time conductor + * @returns {TickSource[]} The tick sources supported by this time system. + */ + TimeSystem.prototype.tickSources = function () { + throw new Error('Not implemented'); + }; + + /** + * + * @returns {TimeSystemDefault[]} At least one set of default values for + * this time system. + */ + TimeSystem.prototype.defaults = function () { + throw new Error('Not implemented'); + }; + + /** + * @return {boolean} + */ + TimeSystem.prototype.isUTCBased = function () { + return true; + }; + + return TimeSystem; +}); diff --git a/platform/features/conductor-v2/conductor/src/ui/MctConductorAxis.js b/platform/features/conductor-v2/conductor/src/ui/MctConductorAxis.js new file mode 100644 index 0000000000..58cb60befc --- /dev/null +++ b/platform/features/conductor-v2/conductor/src/ui/MctConductorAxis.js @@ -0,0 +1,146 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web 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 Web 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( + [ + "d3" + ], + function (d3) { + var PADDING = 1; + + /** + * The mct-conductor-axis renders a horizontal axis with regular + * labelled 'ticks'. It requires 'start' and 'end' integer values to + * be specified as attributes. + */ + function MCTConductorAxis(conductor, formatService) { + // Dependencies + this.d3 = d3; + this.conductor = conductor; + this.formatService = formatService; + + // Runtime properties (set by 'link' function) + this.target = undefined; + this.xScale = undefined; + this.xAxis = undefined; + this.axisElement = undefined; + + // Angular Directive interface + this.link = this.link.bind(this); + this.restrict = "E"; + this.template = + "
"; + this.priority = 1000; + + //Bind all class functions to 'this' + Object.keys(MCTConductorAxis.prototype).filter(function (key) { + return typeof MCTConductorAxis.prototype[key] === 'function'; + }).forEach(function (key) { + this[key] = this[key].bind(this); + }.bind(this)); + } + + MCTConductorAxis.prototype.setScale = function () { + var width = this.target.offsetWidth; + var timeSystem = this.conductor.timeSystem(); + var bounds = this.conductor.bounds(); + + if (timeSystem.isUTCBased()) { + this.xScale = this.xScale || this.d3.scaleUtc(); + this.xScale.domain([new Date(bounds.start), new Date(bounds.end)]); + } else { + this.xScale = this.xScale || this.d3.scaleLinear(); + this.xScale.domain([bounds.start, bounds.end]); + } + + this.xScale.range([PADDING, width - PADDING * 2]); + this.axisElement.call(this.xAxis); + }; + + MCTConductorAxis.prototype.changeTimeSystem = function (timeSystem) { + var key = timeSystem.formats()[0]; + if (key !== undefined) { + var format = this.formatService.getFormat(key); + var bounds = this.conductor.bounds(); + + if (timeSystem.isUTCBased()) { + this.xScale = this.d3.scaleUtc(); + } else { + this.xScale = this.d3.scaleLinear(); + } + + this.xAxis.scale(this.xScale); + //Define a custom format function + this.xAxis.tickFormat(function (tickValue) { + // Normalize date representations to numbers + if (tickValue instanceof Date) { + tickValue = tickValue.getTime(); + } + return format.format(tickValue, { + min: bounds.start, + max: bounds.end + }); + }); + this.axisElement.call(this.xAxis); + } + }; + + MCTConductorAxis.prototype.destroy = function () { + this.conductor.off('timeSystem', this.changeTimeSystem); + this.conductor.off('bounds', this.setScale); + }; + + MCTConductorAxis.prototype.link = function (scope, element) { + var conductor = this.conductor; + this.target = element[0].firstChild; + var height = this.target.offsetHeight; + var vis = this.d3.select(this.target) + .append('svg:svg') + .attr('width', '100%') + .attr('height', height); + + this.xAxis = this.d3.axisTop(); + + // draw x axis with labels and move to the bottom of the chart area + this.axisElement = vis.append("g") + .attr("transform", "translate(0," + (height - PADDING) + ")"); + + scope.resize = this.setScale; + + conductor.on('timeSystem', this.changeTimeSystem); + + //On conductor bounds changes, redraw ticks + conductor.on('bounds', this.setScale); + + scope.$on("$destroy", this.destroy); + + if (conductor.timeSystem() !== undefined) { + this.changeTimeSystem(conductor.timeSystem()); + this.setScale(); + } + }; + + return function (conductor, formatService) { + return new MCTConductorAxis(conductor, formatService); + }; + } +); diff --git a/platform/features/conductor-v2/conductor/src/ui/MctConductorAxisSpec.js b/platform/features/conductor-v2/conductor/src/ui/MctConductorAxisSpec.js new file mode 100644 index 0000000000..0fe3c4cf3f --- /dev/null +++ b/platform/features/conductor-v2/conductor/src/ui/MctConductorAxisSpec.js @@ -0,0 +1,146 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web 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 Web 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(['./MctConductorAxis'], function (MctConductorAxis) { + describe("The MctConductorAxis directive", function () { + var directive, + mockConductor, + mockFormatService, + mockScope, + mockElement, + mockTarget, + mockBounds, + d3; + + beforeEach(function () { + mockScope = jasmine.createSpyObj("scope", [ + "$on" + ]); + + //Add some HTML elements + mockTarget = { + offsetWidth: 0, + offsetHeight: 0 + }; + mockElement = { + firstChild: mockTarget + }; + mockBounds = { + start: 100, + end: 200 + }; + mockConductor = jasmine.createSpyObj("conductor", [ + "timeSystem", + "bounds", + "on", + "off" + ]); + mockConductor.bounds.andReturn(mockBounds); + + mockFormatService = jasmine.createSpyObj("formatService", [ + "getFormat" + ]); + + var d3Functions = [ + "scale", + "scaleUtc", + "scaleLinear", + "select", + "append", + "attr", + "axisTop", + "call", + "tickFormat", + "domain", + "range" + ]; + d3 = jasmine.createSpyObj("d3", d3Functions); + d3Functions.forEach(function (func) { + d3[func].andReturn(d3); + }); + + directive = new MctConductorAxis(mockConductor, mockFormatService); + directive.d3 = d3; + directive.link(mockScope, [mockElement]); + }); + + it("listens for changes to time system and bounds", function () { + expect(mockConductor.on).toHaveBeenCalledWith("timeSystem", directive.changeTimeSystem); + expect(mockConductor.on).toHaveBeenCalledWith("bounds", directive.setScale); + }); + + it("on scope destruction, deregisters listeners", function () { + expect(mockScope.$on).toHaveBeenCalledWith("$destroy", directive.destroy); + directive.destroy(); + expect(mockConductor.off).toHaveBeenCalledWith("timeSystem", directive.changeTimeSystem); + expect(mockConductor.off).toHaveBeenCalledWith("bounds", directive.setScale); + }); + + describe("when the time system changes", function () { + var mockTimeSystem; + var mockFormat; + + beforeEach(function () { + mockTimeSystem = jasmine.createSpyObj("timeSystem", [ + "formats", + "isUTCBased" + ]); + mockFormat = jasmine.createSpyObj("format", [ + "format" + ]); + + mockTimeSystem.formats.andReturn(["mockFormat"]); + mockFormatService.getFormat.andReturn(mockFormat); + }); + + it("uses a UTC scale for UTC time systems", function () { + mockTimeSystem.isUTCBased.andReturn(true); + directive.changeTimeSystem(mockTimeSystem); + expect(d3.scaleUtc).toHaveBeenCalled(); + expect(d3.scaleLinear).not.toHaveBeenCalled(); + }); + + it("uses a linear scale for non-UTC time systems", function () { + mockTimeSystem.isUTCBased.andReturn(false); + directive.changeTimeSystem(mockTimeSystem); + expect(d3.scaleLinear).toHaveBeenCalled(); + expect(d3.scaleUtc).not.toHaveBeenCalled(); + }); + + it("sets axis domain to time conductor bounds", function () { + mockTimeSystem.isUTCBased.andReturn(false); + mockConductor.timeSystem.andReturn(mockTimeSystem); + + directive.setScale(); + expect(d3.domain).toHaveBeenCalledWith([mockBounds.start, mockBounds.end]); + }); + + it("uses the format specified by the time system to format tick" + + " labels", function () { + directive.changeTimeSystem(mockTimeSystem); + expect(d3.tickFormat).toHaveBeenCalled(); + d3.tickFormat.mostRecentCall.args[0](); + expect(mockFormat.format).toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/platform/features/conductor-v2/conductor/src/ui/NumberFormat.js b/platform/features/conductor-v2/conductor/src/ui/NumberFormat.js new file mode 100644 index 0000000000..ac3f40cb7b --- /dev/null +++ b/platform/features/conductor-v2/conductor/src/ui/NumberFormat.js @@ -0,0 +1,53 @@ +/***************************************************************************** + * 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([], function () { + + /** + * Formatter for basic numbers. Provides basic support for non-UTC + * numbering systems + * + * @implements {Format} + * @constructor + * @memberof platform/commonUI/formats + */ + function NumberFormat() { + } + + NumberFormat.prototype.format = function (value) { + if (isNaN(value)) { + return ''; + } else { + return '' + value; + } + }; + + NumberFormat.prototype.parse = function (text) { + return parseFloat(text); + }; + + NumberFormat.prototype.validate = function (text) { + return !isNaN(text); + }; + + return NumberFormat; +}); diff --git a/platform/features/conductor-v2/conductor/src/ui/NumberFormatSpec.js b/platform/features/conductor-v2/conductor/src/ui/NumberFormatSpec.js new file mode 100644 index 0000000000..f56c6e67d3 --- /dev/null +++ b/platform/features/conductor-v2/conductor/src/ui/NumberFormatSpec.js @@ -0,0 +1,49 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web 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 Web 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(['./NumberFormat'], function (NumberFormat) { + describe("The NumberFormat class", function () { + var format; + beforeEach(function () { + format = new NumberFormat(); + }); + + it("The format function takes a string and produces a number", function () { + var text = format.format(1); + expect(text).toBe("1"); + expect(typeof text).toBe("string"); + }); + + it("The parse function takes a string and produces a number", function () { + var number = format.parse("1"); + expect(number).toBe(1); + expect(typeof number).toBe("number"); + }); + + it("validates that the input is a number", function () { + expect(format.validate("1")).toBe(true); + expect(format.validate(1)).toBe(true); + expect(format.validate("1.1")).toBe(true); + expect(format.validate("abc")).toBe(false); + }); + }); +}); diff --git a/platform/features/conductor-v2/conductor/src/ui/TimeConductorController.js b/platform/features/conductor-v2/conductor/src/ui/TimeConductorController.js new file mode 100644 index 0000000000..83bc0558c6 --- /dev/null +++ b/platform/features/conductor-v2/conductor/src/ui/TimeConductorController.js @@ -0,0 +1,243 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web 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 Web 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( + [ + './TimeConductorValidation' + ], + function (TimeConductorValidation) { + + function TimeConductorController($scope, $window, timeConductor, conductorViewService, timeSystems) { + + var self = this; + + //Bind all class functions to 'this' + Object.keys(TimeConductorController.prototype).filter(function (key) { + return typeof TimeConductorController.prototype[key] === 'function'; + }).forEach(function (key) { + self[key] = self[key].bind(self); + }); + + this.$scope = $scope; + this.$window = $window; + this.conductorViewService = conductorViewService; + this.conductor = timeConductor; + this.modes = conductorViewService.availableModes(); + this.validation = new TimeConductorValidation(this.conductor); + + // Construct the provided time system definitions + this.timeSystems = timeSystems.map(function (timeSystemConstructor) { + return timeSystemConstructor(); + }); + + //Set the initial state of the view based on current time conductor + this.initializeScope(); + + this.conductor.on('bounds', this.setFormFromBounds); + this.conductor.on('timeSystem', this.changeTimeSystem); + + // If no mode selected, select fixed as the default + if (!this.conductorViewService.mode()) { + this.setMode('fixed'); + } + } + + /** + * @private + */ + TimeConductorController.prototype.initializeScope = function () { + //Set time Conductor bounds in the form + this.$scope.boundsModel = this.conductor.bounds(); + + //If conductor has a time system selected already, populate the + //form from it + this.$scope.timeSystemModel = {}; + if (this.conductor.timeSystem()) { + this.setFormFromTimeSystem(this.conductor.timeSystem()); + } + + //Represents the various modes, and the currently selected mode + //in the view + this.$scope.modeModel = { + options: this.conductorViewService.availableModes() + }; + + var mode = this.conductorViewService.mode(); + if (mode) { + //If view already defines a mode (eg. controller is being + // initialized after navigation), then pre-populate form. + this.setFormFromMode(mode); + var deltas = this.conductorViewService.deltas(); + if (deltas) { + this.setFormFromDeltas(deltas); + } + + } + + this.setFormFromBounds(this.conductor.bounds()); + + // Watch scope for selection of mode or time system by user + this.$scope.$watch('modeModel.selectedKey', this.setMode); + + this.$scope.$on('$destroy', this.destroy); + }; + + TimeConductorController.prototype.destroy = function () { + this.conductor.off('bounds', this.setFormFromBounds); + this.conductor.off('timeSystem', this.changeTimeSystem); + }; + + /** + * Called when the bounds change in the time conductor. Synchronizes + * the bounds values in the time conductor with those in the form + * + * @private + */ + TimeConductorController.prototype.setFormFromBounds = function (bounds) { + this.$scope.boundsModel.start = bounds.start; + this.$scope.boundsModel.end = bounds.end; + if (!this.pendingUpdate) { + this.pendingUpdate = true; + this.$window.requestAnimationFrame(function () { + this.pendingUpdate = false; + this.$scope.$digest(); + }.bind(this)); + } + }; + + /** + * @private + */ + TimeConductorController.prototype.setFormFromMode = function (mode) { + this.$scope.modeModel.selectedKey = mode; + //Synchronize scope with time system on mode + this.$scope.timeSystemModel.options = + this.conductorViewService.availableTimeSystems() + .map(function (t) { + return t.metadata; + }); + }; + + /** + * @private + */ + TimeConductorController.prototype.setFormFromDeltas = function (deltas) { + this.$scope.boundsModel.startDelta = deltas.start; + this.$scope.boundsModel.endDelta = deltas.end; + }; + + /** + * @private + */ + TimeConductorController.prototype.setFormFromTimeSystem = function (timeSystem) { + this.$scope.timeSystemModel.selected = timeSystem; + this.$scope.timeSystemModel.format = timeSystem.formats()[0]; + this.$scope.timeSystemModel.deltaFormat = timeSystem.deltaFormat(); + }; + + + /** + * Called when form values are changed. Synchronizes the form with + * the time conductor + * @param formModel + */ + TimeConductorController.prototype.updateBoundsFromForm = function (boundsModel) { + this.conductor.bounds({ + start: boundsModel.start, + end: boundsModel.end + }); + }; + + /** + * Called when the delta values in the form change. Validates and + * sets the new deltas on the Mode. + * @param boundsModel + * @see TimeConductorMode + */ + TimeConductorController.prototype.updateDeltasFromForm = function (boundsFormModel) { + var deltas = { + start: boundsFormModel.startDelta, + end: boundsFormModel.endDelta + }; + if (this.validation.validateStartDelta(deltas.start) && this.validation.validateEndDelta(deltas.end)) { + //Sychronize deltas between form and mode + this.conductorViewService.deltas(deltas); + } + }; + + /** + * Change the selected Time Conductor mode. This will call destroy + * and initialization functions on the relevant modes, setting + * default values for bound and deltas in the form. + * + * @private + * @param newModeKey + * @param oldModeKey + */ + TimeConductorController.prototype.setMode = function (newModeKey, oldModeKey) { + if (newModeKey !== oldModeKey) { + this.conductorViewService.mode(newModeKey); + this.setFormFromMode(newModeKey); + } + }; + + /** + * Respond to time system selection from UI + * + * Allows time system to be changed by key. This supports selection + * from the menu. Resolves a TimeSystem object and then invokes + * TimeConductorController#setTimeSystem + * @param key + * @see TimeConductorController#setTimeSystem + */ + TimeConductorController.prototype.selectTimeSystemByKey = function (key) { + var selected = this.timeSystems.filter(function (timeSystem) { + return timeSystem.metadata.key === key; + })[0]; + this.conductor.timeSystem(selected, selected.defaults().bounds); + }; + + /** + * Handles time system change from time conductor + * + * Sets the selected time system. Will populate form with the default + * bounds and deltas defined in the selected time system. + * + * @private + * @param newTimeSystem + */ + TimeConductorController.prototype.changeTimeSystem = function (newTimeSystem) { + if (newTimeSystem && (newTimeSystem !== this.$scope.timeSystemModel.selected)) { + if (newTimeSystem.defaults()) { + var deltas = newTimeSystem.defaults().deltas || {start: 0, end: 0}; + var bounds = newTimeSystem.defaults().bounds || {start: 0, end: 0}; + + this.setFormFromDeltas(deltas); + this.setFormFromBounds(bounds); + } + this.setFormFromTimeSystem(newTimeSystem); + } + }; + + return TimeConductorController; + } +); diff --git a/platform/features/conductor-v2/conductor/src/ui/TimeConductorControllerSpec.js b/platform/features/conductor-v2/conductor/src/ui/TimeConductorControllerSpec.js new file mode 100644 index 0000000000..39759c60f5 --- /dev/null +++ b/platform/features/conductor-v2/conductor/src/ui/TimeConductorControllerSpec.js @@ -0,0 +1,335 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web 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 Web 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(['./TimeConductorController'], function (TimeConductorController) { + describe("The time conductor controller", function () { + var mockScope; + var mockWindow; + var mockTimeConductor; + var mockConductorViewService; + var mockTimeSystems; + var controller; + + beforeEach(function () { + mockScope = jasmine.createSpyObj("$scope", [ + "$watch", + "$on" + ]); + mockWindow = jasmine.createSpyObj("$window", ["requestAnimationFrame"]); + mockTimeConductor = jasmine.createSpyObj( + "TimeConductor", + [ + "bounds", + "timeSystem", + "on", + "off" + ] + ); + mockTimeConductor.bounds.andReturn({start: undefined, end: undefined}); + + mockConductorViewService = jasmine.createSpyObj( + "ConductorViewService", + [ + "availableModes", + "mode", + "availableTimeSystems", + "deltas" + ] + ); + mockConductorViewService.availableModes.andReturn([]); + mockConductorViewService.availableTimeSystems.andReturn([]); + + mockTimeSystems = []; + }); + + function getListener(name) { + return mockTimeConductor.on.calls.filter(function (call) { + return call.args[0] === name; + })[0].args[1]; + } + + describe("", function () { + beforeEach(function () { + controller = new TimeConductorController( + mockScope, + mockWindow, + mockTimeConductor, + mockConductorViewService, + mockTimeSystems + ); + }); + + }); + + describe("when time conductor state changes", function () { + var mockFormat; + var mockDeltaFormat; + var defaultBounds; + var defaultDeltas; + var mockDefaults; + var timeSystem; + var tsListener; + + beforeEach(function () { + mockFormat = {}; + mockDeltaFormat = {}; + defaultBounds = { + start: 2, + end: 3 + }; + defaultDeltas = { + start: 10, + end: 20 + }; + mockDefaults = { + deltas: defaultDeltas, + bounds: defaultBounds + }; + timeSystem = { + formats: function () { + return [mockFormat]; + }, + deltaFormat: function () { + return mockDeltaFormat; + }, + defaults: function () { + return mockDefaults; + } + }; + + controller = new TimeConductorController( + mockScope, + mockWindow, + mockTimeConductor, + mockConductorViewService, + mockTimeSystems + ); + + tsListener = getListener("timeSystem"); + }); + + it("listens for changes to conductor state", function () { + expect(mockTimeConductor.on).toHaveBeenCalledWith("timeSystem", controller.changeTimeSystem); + expect(mockTimeConductor.on).toHaveBeenCalledWith("bounds", controller.setFormFromBounds); + }); + + it("deregisters conductor listens when scope is destroyed", function () { + expect(mockScope.$on).toHaveBeenCalledWith("$destroy", controller.destroy); + + controller.destroy(); + expect(mockTimeConductor.off).toHaveBeenCalledWith("timeSystem", controller.changeTimeSystem); + expect(mockTimeConductor.off).toHaveBeenCalledWith("bounds", controller.setFormFromBounds); + }); + + it("when time system changes, sets time system on scope", function () { + expect(tsListener).toBeDefined(); + tsListener(timeSystem); + + expect(mockScope.timeSystemModel).toBeDefined(); + expect(mockScope.timeSystemModel.selected).toBe(timeSystem); + expect(mockScope.timeSystemModel.format).toBe(mockFormat); + expect(mockScope.timeSystemModel.deltaFormat).toBe(mockDeltaFormat); + }); + + it("when time system changes, sets defaults on scope", function () { + expect(tsListener).toBeDefined(); + tsListener(timeSystem); + + expect(mockScope.boundsModel.start).toEqual(defaultBounds.start); + expect(mockScope.boundsModel.end).toEqual(defaultBounds.end); + + expect(mockScope.boundsModel.startDelta).toEqual(defaultDeltas.start); + expect(mockScope.boundsModel.endDelta).toEqual(defaultDeltas.end); + }); + + it("when bounds change, sets them on scope", function () { + var bounds = { + start: 1, + end: 2 + }; + + var boundsListener = getListener("bounds"); + expect(boundsListener).toBeDefined(); + boundsListener(bounds); + + expect(mockScope.boundsModel).toBeDefined(); + expect(mockScope.boundsModel.start).toEqual(bounds.start); + expect(mockScope.boundsModel.end).toEqual(bounds.end); + }); + }); + + describe("when user makes changes from UI", function () { + var mode = "realtime"; + var ts1Metadata; + var ts2Metadata; + var ts3Metadata; + var mockTimeSystemConstructors; + + beforeEach(function () { + mode = "realtime"; + ts1Metadata = { + 'key': 'ts1', + 'name': 'Time System One', + 'cssClass': 'cssClassOne' + }; + ts2Metadata = { + 'key': 'ts2', + 'name': 'Time System Two', + 'cssClass': 'cssClassTwo' + }; + ts3Metadata = { + 'key': 'ts3', + 'name': 'Time System Three', + 'cssClass': 'cssClassThree' + }; + mockTimeSystems = [ + { + metadata: ts1Metadata + }, + { + metadata: ts2Metadata + }, + { + metadata: ts3Metadata + } + ]; + + //Wrap in mock constructors + mockTimeSystemConstructors = mockTimeSystems.map(function (mockTimeSystem) { + return function () { + return mockTimeSystem; + }; + }); + }); + + it("sets the mode on scope", function () { + controller = new TimeConductorController( + mockScope, + mockWindow, + mockTimeConductor, + mockConductorViewService, + mockTimeSystemConstructors + ); + + mockConductorViewService.availableTimeSystems.andReturn(mockTimeSystems); + controller.setMode(mode); + + expect(mockScope.modeModel.selectedKey).toEqual(mode); + }); + + it("sets available time systems on scope when mode changes", function () { + controller = new TimeConductorController( + mockScope, + mockWindow, + mockTimeConductor, + mockConductorViewService, + mockTimeSystemConstructors + ); + + mockConductorViewService.availableTimeSystems.andReturn(mockTimeSystems); + controller.setMode(mode); + + expect(mockScope.timeSystemModel.options.length).toEqual(3); + expect(mockScope.timeSystemModel.options[0]).toEqual(ts1Metadata); + expect(mockScope.timeSystemModel.options[1]).toEqual(ts2Metadata); + expect(mockScope.timeSystemModel.options[2]).toEqual(ts3Metadata); + }); + + it("sets bounds on the time conductor", function () { + var formModel = { + start: 1, + end: 10 + }; + + + controller = new TimeConductorController( + mockScope, + mockWindow, + mockTimeConductor, + mockConductorViewService, + mockTimeSystemConstructors + ); + + controller.updateBoundsFromForm(formModel); + expect(mockTimeConductor.bounds).toHaveBeenCalledWith(formModel); + }); + + it("applies deltas when they change in form", function () { + var deltas = { + start: 1000, + end: 2000 + }; + var formModel = { + startDelta: deltas.start, + endDelta: deltas.end + }; + + controller = new TimeConductorController( + mockScope, + mockWindow, + mockTimeConductor, + mockConductorViewService, + mockTimeSystemConstructors + ); + + controller.updateDeltasFromForm(formModel); + expect(mockConductorViewService.deltas).toHaveBeenCalledWith(deltas); + }); + + it("sets the time system on the time conductor", function () { + var defaultBounds = { + start: 5, + end: 6 + }; + var timeSystem = { + metadata: { + key: 'testTimeSystem' + }, + defaults: function () { + return { + bounds: defaultBounds + }; + } + }; + + mockTimeSystems = [ + // Wrap as constructor function + function () { + return timeSystem; + } + ]; + + controller = new TimeConductorController( + mockScope, + mockWindow, + mockTimeConductor, + mockConductorViewService, + mockTimeSystems + ); + + controller.selectTimeSystemByKey('testTimeSystem'); + expect(mockTimeConductor.timeSystem).toHaveBeenCalledWith(timeSystem, defaultBounds); + }); + }); + + }); +}); diff --git a/platform/features/conductor-v2/conductor/src/ui/TimeConductorMode.js b/platform/features/conductor-v2/conductor/src/ui/TimeConductorMode.js new file mode 100644 index 0000000000..1f9d3656af --- /dev/null +++ b/platform/features/conductor-v2/conductor/src/ui/TimeConductorMode.js @@ -0,0 +1,201 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web 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 Web 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( + [], + function () { + + /** + * Supports mode-specific time conductor behavior. + * + * @constructor + * @param {TimeConductorMetadata} metadata + */ + function TimeConductorMode(metadata, conductor, timeSystems) { + this.conductor = conductor; + + this.mdata = metadata; + this.dlts = undefined; + this.source = undefined; + this.sourceUnlisten = undefined; + this.systems = timeSystems; + this.availableSources = undefined; + this.changeTimeSystem = this.changeTimeSystem.bind(this); + this.tick = this.tick.bind(this); + + //Set the time system initially + if (conductor.timeSystem()) { + this.changeTimeSystem(conductor.timeSystem()); + } + + //Listen for subsequent changes to time system + conductor.on('timeSystem', this.changeTimeSystem); + + if (metadata.key === 'fixed') { + //Fixed automatically supports all time systems + this.availableSystems = timeSystems; + } else { + this.availableSystems = timeSystems.filter(function (timeSystem) { + //Only include time systems that have tick sources that + // support the current mode + return timeSystem.tickSources().some(function (tickSource) { + return metadata.key === tickSource.metadata.mode; + }); + }); + } + } + + /** + * Get or set the currently selected time system + * @param timeSystem + * @returns {TimeSystem} the currently selected time system + */ + TimeConductorMode.prototype.changeTimeSystem = function (timeSystem) { + // On time system change, apply default deltas + var defaults = timeSystem.defaults() || { + bounds: { + start: 0, + end: 0 + }, + deltas: { + start: 0, + end: 0 + } + }; + + this.conductor.bounds(defaults.bounds); + this.deltas(defaults.deltas); + + // Tick sources are mode-specific, so restrict tick sources to only those supported by the current mode. + var key = this.mdata.key; + var tickSources = timeSystem.tickSources(); + if (tickSources) { + this.availableSources = tickSources.filter(function (source) { + return source.metadata.mode === key; + }); + } + + // Set an appropriate tick source from the new time system + this.tickSource(this.availableTickSources(timeSystem)[0]); + }; + + /** + * @returns {ModeMetadata} + */ + TimeConductorMode.prototype.metadata = function () { + return this.mdata; + }; + + TimeConductorMode.prototype.availableTimeSystems = function () { + return this.availableSystems; + }; + + /** + * Tick sources are mode-specific. This returns a filtered list of the tick sources available in the currently selected mode + * @param timeSystem + * @returns {Array.} + */ + TimeConductorMode.prototype.availableTickSources = function (timeSystem) { + return this.availableSources; + }; + + /** + * Get or set tick source. Setting tick source will also start + * listening to it and unlisten from any existing tick source + * @param tickSource + * @returns {TickSource} + */ + TimeConductorMode.prototype.tickSource = function (tickSource) { + if (arguments.length > 0) { + if (this.sourceUnlisten) { + this.sourceUnlisten(); + } + this.source = tickSource; + if (tickSource) { + this.sourceUnlisten = tickSource.listen(this.tick); + //Now following a tick source + this.conductor.follow(true); + } else { + this.conductor.follow(false); + } + } + return this.source; + }; + + TimeConductorMode.prototype.destroy = function () { + this.conductor.off('timeSystem', this.changeTimeSystem); + + if (this.sourceUnlisten) { + this.sourceUnlisten(); + } + }; + + /** + * @private + * @param {number} time some value that is valid in the current TimeSystem + */ + TimeConductorMode.prototype.tick = function (time) { + var deltas = this.deltas(); + var startTime = time; + var endTime = time; + + if (deltas) { + startTime = time - deltas.start; + endTime = time + deltas.end; + } + this.conductor.bounds({ + start: startTime, + end: endTime + }); + }; + + /** + * Get or set the current value for the deltas used by this time system. + * On change, the new deltas will be used to calculate and set the + * bounds on the time conductor. + * @param deltas + * @returns {TimeSystemDeltas} + */ + TimeConductorMode.prototype.deltas = function (deltas) { + if (arguments.length !== 0) { + var oldEnd = this.conductor.bounds().end; + + if (this.dlts && this.dlts.end !== undefined) { + //Calculate the previous raw end value (without delta) + oldEnd = oldEnd - this.dlts.end; + } + + this.dlts = deltas; + + var newBounds = { + start: oldEnd - this.dlts.start, + end: oldEnd + this.dlts.end + }; + + this.conductor.bounds(newBounds); + } + return this.dlts; + }; + + return TimeConductorMode; + } +); diff --git a/platform/features/conductor-v2/conductor/src/ui/TimeConductorModeSpec.js b/platform/features/conductor-v2/conductor/src/ui/TimeConductorModeSpec.js new file mode 100644 index 0000000000..6ea5ce79c2 --- /dev/null +++ b/platform/features/conductor-v2/conductor/src/ui/TimeConductorModeSpec.js @@ -0,0 +1,210 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web 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 Web 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(['./TimeConductorMode'], function (TimeConductorMode) { + describe("The Time Conductor Mode", function () { + var mockTimeConductor, + fixedModeMetaData, + mockTimeSystems, + fixedTimeSystem, + + realtimeModeMetaData, + realtimeTimeSystem, + mockTickSource, + + mockBounds, + mode; + + beforeEach(function () { + fixedModeMetaData = { + key: "fixed" + }; + realtimeModeMetaData = { + key: "realtime" + }; + mockBounds = { + start: 0, + end: 1 + }; + + fixedTimeSystem = jasmine.createSpyObj("timeSystem", [ + "defaults", + "tickSources" + ]); + fixedTimeSystem.tickSources.andReturn([]); + + mockTickSource = jasmine.createSpyObj("tickSource", [ + "listen" + ]); + mockTickSource.metadata = { + mode: "realtime" + }; + realtimeTimeSystem = jasmine.createSpyObj("realtimeTimeSystem", [ + "defaults", + "tickSources" + ]); + realtimeTimeSystem.tickSources.andReturn([mockTickSource]); + + //Do not return any time systems initially for a default + // construction configuration that works without any additional work + mockTimeSystems = []; + + mockTimeConductor = jasmine.createSpyObj("timeConductor", [ + "bounds", + "timeSystem", + "on", + "off", + "follow" + ]); + mockTimeConductor.bounds.andReturn(mockBounds); + }); + + it("Reacts to changes in conductor time system", function () { + mode = new TimeConductorMode(fixedModeMetaData, mockTimeConductor, mockTimeSystems); + expect(mockTimeConductor.on).toHaveBeenCalledWith("timeSystem", mode.changeTimeSystem); + }); + + it("Stops listening to time system changes on destroy", function () { + mode = new TimeConductorMode(fixedModeMetaData, mockTimeConductor, mockTimeSystems); + mode.destroy(); + expect(mockTimeConductor.off).toHaveBeenCalledWith("timeSystem", mode.changeTimeSystem); + }); + + it("Filters available time systems to those with tick sources that" + + " support this mode", function () { + mockTimeSystems = [fixedTimeSystem, realtimeTimeSystem]; + mode = new TimeConductorMode(realtimeModeMetaData, mockTimeConductor, mockTimeSystems); + + var availableTimeSystems = mode.availableTimeSystems(); + expect(availableTimeSystems.length).toBe(1); + expect(availableTimeSystems.indexOf(fixedTimeSystem)).toBe(-1); + expect(availableTimeSystems.indexOf(realtimeTimeSystem)).toBe(0); + }); + + describe("Changing the time system", function () { + var defaults; + + beforeEach(function () { + defaults = { + bounds: { + start: 1, + end: 2 + }, + deltas: { + start: 3, + end: 4 + } + }; + + fixedTimeSystem.defaults.andReturn(defaults); + + }); + it ("sets defaults from new time system", function () { + mode = new TimeConductorMode(fixedModeMetaData, mockTimeConductor, mockTimeSystems); + spyOn(mode, "deltas"); + mode.deltas.andCallThrough(); + + mode.changeTimeSystem(fixedTimeSystem); + expect(mockTimeConductor.bounds).toHaveBeenCalledWith(defaults.bounds); + expect(mode.deltas).toHaveBeenCalledWith(defaults.deltas); + }); + it ("If a tick source is available, sets the tick source", function () { + mode = new TimeConductorMode(realtimeModeMetaData, mockTimeConductor, mockTimeSystems); + mode.changeTimeSystem(realtimeTimeSystem); + + var currentTickSource = mode.tickSource(); + expect(currentTickSource).toBe(mockTickSource); + }); + }); + + describe("Setting a tick source", function () { + var mockUnlistener; + + beforeEach(function () { + mockUnlistener = jasmine.createSpy("unlistener"); + mockTickSource.listen.andReturn(mockUnlistener); + + mode = new TimeConductorMode(realtimeModeMetaData, mockTimeConductor, mockTimeSystems); + mode.tickSource(mockTickSource); + }); + + it ("Unlistens from old tick source", function () { + mode.tickSource(mockTickSource); + expect(mockUnlistener).toHaveBeenCalled(); + }); + + it ("Listens to new tick source", function () { + expect(mockTickSource.listen).toHaveBeenCalledWith(mode.tick); + }); + + it ("Sets 'follow' state on time conductor", function () { + expect(mockTimeConductor.follow).toHaveBeenCalledWith(true); + }); + + it ("on destroy, unlistens from tick source", function () { + mode.destroy(); + expect(mockUnlistener).toHaveBeenCalled(); + }); + }); + + describe("setting deltas", function () { + beforeEach(function () { + mode = new TimeConductorMode(realtimeModeMetaData, mockTimeConductor, mockTimeSystems); + }); + it ("sets the bounds on the time conductor based on new delta" + + " values", function () { + var deltas = { + start: 20, + end: 10 + }; + + mode.deltas(deltas); + + expect(mockTimeConductor.bounds).toHaveBeenCalledWith({ + start: mockBounds.end - deltas.start, + end: mockBounds.end + deltas.end + }); + }); + }); + + describe("ticking", function () { + beforeEach(function () { + mode = new TimeConductorMode(realtimeModeMetaData, mockTimeConductor, mockTimeSystems); + }); + it ("sets bounds based on current delta values", function () { + var deltas = { + start: 20, + end: 10 + }; + var time = 100; + + mode.deltas(deltas); + mode.tick(time); + + expect(mockTimeConductor.bounds).toHaveBeenCalledWith({ + start: time - deltas.start, + end: time + deltas.end + }); + }); + }); + }); +}); diff --git a/platform/features/conductor-v2/conductor/src/ui/TimeConductorValidation.js b/platform/features/conductor-v2/conductor/src/ui/TimeConductorValidation.js new file mode 100644 index 0000000000..3778c26a21 --- /dev/null +++ b/platform/features/conductor-v2/conductor/src/ui/TimeConductorValidation.js @@ -0,0 +1,69 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web 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 Web 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( + [], + function () { + + /** + * Form validation for the TimeConductorController. + * @param conductor + * @constructor + */ + function TimeConductorValidation(conductor) { + var self = this; + this.conductor = conductor; + + /* + * Bind all class functions to 'this' + */ + Object.keys(TimeConductorValidation.prototype).filter(function (key) { + return typeof TimeConductorValidation.prototype[key] === 'function'; + }).forEach(function (key) { + self[key] = self[key].bind(self); + }); + } + + /** + * Validation methods below are invoked directly from controls in the TimeConductor form + */ + TimeConductorValidation.prototype.validateStart = function (start) { + var bounds = this.conductor.bounds(); + return this.conductor.validateBounds({start: start, end: bounds.end}) === true; + }; + + TimeConductorValidation.prototype.validateEnd = function (end) { + var bounds = this.conductor.bounds(); + return this.conductor.validateBounds({start: bounds.start, end: end}) === true; + }; + + TimeConductorValidation.prototype.validateStartDelta = function (startDelta) { + return !isNaN(startDelta) && startDelta > 0; + }; + + TimeConductorValidation.prototype.validateEndDelta = function (endDelta) { + return !isNaN(endDelta) && endDelta >= 0; + }; + + return TimeConductorValidation; + } +); diff --git a/platform/features/conductor-v2/conductor/src/ui/TimeConductorValidationSpec.js b/platform/features/conductor-v2/conductor/src/ui/TimeConductorValidationSpec.js new file mode 100644 index 0000000000..833ba30993 --- /dev/null +++ b/platform/features/conductor-v2/conductor/src/ui/TimeConductorValidationSpec.js @@ -0,0 +1,73 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web 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 Web 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(['./TimeConductorValidation'], function (TimeConductorValidation) { + describe("The Time Conductor Validation class", function () { + var timeConductorValidation, + mockTimeConductor; + + beforeEach(function () { + mockTimeConductor = jasmine.createSpyObj("timeConductor", [ + "validateBounds", + "bounds" + ]); + timeConductorValidation = new TimeConductorValidation(mockTimeConductor); + }); + + describe("Validates start and end values using Time Conductor", function () { + beforeEach(function () { + var mockBounds = { + start: 10, + end: 20 + }; + + mockTimeConductor.bounds.andReturn(mockBounds); + + }); + it("Validates start values using Time Conductor", function () { + var startValue = 30; + timeConductorValidation.validateStart(startValue); + expect(mockTimeConductor.validateBounds).toHaveBeenCalled(); + }); + it("Validates end values using Time Conductor", function () { + var endValue = 40; + timeConductorValidation.validateEnd(endValue); + expect(mockTimeConductor.validateBounds).toHaveBeenCalled(); + }); + }); + + it("Validates that start delta is valid number > 0", function () { + expect(timeConductorValidation.validateStartDelta(-1)).toBe(false); + expect(timeConductorValidation.validateStartDelta("abc")).toBe(false); + expect(timeConductorValidation.validateStartDelta("1")).toBe(true); + expect(timeConductorValidation.validateStartDelta(1)).toBe(true); + }); + + it("Validates that end delta is valid number >= 0", function () { + expect(timeConductorValidation.validateEndDelta(-1)).toBe(false); + expect(timeConductorValidation.validateEndDelta("abc")).toBe(false); + expect(timeConductorValidation.validateEndDelta("1")).toBe(true); + expect(timeConductorValidation.validateEndDelta(0)).toBe(true); + expect(timeConductorValidation.validateEndDelta(1)).toBe(true); + }); + }); +}); diff --git a/platform/features/conductor-v2/conductor/src/ui/TimeConductorViewService.js b/platform/features/conductor-v2/conductor/src/ui/TimeConductorViewService.js new file mode 100644 index 0000000000..8cbc349520 --- /dev/null +++ b/platform/features/conductor-v2/conductor/src/ui/TimeConductorViewService.js @@ -0,0 +1,202 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web 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 Web 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( + [ + './TimeConductorMode' + ], + function (TimeConductorMode) { + + /** + * A class representing the state of the time conductor view. This + * exposes details of the UI that are not represented on the + * TimeConductor API itself such as modes and deltas. + * + * @param conductor + * @param timeSystems + * @constructor + */ + function TimeConductorViewService(conductor, timeSystems) { + this.systems = timeSystems.map(function (timeSystemConstructor) { + return timeSystemConstructor(); + }); + + this.conductor = conductor; + this.currentMode = undefined; + + /** + * @typedef {object} ModeMetadata + * @property {string} key A unique identifying key for this mode + * @property {string} cssClass The css class for the glyph + * representing this mode + * @property {string} label A short label for this mode + * @property {string} name A longer name for the mode + * @property {string} description A description of the mode + */ + this.availModes = { + 'fixed': { + key: 'fixed', + cssclass: 'icon-calendar', + label: 'Fixed', + name: 'Fixed Timespan Mode', + description: 'Query and explore data that falls between two fixed datetimes.' + } + }; + + function hasTickSource(sourceType, timeSystem) { + return timeSystem.tickSources().some(function (tickSource) { + return tickSource.metadata.mode === sourceType; + }); + } + + var timeSystemsForMode = function (sourceType) { + return this.systems.filter(hasTickSource.bind(this, sourceType)); + }.bind(this); + + //Only show 'real-time mode' if appropriate time systems available + if (timeSystemsForMode('realtime').length > 0) { + var realtimeMode = { + key: 'realtime', + cssclass: 'icon-clock', + label: 'Real-time', + name: 'Real-time Mode', + description: 'Monitor real-time streaming data as it comes in. The Time Conductor and displays will automatically advance themselves based on a UTC clock.' + }; + this.availModes[realtimeMode.key] = realtimeMode; + } + + //Only show 'LAD mode' if appropriate time systems available + if (timeSystemsForMode('lad').length > 0) { + var ladMode = { + key: 'lad', + cssclass: 'icon-database', + label: 'LAD', + name: 'LAD Mode', + description: 'Latest Available Data mode monitors real-time streaming data as it comes in. The Time Conductor and displays will only advance when data becomes available.' + }; + this.availModes[ladMode.key] = ladMode; + } + } + + /** + * Getter/Setter for the Time Conductor Mode. Modes determine the + * behavior of the time conductor, especially with regards to the + * bounds and how they change with time. + * + * In fixed mode, the bounds do not change with time, but can be + * modified by the used + * + * In realtime mode, the bounds change with time. Bounds are not + * directly modifiable by the user, however deltas can be. + * + * In Latest Available Data (LAD) mode, the bounds are updated when + * data is received. As with realtime mode the + * + * @param {string} newModeKey One of 'fixed', 'realtime', or 'LAD' + * @returns {string} the current mode, one of 'fixed', 'realtime', + * or 'LAD'. + * + */ + TimeConductorViewService.prototype.mode = function (newModeKey) { + function contains(timeSystems, ts) { + return timeSystems.filter(function (t) { + return t.metadata.key === ts.metadata.key; + }).length > 0; + } + + if (arguments.length === 1) { + var timeSystem = this.conductor.timeSystem(); + var modes = this.availableModes(); + var modeMetaData = modes[newModeKey]; + + if (this.currentMode) { + this.currentMode.destroy(); + } + this.currentMode = new TimeConductorMode(modeMetaData, this.conductor, this.systems); + + // If no time system set on time conductor, or the currently selected time system is not available in + // the new mode, default to first available time system + if (!timeSystem || !contains(this.currentMode.availableTimeSystems(), timeSystem)) { + timeSystem = this.currentMode.availableTimeSystems()[0]; + this.conductor.timeSystem(timeSystem, timeSystem.defaults().bounds); + } + } + return this.currentMode ? this.currentMode.metadata().key : undefined; + }; + + /** + * @typedef {object} Delta + * @property {number} start Used to set the start bound of the + * TimeConductor on tick. A positive value that will be subtracted + * from the value provided by a tick source to determine the start + * bound. + * @property {number} end Used to set the end bound of the + * TimeConductor on tick. A positive value that will be added + * from the value provided by a tick source to determine the start + * bound. + */ + /** + * Deltas define the offset from the latest time value provided by + * the current tick source. Deltas are only valid in realtime or LAD + * modes. + * + * Realtime mode: + * - start: A time in ms before now which will be used to + * determine the 'start' bound on tick + * - end: A time in ms after now which will be used to determine + * the 'end' bound on tick + * + * LAD mode: + * - start: A time in ms before the timestamp of the last data + * received which will be used to determine the 'start' bound on + * tick + * - end: A time in ms after the timestamp of the last data received + * which will be used to determine the 'end' bound on tick + * @returns {Delta} current value of the deltas + */ + TimeConductorViewService.prototype.deltas = function () { + //Deltas stored on mode. Use .apply to preserve arguments + return this.currentMode.deltas.apply(this.currentMode, arguments); + }; + + /** + * Availability of modes depends on the time systems and tick + * sources available. For example, Latest Available Data mode will + * not be available if there are no time systems and tick sources + * that support LAD mode. + * @returns {ModeMetadata[]} + */ + TimeConductorViewService.prototype.availableModes = function () { + return this.availModes; + }; + + /** + * Availability of time systems depends on the currently selected + * mode. Time systems and tick sources are mode dependent + */ + TimeConductorViewService.prototype.availableTimeSystems = function () { + return this.currentMode.availableTimeSystems(); + }; + + return TimeConductorViewService; + } +); diff --git a/platform/features/conductor-v2/conductor/src/ui/TimeConductorViewServiceSpec.js b/platform/features/conductor-v2/conductor/src/ui/TimeConductorViewServiceSpec.js new file mode 100644 index 0000000000..39e7810320 --- /dev/null +++ b/platform/features/conductor-v2/conductor/src/ui/TimeConductorViewServiceSpec.js @@ -0,0 +1,185 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web 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 Web 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(['./TimeConductorViewService'], function (TimeConductorViewService) { + describe("The Time Conductor view service", function () { + var mockTimeConductor; + var basicTimeSystem; + var tickingTimeSystem; + var viewService; + var tickingTimeSystemDefaults; + + function mockConstructor(object) { + return function () { + return object; + }; + } + + beforeEach(function () { + mockTimeConductor = jasmine.createSpyObj("timeConductor", [ + "timeSystem", + "bounds", + "follow", + "on", + "off" + ]); + + basicTimeSystem = jasmine.createSpyObj("basicTimeSystem", [ + "tickSources", + "defaults" + ]); + basicTimeSystem.metadata = { + key: "basic" + }; + basicTimeSystem.tickSources.andReturn([]); + basicTimeSystem.defaults.andReturn({ + bounds: { + start: 0, + end: 1 + }, + deltas: { + start: 0, + end: 0 + } + }); + //Initialize conductor + mockTimeConductor.timeSystem.andReturn(basicTimeSystem); + mockTimeConductor.bounds.andReturn({start: 0, end: 1}); + + tickingTimeSystem = jasmine.createSpyObj("tickingTimeSystem", [ + "tickSources", + "defaults" + ]); + tickingTimeSystem.metadata = { + key: "ticking" + }; + tickingTimeSystemDefaults = { + bounds: { + start: 100, + end: 200 + }, + deltas: { + start: 1000, + end: 500 + } + }; + tickingTimeSystem.defaults.andReturn(tickingTimeSystemDefaults); + }); + + it("At a minimum supports fixed mode", function () { + var mockTimeSystems = [mockConstructor(basicTimeSystem)]; + viewService = new TimeConductorViewService(mockTimeConductor, mockTimeSystems); + + var availableModes = viewService.availableModes(); + expect(availableModes.fixed).toBeDefined(); + }); + + it("Supports realtime mode if appropriate tick source(s) availables", function () { + var mockTimeSystems = [mockConstructor(tickingTimeSystem)]; + var mockRealtimeTickSource = { + metadata: { + mode: 'realtime' + } + }; + tickingTimeSystem.tickSources.andReturn([mockRealtimeTickSource]); + + viewService = new TimeConductorViewService(mockTimeConductor, mockTimeSystems); + + var availableModes = viewService.availableModes(); + expect(availableModes.realtime).toBeDefined(); + }); + + it("Supports LAD mode if appropriate tick source(s) available", function () { + var mockTimeSystems = [mockConstructor(tickingTimeSystem)]; + var mockLADTickSource = { + metadata: { + mode: 'lad' + } + }; + tickingTimeSystem.tickSources.andReturn([mockLADTickSource]); + + viewService = new TimeConductorViewService(mockTimeConductor, mockTimeSystems); + + var availableModes = viewService.availableModes(); + expect(availableModes.lad).toBeDefined(); + }); + + describe("when mode is changed", function () { + + it("destroys previous mode", function () { + var mockTimeSystems = [mockConstructor(basicTimeSystem)]; + + var oldMode = jasmine.createSpyObj("conductorMode", [ + "destroy" + ]); + + viewService = new TimeConductorViewService(mockTimeConductor, mockTimeSystems); + viewService.currentMode = oldMode; + viewService.mode('fixed'); + expect(oldMode.destroy).toHaveBeenCalled(); + }); + + describe("the time system", function () { + it("is retained if available in new mode", function () { + var mockTimeSystems = [mockConstructor(basicTimeSystem), mockConstructor(tickingTimeSystem)]; + var mockRealtimeTickSource = { + metadata: { + mode: 'realtime' + }, + listen: function () {} + }; + tickingTimeSystem.tickSources.andReturn([mockRealtimeTickSource]); + + viewService = new TimeConductorViewService(mockTimeConductor, mockTimeSystems); + + //Set time system to one known to support realtime mode + mockTimeConductor.timeSystem.andReturn(tickingTimeSystem); + + //Select realtime mode + mockTimeConductor.timeSystem.reset(); + viewService.mode('realtime'); + expect(mockTimeConductor.timeSystem).not.toHaveBeenCalledWith(tickingTimeSystem, tickingTimeSystemDefaults.bounds); + }); + it("is defaulted if selected time system not available in new mode", function () { + var mockTimeSystems = [mockConstructor(basicTimeSystem), mockConstructor(tickingTimeSystem)]; + var mockRealtimeTickSource = { + metadata: { + mode: 'realtime' + }, + listen: function () {} + }; + tickingTimeSystem.tickSources.andReturn([mockRealtimeTickSource]); + + viewService = new TimeConductorViewService(mockTimeConductor, mockTimeSystems); + + //Set time system to one known to not support realtime mode + mockTimeConductor.timeSystem.andReturn(basicTimeSystem); + + //Select realtime mode + mockTimeConductor.timeSystem.reset(); + viewService.mode('realtime'); + expect(mockTimeConductor.timeSystem).toHaveBeenCalledWith(tickingTimeSystem, tickingTimeSystemDefaults.bounds); + }); + }); + }); + }); +}); diff --git a/platform/features/conductor-v2/utcTimeSystem/bundle.js b/platform/features/conductor-v2/utcTimeSystem/bundle.js new file mode 100644 index 0000000000..5b13e605d4 --- /dev/null +++ b/platform/features/conductor-v2/utcTimeSystem/bundle.js @@ -0,0 +1,40 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web 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 Web 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([ + "./src/UTCTimeSystem", + 'legacyRegistry' +], function ( + UTCTimeSystem, + legacyRegistry +) { + legacyRegistry.register("platform/features/conductor-v2/utcTimeSystem", { + "extensions": { + "timeSystems": [ + { + "implementation": UTCTimeSystem, + "depends": ["$timeout"] + } + ] + } + }); +}); diff --git a/platform/features/conductor-v2/utcTimeSystem/src/UTCTimeSystem.js b/platform/features/conductor-v2/utcTimeSystem/src/UTCTimeSystem.js new file mode 100644 index 0000000000..b6e969c3eb --- /dev/null +++ b/platform/features/conductor-v2/utcTimeSystem/src/UTCTimeSystem.js @@ -0,0 +1,78 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web 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 Web 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([ + '../../conductor/src/timeSystems/TimeSystem', + '../../conductor/src/timeSystems/LocalClock' +], function (TimeSystem, LocalClock) { + var FIFTEEN_MINUTES = 15 * 60 * 1000, + DEFAULT_PERIOD = 1000; + + /** + * This time system supports UTC dates and provides a ticking clock source. + * @implements TimeSystem + * @constructor + */ + function UTCTimeSystem($timeout) { + TimeSystem.call(this); + + /** + * Some metadata, which will be used to identify the time system in + * the UI + * @type {{key: string, name: string, cssclass: string}} + */ + this.metadata = { + 'key': 'utc', + 'name': 'UTC', + 'cssclass': 'icon-clock' + }; + + this.fmts = ['utc']; + this.sources = [new LocalClock($timeout, DEFAULT_PERIOD)]; + } + + UTCTimeSystem.prototype = Object.create(TimeSystem.prototype); + + UTCTimeSystem.prototype.formats = function () { + return this.fmts; + }; + + UTCTimeSystem.prototype.deltaFormat = function () { + return 'duration'; + }; + + UTCTimeSystem.prototype.tickSources = function () { + return this.sources; + }; + + UTCTimeSystem.prototype.defaults = function (key) { + var now = Math.ceil(Date.now() / 1000) * 1000; + return { + key: 'utc-default', + name: 'UTC time system defaults', + deltas: {start: FIFTEEN_MINUTES, end: 0}, + bounds: {start: now - FIFTEEN_MINUTES, end: now} + }; + }; + + return UTCTimeSystem; +}); diff --git a/platform/features/conductor-v2/utcTimeSystem/src/UTCTimeSystemSpec.js b/platform/features/conductor-v2/utcTimeSystem/src/UTCTimeSystemSpec.js new file mode 100644 index 0000000000..808eade72e --- /dev/null +++ b/platform/features/conductor-v2/utcTimeSystem/src/UTCTimeSystemSpec.js @@ -0,0 +1,46 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web 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 Web 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(['./UTCTimeSystem'], function (UTCTimeSystem) { + describe("The UTCTimeSystem class", function () { + var timeSystem, + mockTimeout; + + beforeEach(function () { + mockTimeout = jasmine.createSpy("timeout"); + timeSystem = new UTCTimeSystem(mockTimeout); + }); + + it("defines at least one format", function () { + expect(timeSystem.formats().length).toBeGreaterThan(0); + }); + + it("defines a tick source", function () { + var tickSources = timeSystem.tickSources(); + expect(tickSources.length).toBeGreaterThan(0); + }); + + it("defines some defaults", function () { + expect(timeSystem.defaults()).toBeDefined(); + }); + }); +}); diff --git a/platform/features/conductor/bundle.js b/platform/features/conductor/bundle.js index 5def15a6b9..c1349b4e58 100644 --- a/platform/features/conductor/bundle.js +++ b/platform/features/conductor/bundle.js @@ -47,6 +47,11 @@ define([ ] } ], + "stylesheets": [ + { + "stylesheetUrl": "css/time-conductor.css" + } + ], "components": [ { "type": "decorator", diff --git a/platform/features/conductor/res/sass/time-conductor.scss b/platform/features/conductor/res/sass/time-conductor.scss new file mode 100644 index 0000000000..bc50b42da5 --- /dev/null +++ b/platform/features/conductor/res/sass/time-conductor.scss @@ -0,0 +1,300 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web 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 Web 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. + *****************************************************************************/ +@import "bourbon"; + +@import "../../../../commonUI/general/res/sass/constants"; +@import "../../../../commonUI/general/res/sass/mixins"; +@import "../../../../commonUI/general/res/sass/mobile/constants"; +@import "../../../../commonUI/general/res/sass/mobile/mixins"; +@import "../../../../commonUI/themes/espresso/res/sass/constants"; +@import "../../../../commonUI/themes/espresso/res/sass/mixins"; + +$ueTimeConductorH: (33px, 18px, 20px); + +@mixin toiLineHovEffects() { + &:before, + &:after { + background-color: $timeControllerToiLineColorHov; + } +} + +.l-time-controller { + $minW: 500px; + $knobHOffset: 0px; + $knobM: ($sliderKnobW + $knobHOffset) * -1; + $rangeValPad: $interiorMargin; + $rangeValOffset: $sliderKnobW + $interiorMargin; + $timeRangeSliderLROffset: 150px + ($sliderKnobW * 2); + $r1H: nth($ueTimeConductorH,1); + $r2H: nth($ueTimeConductorH,2); + $r3H: nth($ueTimeConductorH,3); + + min-width: $minW; + font-size: 0.8rem; + + .l-time-range-inputs-holder, + .l-time-range-slider-holder, + .l-time-range-ticks-holder + { + box-sizing: border-box; + position: relative; + &:not(:first-child) { + margin-top: $interiorMargin; + } + } + .l-time-range-slider, + .l-time-range-ticks { + @include absPosDefault(0, visible); + left: $timeRangeSliderLROffset; right: $timeRangeSliderLROffset; + } + + .l-time-range-inputs-holder { + border-top: 1px solid $colorInteriorBorder; + padding-top: $interiorMargin; + &.l-flex-row, + .l-flex-row { + @include align-items(center); + .flex-elem { + height: auto; + line-height: normal; + } + } + .type-icon { + font-size: 120%; + vertical-align: middle; + } + .l-time-range-input-w, + .l-time-range-inputs-elem { + margin-right: $interiorMargin; + .lbl { + color: $colorPlotLabelFg; + } + .ui-symbol.icon { + font-size: 11px; + } + } + .l-time-range-input-w { + // Wraps a datetime text input field + position: relative; + input[type="text"] { + width: 200px; + &.picker-icon { + padding-right: 20px; + } + } + .icon-calendar { + position: absolute; + right: 5px; + top: 5px; + } + } + } + + .l-time-range-slider-holder { + height: $r2H; + .range-holder { + box-shadow: none; + background: none; + border: none; + .range { + .toi-line { + $myC: $timeControllerToiLineColor; + $myW: 8px; + @include transform(translateX(50%)); + position: absolute; + top: 0; right: 0; bottom: 0px; left: auto; + width: $myW; + height: auto; + z-index: 2; + &:before { + // Vert line + background-color: $myC; + position: absolute; + content: ""; + top: 0; right: auto; bottom: -10px; left: floor($myW/2) - 1; + width: 1px; + } + } + &:hover .toi-line { + @include toiLineHovEffects; + } + } + } + &:not(:active) { + .knob, + .range { + @include transition-property(left, right); + @include transition-duration(500ms); + @include transition-timing-function(ease-in-out); + } + } + } + + .l-time-range-ticks-holder { + height: $r3H; + .l-time-range-ticks { + border-top: 1px solid $colorTick; + .tick { + background-color: $colorTick; + border:none; + height: 5px; + width: 1px; + margin-left: -1px; + position: absolute; + &:first-child { + margin-left: 0; + } + .l-time-range-tick-label { + @include webkitProp(transform, translateX(-50%)); + color: $colorPlotLabelFg; + display: inline-block; + font-size: 0.7rem; + position: absolute; + top: 5px; + white-space: nowrap; + z-index: 2; + } + } + } + } + + .knob { + z-index: 2; + &:before { + $mTB: 2px; + $grippyW: 3px; + $mLR: ($sliderKnobW - $grippyW)/2; + @include bgStripes($c: pullForward($sliderColorKnob, 20%), $a: 1, $bgsize: 4px, $angle: 0deg); + content: ''; + display: block; + position: absolute; + top: $mTB; right: $mLR; bottom: $mTB; left: $mLR; + } + .range-value { + @include trans-prop-nice-fade(.25s); + font-size: 0.7rem; + position: absolute; + height: $r2H; + line-height: $r2H; + white-space: nowrap; + z-index: 1; + } + &:hover { + .range-value { + color: $sliderColorKnobHov; + } + } + &.knob-l { + margin-left: $knobM; + .range-value { + text-align: right; + right: $rangeValOffset; + } + } + &.knob-r { + margin-right: $knobM; + .range-value { + left: $rangeValOffset; + } + &:hover + .range-holder .range .toi-line { + @include toiLineHovEffects; + } + } + } + + .l-time-domain-selector { + position: absolute; + right: 0px; + top: $interiorMargin; + } + +} + +.s-time-range-val { + border-radius: $controlCr; + background-color: $colorInputBg; + padding: 1px 1px 0 $interiorMargin; +} + +/******************************************************************** MOBILE */ + +@include phoneandtablet { + .l-time-controller { + min-width: 0; + .l-time-range-slider-holder, + .l-time-range-ticks-holder { + display: none; + } + } +} + +@include phone { + .l-time-controller { + .l-time-range-inputs-holder { + &.l-flex-row, + .l-flex-row { + @include align-items(flex-start); + } + .l-time-range-inputs-elem { + &.type-icon { + margin-top: 3px; + } + } + .t-inputs-w, + .l-time-range-inputs-elem { + @include flex-direction(column); + .l-time-range-input-w:not(:first-child) { + &:not(:first-child) { + margin-top: $interiorMargin; + } + margin-right: 0; + } + .l-time-range-inputs-elem { + &.lbl { display: none; } + } + } + } + } +} + +@include phonePortrait { + .l-time-controller { + .l-time-range-inputs-holder { + .t-inputs-w, + .l-time-range-inputs-elem { + @include flex(1 1 auto); + padding-top: 25px; // Make room for the ever lovin' Time Domain Selector + .flex-elem { + @include flex(1 1 auto); + width: 100%; + } + input[type="text"] { + width: 100%; + } + } + } + } + .l-time-domain-selector { + right: auto; + left: 20px; + } +} \ No newline at end of file diff --git a/platform/features/layout/res/templates/layout.html b/platform/features/layout/res/templates/layout.html index ed1f9a50c8..b2ffa16808 100644 --- a/platform/features/layout/res/templates/layout.html +++ b/platform/features/layout/res/templates/layout.html @@ -26,11 +26,10 @@ ng-repeat="childObject in composition" ng-style="controller.getFrameStyle(childObject.getId())"> -
- - -
+ + thead').first(); + this.sizingTableBody = this.element.find('.sizing-table>tbody').first(); this.$scope.sizingRow = {}; this.scrollable.on('scroll', this.onScroll.bind(this)); @@ -261,8 +261,8 @@ define( * for individual rows. */ MCTTableController.prototype.setElementSizes = function () { - var thead = this.thead, - tbody = this.tbody, + var thead = this.resultsHeader, + tbody = this.sizingTableBody, firstRow = tbody.find('tr'), column = firstRow.find('td'), headerHeight = thead.prop('offsetHeight'), diff --git a/platform/features/table/src/controllers/TelemetryTableController.js b/platform/features/table/src/controllers/TelemetryTableController.js index ebf6d690d3..1e77e16278 100644 --- a/platform/features/table/src/controllers/TelemetryTableController.js +++ b/platform/features/table/src/controllers/TelemetryTableController.js @@ -96,11 +96,6 @@ define( } }) ); - - //Change of bounds in time conductor - this.changeListeners.push(this.$scope.$on('telemetry:display:bounds', - this.subscribe.bind(this)) - ); }; /** diff --git a/platform/features/table/test/controllers/MCTTableControllerSpec.js b/platform/features/table/test/controllers/MCTTableControllerSpec.js index 94ce7c71c2..285403423f 100644 --- a/platform/features/table/test/controllers/MCTTableControllerSpec.js +++ b/platform/features/table/test/controllers/MCTTableControllerSpec.js @@ -22,9 +22,16 @@ define( [ + "zepto", "../../src/controllers/MCTTableController" ], - function (MCTTableController) { + function ($, MCTTableController) { + + var MOCK_ELEMENT_TEMPLATE = + '
' + + '
' + + '
' + + '
'; describe('The MCTTable Controller', function () { @@ -55,19 +62,7 @@ define( watches[event] = callback; }); - mockElement = jasmine.createSpyObj('element', [ - 'find', - 'prop', - 'on' - ]); - mockElement.find.andReturn(mockElement); - mockElement.prop.andReturn(0); - mockElement[0] = { - scrollTop: 0, - scrollHeight: 500, - offsetHeight: 1000 - }; - + mockElement = $(MOCK_ELEMENT_TEMPLATE); mockExportService = jasmine.createSpyObj('exportService', [ 'exportCSV' ]); diff --git a/platform/features/timeline/bundle.js b/platform/features/timeline/bundle.js index 12effc50a3..42b3c948f9 100644 --- a/platform/features/timeline/bundle.js +++ b/platform/features/timeline/bundle.js @@ -182,6 +182,16 @@ define([ "capacity" ], "pattern": "^-?\\d+(\\.\\d*)?$" + }, + { + "name": "Battery starting SOC (%)", + "control": "textfield", + "required": false, + "conversion": "number", + "property": [ + "startingSOC" + ], + "pattern": "^([0-9](\\.\\d*)?|[1-9][0-9](\\.\\d*)?|100)%?$" } ], "model": { diff --git a/platform/features/timeline/src/capabilities/GraphCapability.js b/platform/features/timeline/src/capabilities/GraphCapability.js index d9810005a3..181c909f79 100644 --- a/platform/features/timeline/src/capabilities/GraphCapability.js +++ b/platform/features/timeline/src/capabilities/GraphCapability.js @@ -37,7 +37,8 @@ define( // Build graphs for this group of utilizations function buildGraphs(utilizations) { var utilizationMap = {}, - result = {}; + result = {}, + startingSOC; // Bucket utilizations by type utilizations.forEach(function (u) { @@ -55,12 +56,14 @@ define( if (domainObject.getModel().type === 'timeline' && result.power && domainObject.getModel().capacity > 0) { + startingSOC = isNaN(parseFloat(domainObject.getModel().startingSOC)) ? + 100 : parseFloat(domainObject.getModel().startingSOC); result.battery = new CumulativeGraph( result.power, 0, domainObject.getModel().capacity, // Watts - domainObject.getModel().capacity, + (startingSOC / 100) * domainObject.getModel().capacity, 1 / 3600000 // millis-to-hour (since units are watt-hours) ); } diff --git a/platform/features/timeline/src/controllers/drag/TimelineDragHandler.js b/platform/features/timeline/src/controllers/drag/TimelineDragHandler.js index 745c3cc5cf..d7bb73c6ec 100644 --- a/platform/features/timeline/src/controllers/drag/TimelineDragHandler.js +++ b/platform/features/timeline/src/controllers/drag/TimelineDragHandler.js @@ -35,7 +35,6 @@ define( */ function TimelineDragHandler(domainObject, objectLoader) { var timespans = {}, - persists = {}, mutations = {}, compositions = {}, dirty = {}; @@ -56,8 +55,6 @@ define( timespans[id] = timespan; // And its mutation capability mutations[id] = object.getCapability('mutation'); - // Also cache the persistence capability for later - persists[id] = object.getCapability('persistence'); // And the composition, for bulk moves compositions[id] = object.getModel().composition || []; }); @@ -71,19 +68,14 @@ define( } // Persist changes for objects by id (when dragging ends) - function doPersist(id) { - var persistence = persists[id], - mutation = mutations[id]; + function finalMutate(id) { + var mutation = mutations[id]; if (mutation) { // Mutate just to update the timestamp (since we // explicitly don't do this during the drag to // avoid firing a ton of refreshes.) mutation.mutate(function () {}); } - if (persistence) { - // Persist the changes - persistence.persist(); - } } // Use the object loader to get objects which have timespans @@ -105,7 +97,7 @@ define( */ persist: function () { // Persist every dirty object... - Object.keys(dirty).forEach(doPersist); + Object.keys(dirty).forEach(finalMutate); // Clear out the dirty list dirty = {}; }, diff --git a/platform/features/timeline/src/controllers/swimlane/TimelineSwimlaneDecorator.js b/platform/features/timeline/src/controllers/swimlane/TimelineSwimlaneDecorator.js index b082d4e1c0..bafd5cd71c 100644 --- a/platform/features/timeline/src/controllers/swimlane/TimelineSwimlaneDecorator.js +++ b/platform/features/timeline/src/controllers/swimlane/TimelineSwimlaneDecorator.js @@ -35,7 +35,6 @@ define( var domainObject = swimlane && swimlane.domainObject, model = (domainObject && domainObject.getModel()) || {}, mutator = domainObject && domainObject.getCapability('mutation'), - persister = domainObject && domainObject.getCapability('persistence'), type = domainObject && domainObject.getCapability('type'), dropHandler = new TimelineSwimlaneDropHandler(swimlane); @@ -48,7 +47,7 @@ define( mutator.mutate(function (m) { m.relationships = m.relationships || {}; m.relationships[ACTIVITY_RELATIONSHIP] = value; - }).then(persister.persist); + }); } } // ...otherwise, use as a getter @@ -63,7 +62,7 @@ define( // Update the link mutator.mutate(function (m) { m.link = value; - }).then(persister.persist); + }); } return model.link; } @@ -84,7 +83,7 @@ define( } // Activities should have the Activity Modes and Activity Link dialog - if (type && type.instanceOf("activity") && mutator && persister) { + if (type && type.instanceOf("activity") && mutator) { swimlane.modes = modes; swimlane.link = link; } diff --git a/platform/features/timeline/src/controllers/swimlane/TimelineSwimlaneDropHandler.js b/platform/features/timeline/src/controllers/swimlane/TimelineSwimlaneDropHandler.js index affa631863..8aa642f6fb 100644 --- a/platform/features/timeline/src/controllers/swimlane/TimelineSwimlaneDropHandler.js +++ b/platform/features/timeline/src/controllers/swimlane/TimelineSwimlaneDropHandler.js @@ -29,16 +29,6 @@ define( * @constructor */ function TimelineSwimlaneDropHandler(swimlane) { - // Utility function; like $q.when, but synchronous (to reduce - // performance impact when wrapping synchronous values) - function asPromise(value) { - return (value && value.then) ? value : { - then: function (callback) { - return asPromise(callback(value)); - } - }; - } - // Check if we are in edit mode (also check parents) function inEditMode() { return swimlane.domainObject.hasCapability('editor') && @@ -75,16 +65,7 @@ define( // Initiate mutation of a domain object function doMutate(domainObject, mutator) { - return asPromise( - domainObject.useCapability("mutation", mutator) - ).then(function () { - // Persist the results of mutation - var persistence = domainObject.getCapability("persistence"); - if (persistence) { - // Persist the changes - persistence.persist(); - } - }); + return domainObject.useCapability("mutation", mutator); } // Check if this swimlane is in a state where a drop-after will diff --git a/platform/features/timeline/test/capabilities/GraphCapabilitySpec.js b/platform/features/timeline/test/capabilities/GraphCapabilitySpec.js index 71a6de2ade..e2ccd376e2 100644 --- a/platform/features/timeline/test/capabilities/GraphCapabilitySpec.js +++ b/platform/features/timeline/test/capabilities/GraphCapabilitySpec.js @@ -101,6 +101,7 @@ define( it("provides a battery graph for timelines with capacity", function () { var mockCallback = jasmine.createSpy('callback'); testModel.capacity = 1000; + testModel.startingSOC = 100; testModel.type = "timeline"; mockDomainObject.useCapability.andReturn(asPromise([ { key: "power", start: 0, end: 15 } diff --git a/platform/features/timeline/test/controllers/drag/TimelineDragHandlerSpec.js b/platform/features/timeline/test/controllers/drag/TimelineDragHandlerSpec.js index 7ea7905971..03bb50504a 100644 --- a/platform/features/timeline/test/controllers/drag/TimelineDragHandlerSpec.js +++ b/platform/features/timeline/test/controllers/drag/TimelineDragHandlerSpec.js @@ -32,7 +32,6 @@ define( mockDomainObjects, mockTimespans, mockMutations, - mockPersists, mockCallback, handler; @@ -66,7 +65,6 @@ define( mockDomainObj.useCapability.andReturn(asPromise(mockTimespans[id])); mockDomainObj.getCapability.andCallFake(function (c) { return { - persistence: mockPersists[id], mutation: mockMutations[id] }[c]; }); @@ -76,17 +74,12 @@ define( beforeEach(function () { mockTimespans = {}; - mockPersists = {}; mockMutations = {}; ['a', 'b', 'c', 'd', 'e', 'f'].forEach(function (id, index) { mockTimespans[id] = jasmine.createSpyObj( 'timespan-' + id, ['getStart', 'getEnd', 'getDuration', 'setStart', 'setEnd', 'setDuration'] ); - mockPersists[id] = jasmine.createSpyObj( - 'persistence-' + id, - ['persist'] - ); mockMutations[id] = jasmine.createSpyObj( 'mutation-' + id, ['mutate'] @@ -209,20 +202,6 @@ define( expect(mockTimespans.c.setStart).toHaveBeenCalledWith(1000); }); - it("persists mutated objects", function () { - handler.start('a', 20); - handler.end('b', 50); - handler.duration('c', 30); - handler.persist(); - expect(mockPersists.a.persist).toHaveBeenCalled(); - expect(mockPersists.b.persist).toHaveBeenCalled(); - expect(mockPersists.c.persist).toHaveBeenCalled(); - expect(mockPersists.d.persist).not.toHaveBeenCalled(); - expect(mockPersists.e.persist).not.toHaveBeenCalled(); - expect(mockPersists.f.persist).not.toHaveBeenCalled(); - }); - - }); } ); diff --git a/platform/features/timeline/test/controllers/swimlane/TimelineSwimlaneDecoratorSpec.js b/platform/features/timeline/test/controllers/swimlane/TimelineSwimlaneDecoratorSpec.js index fb251c22cc..2d777656ee 100644 --- a/platform/features/timeline/test/controllers/swimlane/TimelineSwimlaneDecoratorSpec.js +++ b/platform/features/timeline/test/controllers/swimlane/TimelineSwimlaneDecoratorSpec.js @@ -50,10 +50,6 @@ define( 'mutation', ['mutate'] ); - mockCapabilities.persistence = jasmine.createSpyObj( - 'persistence', - ['persist'] - ); mockCapabilities.type = jasmine.createSpyObj( 'type', ['instanceOf'] @@ -115,11 +111,6 @@ define( .toHaveBeenCalledWith(jasmine.any(Function)); mockCapabilities.mutation.mutate.mostRecentCall.args[0](testModel); expect(testModel.relationships.modes).toEqual(['abc', 'xyz']); - - // Verify that persistence is called when promise resolves - expect(mockCapabilities.persistence.persist).not.toHaveBeenCalled(); - mockPromise.then.mostRecentCall.args[0](); - expect(mockCapabilities.persistence.persist).toHaveBeenCalled(); }); it("mutates modes when used as a setter", function () { @@ -128,11 +119,6 @@ define( .toHaveBeenCalledWith(jasmine.any(Function)); mockCapabilities.mutation.mutate.mostRecentCall.args[0](testModel); expect(testModel.link).toEqual("http://www.noaa.gov"); - - // Verify that persistence is called when promise resolves - expect(mockCapabilities.persistence.persist).not.toHaveBeenCalled(); - mockPromise.then.mostRecentCall.args[0](); - expect(mockCapabilities.persistence.persist).toHaveBeenCalled(); }); it("does not mutate modes when unchanged", function () { diff --git a/platform/features/timeline/test/controllers/swimlane/TimelineSwimlaneDropHandlerSpec.js b/platform/features/timeline/test/controllers/swimlane/TimelineSwimlaneDropHandlerSpec.js index 6e76385139..1ea986ae16 100644 --- a/platform/features/timeline/test/controllers/swimlane/TimelineSwimlaneDropHandlerSpec.js +++ b/platform/features/timeline/test/controllers/swimlane/TimelineSwimlaneDropHandlerSpec.js @@ -29,7 +29,6 @@ define( mockOtherObject, mockActionCapability, mockEditorCapability, - mockPersistence, mockContext, mockAction, handler; @@ -76,7 +75,6 @@ define( ["getId", "getCapability", "useCapability", "hasCapability"] ); mockActionCapability = jasmine.createSpyObj("action", ["perform", "getActions"]); - mockPersistence = jasmine.createSpyObj("persistence", ["persist"]); mockContext = jasmine.createSpyObj('context', ['getParent']); mockActionCapability.getActions.andReturn([mockAction]); @@ -89,14 +87,12 @@ define( mockSwimlane.domainObject.getCapability.andCallFake(function (c) { return { action: mockActionCapability, - persistence: mockPersistence, editor: mockEditorCapability }[c]; }); mockSwimlane.parent.domainObject.getCapability.andCallFake(function (c) { return { action: mockActionCapability, - persistence: mockPersistence, editor: mockEditorCapability }[c]; }); @@ -162,8 +158,6 @@ define( mockSwimlane.domainObject.useCapability.mostRecentCall .args[1](testModel); expect(testModel.composition).toEqual(['c', 'd']); - // Finally, should also have persisted - expect(mockPersistence.persist).toHaveBeenCalled(); }); it("inserts after as a peer when highlighted at the bottom", function () { diff --git a/platform/forms/res/templates/controls/textfield.html b/platform/forms/res/templates/controls/textfield.html index 5afb3b2833..d79e1dd31a 100644 --- a/platform/forms/res/templates/controls/textfield.html +++ b/platform/forms/res/templates/controls/textfield.html @@ -24,6 +24,7 @@ diff --git a/platform/forms/src/MCTControl.js b/platform/forms/src/MCTControl.js index 680fd9dbe9..a15a6b208f 100644 --- a/platform/forms/src/MCTControl.js +++ b/platform/forms/src/MCTControl.js @@ -71,6 +71,9 @@ define( // Allow controls to trigger blur-like events ngBlur: "&", + // Allow controls to trigger blur-like events + ngMouseup: "&", + // The state of the form value itself ngModel: "=", diff --git a/platform/representation/src/gestures/GestureProvider.js b/platform/representation/src/gestures/GestureProvider.js index 5e170f68ac..051dafd5a7 100644 --- a/platform/representation/src/gestures/GestureProvider.js +++ b/platform/representation/src/gestures/GestureProvider.js @@ -72,7 +72,7 @@ define( // Assemble all gestures into a map, for easy look up gestures.forEach(function (gesture) { - gestureMap[gesture.key] = gesture; + gestureMap[gesture.key] = gestureMap[gesture.key] || gesture; }); this.gestureMap = gestureMap; diff --git a/platform/search/bundle.js b/platform/search/bundle.js index 61a266d049..7f8a64ab04 100644 --- a/platform/search/bundle.js +++ b/platform/search/bundle.js @@ -28,6 +28,7 @@ define([ "text!./res/templates/search-item.html", "text!./res/templates/search.html", "text!./res/templates/search-menu.html", + "text!./src/services/GenericSearchWorker.js", 'legacyRegistry' ], function ( SearchController, @@ -37,6 +38,7 @@ define([ searchItemTemplate, searchTemplate, searchMenuTemplate, + searchWorkerText, legacyRegistry ) { @@ -114,7 +116,7 @@ define([ "workers": [ { "key": "genericSearchWorker", - "scriptUrl": "services/GenericSearchWorker.js" + "scriptText": searchWorkerText } ] } diff --git a/src/BundleRegistry.js b/src/BundleRegistry.js index ccc1f04c60..9089d0ee9a 100644 --- a/src/BundleRegistry.js +++ b/src/BundleRegistry.js @@ -24,10 +24,28 @@ define(function () { function BundleRegistry() { this.bundles = {}; + this.knownBundles = {}; } BundleRegistry.prototype.register = function (path, definition) { - this.bundles[path] = definition; + if (this.knownBundles.hasOwnProperty(path)) { + throw new Error('Cannot register bundle with duplicate path', path); + } + this.knownBundles[path] = definition; + }; + + BundleRegistry.prototype.enable = function (path) { + if (!this.knownBundles[path]) { + throw new Error('Unknown bundle ' + path); + } + this.bundles[path] = this.knownBundles[path]; + }; + + BundleRegistry.prototype.disable = function (path) { + if (!this.bundles[path]) { + throw new Error('Tried to disable inactive bundle ' + path); + } + delete this.bundles[path]; }; BundleRegistry.prototype.contains = function (path) { @@ -42,8 +60,14 @@ define(function () { return Object.keys(this.bundles); }; - BundleRegistry.prototype.remove = function (path) { + BundleRegistry.prototype.remove = BundleRegistry.prototype.disable; + + BundleRegistry.prototype.delete = function (path) { + if (!this.knownBundles[path]) { + throw new Error('Cannot remove Unknown Bundle ' + path); + } delete this.bundles[path]; + delete this.knownBundles[path]; }; return BundleRegistry; diff --git a/src/BundleRegistrySpec.js b/src/BundleRegistrySpec.js index 2dfa238d88..b301db3d9c 100644 --- a/src/BundleRegistrySpec.js +++ b/src/BundleRegistrySpec.js @@ -51,6 +51,7 @@ define(['./BundleRegistry'], function (BundleRegistry) { beforeEach(function () { testBundleDef = { someKey: "some value" }; bundleRegistry.register(testPath, testBundleDef); + bundleRegistry.enable(testPath); }); it("lists registered bundles", function () { diff --git a/src/MCT.js b/src/MCT.js new file mode 100644 index 0000000000..c1b328b736 --- /dev/null +++ b/src/MCT.js @@ -0,0 +1,275 @@ +/***************************************************************************** + * 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([ + 'EventEmitter', + 'legacyRegistry', + 'uuid', + './api/api', + 'text!./adapter/templates/edit-object-replacement.html', + './selection/Selection', + './api/objects/object-utils', + './ui/ViewRegistry' +], function ( + EventEmitter, + legacyRegistry, + uuid, + api, + editObjectTemplate, + Selection, + objectUtils, + ViewRegistry +) { + /** + * Open MCT is an extensible web application for building mission + * control user interfaces. This module is itself an instance of + * [MCT]{@link module:openmct.MCT}, which provides an interface for + * configuring and executing the application. + * + * @exports openmct + */ + + /** + * The Open MCT application. This may be configured by installing plugins + * or registering extensions before the application is started. + * @class MCT + * @memberof module:openmct + * @augments {EventEmitter} + */ + function MCT() { + EventEmitter.call(this); + this.legacyBundle = { extensions: { + services: [ + { + key: "openmct", + implementation: function () { + return this; + }.bind(this) + } + ] + } }; + + /** + * Tracks current selection state of the application. + * @private + */ + this.selection = new Selection(); + + /** + * MCT's time conductor, which may be used to synchronize view contents + * for telemetry- or time-based views. + * @type {module:openmct.TimeConductor} + * @memberof module:openmct.MCT# + * @name conductor + */ + this.conductor = new api.TimeConductor(); + + /** + * An interface for interacting with the composition of domain objects. + * The composition of a domain object is the list of other domain + * objects it "contains" (for instance, that should be displayed + * beneath it in the tree.) + * + * `composition` may be called as a function, in which case it acts + * as [`composition.get`]{@link module:openmct.CompositionAPI#get}. + * + * @type {module:openmct.CompositionAPI} + * @memberof module:openmct.MCT# + * @name composition + */ + this.composition = new api.CompositionAPI(); + + /** + * Registry for views of domain objects which should appear in the + * main viewing area. + * + * @type {module:openmct.ViewRegistry} + * @memberof module:openmct.MCT# + * @name mainViews + */ + this.mainViews = new ViewRegistry(); + + /** + * Registry for views which should appear in the Inspector area. + * These views will be chosen based on selection state, so + * providers should be prepared to test arbitrary objects for + * viewability. + * + * @type {module:openmct.ViewRegistry} + * @memberof module:openmct.MCT# + * @name inspectors + */ + this.inspectors = new ViewRegistry(); + + /** + * Registry for views which should appear in Edit Properties + * dialogs, and similar user interface elements used for + * modifying domain objects external to its regular views. + * + * @type {module:openmct.ViewRegistry} + * @memberof module:openmct.MCT# + * @name propertyEditors + */ + this.propertyEditors = new ViewRegistry(); + + /** + * Registry for views which should appear in the status indicator area. + * @type {module:openmct.ViewRegistry} + * @memberof module:openmct.MCT# + * @name indicators + */ + this.indicators = new ViewRegistry(); + + /** + * Registry for views which should appear in the toolbar area while + * editing. + * + * These views will be chosen based on selection state, so + * providers should be prepared to test arbitrary objects for + * viewability. + * + * @type {module:openmct.ViewRegistry} + * @memberof module:openmct.MCT# + * @name toolbars + */ + this.toolbars = new ViewRegistry(); + + /** + * Registry for domain object types which may exist within this + * instance of Open MCT. + * + * @type {module:openmct.TypeRegistry} + * @memberof module:openmct.MCT# + * @name types + */ + this.types = new api.TypeRegistry(); + + /** + * Utilities for attaching common behaviors to views. + * + * @type {module:openmct.GestureAPI} + * @memberof module:openmct.MCT# + * @name gestures + */ + this.gestures = new api.GestureAPI(); + + /** + * An interface for interacting with domain objects and the domain + * object hierarchy. + * + * @type {module:openmct.ObjectAPI} + * @memberof module:openmct.MCT# + * @name objects + */ + this.objects = new api.ObjectAPI(); + + /** + * An interface for retrieving and interpreting telemetry data associated + * with a domain object. + * + * @type {module:openmct.TelemetryAPI} + * @memberof module:openmct.MCT# + * @name telemetry + */ + this.telemetry = new api.TelemetryAPI(); + + this.TimeConductor = this.conductor; // compatibility for prototype + this.on('navigation', this.selection.clear.bind(this.selection)); + } + + MCT.prototype = Object.create(EventEmitter.prototype); + + Object.keys(api).forEach(function (k) { + MCT.prototype[k] = api[k]; + }); + MCT.prototype.MCT = MCT; + + MCT.prototype.legacyExtension = function (category, extension) { + this.legacyBundle.extensions[category] = + this.legacyBundle.extensions[category] || []; + this.legacyBundle.extensions[category].push(extension); + }; + + /** + * Set path to where assets are hosted. This should be the path to main.js. + * @memberof module:openmct.MCT# + * @method setAssetPath + */ + MCT.prototype.setAssetPath = function (path) { + this.legacyExtension('constants', { + key: "ASSETS_PATH", + value: path + }); + }; + + /** + * Start running Open MCT. This should be called only after any plugins + * have been installed. + * @fires module:openmct.MCT~start + * @memberof module:openmct.MCT# + * @method start + * @param {HTMLElement} [domElement] the DOM element in which to run + * MCT; if undefined, MCT will be run in the body of the document + */ + MCT.prototype.start = function (domElement) { + if (!domElement) { + domElement = document.body; + } + + var appDiv = document.createElement('div'); + appDiv.setAttribute('ng-view', ''); + appDiv.className = 'user-environ'; + domElement.appendChild(appDiv); + + this.legacyExtension('runs', { + depends: ['navigationService'], + implementation: function (navigationService) { + navigationService + .addListener(this.emit.bind(this, 'navigation')); + }.bind(this) + }); + + legacyRegistry.register('adapter', this.legacyBundle); + legacyRegistry.enable('adapter'); + /** + * Fired by [MCT]{@link module:openmct.MCT} when the application + * is started. + * @event start + * @memberof module:openmct.MCT~ + */ + this.emit('start'); + }; + + + /** + * Install a plugin in MCT. + * + * @param {Function} plugin a plugin install function which will be + * invoked with the mct instance. + * @memberof module:openmct.MCT# + */ + MCT.prototype.install = function (plugin) { + plugin(this); + }; + + return MCT; +}); diff --git a/src/adapter/actions/ActionDialogDecorator.js b/src/adapter/actions/ActionDialogDecorator.js new file mode 100644 index 0000000000..d80e72741a --- /dev/null +++ b/src/adapter/actions/ActionDialogDecorator.js @@ -0,0 +1,57 @@ +/***************************************************************************** + * 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([ + '../../api/objects/object-utils' +], function (objectUtils) { + function ActionDialogDecorator(mct, actionService) { + this.mct = mct; + this.actionService = actionService; + } + + ActionDialogDecorator.prototype.getActions = function (context) { + var mct = this.mct; + + return this.actionService.getActions(context).map(function (action) { + if (action.dialogService) { + var domainObject = objectUtils.toNewFormat( + context.domainObject.getModel(), + objectUtils.parseKeyString(context.domainObject.getId()) + ); + var providers = mct.propertyEditors.get(domainObject); + + if (providers.length > 0) { + action.dialogService = Object.create(action.dialogService); + action.dialogService.getUserInput = function (form, value) { + return new mct.Dialog( + providers[0].view(context.domainObject), + form.title + ).show(); + }; + } + } + return action; + }); + }; + + return ActionDialogDecorator; +}); diff --git a/src/adapter/bundle.js b/src/adapter/bundle.js new file mode 100644 index 0000000000..0e2a4cf304 --- /dev/null +++ b/src/adapter/bundle.js @@ -0,0 +1,127 @@ +/***************************************************************************** + * 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([ + 'legacyRegistry', + './actions/ActionDialogDecorator', + './directives/MCTView', + './services/Instantiate', + './capabilities/APICapabilityDecorator', + './policies/AdapterCompositionPolicy', + './runs/AlternateCompositionInitializer' +], function ( + legacyRegistry, + ActionDialogDecorator, + MCTView, + Instantiate, + APICapabilityDecorator, + AdapterCompositionPolicy, + AlternateCompositionInitializer +) { + legacyRegistry.register('src/adapter', { + "extensions": { + "directives": [ + { + key: "mctView", + implementation: MCTView, + depends: [ + "newViews[]", + "openmct" + ] + } + ], + services: [ + { + key: "instantiate", + priority: "mandatory", + implementation: Instantiate, + depends: [ + "capabilityService", + "identifierService", + "cacheService" + ] + } + ], + components: [ + { + type: "decorator", + provides: "capabilityService", + implementation: APICapabilityDecorator, + depends: [ + "$injector" + ] + }, + { + type: "decorator", + provides: "actionService", + implementation: ActionDialogDecorator, + depends: ["openmct"] + } + ], + policies: [ + { + category: "composition", + implementation: AdapterCompositionPolicy, + depends: ["openmct"] + } + ], + runs: [ + { + implementation: AlternateCompositionInitializer, + depends: ["openmct"] + } + ], + licenses: [ + { + "name": "almond", + "version": "0.3.3", + "description": "Lightweight RequireJS replacement for builds", + "author": "jQuery Foundation", + "website": "https://github.com/requirejs/almond", + "copyright": "Copyright jQuery Foundation and other contributors, https://jquery.org/", + "license": "license-mit", + "link": "https://github.com/requirejs/almond/blob/master/LICENSE" + }, + { + "name": "lodash", + "version": "3.10.1", + "description": "Utility functions", + "author": "Dojo Foundation", + "website": "https://lodash.com", + "copyright": "Copyright 2012-2015 The Dojo Foundation", + "license": "license-mit", + "link": "https://raw.githubusercontent.com/lodash/lodash/3.10.1/LICENSE" + }, + { + "name": "EventEmitter3", + "version": "1.2.0", + "description": "Event-driven programming support", + "author": "Arnout Kazemier", + "website": "https://github.com/primus/eventemitter3", + "copyright": "Copyright (c) 2014 Arnout Kazemier", + "license": "license-mit", + "link": "https://github.com/primus/eventemitter3/blob/1.2.0/LICENSE" + } + ] + } + }); +}); diff --git a/src/adapter/capabilities/APICapabilityDecorator.js b/src/adapter/capabilities/APICapabilityDecorator.js new file mode 100644 index 0000000000..01bd6a32ef --- /dev/null +++ b/src/adapter/capabilities/APICapabilityDecorator.js @@ -0,0 +1,59 @@ +/***************************************************************************** + * 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([ + './synchronizeMutationCapability', + './AlternateCompositionCapability' +], function ( + synchronizeMutationCapability, + AlternateCompositionCapability +) { + + /** + * Overrides certain capabilities to keep consistency between old API + * and new API. + */ + function APICapabilityDecorator($injector, capabilityService) { + this.$injector = $injector; + this.capabilityService = capabilityService; + } + + APICapabilityDecorator.prototype.getCapabilities = function ( + model + ) { + var capabilities = this.capabilityService.getCapabilities(model); + if (capabilities.mutation) { + capabilities.mutation = + synchronizeMutationCapability(capabilities.mutation); + } + if (AlternateCompositionCapability.appliesTo(model)) { + capabilities.composition = function (domainObject) { + return new AlternateCompositionCapability(this.$injector, domainObject); + }.bind(this); + } + + return capabilities; + }; + + return APICapabilityDecorator; + +}); diff --git a/src/adapter/capabilities/AlternateCompositionCapability.js b/src/adapter/capabilities/AlternateCompositionCapability.js new file mode 100644 index 0000000000..20cceb9d10 --- /dev/null +++ b/src/adapter/capabilities/AlternateCompositionCapability.js @@ -0,0 +1,107 @@ +/***************************************************************************** + * 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 Web 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. + *****************************************************************************/ + +/** + * Module defining AlternateCompositionCapability. Created by vwoeltje on 11/7/14. + */ +define([ + '../../api/objects/object-utils' +], function (objectUtils) { + function AlternateCompositionCapability($injector, domainObject) { + this.domainObject = domainObject; + this.getDependencies = function () { + this.instantiate = $injector.get("instantiate"); + this.contextualize = $injector.get("contextualize"); + this.getDependencies = undefined; + this.openmct = $injector.get("openmct"); + }.bind(this); + } + + AlternateCompositionCapability.prototype.add = function (child, index) { + if (typeof index !== 'undefined') { + // At first glance I don't see a location in the existing + // codebase where add is called with an index. Won't support. + throw new Error( + 'Composition Capability does not support adding at index' + ); + } + + function addChildToComposition(model) { + var existingIndex = model.composition.indexOf(child.getId()); + if (existingIndex === -1) { + model.composition.push(child.getId()); + } + } + + return this.domainObject.useCapability( + 'mutation', + addChildToComposition + ) + .then(this.invoke.bind(this)) + .then(function (children) { + return children.filter(function (c) { + return c.getId() === child.getId(); + })[0]; + }); + }; + + AlternateCompositionCapability.prototype.contextualizeChild = function ( + child + ) { + if (this.getDependencies) { + this.getDependencies(); + } + + var keyString = objectUtils.makeKeyString(child.key); + var oldModel = objectUtils.toOldFormat(child); + var newDO = this.instantiate(oldModel, keyString); + return this.contextualize(newDO, this.domainObject); + + }; + + AlternateCompositionCapability.prototype.invoke = function () { + var newFormatDO = objectUtils.toNewFormat( + this.domainObject.getModel(), + this.domainObject.getId() + ); + + if (this.getDependencies) { + this.getDependencies(); + } + + var collection = this.openmct.composition.get(newFormatDO); + return collection.load() + .then(function (children) { + collection.destroy(); + return children.map(this.contextualizeChild, this); + }.bind(this)); + }; + + AlternateCompositionCapability.appliesTo = function (model) { + // Will get replaced by a runs exception to properly + // bind to running openmct instance + return false; + }; + + return AlternateCompositionCapability; + } +); diff --git a/src/adapter/capabilities/synchronizeMutationCapability.js b/src/adapter/capabilities/synchronizeMutationCapability.js new file mode 100644 index 0000000000..e2aa14a96a --- /dev/null +++ b/src/adapter/capabilities/synchronizeMutationCapability.js @@ -0,0 +1,49 @@ +/***************************************************************************** + * 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([ + +], function ( + +) { + + /** + * Wraps the mutation capability and synchronizes the mutation + */ + function synchronizeMutationCapability(mutationConstructor) { + + return function makeCapability(domainObject) { + var capability = mutationConstructor(domainObject); + var oldListen = capability.listen.bind(capability); + capability.listen = function (listener) { + return oldListen(function (newModel) { + capability.domainObject.model = + JSON.parse(JSON.stringify(newModel)); + listener(newModel); + }); + }; + return capability; + }; + } + + return synchronizeMutationCapability; +}); diff --git a/src/adapter/directives/MCTView.js b/src/adapter/directives/MCTView.js new file mode 100644 index 0000000000..21306d0bf5 --- /dev/null +++ b/src/adapter/directives/MCTView.js @@ -0,0 +1,87 @@ +/***************************************************************************** + * 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([ + 'angular', + './Region', + '../../api/objects/object-utils' +], function ( + angular, + Region, + objectUtils +) { + function MCTView(newViews, PublicAPI) { + var definitions = {}; + + newViews.forEach(function (newView) { + definitions[newView.region] = definitions[newView.region] || {}; + definitions[newView.region][newView.key] = newView.factory; + }); + + return { + restrict: 'E', + link: function (scope, element, attrs) { + var key, mctObject, regionId, region; + + function maybeShow() { + if (!definitions[regionId] || !definitions[regionId][key] || !mctObject) { + return; + } + + region.show(definitions[regionId][key].view(mctObject)); + } + + function setKey(k) { + key = k; + maybeShow(); + } + + function setObject(obj) { + mctObject = undefined; + PublicAPI.Objects.get(objectUtils.parseKeyString(obj.getId())) + .then(function (mobj) { + mctObject = mobj; + maybeShow(); + }); + } + + function setRegionId(r) { + regionId = r; + maybeShow(); + } + + region = new Region(element[0]); + + scope.$watch('key', setKey); + scope.$watch('region', setRegionId); + scope.$watch('mctObject', setObject); + }, + scope: { + key: "=", + region: "=", + mctObject: "=" + } + }; + } + + return MCTView; +}); diff --git a/src/adapter/directives/Region.js b/src/adapter/directives/Region.js new file mode 100644 index 0000000000..dcd3305acd --- /dev/null +++ b/src/adapter/directives/Region.js @@ -0,0 +1,45 @@ +/***************************************************************************** + * 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([], function () { + function Region(element) { + this.activeView = undefined; + this.element = element; + } + + Region.prototype.clear = function () { + if (this.activeView) { + this.activeView.destroy(); + this.activeView = undefined; + } + }; + + Region.prototype.show = function (view) { + this.clear(); + this.activeView = view; + if (this.activeView) { + this.activeView.show(this.element); + } + }; + + return Region; +}); diff --git a/src/adapter/policies/AdapterCompositionPolicy.js b/src/adapter/policies/AdapterCompositionPolicy.js new file mode 100644 index 0000000000..50e9312d54 --- /dev/null +++ b/src/adapter/policies/AdapterCompositionPolicy.js @@ -0,0 +1,48 @@ +/***************************************************************************** + * 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([], function () { + function AdapterCompositionPolicy(mct) { + this.mct = mct; + } + + AdapterCompositionPolicy.prototype.allow = function ( + containerType, + childType + ) { + var containerObject = containerType.getInitialModel(); + var childObject = childType.getInitialModel(); + + containerObject.type = containerType.getKey(); + childObject.type = childType.getKey(); + + var composition = this.mct.Composition(containerObject); + + if (composition) { + return composition.canContain(childObject); + } + + return true; + }; + + return AdapterCompositionPolicy; +}); diff --git a/src/adapter/runs/AlternateCompositionInitializer.js b/src/adapter/runs/AlternateCompositionInitializer.js new file mode 100644 index 0000000000..c9169ab55b --- /dev/null +++ b/src/adapter/runs/AlternateCompositionInitializer.js @@ -0,0 +1,36 @@ +/***************************************************************************** + * 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 Web 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([ + '../capabilities/AlternateCompositionCapability' +], function (AlternateCompositionCapability) { + // Present to work around the need for openmct to be used + // from AlternateCompositionCapability.appliesTo, even though it + // cannot be injected. + function AlternateCompositionInitializer(openmct) { + AlternateCompositionCapability.appliesTo = function (model) { + return !model.composition && !!openmct.composition.get(model); + }; + } + + return AlternateCompositionInitializer; +}); diff --git a/src/adapter/services/Instantiate.js b/src/adapter/services/Instantiate.js new file mode 100644 index 0000000000..3b4c190705 --- /dev/null +++ b/src/adapter/services/Instantiate.js @@ -0,0 +1,49 @@ +/***************************************************************************** + * 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( + ['../../../platform/core/src/objects/DomainObjectImpl'], + function (DomainObjectImpl) { + + /** + * Overrides platform version of instantiate, passes Id with model such + * that capability detection can utilize new format domain objects. + */ + function Instantiate( + capabilityService, + identifierService, + cacheService + ) { + return function (model, id) { + id = id || identifierService.generate(); + var old_id = model.id; + model.id = id; + var capabilities = capabilityService.getCapabilities(model); + model.id = old_id; + cacheService.put(id, model); + return new DomainObjectImpl(id, model, capabilities); + }; + } + + return Instantiate; + } +); diff --git a/src/adapter/templates/edit-object-replacement.html b/src/adapter/templates/edit-object-replacement.html new file mode 100644 index 0000000000..f8fc33ca07 --- /dev/null +++ b/src/adapter/templates/edit-object-replacement.html @@ -0,0 +1,46 @@ +
+
+
+ + + +
+
+ + + + + +
+
+
+
+ +
+ + + + +
+ + +
+
+
diff --git a/src/api/TimeConductor.js b/src/api/TimeConductor.js new file mode 100644 index 0000000000..135e29f1a5 --- /dev/null +++ b/src/api/TimeConductor.js @@ -0,0 +1,202 @@ +/***************************************************************************** + * 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(['EventEmitter'], function (EventEmitter) { + + /** + * The public API for setting and querying time conductor state. The + * time conductor is the means by which the temporal bounds of a view + * are controlled. Time-sensitive views will typically respond to + * changes to bounds or other properties of the time conductor and + * update the data displayed based on the time conductor state. + * + * The TimeConductor extends the EventEmitter class. A number of events are + * fired when properties of the time conductor change, which are + * documented below. + * @interface + * @memberof module:openmct + */ + function TimeConductor() { + EventEmitter.call(this); + + //The Time System + this.system = undefined; + //The Time Of Interest + this.toi = undefined; + + this.boundsVal = { + start: undefined, + end: undefined + }; + + //Default to fixed mode + this.followMode = false; + } + + TimeConductor.prototype = Object.create(EventEmitter.prototype); + + /** + * Validate the given bounds. This can be used for pre-validation of + * bounds, for example by views validating user inputs. + * @param bounds The start and end time of the conductor. + * @returns {string | true} A validation error, or true if valid + * @memberof module:openmct.TimeConductor# + * @method validateBounds + */ + TimeConductor.prototype.validateBounds = function (bounds) { + if ((bounds.start === undefined) || + (bounds.end === undefined) || + isNaN(bounds.start) || + isNaN(bounds.end) + ) { + return "Start and end must be specified as integer values"; + } else if (bounds.start > bounds.end) { + return "Specified start date exceeds end bound"; + } + return true; + }; + + function throwOnError(validationResult) { + if (validationResult !== true) { + throw new Error(validationResult); + } + } + + /** + * Get or set the follow mode of the time conductor. In follow mode the + * time conductor ticks, regularly updating the bounds from a timing + * source appropriate to the selected time system and mode of the time + * conductor. + * @fires module:openmct.TimeConductor~follow + * @param {boolean} followMode + * @returns {boolean} + * @memberof module:openmct.TimeConductor# + * @method follow + */ + TimeConductor.prototype.follow = function (followMode) { + if (arguments.length > 0) { + this.followMode = followMode; + /** + * The TimeConductor has toggled into or out of follow mode. + * @event follow + * @memberof module:openmct.TimeConductor~ + * @property {boolean} followMode true if follow mode is + * enabled, otherwise false. + */ + this.emit('follow', this.followMode); + } + return this.followMode; + }; + + /** + * @typedef {Object} TimeConductorBounds + * @property {number} start The start time displayed by the time conductor in ms since epoch. Epoch determined by current time system + * @property {number} end The end time displayed by the time conductor in ms since epoch. + * @memberof module:openmct.TimeConductor~ + */ + + /** + * Get or set the start and end time of the time conductor. Basic validation + * of bounds is performed. + * + * @param {module:openmct.TimeConductorBounds~TimeConductorBounds} newBounds + * @throws {Error} Validation error + * @fires module:openmct.TimeConductor~bounds + * @returns {module:openmct.TimeConductorBounds~TimeConductorBounds} + * @memberof module:openmct.TimeConductor# + * @method bounds + */ + TimeConductor.prototype.bounds = function (newBounds) { + if (arguments.length > 0) { + throwOnError(this.validateBounds(newBounds)); + this.boundsVal = newBounds; + /** + * The start time, end time, or both have been updated. + * @event bounds + * @memberof module:openmct.TimeConductor~ + * @property {TimeConductorBounds} bounds + */ + this.emit('bounds', this.boundsVal); + } + return this.boundsVal; + }; + + /** + * Get or set the time system of the TimeConductor. Time systems determine + * units, epoch, and other aspects of time representation. When changing + * the time system in use, new valid bounds must also be provided. + * @param {TimeSystem} newTimeSystem + * @param {module:openmct.TimeConductor~TimeConductorBounds} bounds + * @fires module:openmct.TimeConductor~timeSystem + * @returns {TimeSystem} The currently applied time system + * @memberof module:openmct.TimeConductor# + * @method timeSystem + */ + TimeConductor.prototype.timeSystem = function (newTimeSystem, bounds) { + if (arguments.length >= 2) { + this.system = newTimeSystem; + /** + * The time system used by the time + * conductor has changed. A change in Time System will always be + * followed by a bounds event specifying new query bounds. + * + * @event module:openmct.TimeConductor~timeSystem + * @property {TimeSystem} The value of the currently applied + * Time System + * */ + this.emit('timeSystem', this.system); + // Do something with bounds here. Try and convert between + // time systems? Or just set defaults when time system changes? + // eg. + this.bounds(bounds); + } else if (arguments.length === 1) { + throw new Error('Must set bounds when changing time system'); + } + return this.system; + }; + + /** + * Get or set the Time of Interest. The Time of Interest is the temporal + * focus of the current view. It can be manipulated by the user from the + * time conductor or from other views. + * @fires module:openmct.TimeConductor~timeOfInterest + * @param newTOI + * @returns {number} the current time of interest + * @memberof module:openmct.TimeConductor# + * @method timeOfInterest + */ + TimeConductor.prototype.timeOfInterest = function (newTOI) { + if (arguments.length > 0) { + this.toi = newTOI; + /** + * The Time of Interest has moved. + * @event timeOfInterest + * @memberof module:openmct.TimeConductor~ + * @property {number} Current time of interest + */ + this.emit('timeOfInterest', this.toi); + } + return this.toi; + }; + + return TimeConductor; +}); diff --git a/src/api/TimeConductorSpec.js b/src/api/TimeConductorSpec.js new file mode 100644 index 0000000000..536667a648 --- /dev/null +++ b/src/api/TimeConductorSpec.js @@ -0,0 +1,110 @@ +/***************************************************************************** + * 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(['./TimeConductor'], function (TimeConductor) { + describe("The Time Conductor", function () { + var tc, + timeSystem, + bounds, + eventListener, + toi, + follow; + + beforeEach(function () { + tc = new TimeConductor(); + timeSystem = {}; + bounds = {start: 0, end: 0}; + eventListener = jasmine.createSpy("eventListener"); + toi = 111; + follow = true; + }); + + it("Supports setting and querying of time of interest and and follow mode", function () { + expect(tc.timeOfInterest()).not.toBe(toi); + tc.timeOfInterest(toi); + expect(tc.timeOfInterest()).toBe(toi); + + expect(tc.follow()).not.toBe(follow); + tc.follow(follow); + expect(tc.follow()).toBe(follow); + }); + + it("Allows setting of valid bounds", function () { + bounds = {start: 0, end: 1}; + expect(tc.bounds()).not.toBe(bounds); + expect(tc.bounds.bind(tc, bounds)).not.toThrow(); + expect(tc.bounds()).toBe(bounds); + }); + + it("Disallows setting of invalid bounds", function () { + bounds = {start: 1, end: 0}; + expect(tc.bounds()).not.toBe(bounds); + expect(tc.bounds.bind(tc, bounds)).toThrow(); + expect(tc.bounds()).not.toBe(bounds); + + bounds = {start: 1}; + expect(tc.bounds()).not.toBe(bounds); + expect(tc.bounds.bind(tc, bounds)).toThrow(); + expect(tc.bounds()).not.toBe(bounds); + }); + + it("Allows setting of time system with bounds", function () { + expect(tc.timeSystem()).not.toBe(timeSystem); + expect(tc.timeSystem.bind(tc, timeSystem, bounds)).not.toThrow(); + expect(tc.timeSystem()).toBe(timeSystem); + }); + + it("Disallows setting of time system without bounds", function () { + expect(tc.timeSystem()).not.toBe(timeSystem); + expect(tc.timeSystem.bind(tc, timeSystem)).toThrow(); + expect(tc.timeSystem()).not.toBe(timeSystem); + }); + + it("Emits an event when time system changes", function () { + expect(eventListener).not.toHaveBeenCalled(); + tc.on("timeSystem", eventListener); + tc.timeSystem(timeSystem, bounds); + expect(eventListener).toHaveBeenCalledWith(timeSystem); + }); + + it("Emits an event when time of interest changes", function () { + expect(eventListener).not.toHaveBeenCalled(); + tc.on("timeOfInterest", eventListener); + tc.timeOfInterest(toi); + expect(eventListener).toHaveBeenCalledWith(toi); + }); + + it("Emits an event when bounds change", function () { + expect(eventListener).not.toHaveBeenCalled(); + tc.on("bounds", eventListener); + tc.bounds(bounds); + expect(eventListener).toHaveBeenCalledWith(bounds); + }); + + it("Emits an event when follow mode changes", function () { + expect(eventListener).not.toHaveBeenCalled(); + tc.on("follow", eventListener); + tc.follow(follow); + expect(eventListener).toHaveBeenCalledWith(follow); + }); + }); +}); diff --git a/src/api/Type.js b/src/api/Type.js new file mode 100644 index 0000000000..717e417b21 --- /dev/null +++ b/src/api/Type.js @@ -0,0 +1,59 @@ +/***************************************************************************** + * 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(function () { + /** + * @typedef TypeDefinition + * @memberof module:openmct.Type~ + * @property {Metadata} metadata displayable metadata about this type + * @property {function (object)} [initialize] a function which initializes + * the model for new domain objects of this type + * @property {boolean} [creatable] true if users should be allowed to + * create this type (default: false) + */ + + /** + * A Type describes a kind of domain object that may appear or be + * created within Open MCT. + * + * @param {module:opemct.Type~TypeDefinition} definition + * @class Type + * @memberof module:openmct + */ + function Type(definition) { + this.definition = definition; + } + + /** + * Check if a domain object is an instance of this type. + * @param domainObject + * @returns {boolean} true if the domain object is of this type + * @memberof module:openmct.Type# + * @method check + */ + Type.prototype.check = function (domainObject) { + // Depends on assignment from MCT. + return domainObject.type === this.key; + }; + + return Type; +}); diff --git a/src/api/api.js b/src/api/api.js new file mode 100644 index 0000000000..f68460578b --- /dev/null +++ b/src/api/api.js @@ -0,0 +1,52 @@ +/***************************************************************************** + * 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([ + './Type', + './TimeConductor', + './objects/ObjectAPI', + './composition/CompositionAPI', + './types/TypeRegistry', + './ui/Dialog', + './ui/GestureAPI', + './telemetry/TelemetryAPI' +], function ( + Type, + TimeConductor, + ObjectAPI, + CompositionAPI, + TypeRegistry, + Dialog, + GestureAPI, + TelemetryAPI +) { + return { + Type: Type, + TimeConductor: TimeConductor, + ObjectAPI: ObjectAPI, + CompositionAPI: CompositionAPI, + Dialog: Dialog, + TypeRegistry: TypeRegistry, + GestureAPI: GestureAPI, + TelemetryAPI: TelemetryAPI + }; +}); diff --git a/src/api/composition/CompositionAPI.js b/src/api/composition/CompositionAPI.js new file mode 100644 index 0000000000..31073bd956 --- /dev/null +++ b/src/api/composition/CompositionAPI.js @@ -0,0 +1,136 @@ +/***************************************************************************** + * 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', + 'EventEmitter', + './DefaultCompositionProvider', + './CompositionCollection' +], function ( + _, + EventEmitter, + DefaultCompositionProvider, + CompositionCollection +) { + /** + * An interface for interacting with the composition of domain objects. + * The composition of a domain object is the list of other domain objects + * it "contains" (for instance, that should be displayed beneath it + * in the tree.) + * + * @interface CompositionAPI + * @returns {module:openmct.CompositionCollection} + * @memberof module:openmct + */ + function CompositionAPI() { + this.registry = []; + this.policies = []; + this.addProvider(new DefaultCompositionProvider()); + } + + /** + * Add a composition provider. + * + * Plugins can add new composition providers to change the loading + * behavior for certain domain objects. + * + * @method addProvider + * @param {module:openmct.CompositionProvider} provider the provider to add + * @memberof module:openmct.CompositionAPI# + */ + CompositionAPI.prototype.addProvider = function (provider) { + this.registry.unshift(provider); + }; + + /** + * Retrieve the composition (if any) of this domain object. + * + * @method get + * @returns {module:openmct.CompositionCollection} + * @memberof module:openmct.CompositionAPI# + */ + CompositionAPI.prototype.get = function (domainObject) { + var provider = _.find(this.registry, function (p) { + return p.appliesTo(domainObject); + }); + + if (!provider) { + return; + } + + return new CompositionCollection(domainObject, provider); + }; + + /** + * A composition policy is a function which either allows or disallows + * placing one object in another's composition. + * + * Open MCT's policy model requires consensus, so any one policy may + * reject composition by returning false. As such, policies should + * generally be written to return true in the default case. + * + * @callback CompositionPolicy + * @memberof module:openmct.CompositionAPI~ + * @param {module:openmct.DomainObject} containingObject the object which + * would act as a container + * @param {module:openmct.DomainObject} containedObject the object which + * would be contained + * @returns {boolean} false if this composition should be disallowed + */ + + /** + * Add a composition policy. Composition policies may disallow domain + * objects from containing other domain objects. + * + * @method addPolicy + * @param {module:openmct.CompositionAPI~CompositionPolicy} policy + * the policy to add + * @memberof module:openmct.CompositionAPI# + */ + CompositionAPI.prototype.addPolicy = function (policy) { + this.policies.push(policy); + }; + + /** + * Check whether or not a domain object is allowed to contain another + * domain object. + * + * @private + * @method checkPolicy + * @param {module:openmct.DomainObject} containingObject the object which + * would act as a container + * @param {module:openmct.DomainObject} containedObject the object which + * would be contained + * @returns {boolean} false if this composition should be disallowed + + * @param {module:openmct.CompositionAPI~CompositionPolicy} policy + * the policy to add + * @memberof module:openmct.CompositionAPI# + */ + CompositionAPI.prototype.checkPolicy = function (container, containee) { + return this.policies.every(function (policy) { + return policy(container, containee); + }); + }; + + return CompositionAPI; +}); diff --git a/src/api/composition/CompositionCollection.js b/src/api/composition/CompositionCollection.js new file mode 100644 index 0000000000..1d0d0669d5 --- /dev/null +++ b/src/api/composition/CompositionCollection.js @@ -0,0 +1,231 @@ +/***************************************************************************** + * 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([ + 'EventEmitter', + 'lodash', + '../objects/object-utils' +], function ( + EventEmitter, + _, + objectUtils +) { + + + /** + * A CompositionCollection represents the list of domain objects contained + * by another domain object. It provides methods for loading this + * list asynchronously, and for modifying this list. + * + * @interface CompositionCollection + * @param {module:openmct.DomainObject} domainObject the domain object + * whose composition will be contained + * @param {module:openmct.CompositionProvider} provider the provider + * to use to retrieve other domain objects + * @param {module:openmct.CompositionAPI} api the composition API, for + * policy checks + * @memberof module:openmct + * @augments EventEmitter + */ + function CompositionCollection(domainObject, provider, api) { + EventEmitter.call(this); + this.domainObject = domainObject; + this.provider = provider; + this.api = api; + if (this.provider.on) { + this.provider.on( + this.domainObject, + 'add', + this.onProviderAdd, + this + ); + this.provider.on( + this.domainObject, + 'remove', + this.onProviderRemove, + this + ); + } + } + + CompositionCollection.prototype = Object.create(EventEmitter.prototype); + + CompositionCollection.prototype.onProviderAdd = function (child) { + this.add(child, true); + }; + + CompositionCollection.prototype.onProviderRemove = function (child) { + this.remove(child, true); + }; + + /** + * Get the index of a domain object within this composition. If the + * domain object is not contained here, -1 will be returned. + * + * A call to [load]{@link module:openmct.CompositionCollection#load} + * must have resolved before using this method. + * + * @param {module:openmct.DomainObject} child the domain object for which + * an index should be retrieved + * @returns {number} the index of that domain object + * @memberof module:openmct.CompositionCollection# + * @name indexOf + */ + CompositionCollection.prototype.indexOf = function (child) { + return _.findIndex(this.loadedChildren, function (other) { + return objectUtils.equals(child, other); + }); + }; + + /** + * Get the index of a domain object within this composition. + * + * A call to [load]{@link module:openmct.CompositionCollection#load} + * must have resolved before using this method. + * + * @param {module:openmct.DomainObject} child the domain object for which + * containment should be checked + * @returns {boolean} true if the domain object is contained here + * @memberof module:openmct.CompositionCollection# + * @name contains + */ + CompositionCollection.prototype.contains = function (child) { + return this.indexOf(child) !== -1; + }; + + /** + * Check if a domain object can be added to this composition. + * + * @param {module:openmct.DomainObject} child the domain object to add + * @memberof module:openmct.CompositionCollection# + * @name canContain + */ + CompositionCollection.prototype.canContain = function (domainObject) { + return this.api.checkPolicy(this.domainObject, domainObject); + }; + + /** + * Add a domain object to this composition. + * + * A call to [load]{@link module:openmct.CompositionCollection#load} + * must have resolved before using this method. + * + * @param {module:openmct.DomainObject} child the domain object to add + * @param {boolean} skipMutate true if the underlying provider should + * not be updated + * @memberof module:openmct.CompositionCollection# + * @name add + */ + CompositionCollection.prototype.add = function (child, skipMutate) { + if (!this.loadedChildren) { + throw new Error("Must load composition before you can add!"); + } + if (!this.canContain(child)) { + throw new Error("This object cannot contain that object."); + } + if (this.contains(child)) { + if (skipMutate) { + return; // don't add twice, don't error. + } + throw new Error("Unable to add child: already in composition"); + } + this.loadedChildren.push(child); + this.emit('add', child); + if (!skipMutate) { + // add after we have added. + this.provider.add(this.domainObject, child); + } + }; + + /** + * Load the domain objects in this composition. + * + * @returns {Promise.>} a promise for + * the domain objects in this composition + * @memberof {module:openmct.CompositionCollection#} + * @name load + */ + CompositionCollection.prototype.load = function () { + return this.provider.load(this.domainObject) + .then(function (children) { + this.loadedChildren = []; + children.map(function (c) { + this.add(c, true); + }, this); + this.emit('load'); + return this.loadedChildren.slice(); + }.bind(this)); + }; + + /** + * Remove a domain object from this composition. + * + * A call to [load]{@link module:openmct.CompositionCollection#load} + * must have resolved before using this method. + * + * @param {module:openmct.DomainObject} child the domain object to remove + * @param {boolean} skipMutate true if the underlying provider should + * not be updated + * @memberof module:openmct.CompositionCollection# + * @name remove + */ + CompositionCollection.prototype.remove = function (child, skipMutate) { + if (!this.contains(child)) { + if (skipMutate) { + return; + } + throw new Error("Unable to remove child: not found in composition"); + } + var index = this.indexOf(child); + var removed = this.loadedChildren.splice(index, 1)[0]; + this.emit('remove', index, child); + if (!skipMutate) { + // trigger removal after we have internally removed it. + this.provider.remove(this.domainObject, removed); + } + }; + + /** + * Stop using this composition collection. This will release any resources + * associated with this collection. + * @name destroy + * @memberof module:openmct.CompositionCollection# + */ + CompositionCollection.prototype.destroy = function () { + if (this.provider.off) { + this.provider.off( + this.domainObject, + 'add', + this.onProviderAdd, + this + ); + this.provider.off( + this.domainObject, + 'remove', + this.onProviderRemove, + this + ); + } + }; + + return CompositionCollection; +}); diff --git a/src/api/composition/DefaultCompositionProvider.js b/src/api/composition/DefaultCompositionProvider.js new file mode 100644 index 0000000000..df048379a5 --- /dev/null +++ b/src/api/composition/DefaultCompositionProvider.js @@ -0,0 +1,150 @@ +/***************************************************************************** + * 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', + 'EventEmitter', + '../objects/ObjectAPI', + '../objects/object-utils' +], function ( + _, + EventEmitter, + ObjectAPI, + objectUtils +) { + /** + * A CompositionProvider provides the underlying implementation of + * composition-related behavior for certain types of domain object. + * + * @interface CompositionProvider + * @memberof module:openmct + * @augments EventEmitter + */ + + function makeEventName(domainObject, event) { + return event + ':' + objectUtils.makeKeyString(domainObject.key); + } + + function DefaultCompositionProvider() { + EventEmitter.call(this); + } + + DefaultCompositionProvider.prototype = + Object.create(EventEmitter.prototype); + + /** + * Check if this provider should be used to load composition for a + * particular domain object. + * @param {module:openmct.DomainObject} domainObject the domain object + * to check + * @returns {boolean} true if this provider can provide + * composition for a given domain object + * @memberof module:openmct.CompositionProvider# + * @method appliesTo + */ + DefaultCompositionProvider.prototype.appliesTo = function (domainObject) { + return !!domainObject.composition; + }; + + /** + * Load any domain objects contained in the composition of this domain + * object. + * @param {module:openmct.DomainObjcet} domainObject the domain object + * for which to load composition + * @returns {Promise.>} a promise for + * the domain objects in this composition + * @memberof module:openmct.CompositionProvider# + * @method load + */ + DefaultCompositionProvider.prototype.load = function (domainObject) { + return Promise.all(domainObject.composition.map(ObjectAPI.get)); + }; + + DefaultCompositionProvider.prototype.on = function ( + domainObject, + event, + listener, + context + ) { + // these can likely be passed through to the mutation service instead + // of using an eventemitter. + this.addListener( + makeEventName(domainObject, event), + listener, + context + ); + }; + + DefaultCompositionProvider.prototype.off = function ( + domainObject, + event, + listener, + context + ) { + // these can likely be passed through to the mutation service instead + // of using an eventemitter. + this.removeListener( + makeEventName(domainObject, event), + listener, + context + ); + }; + + /** + * Remove a domain object from another domain object's composition. + * + * This method is optional; if not present, adding to a domain object's + * composition using this provider will be disallowed. + * + * @param {module:openmct.DomainObject} domainObject the domain object + * which should have its composition modified + * @param {module:openmct.DomainObject} child the domain object to remove + * @memberof module:openmct.CompositionProvider# + * @method remove + */ + DefaultCompositionProvider.prototype.remove = function (domainObject, child) { + // TODO: this needs to be synchronized via mutation + var index = domainObject.composition.indexOf(child); + domainObject.composition.splice(index, 1); + this.emit(makeEventName(domainObject, 'remove'), child); + }; + + /** + * Add a domain object to another domain object's composition. + * + * This method is optional; if not present, adding to a domain object's + * composition using this provider will be disallowed. + * + * @param {module:openmct.DomainObject} domainObject the domain object + * which should have its composition modified + * @param {module:openmct.DomainObject} child the domain object to add + * @memberof module:openmct.CompositionProvider# + * @method add + */ + DefaultCompositionProvider.prototype.add = function (domainObject, child) { + // TODO: this needs to be synchronized via mutation + domainObject.composition.push(child.key); + this.emit(makeEventName(domainObject, 'add'), child); + }; + + return DefaultCompositionProvider; +}); diff --git a/src/api/objects/LegacyObjectAPIInterceptor.js b/src/api/objects/LegacyObjectAPIInterceptor.js new file mode 100644 index 0000000000..88758710e4 --- /dev/null +++ b/src/api/objects/LegacyObjectAPIInterceptor.js @@ -0,0 +1,128 @@ +/***************************************************************************** + * 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([ + './object-utils', + './objectEventEmitter' +], function ( + utils, + objectEventEmitter +) { + function ObjectServiceProvider(objectService, instantiate, topic) { + this.objectService = objectService; + this.instantiate = instantiate; + + this.generalTopic = topic('mutation'); + this.bridgeEventBuses(); + } + + /** + * Bridges old and new style mutation events to provide compatibility between the two APIs + * @private + */ + ObjectServiceProvider.prototype.bridgeEventBuses = function () { + var removeGeneralTopicListener; + var handleLegacyMutation; + + var handleMutation = function (newStyleObject) { + var keyString = utils.makeKeyString(newStyleObject.key); + var oldStyleObject = this.instantiate(utils.toOldFormat(newStyleObject), keyString); + + // Don't trigger self + removeGeneralTopicListener(); + + oldStyleObject.getCapability('mutation').mutate(function () { + return utils.toOldFormat(newStyleObject); + }); + + removeGeneralTopicListener = this.generalTopic.listen(handleLegacyMutation); + }.bind(this); + + handleLegacyMutation = function (legacyObject) { + var newStyleObject = utils.toNewFormat(legacyObject.getModel(), legacyObject.getId()); + + //Don't trigger self + objectEventEmitter.off('mutation', handleMutation); + objectEventEmitter.emit(newStyleObject.key.identifier + ":*", newStyleObject); + objectEventEmitter.on('mutation', handleMutation); + }.bind(this); + + objectEventEmitter.on('mutation', handleMutation); + removeGeneralTopicListener = this.generalTopic.listen(handleLegacyMutation); + }; + + ObjectServiceProvider.prototype.save = function (object) { + var key = object.key; + + return object.getCapability('persistence') + .persist() + .then(function () { + return utils.toNewFormat(object, key); + }); + }; + + ObjectServiceProvider.prototype.delete = function (object) { + // TODO! + }; + + ObjectServiceProvider.prototype.get = function (key) { + var keyString = utils.makeKeyString(key); + return this.objectService.getObjects([keyString]) + .then(function (results) { + var model = results[keyString].getModel(); + return utils.toNewFormat(model, key); + }); + }; + + // Injects new object API as a decorator so that it hijacks all requests. + // Object providers implemented on new API should just work, old API should just work, many things may break. + function LegacyObjectAPIInterceptor(openmct, ROOTS, instantiate, topic, objectService) { + this.getObjects = function (keys) { + var results = {}, + promises = keys.map(function (keyString) { + var key = utils.parseKeyString(keyString); + return openmct.objects.get(key) + .then(function (object) { + object = utils.toOldFormat(object); + results[keyString] = instantiate(object, keyString); + }); + }); + + return Promise.all(promises) + .then(function () { + return results; + }); + }; + + openmct.objects.supersecretSetFallbackProvider( + new ObjectServiceProvider(objectService, instantiate, topic) + ); + + ROOTS.forEach(function (r) { + openmct.objects.addRoot(utils.parseKeyString(r.id)); + }); + + return this; + } + + return LegacyObjectAPIInterceptor; +}); diff --git a/src/api/objects/MutableObject.js b/src/api/objects/MutableObject.js new file mode 100644 index 0000000000..003bc2c5b9 --- /dev/null +++ b/src/api/objects/MutableObject.js @@ -0,0 +1,90 @@ +/***************************************************************************** + * 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', + './objectEventEmitter' +], function ( + _, + objectEventEmitter +) { + var ANY_OBJECT_EVENT = "mutation"; + + /** + * The MutableObject wraps a DomainObject and provides getters and + * setters for + * @param eventEmitter + * @param object + * @interface MutableObject + */ + function MutableObject(object) { + this.object = object; + this.unlisteners = []; + } + + function qualifiedEventName(object, eventName) { + return [object.key.identifier, eventName].join(':'); + } + + MutableObject.prototype.stopListening = function () { + this.unlisteners.forEach(function (unlisten) { + unlisten(); + }); + }; + + /** + * Observe changes to this domain object. + * @param {string} path the property to observe + * @param {Function} callback a callback to invoke when new values for + * this property are observed + * @method on + * @memberof module:openmct.MutableObject# + */ + MutableObject.prototype.on = function (path, callback) { + var fullPath = qualifiedEventName(this.object, path); + objectEventEmitter.on(fullPath, callback); + this.unlisteners.push(objectEventEmitter.off.bind(objectEventEmitter, fullPath, callback)); + }; + + /** + * Modify this domain object. + * @param {string} path the property to modify + * @param {*} value the new value for this property + * @method set + * @memberof module:openmct.MutableObject# + */ + MutableObject.prototype.set = function (path, value) { + + _.set(this.object, path, value); + _.set(this.object, 'modified', Date.now()); + + //Emit event specific to property + objectEventEmitter.emit(qualifiedEventName(this.object, path), value); + //Emit wildcare event + objectEventEmitter.emit(qualifiedEventName(this.object, '*'), this.object); + + //Emit a general "any object" event + objectEventEmitter.emit(ANY_OBJECT_EVENT, this.object); + }; + + return MutableObject; +}); diff --git a/src/api/objects/ObjectAPI.js b/src/api/objects/ObjectAPI.js new file mode 100644 index 0000000000..eb67c7adbd --- /dev/null +++ b/src/api/objects/ObjectAPI.js @@ -0,0 +1,234 @@ +/***************************************************************************** + * 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', + './object-utils', + './MutableObject' +], function ( + _, + utils, + MutableObject +) { + + + /** + * Utilities for loading, saving, and manipulating domain objects. + * @interface ObjectAPI + * @memberof module:openmct + * @implements {module:openmct.ObjectProvider} + */ + + function ObjectAPI() { + this.providers = {}; + this.rootRegistry = []; + this.rootProvider = { + 'get': function () { + return Promise.resolve({ + name: 'The root object', + type: 'root', + composition: this.rootRegistry + }); + }.bind(this) + }; + } + + ObjectAPI.prototype.supersecretSetFallbackProvider = function (p) { + this.fallbackProvider = p; + }; + + // Retrieve the provider for a given key. + ObjectAPI.prototype.getProvider = function (key) { + if (key.identifier === 'ROOT') { + return this.rootProvider; + } + return this.providers[key.namespace] || this.fallbackProvider; + }; + + + /** + * Register a new object provider for a particular namespace. + * + * @param {string} namespace the namespace for which to provide objects + * @param {module:openmct.ObjectProvider} provider the provider which + * will handle loading domain objects from this namespace + * @memberof {module:openmct.ObjectAPI#} + * @name addProvider + */ + ObjectAPI.prototype.addProvider = function (namespace, provider) { + this.providers[namespace] = provider; + }; + + /** + * Provides the ability to read, write, and delete domain objects. + * + * When registering a new object provider, all methods on this interface + * are optional. + * + * @interface ObjectProvider + * @memberof module:openmct + */ + + /** + * Save this domain object in its current state. + * + * @method save + * @memberof module:openmct.ObjectProvider# + * @param {module:openmct.DomainObject} domainObject the domain object to + * save + * @returns {Promise} a promise which will resolve when the domain object + * has been saved, or be rejected if it cannot be saved + */ + + /** + * Delete this domain object. + * + * @method delete + * @memberof module:openmct.ObjectProvider# + * @param {module:openmct.DomainObject} domainObject the domain object to + * delete + * @returns {Promise} a promise which will resolve when the domain object + * has been deleted, or be rejected if it cannot be deleted + */ + + /** + * Get a domain object. + * + * @method get + * @memberof module:openmct.ObjectProvider# + * @param {string} key the key for the domain object to load + * @returns {Promise} a promise which will resolve when the domain object + * has been saved, or be rejected if it cannot be saved + */ + + [ + 'save', + 'delete', + 'get' + ].forEach(function (method) { + ObjectAPI.prototype[method] = function () { + var key = arguments[0], + provider = this.getProvider(key); + + if (!provider) { + throw new Error('No Provider Matched'); + } + + if (!provider[method]) { + throw new Error('Provider does not support [' + method + '].'); + } + + return provider[method].apply(provider, arguments); + }; + }); + + /** + * Add a root-level object. + * @param {module:openmct.DomainObject} domainObject the root-level object + * to add. + * @method addRoot + * @memberof module:openmct.ObjectAPI# + */ + ObjectAPI.prototype.addRoot = function (key) { + this.rootRegistry.unshift(key); + }; + + /** + * Remove a root-level object. + * @param {module:openmct.ObjectAPI~Identifier} id the identifier of the + * root-level object to remove. + * @method removeRoot + * @memberof module:openmct.ObjectAPI# + */ + ObjectAPI.prototype.removeRoot = function (key) { + this.rootRegistry = this.rootRegistry.filter(function (k) { + return ( + k.identifier !== key.identifier || + k.namespace !== key.namespace + ); + }); + }; + + /** + * Modify a domain object. + * @param {module:openmct.DomainObject} object the object to mutate + * @param {string} path the property to modify + * @param {*} value the new value for this property + * @method mutate + * @memberof module:openmct.ObjectAPI# + */ + ObjectAPI.prototype.mutate = function (domainObject, path, value) { + return new MutableObject(domainObject).set(path, value); + }; + + /** + * Observe changes to a domain object. + * @param {module:openmct.DomainObject} object the object to observe + * @param {string} path the property to observe + * @param {Function} callback a callback to invoke when new values for + * this property are observed + * @method observe + * @memberof module:openmct.ObjectAPI# + */ + ObjectAPI.prototype.observe = function (domainObject, path, callback) { + var mutableObject = new MutableObject(domainObject); + mutableObject.on(path, callback); + return mutableObject.stopListening.bind(mutableObject); + }; + + /** + * Uniquely identifies a domain object. + * + * @typedef Identifier + * @memberof module:openmct.ObjectAPI~ + * @property {string} namespace the namespace to/from which this domain + * object should be loaded/stored. + * @property {string} key a unique identifier for the domain object + * within that namespace + */ + + /** + * A domain object is an entity of relevance to a user's workflow, that + * should appear as a distinct and meaningful object within the user + * interface. Examples of domain objects are folders, telemetry sensors, + * and so forth. + * + * A few common properties are defined for domain objects. Beyond these, + * individual types of domain objects may add more as they see fit. + * + * @property {module:openmct.ObjectAPI~Identifier} identifier a key/namespace pair which + * uniquely identifies this domain object + * @property {string} type the type of domain object + * @property {string} name the human-readable name for this domain object + * @property {string} [creator] the user name of the creator of this domain + * object + * @property {number} [modified] the time, in milliseconds since the UNIX + * epoch, at which this domain object was last modified + * @property {module:openmct.ObjectAPI~Identifier[]} [composition] if + * present, this will be used by the default composition provider + * to load domain objects + * @typedef DomainObject + * @memberof module:openmct + */ + + return ObjectAPI; +}); diff --git a/src/api/objects/bundle.js b/src/api/objects/bundle.js new file mode 100644 index 0000000000..83a865b84e --- /dev/null +++ b/src/api/objects/bundle.js @@ -0,0 +1,51 @@ +/***************************************************************************** + * 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. + *****************************************************************************/ +/*global define*/ + +define([ + './LegacyObjectAPIInterceptor', + 'legacyRegistry' +], function ( + LegacyObjectAPIInterceptor, + legacyRegistry +) { + legacyRegistry.register('src/api/objects', { + name: 'Object API', + description: 'The public Objects API', + extensions: { + components: [ + { + provides: "objectService", + type: "decorator", + priority: "mandatory", + implementation: LegacyObjectAPIInterceptor, + depends: [ + "openmct", + "roots[]", + "instantiate", + "topic" + ] + } + ] + } + }); +}); diff --git a/src/api/objects/object-utils.js b/src/api/objects/object-utils.js new file mode 100644 index 0000000000..f749a8110b --- /dev/null +++ b/src/api/objects/object-utils.js @@ -0,0 +1,109 @@ +/***************************************************************************** + * 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([ + +], function ( + +) { + + // take a key string and turn it into a key object + // 'scratch:root' ==> {namespace: 'scratch', identifier: 'root'} + var parseKeyString = function (key) { + if (typeof key === 'object') { + return key; + } + var namespace = '', + identifier = key; + for (var i = 0, escaped = false; i < key.length; i++) { + if (escaped) { + escaped = false; + namespace += key[i]; + } else { + if (key[i] === "\\") { + escaped = true; + } else if (key[i] === ":") { + // namespace = key.slice(0, i); + identifier = key.slice(i + 1); + break; + } + namespace += key[i]; + } + } + + if (key === namespace) { + namespace = ''; + } + + return { + namespace: namespace, + identifier: identifier + }; + }; + + // take a key and turn it into a key string + // {namespace: 'scratch', identifier: 'root'} ==> 'scratch:root' + var makeKeyString = function (key) { + if (typeof key === 'string') { + return key; + } + if (!key.namespace) { + return key.identifier; + } + return [ + key.namespace.replace(':', '\\:'), + key.identifier.replace(':', '\\:') + ].join(':'); + }; + + // Converts composition to use key strings instead of keys + var toOldFormat = function (model) { + model = JSON.parse(JSON.stringify(model)); + delete model.key; + if (model.composition) { + model.composition = model.composition.map(makeKeyString); + } + return model; + }; + + // converts composition to use keys instead of key strings + var toNewFormat = function (model, key) { + model = JSON.parse(JSON.stringify(model)); + model.key = key; + if (model.composition) { + model.composition = model.composition.map(parseKeyString); + } + return model; + }; + + var equals = function (a, b) { + return makeKeyString(a.key) === makeKeyString(b.key); + }; + + return { + toOldFormat: toOldFormat, + toNewFormat: toNewFormat, + makeKeyString: makeKeyString, + parseKeyString: parseKeyString, + equals: equals + }; +}); diff --git a/src/api/objects/objectEventEmitter.js b/src/api/objects/objectEventEmitter.js new file mode 100644 index 0000000000..8f8bf5d325 --- /dev/null +++ b/src/api/objects/objectEventEmitter.js @@ -0,0 +1,32 @@ +/***************************************************************************** + * 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([ + "EventEmitter" +], function ( + EventEmitter +) { + /** + * Provides a singleton event bus for sharing between objects. + */ + return new EventEmitter(); +}); diff --git a/src/api/telemetry/TelemetryAPI.js b/src/api/telemetry/TelemetryAPI.js new file mode 100644 index 0000000000..6e310b1aa8 --- /dev/null +++ b/src/api/telemetry/TelemetryAPI.js @@ -0,0 +1,319 @@ +/***************************************************************************** + * 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', + 'EventEmitter' +], function ( + _, + EventEmitter +) { + /** + * A LimitEvaluator may be used to detect when telemetry values + * have exceeded nominal conditions. + * + * @interface LimitEvaluator + * @memberof module:openmct.TelemetryAPI~ + */ + + /** + * Check for any limit violations associated with a telemetry datum. + * @method evaluate + * @param {*} datum the telemetry datum to evaluate + * @param {TelemetryProperty} the property to check for limit violations + * @memberof module:openmct.TelemetryAPI~LimitEvaluator + * @returns {module:openmct.TelemetryAPI~LimitViolation} metadata about + * the limit violation, or undefined if a value is within limits + */ + + /** + * A violation of limits defined for a telemetry property. + * @typedef LimitViolation + * @memberof {module:openmct.TelemetryAPI~} + * @property {string} cssclass the class (or space-separated classes) to + * apply to display elements for values which violate this limit + * @property {string} name the human-readable name for the limit violation + */ + + /** + * A TelemetryFormatter converts telemetry values for purposes of + * display as text. + * + * @interface TelemetryFormatter + * @memberof module:openmct.TelemetryAPI~ + */ + + /** + * Retrieve the 'key' from the datum and format it accordingly to + * telemetry metadata in domain object. + * + * @method format + * @memberof module:openmct.TelemetryAPI~TelemetryFormatter# + */ + + + + + // format map is a placeholder until we figure out format service. + var FORMAT_MAP = { + generic: function (range) { + return function (datum) { + return datum[range.key]; + }; + }, + enum: function (range) { + var enumMap = _.indexBy(range.enumerations, 'value'); + return function (datum) { + try { + return enumMap[datum[range.valueKey]].text; + } catch (e) { + return datum[range.valueKey]; + } + }; + } + }; + + FORMAT_MAP.number = + FORMAT_MAP.float = + FORMAT_MAP.integer = + FORMAT_MAP.ascii = + FORMAT_MAP.generic; + + /** + * Describes a property which would be found in a datum of telemetry + * associated with a particular domain object. + * + * @typedef TelemetryProperty + * @memberof module:openmct.TelemetryAPI~ + * @property {string} key the name of the property in the datum which + * contains this telemetry value + * @property {string} name the human-readable name for this property + * @property {string} [units] the units associated with this property + * @property {boolean} [temporal] true if this property is a timestamp, or + * may be otherwise used to order telemetry in a time-like + * fashion; default is false + * @property {boolean} [numeric] true if the values for this property + * can be interpreted plainly as numbers; default is true + * @property {boolean} [enumerated] true if this property may have only + * certain specific values; default is false + * @property {string} [values] for enumerated states, an ordered list + * of possible values + */ + + /** + * Describes and bounds requests for telemetry data. + * + * @typedef TelemetryRequest + * @memberof module:openmct.TelemetryAPI~ + * @property {string} sort the key of the property to sort by. This may + * be prefixed with a "+" or a "-" sign to sort in ascending + * or descending order respectively. If no prefix is present, + * ascending order will be used. + * @property {*} start the lower bound for values of the sorting property + * @property {*} end the upper bound for values of the sorting property + * @property {string[]} strategies symbolic identifiers for strategies + * (such as `minmax`) which may be recognized by providers; + * these will be tried in order until an appropriate provider + * is found + */ + + /** + * Provides telemetry data. To connect to new data sources, new + * TelemetryProvider implementations should be + * [registered]{@link module:openmct.TelemetryAPI#addProvider}. + * + * @interface TelemetryProvider + * @memberof module:openmct.TelemetryAPI~ + */ + + + + /** + * An interface for retrieving telemetry data associated with a domain + * object. + * + * @interface TelemetryAPI + * @augments module:openmct.TelemetryAPI~TelemetryProvider + * @memberof module:openmct + */ + function TelemetryAPI() { + this.providersByStrategy = {}; + this.defaultProviders = []; + } + + /** + * Check if this provider can supply telemetry data associated with + * this domain object. + * + * @method canProvideTelemetry + * @param {module:openmct.DomainObject} domainObject the object for + * which telemetry would be provided + * @returns {boolean} true if telemetry can be provided + * @memberof module:openmct.TelemetryAPI~TelemetryProvider# + */ + TelemetryAPI.prototype.canProvideTelemetry = function (domainObject) { + return this.defaultProviders.some(function (provider) { + return provider.canProvideTelemetry(domainObject); + }); + }; + + /** + * Register a telemetry provider with the telemetry service. This + * allows you to connect alternative telemetry sources. + * @method addProvider + * @memberof module:openmct.TelemetryAPI# + * @param {module:openmct.TelemetryAPI~TelemetryProvider} provider the new + * telemetry provider + * @param {string} [strategy] the request strategy supported by + * this provider. If omitted, this will be used as a + * default provider (when no strategy is requested or no + * matching strategy is found.) + */ + TelemetryAPI.prototype.addProvider = function (provider, strategy) { + if (!strategy) { + this.defaultProviders.push(provider); + } else { + this.providersByStrategy[strategy] = + this.providersByStrategy[strategy] || []; + this.providersByStrategy[strategy].push(provider); + } + }; + + /** + * @private + */ + TelemetryAPI.prototype.findProvider = function (domainObject, strategy) { + function supportsDomainObject(provider) { + return provider.canProvideTelemetry(domainObject); + } + + if (strategy) { + var eligibleProviders = + (this.providersByStrategy[strategy] || []) + .filter(supportsDomainObject); + if (eligibleProviders.length > 0) { + return eligibleProviders[0]; + } + } + + return this.defaultProviders.filter(supportsDomainObject)[0]; + }; + + /** + * Request historical telemetry for a domain object. + * The `options` argument allows you to specify filters + * (start, end, etc.), sort order, and strategies for retrieving + * telemetry (aggregation, latest available, etc.). + * + * @method request + * @memberof module:openmct.TelemetryAPI~TelemetryProvider# + * @param {module:openmct.DomainObject} domainObject the object + * which has associated telemetry + * @param {module:openmct.TelemetryAPI~TelemetryRequest} options + * options for this historical request + * @returns {Promise.} a promise for an array of + * telemetry data + */ + TelemetryAPI.prototype.request = function (domainObject, options) { + var provider = this.findProvider(domainObject, options.strategy); + return provider ? + provider.request(domainObject, options) : + Promise.reject([]); + }; + + /** + * Subscribe to realtime telemetry for a specific domain object. + * The callback will be called whenever data is received from a + * realtime provider. + * + * @method subscribe + * @memberof module:openmct.TelemetryAPI~TelemetryProvider# + * @param {module:openmct.DomainObject} domainObject the object + * which has associated telemetry + * @param {Function} callback the callback to invoke with new data, as + * it becomes available + * @param {module:openmct.TelemetryAPI~TelemetryRequest} options + * options for this request + * @returns {Function} a function which may be called to terminate + * the subscription + */ + + /** + * Get a list of all telemetry properties defined for this + * domain object. + * + * @param {module:openmct.DomainObject} domainObject the domain + * object for which to request telemetry + * @returns {module:openmct.TelemetryAPI~TelemetryProperty[]} + * telemetry metadata + * @method properties + * @memberof module:openmct.TelemetryAPI~TelemetryProvider# + */ + + /** + * Telemetry formatters help you format telemetry values for + * display. Under the covers, they use telemetry metadata to + * interpret your telemetry data, and then they use the format API + * to format that data for display. + * + * This method is optional. + * If a provider does not implement this method, it is presumed + * that all telemetry associated with this domain object can + * be formatted correctly by string coercion. + * + * @param {module:openmct.DomainObject} domainObject the domain + * object for which to format telemetry + * @returns {module:openmct.TelemetryAPI~TelemetryFormatter} + * @method formatter + * @memberof module:openmct.TelemetryAPI~TelemetryProvider# + */ + + /** + * Get a limit evaluator for this domain object. + * Limit Evaluators help you evaluate limit and alarm status of individual telemetry datums for display purposes without having to interact directly with the Limit API. + * + * This method is optional. + * If a provider does not implement this method, it is presumed + * that no limits are defined for this domain object's telemetry. + * + * @param {module:openmct.DomainObject} domainObject the domain + * object for which to evaluate limits + * @returns {module:openmct.TelemetryAPI~LimitEvaluator} + * @method limitEvaluator + * @memberof module:openmct.TelemetryAPI~TelemetryProvider# + */ + _.forEach({ + subscribe: undefined, + properties: [], + formatter: undefined, + limitEvaluator: undefined + }, function (defaultValue, method) { + TelemetryAPI.prototype[method] = function (domainObject) { + var provider = this.findProvider(domainObject); + return provider ? + provider[method].apply(provider, arguments) : + defaultValue; + }; + }); + + return TelemetryAPI; +}); diff --git a/src/api/telemetry/bundle.js b/src/api/telemetry/bundle.js new file mode 100644 index 0000000000..a389ebf804 --- /dev/null +++ b/src/api/telemetry/bundle.js @@ -0,0 +1,45 @@ +/***************************************************************************** + * 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([ + './TelemetryAPI', + 'legacyRegistry' +], function ( + TelemetryAPI, + legacyRegistry +) { + legacyRegistry.register('api/telemetry-api', { + name: 'Telemetry API', + description: 'The public Telemetry API', + extensions: { + runs: [ + { + key: "TelemetryAPI", + implementation: TelemetryAPI, + depends: [ + 'formatService' + ] + } + ] + } + }); +}); diff --git a/src/api/types/TypeRegistry.js b/src/api/types/TypeRegistry.js new file mode 100644 index 0000000000..e01bea4a9d --- /dev/null +++ b/src/api/types/TypeRegistry.js @@ -0,0 +1,51 @@ +/***************************************************************************** + * 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([], function () { + + /** + * A TypeRegistry maintains the definitions for different types + * that domain objects may have. + * @interface TypeRegistry + * @memberof module:openmct + */ + function TypeRegistry() { + this.types = {}; + } + + /** + * Register a new type of view. + * + * @param {string} typeKey a string identifier for this type + * @param {module:openmct.Type} type the type to add + * @method addProvider + * @memberof module:openmct.TypeRegistry# + */ + TypeRegistry.prototype.addType = function (typeKey, type) { + this.types[typeKey] = type; + }; + + + return TypeRegistry; +}); + + diff --git a/src/api/ui/Dialog.js b/src/api/ui/Dialog.js new file mode 100644 index 0000000000..63408f31ab --- /dev/null +++ b/src/api/ui/Dialog.js @@ -0,0 +1,107 @@ +/***************************************************************************** + * 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(['text!./dialog.html', 'zepto'], function (dialogTemplate, $) { + + /** + * A dialog may be displayed to show blocking content to users. + * @param {module:openmct.View} view the view to show in the dialog + * @param {string} [title] the title for this dialog + * @constructor + * @memberof module:openmct + */ + function Dialog(view, title) { + this.view = view; + this.title = title; + this.showing = false; + this.enabledState = true; + } + + /** + * Display this dialog. + * @returns {Promise} a promise that will be resolved if the user + * chooses "OK", an rejected if the user chooses "cancel" + * @method show + * @memberof module:openmct.Dialog# + */ + Dialog.prototype.show = function () { + if (this.showing) { + throw new Error("Dialog already showing."); + } + + var $body = $('body'); + var $dialog = $(dialogTemplate); + var $contents = $dialog.find('.contents .editor'); + var $close = $dialog.find('.close'); + + var $ok = $dialog.find('.ok'); + var $cancel = $dialog.find('.cancel'); + + if (this.title) { + $dialog.find('.title').text(this.title); + } + + $body.append($dialog); + this.view.show($contents[0]); + this.$dialog = $dialog; + this.$ok = $ok; + this.showing = true; + + [$ok, $cancel, $close].forEach(function ($button) { + $button.on('click', this.hide.bind(this)); + }.bind(this)); + + return new Promise(function (resolve, reject) { + $ok.on('click', resolve); + $cancel.on('click', reject); + $close.on('click', reject); + }); + }; + + Dialog.prototype.hide = function () { + if (!this.showing) { + return; + } + this.$dialog.remove(); + this.view.destroy(); + this.showing = false; + }; + + /** + * Get or set the "enabled" state of the OK button for this dialog. + * @param {boolean} [state] true to enable, false to disable + * @returns {boolean} true if enabled, false if disabled + * @method enabled + * @memberof module:openmct.Dialog# + */ + Dialog.prototype.enabled = function (state) { + if (state !== undefined) { + this.enabledState = state; + if (this.showing) { + this.$ok.toggleClass('disabled', !state); + } + } + return this.enabledState; + }; + + return Dialog; +}); diff --git a/src/api/ui/GestureAPI.js b/src/api/ui/GestureAPI.js new file mode 100644 index 0000000000..211318673a --- /dev/null +++ b/src/api/ui/GestureAPI.js @@ -0,0 +1,68 @@ +/***************************************************************************** + * 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([], function () { + /** + * Allows support for common user actions to be attached to views. + * @interface GestureAPI + * @memberof module:openmct + */ + function GestureAPI(selectGesture, contextMenuGesture) { + this.selectGesture = selectGesture; + this.contextMenuGesture = contextMenuGesture; + } + + /** + * Designate an HTML element as selectable, and associated with a + * particular object. + * + * @param {HTMLElement} htmlElement the element to make selectable + * @param {*} item the object which should become selected when this + * element is clicked. + * @returns {Function} a function to remove selectability from this + * HTML element. + * @method selectable + * @memberof module:openmct.GestureAPI# + */ + GestureAPI.prototype.selectable = function (htmlElement, item) { + return this.selectGesture.apply(htmlElement, item); + }; + + + /** + * Designate an HTML element as having a context menu associated with + * the provided item. + * + * @private + * @param {HTMLElement} htmlElement the element to make selectable + * @param {*} item the object for which a context menu should appear + * @returns {Function} a function to remove this geture from this + * HTML element. + * @method selectable + * @memberof module:openmct.GestureAPI# + */ + GestureAPI.prototype.contextMenu = function (htmlElement, item) { + return this.contextMenuGesture.apply(htmlElement, item); + }; + + return GestureAPI; +}); diff --git a/src/api/ui/dialog.html b/src/api/ui/dialog.html new file mode 100644 index 0000000000..83181c5c78 --- /dev/null +++ b/src/api/ui/dialog.html @@ -0,0 +1,21 @@ +
+
+
+ x +
+
+
+
+
+
+
+
+ OK + Cancel +
+
+
+
+ + + diff --git a/src/defaultRegistry.js b/src/defaultRegistry.js new file mode 100644 index 0000000000..efabce4d97 --- /dev/null +++ b/src/defaultRegistry.js @@ -0,0 +1,139 @@ +/***************************************************************************** + * 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([ + 'legacyRegistry', + + '../src/adapter/bundle', + '../src/api/objects/bundle', + + '../example/builtins/bundle', + '../example/composite/bundle', + '../example/eventGenerator/bundle', + '../example/export/bundle', + '../example/extensions/bundle', + '../example/forms/bundle', + '../example/generator/bundle', + '../example/identity/bundle', + '../example/imagery/bundle', + '../example/mobile/bundle', + '../example/msl/bundle', + '../example/notifications/bundle', + '../example/persistence/bundle', + '../example/plotOptions/bundle', + '../example/policy/bundle', + '../example/profiling/bundle', + '../example/scratchpad/bundle', + '../example/taxonomy/bundle', + '../example/worker/bundle', + '../example/localTimeSystem/bundle', + + '../platform/commonUI/about/bundle', + '../platform/commonUI/browse/bundle', + '../platform/commonUI/dialog/bundle', + '../platform/commonUI/edit/bundle', + '../platform/commonUI/formats/bundle', + '../platform/commonUI/general/bundle', + '../platform/commonUI/inspect/bundle', + '../platform/commonUI/mobile/bundle', + '../platform/commonUI/notification/bundle', + '../platform/commonUI/regions/bundle', + '../platform/commonUI/themes/espresso/bundle', + '../platform/commonUI/themes/snow/bundle', + '../platform/containment/bundle', + '../platform/core/bundle', + '../platform/entanglement/bundle', + '../platform/execution/bundle', + '../platform/exporters/bundle', + '../platform/features/clock/bundle', + '../platform/features/conductor/bundle', + '../platform/features/conductor-v2/conductor/bundle', + '../platform/features/conductor-v2/compatibility/bundle', + '../platform/features/conductor-v2/utcTimeSystem/bundle', + '../platform/features/imagery/bundle', + '../platform/features/layout/bundle', + '../platform/features/pages/bundle', + '../platform/features/plot/bundle', + '../platform/features/static-markup/bundle', + '../platform/features/table/bundle', + '../platform/features/timeline/bundle', + '../platform/forms/bundle', + '../platform/framework/bundle', + '../platform/framework/src/load/Bundle', + '../platform/identity/bundle', + '../platform/persistence/aggregator/bundle', + '../platform/persistence/couch/bundle', + '../platform/persistence/elastic/bundle', + '../platform/persistence/local/bundle', + '../platform/persistence/queue/bundle', + '../platform/policy/bundle', + '../platform/representation/bundle', + '../platform/search/bundle', + '../platform/status/bundle', + '../platform/telemetry/bundle' +], function (legacyRegistry) { + + var DEFAULTS = [ + 'src/adapter', + 'src/api/objects', + 'platform/framework', + 'platform/core', + 'platform/representation', + 'platform/commonUI/about', + 'platform/commonUI/browse', + 'platform/commonUI/edit', + 'platform/commonUI/dialog', + 'platform/commonUI/formats', + 'platform/commonUI/general', + 'platform/commonUI/inspect', + 'platform/commonUI/mobile', + 'platform/commonUI/themes/espresso', + 'platform/commonUI/notification', + 'platform/containment', + 'platform/execution', + 'platform/exporters', + 'platform/telemetry', + 'platform/features/clock', + 'platform/features/imagery', + 'platform/features/layout', + 'platform/features/pages', + 'platform/features/plot', + 'platform/features/timeline', + 'platform/features/table', + 'platform/forms', + 'platform/identity', + 'platform/persistence/aggregator', + 'platform/persistence/local', + 'platform/persistence/queue', + 'platform/policy', + 'platform/entanglement', + 'platform/search', + 'platform/status', + 'platform/commonUI/regions' + ]; + + DEFAULTS.forEach(function (bundlePath) { + legacyRegistry.enable(bundlePath); + }); + + return legacyRegistry; +}); diff --git a/src/end.frag b/src/end.frag new file mode 100644 index 0000000000..9746cce6b8 --- /dev/null +++ b/src/end.frag @@ -0,0 +1,2 @@ + return require('openmct'); +})); diff --git a/src/selection/ContextManager.js b/src/selection/ContextManager.js new file mode 100644 index 0000000000..1bd524dcde --- /dev/null +++ b/src/selection/ContextManager.js @@ -0,0 +1,78 @@ +/***************************************************************************** + * 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(['zepto'], function ($) { + /** + * @typedef Context + * @property {*} item + * @property {HTMLElement} element + * @property {Context} parent the containing context (may be undefined) + * @memberof module:openmct + */ + + + function ContextManager() { + this.counter = 0; + this.contexts = {}; + } + + ContextManager.prototype.nextId = function () { + this.counter += 1; + return "context-" + this.counter; + }; + + ContextManager.prototype.context = function (item, htmlElement) { + var $element = $(htmlElement); + var id = $element.attr('data-context') || this.nextId(); + + $element.attr('data-context', id); + + if (this.contexts[id] && this.contexts[id].item !== item) { + this.release(htmlElement); + } + + if (!this.contexts[id]) { + var $parent = $element.closest('[data-context]'); + var parentId = $parent.attr('data-context'); + var parentContext = parentId ? this.contexts[parentId] : undefined; + this.contexts[id] = { + item: item, + element: htmlElement, + parent: parentContext + }; + } + + return this.contexts[id]; + }; + + ContextManager.prototype.release = function (htmlElement) { + var $element = $(htmlElement); + var id = $element.attr('data-context'); + + if (id) { + delete this.contexts[id]; + $element.removeAttr('data-context'); + } + }; + + return ContextManager; +}); diff --git a/src/selection/HoverGesture.js b/src/selection/HoverGesture.js new file mode 100644 index 0000000000..5c1c7ae2ad --- /dev/null +++ b/src/selection/HoverGesture.js @@ -0,0 +1,58 @@ +/***************************************************************************** + * 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(['zepto'], function ($) { + function HoverGesture(hoverManager) { + this.hoverManager = hoverManager; + } + + HoverGesture.prototype.apply = function (htmlElement) { + var $element = $(htmlElement); + var hoverManager = this.hoverManager; + + function update() { + $(hoverManager.all()).removeClass('hovering'); + $(hoverManager.top()).addClass('hovering'); + } + + function enter() { + hoverManager.add(htmlElement); + update(); + } + + function leave() { + hoverManager.remove(htmlElement); + update(); + } + + $element.on('mouseenter', enter); + $element.on('mouseleave', leave); + + return function () { + leave(); + $element.off('mouseenter', enter); + $element.off('mouseleave', leave); + }.bind(this); + }; + + return HoverGesture; +}); diff --git a/src/selection/SelectGesture.js b/src/selection/SelectGesture.js new file mode 100644 index 0000000000..aaea15a484 --- /dev/null +++ b/src/selection/SelectGesture.js @@ -0,0 +1,60 @@ +/***************************************************************************** + * 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(['zepto'], function ($) { + function SelectGesture(selection, contextManager) { + this.selection = selection; + this.contextManager = contextManager; + } + + SelectGesture.prototype.apply = function (htmlElement, item) { + var $element = $(htmlElement); + var contextManager = this.contextManager; + var selection = this.selection; + var path = contextManager.path(item, htmlElement); + + function select() { + selection.add(path); + } + + function change() { + var selected = selection.primary(); + $element.toggleClass( + 'selected', + selected && path.matches(selected) + ); + } + + $element.addClass('selectable'); + $element.on('click', select); + selection.on('change', change); + change(); // Initialize + + return function () { + contextManager.release(htmlElement); + $element.off('click', select); + selection.off('change', change); + }; + }; + + return SelectGesture; +}); diff --git a/src/selection/Selection.js b/src/selection/Selection.js new file mode 100644 index 0000000000..e6e3c19287 --- /dev/null +++ b/src/selection/Selection.js @@ -0,0 +1,69 @@ +/***************************************************************************** + * 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(['EventEmitter'], function (EventEmitter) { + + /** + * Manages selection state for Open MCT + * @private + */ + function Selection() { + EventEmitter.call(this); + this.selected = []; + } + + Selection.prototype = Object.create(EventEmitter.prototype); + + Selection.prototype.add = function (context) { + this.clear(); // Only allow single select as initial simplification + this.selected.push(context); + this.emit('change'); + }; + + Selection.prototype.remove = function (path) { + this.selected = this.selected.filter(function (otherPath) { + return !path.matches(otherPath); + }); + this.emit('change'); + }; + + Selection.prototype.contains = function (path) { + return this.selected.some(function (otherPath) { + return path.matches(otherPath); + }); + }; + + Selection.prototype.clear = function () { + this.selected = []; + this.emit('change'); + }; + + Selection.prototype.primary = function () { + return this.selected[this.selected.length - 1]; + }; + + Selection.prototype.all = function () { + return this.selected; + }; + + return Selection; +}); diff --git a/src/start.frag b/src/start.frag new file mode 100644 index 0000000000..5cd7cf2114 --- /dev/null +++ b/src/start.frag @@ -0,0 +1,39 @@ +/***************************************************************************** + * 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. + *****************************************************************************/ + +/** + * Open MCT https://nasa.github.io/openmct/ + * Version @@version + * Built @@timestamp + * Revision @@revision + * Branch @@branch + */ + +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + define([], factory); + } else if (typeof exports === 'object') { + module.exports = factory(); + } else { + root.MCT = factory(); + } +}(this, function() { diff --git a/src/ui/ViewRegistry.js b/src/ui/ViewRegistry.js new file mode 100644 index 0000000000..67bd494f4d --- /dev/null +++ b/src/ui/ViewRegistry.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([], function () { + /** + * A ViewRegistry maintains the definitions for different kinds of views + * that may occur in different places in the user interface. + * @interface ViewRegistry + * @memberof module:openmct + */ + function ViewRegistry() { + this.providers = []; + } + + + /** + * @private for platform-internal use + * @param {*} item the object to be viewed + * @returns {module:openmct.ViewProvider[]} any providers + * which can provide views of this object + */ + ViewRegistry.prototype.get = function (item) { + return this.providers.filter(function (provider) { + return provider.canView(item); + }); + }; + + /** + * Register a new type of view. + * + * @param {module:openmct.ViewProvider} provider the provider for this view + * @method addProvider + * @memberof module:openmct.ViewRegistry# + */ + ViewRegistry.prototype.addProvider = function (provider) { + this.providers.push(provider); + }; + + /** + * A View is used to provide displayable content, and to react to + * associated life cycle events. + * + * @name View + * @interface + * @memberof module:openmct + */ + + /** + * Populate the supplied DOM element with the contents of this view. + * + * View implementations should use this method to attach any + * listeners or acquire other resources that are necessary to keep + * the contents of this view up-to-date. + * + * @param {HTMLElement} container the DOM element to populate + * @method show + * @memberof module:openmct.View# + */ + + /** + * Release any resources associated with this view. + * + * View implementations should use this method to detach any + * listeners or release other resources that are no longer necessary + * once a view is no longer used. + * + * @method destroy + * @memberof module:openmct.View# + */ + + /** + * Exposes types of views in Open MCT. + * + * @interface ViewProvider + * @memberof module:openmct + */ + + /** + * Check if this provider can supply views for a domain object. + * + * When called by Open MCT, this may include additional arguments + * which are on the path to the object to be viewed; for instance, + * when viewing "A Folder" within "My Items", this method will be + * invoked with "A Folder" (as a domain object) as the first argument, + * and "My Items" as the second argument. + * + * @method canView + * @memberof module:openmct.ViewProvider# + * @param {module:openmct.DomainObject} domainObject the domain object + * to be viewed + * @returns {boolean} true if this domain object can be viewed using + * this provider + */ + + /** + * Provide a view of this object. + * + * When called by Open MCT, this may include additional arguments + * which are on the path to the object to be viewed; for instance, + * when viewing "A Folder" within "My Items", this method will be + * invoked with "A Folder" (as a domain object) as the first argument, + * and "My Items" as the second argument. + * + * @method view + * @memberof module:openmct.ViewProvider# + * @param {*} object the object to be viewed + * @returns {module:openmct.View} a view of this domain object + */ + + /** + * Get metadata associated with this view provider. This may be used + * to populate the user interface with options associated with this + * view provider. + * + * @method metadata + * @memberof module:openmct.ViewProvider# + * @returns {module:openmct.ViewProvider~ViewMetadata} view metadata + */ + + /** + * @typedef ViewMetadata + * @memberof module:openmct.ViewProvider~ + * @property {string} name the human-readable name of this view + * @property {string} key a machine-readable name for this view + * @property {string} [description] a longer-form description (typically + * a single sentence or short paragraph) of this kind of view + * @property {string} cssclass the CSS class to apply to labels for this + * view (to add icons, for instance) + */ + + return ViewRegistry; + +}); diff --git a/test-main.js b/test-main.js index e006094a24..b300560588 100644 --- a/test-main.js +++ b/test-main.js @@ -53,6 +53,7 @@ requirejs.config({ "angular": "bower_components/angular/angular.min", "angular-route": "bower_components/angular-route/angular-route.min", "csv": "bower_components/comma-separated-values/csv.min", + "EventEmitter": "bower_components/eventemitter3/index", "es6-promise": "bower_components/es6-promise/es6-promise.min", "html2canvas": "bower_components/html2canvas/build/html2canvas.min", "moment": "bower_components/moment/moment", @@ -61,7 +62,9 @@ requirejs.config({ "screenfull": "bower_components/screenfull/dist/screenfull.min", "text": "bower_components/text/text", "uuid": "bower_components/node-uuid/uuid", - "zepto": "bower_components/zepto/zepto.min" + "zepto": "bower_components/zepto/zepto.min", + "lodash": "bower_components/lodash/lodash", + "d3": "bower_components/d3/d3.min" }, "shim": { @@ -71,6 +74,9 @@ requirejs.config({ "angular-route": { "deps": [ "angular" ] }, + "EventEmitter": { + "exports": "EventEmitter" + }, "moment-duration-format": { "deps": [ "moment" ] }, @@ -79,6 +85,12 @@ requirejs.config({ }, "zepto": { "exports": "Zepto" + }, + "lodash": { + "exports": "lodash" + }, + "d3": { + "exports": "d3" } },