diff --git a/Procfile b/Procfile index aa6094edfe..1e13b4ae05 100644 --- a/Procfile +++ b/Procfile @@ -1 +1 @@ -web: node app.js --port $PORT --include example/localstorage \ No newline at end of file +web: node app.js --port $PORT diff --git a/README.md b/README.md index 42cd060282..6a412412ef 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,21 @@ This build will: Run as `mvn clean install`. +### Building Documentation + +Open MCT Web's documentation is generated by an +[npm](https://www.npmjs.com/)-based build: + +* `npm install` _(only needs to run once)_ +* `npm run docs` + +Documentation will be generated in `target/docs`. Note that diagram +generation is dependent on having [Cairo](http://cairographics.org/download/) +installed; see +[node-canvas](https://github.com/Automattic/node-canvas#installation)'s +documentation for help with installation. + + # Glossary Certain terms are used throughout Open MCT Web with consistent meanings diff --git a/docs/gendocs.js b/docs/gendocs.js index 2fcda7214e..cd61b9a9bf 100644 --- a/docs/gendocs.js +++ b/docs/gendocs.js @@ -30,7 +30,8 @@ var CONSTANTS = { DIAGRAM_WIDTH: 800, DIAGRAM_HEIGHT: 500 - }; + }, + TOC_HEAD = "# Table of Contents"; GLOBAL.window = GLOBAL.window || GLOBAL; // nomnoml expects window to be defined (function () { @@ -44,6 +45,7 @@ GLOBAL.window = GLOBAL.window || GLOBAL; // nomnoml expects window to be define split = require("split"), stream = require("stream"), nomnoml = require('nomnoml'), + toc = require("markdown-toc"), Canvas = require('canvas'), options = require("minimist")(process.argv.slice(2)); @@ -110,6 +112,9 @@ GLOBAL.window = GLOBAL.window || GLOBAL; // nomnoml expects window to be define done(); }; transform._flush = function (done) { + // Prepend table of contents + markdown = + [ TOC_HEAD, toc(markdown).content, "", markdown ].join("\n"); this.push("\n"); this.push(marked(markdown)); this.push("\n\n"); @@ -133,8 +138,8 @@ GLOBAL.window = GLOBAL.window || GLOBAL; // nomnoml expects window to be define customRenderer.link = function (href, title, text) { // ...but only if they look like relative paths return (href || "").indexOf(":") === -1 && href[0] !== "/" ? - renderer.link(href.replace(/\.md/, ".html"), title, text) : - renderer.link.apply(renderer, arguments); + renderer.link(href.replace(/\.md/, ".html"), title, text) : + renderer.link.apply(renderer, arguments); }; return customRenderer; } @@ -179,13 +184,17 @@ GLOBAL.window = GLOBAL.window || GLOBAL; // nomnoml expects window to be define glob(options['in'] + "/**/*.@(html|css|png)", {}, function (err, files) { files.forEach(function (file) { var destination = file.replace(options['in'], options.out), - destPath = path.dirname(destination); - + destPath = path.dirname(destination), + streamOptions = {}; + if (file.match(/png$/)){ + streamOptions.encoding = 'binary'; + } else { + streamOptions.encoding = 'utf8'; + } + mkdirp(destPath, function (err) { - fs.createReadStream(file, { encoding: 'utf8' }) - .pipe(fs.createWriteStream(destination, { - encoding: 'utf8' - })); + fs.createReadStream(file, streamOptions) + .pipe(fs.createWriteStream(destination, streamOptions)); }); }); }); diff --git a/docs/src/architecture/Platform.md b/docs/src/architecture/Platform.md index 80f9e487f5..a59a6ebf9c 100644 --- a/docs/src/architecture/Platform.md +++ b/docs/src/architecture/Platform.md @@ -35,16 +35,26 @@ in __any of these tiers__. * _DOM_: The rendered HTML document, composed from HTML templates which have been processed by AngularJS and will be updated by AngularJS to reflect changes from the presentation layer. User interactions - are initiated from here and invoke behavior in the presentation layer. + are initiated from here and invoke behavior in the presentation layer. HTML  + templates are written in Angular’s template syntax; see the [Angular documentation on templates](https://docs.angularjs.org/guide/templates)​.  + These describe the page as actually seen by the user. Conceptually,  + stylesheets (controlling the look­and­feel of the rendered templates) belong  + in this grouping as well.  * [_Presentation layer_](#presentation-layer): The presentation layer is responsible for updating (and providing information to update) the displayed state of the application. The presentation layer consists primarily of _controllers_ and _directives_. The presentation layer is concerned with inspecting the information model and preparing it for display. -* [_Information model_](#information-model): The information model - describes the state and behavior of the objects with which the user - interacts. +* [_Information model_](#information-model): ​Provides a common (within Open MCT  + Web) set of interfaces for dealing with “things” ­ domain objects ­ within the  + system. User­facing concerns in a Open MCT Web application are expressed as  + domain objects; examples include folders (used to organize other domain  + objects), layouts (used to build displays), or telemetry points (used as  + handles for streams of remote measurements.) These domain objects expose a  + common set of interfaces to allow reusable user interfaces to be built in the  + presentation and template tiers; the specifics of these behaviors are then  + mapped to interactions with underlying services.  * [_Service infrastructure_](#service-infrastructure): The service infrastructure is responsible for providing the underlying general functionality needed to support the information model. This includes @@ -52,7 +62,9 @@ in __any of these tiers__. back-end. * _Back-end_: The back-end is out of the scope of Open MCT Web, except for the interfaces which are utilized by adapters participating in the - service infrastructure. + service infrastructure. Includes the underlying persistence stores, telemetry  + streams, and so forth which the Open MCT Web client is being used to interact  + with. ## Application Start-up diff --git a/docs/src/guide/index.md b/docs/src/guide/index.md index c575439d48..7b35dd66cc 100644 --- a/docs/src/guide/index.md +++ b/docs/src/guide/index.md @@ -1,3 +1,2334 @@ -# Developer Guide +# Open MCT Web Developer Guide +Victor Woeltjen -This is a placeholder for the developer guide. +[victor.woeltjen@nasa.gov](mailto:victor.woeltjen@nasa.gov) + +September 23, 2015 +Document Version 1.1 + +Date | Version | Summary of Changes | Author +------------------- | --------- | ----------------------- | --------------- +April 29, 2015 | 0 | Initial Draft | Victor Woeltjen +May 12, 2015 | 0.1 | | Victor Woeltjen +June 4, 2015 | 1.0 | Name Changes | Victor Woeltjen +October 4, 2015 | 1.1 | Conversion to MarkDown | Andrew Henry + +# Introduction +The purpose of this guide is to familiarize software developers with the Open +MCT Web platform. + +## What is Open MCT Web +Open MCT Web is a platform for building user interface and display tools, +developed at the NASA Ames Research Center in collaboration with teams at the +Jet Propulsion Laboratory. It is written in HTML5, CSS3, and JavaScript, using +[AngularJS](http://www.angularjs.org) as a framework. Its intended use is to +create single-page web applications which integrate data and behavior from a +variety of sources and domains. + +Open MCT Web has been developed to support the remote operation of space +vehicles, so some of its features are specific to that task; however, it is +flexible enough to be adapted to a variety of other application domains where a +display tool oriented toward browsing, composing, and visualizing would be +useful. + +Open MCT Web provides: + +* A common user interface paradigm which can be applied to a variety of domains +and tasks. Open MCT Web is more than a widget toolkit - it provides a standard +tree-on-the-left, view-on-the-right browsing environment which you customize by +adding new browsable object types, visualizations, and back-end adapters. +* A plugin framework and an extensible API for introducing new application +features of a variety of types. +* A set of general-purpose object types and visualizations, as well as some +visualizations and infrastructure specific to telemetry display. + +## Client-Server Relationship +Open MCT Web is client software - it runs entirely in the user's web browser. As +such, it is largely 'server agnostic'; any web server capable of serving files +from paths is capable of providing Open MCT Web. + +While Open MCT Web can be configured to run as a standalone client, this is +rarely very useful. Instead, it is intended to be used as a display and +interaction layer for information obtained from a variety of back-end services. +Doing so requires authoring or utilizing adapter plugins which allow Open MCT +Web to interact with these services. + +Typically, the pattern here is to provide a known interface that Open MCT Web +can utilize, and implement it such that it interacts with whatever back-end +provides the relevant information. Examples of back-ends that can be utilized in +this fashion include databases for the persistence of user-created objects, or +sources of telemetry data. + +See the [Architecture Guide](../architecture/index.md#Overview) for information +on the client-server relationship. + +## Developing with Open MCT Web +Building applications with Open MCT Web typically means authoring and utilizing +a set of plugins which provide application-specific details about how Open MCT +Web should behave. + +### Technologies + +Open MCT Web sources are written in JavaScript, with a number of configuration +files written in JSON. Displayable components are written in HTML5 and CSS3. +Open MCT Web is built using [AngularJS](http://www.angularjs.org) from Google. A +good understanding of Angular is recommended for developers working with Open +MCT Web. + +### Forking +Open MCT Web does not currently have a single stand-alone artifact that can be +used as a library. Instead, the recommended approach for creating a new +application is to start by forking/branching Open MCT Web, and then adding new +features from there. Put another way, Open MCT Web's source structure is built +to serve as a template for specific applications. + +Forking in this manner should not require that you edit Open MCT Web's sources. +The preferred approach is to create a new directory (peer to `index.html`) for +the new application, then add new bundles (as described in the Framework +chapter) within that directory. + +To initially clone the Open MCT Web repository: +`git clone -b open-master` + +To create a fork to begin working on a new application using Open MCT Web: + + cd + git checkout open-master + git checkout -b + +As a convention used internally, applications built using Open MCT Web have +master branch names with an identifying prefix. For instance, if building an +application called 'Foo', the last statement above would look like: + + git checkout -b foo-master + +This convention is not enforced or understood by Open MCT Web in any way; it is +mentioned here as a more general recommendation. + +# Overview + +Open MCT Web is implemented as a framework component which manages a set of +other components. These components, called _bundles_, act as containers to group +sets of related functionality; individual units of functionality are expressed +within these bundles as _extensions_. + +Extensions declare dependencies on other extensions (either individually or +categorically), and the framework provides actual extension instances at +run-time to satisfy these declared dependency. This dependency injection +approach allows software components which have been authored separately (e.g. as +plugins) but to collaborate at run-time. + +Open MCT Web's framework layer is implemented on top of AngularJS's [dependency +injection mechanism](https://docs.angularjs.org/guide/di) and is modelled after +[OSGi](hhttp://www.osgi.org/) and its [Declarative Services component model](http://wiki.osgi.org/wiki/Declarative_Services). +In particular, this is where the term _bundle_ comes from. + +## Framework Overview + +The framework's role in the application is to manage connections between +bundles. All application-specific behavior is provided by individual bundles, or +as the result of their collaboration. + +The framework is described in more detail in the [Framework Overview](../architecture/Framework.md#Overview) of the +architecture guide. + +### Tiers +While all bundles in a running Open MCT Web instance are effectively peers, it +is useful to think of them as a tiered architecture, where each tier adds more +specificity to the application. +```nomnoml +#direction: down +[Plugins (Features external to OpenMCTWeb) *Bundle]->[OpenMCTWeb | +[Application (Plots, layouts, ElasticSearch wrapper) *Bundle]->[Platform (Core API, common UI, infrastructure) *Bundle] +[Platform (Core API, common UI, infrastructure) *Bundle]->[Framework (RequireJS, AngularJS, bundle loader)]] +``` + +* __Framework__ : This tier is responsible for wiring together the set of +configured components (called _bundles_) together to instantiate the running +application. It is responsible for mediating between AngularJS (in particular, +its dependency injection mechanism) and RequireJS (to load scripts at run-time.) +It additionally interprets bundle definitions (see explanation below, as well as +further detail in the Framework chapter.) At this tier, we are at our most +general: We know only that we are a plugin-based application. +* __Platform__: Components in the Platform tier describe both the general user +interface and corresponding developer-facing interfaces of Open MCT Web. This +tier provides the general infrastructure for applications. It is less general +than the framework tier, insofar as this tier introduces a specific user +interface paradigm, but it is still non-specific as to what useful features +will be provided. Although they can be removed or replaced easily, bundles +provided by the Platform tier generally should not be thought of as optional. +* __Application__: The application tier consists of components which utilize the +infrastructure provided by the Platform to provide functionality which will (or +could) be useful to specific applications built using Open MCT Web. These +include adapters to specific persistence back-ends (such as ElasticSearch or +CouchDB) as well as bundles which describe more user-facing features (such as +_Plot_ views for visualizing time series data, or _Layout_ objects for +display-building.) Bundles from this tier can be added or removed without +compromising basic application functionality, with the caveat that at least one +persistence adapter needs to be present. +* __Plugins__: Conceptually, this tier is not so different from the application +tier; it consists of bundles describing new features, back-end adapters, that +are specific to the application being built on Open MCT Web. It is described as +a separate tier here because it has one important distinction from the +application tier: It consists of bundles that are not included with the platform +(either authored anew for the specific application, or obtained from elsewhere.) + +Note that bundles in any tier can go off and consult back-end services. In +practice, this responsibility is handled at the Application and/or Plugin tiers; +Open MCT Web is built to be server-agnostic, so any back-end is considered an +application-specific detail. + +## Platform Overview + +The "tiered" architecture described in the preceding text describes a way of +thinking of and categorizing software components of a Open MCT Web application, +as well as the framework layer's role in mediating between these components. +Once the framework layer has wired these software components together, however, +the application's logical architecture emerges. + +An overview of the logical architecture of the platform is given in the [Platform Architecture](../architecture/Platform.md#PlatformArchitecture) +section of the Platform guide + +### Web Services + +As mentioned in the Introduction, Open MCT Web is a platform single-page +applications which runs entirely in the browser. Most applications will want to +additionally interact with server-side resources, to (for example) read +telemetry data or store user-created objects. This interaction is handled by +individual bundles using APIs which are supported in browser (such as +`XMLHttpRequest`, typically wrapped by Angular's `$http`.) + +```nomnoml +#direction: right +[Web Service #1] <- [Web Browser] +[Web Service #2] <- [Web Browser] +[Web Service #3] <- [Web Browser] +[ Web Browser | + [ Open MCT Web | + [Plugin Bundle #1]-->[Core API] + [Core API]<--[Plugin Bundle #2] + [Platform Bundle #1]-->[Core API] + [Platform Bundle #2]-->[Core API] + [Platform Bundle #3]-->[Core API] + [Core API]<--[Platform Bundle #4] + [Core API]<--[Platform Bundle #5] + [Core API]<--[Plugin Bundle #3] + ] + [Open MCT Web] ->[Browser APIs] +] +``` + +This architectural approach ensures a loose coupling between applications built +using Open MCT Web and the backends which support them. + +### Glossary + +Certain terms are used throughout Open MCT Web with consistent meanings or +conventions. Other developer documentation, particularly in-line documentation, +may presume an understanding of these terms. + +* __bundle__: A bundle is a removable, reusable grouping of software elements. +The application is composed of bundles. Plug-ins are bundles. +* __capability__: A JavaScript object which exposes dynamic behavior or +non-persistent state associated with a domain object. +* __category__: A machine-readable identifier for a group that something may +belong to. +* __composition __: In the context of a domain object, this refers to the set of +other domain objects that compose or are contained by that object. A domain +object's composition is the set of domain objects that should appear immediately + beneath it in a tree hierarchy. A domain object's composition is described in +its model as an array of identifiers; its composition capability provides a +means to retrieve the actual domain object instances associated with these +identifiers asynchronously. +* __description__: When used as an object property, this refers to the human- +readable description of a thing; usually a single sentence or short paragraph. +(Most often used in the context of extensions, domain object models, or other +similar application-specific objects.) +* __domain object __: A meaningful object to the user; a distinct thing in the +work support by Open MCT Web. Anything that appears in the left-hand tree is a +domain object. +* __extension __: An extension is a unit of functionality exposed to the platform +in a declarative fashion by a bundle. The term 'extension category' is used to +distinguish types of extensions from specific extension instances. +* __id__: A string which uniquely identifies a domain object. +* __key__: When used as an object property, this refers to the machine-readable +identifier for a specific thing in a set of things. (Most often used in the +context of extensions or other similar application-specific object sets.) This +term is chosen to avoid attaching ambiguous meanings to 'id'. +* __model__: The persistent state associated with a domain object. A domain +object's model is a JavaScript object which can be converted to JSON without +losing information (that is, it contains no methods.) +* __name__: When used as an object property, this refers to the human-readable +name for a thing. (Most often used in the context of extensions, domain object +models, or other similar application-specific objects.) +* __navigation__: Refers to the current state of the application with respect to +the user's expressed interest in a specific domain object; e.g. when a user +clicks on a domain object in the tree, they are navigating to it, and it is +thereafter considered the navigated object (until the user makes another such +choice.) This term is used to distinguish navigation from selection, which +occurs in an editing context. +* __space__: A machine-readable name used to identify a persistence store. +Interactions with persistence with generally involve a space parameter in some +form, to distinguish multiple persistence stores from one another (for cases +where there are multiple valid persistence locations available.) +* __source__: A machine-readable name used to identify a source of telemetry +data. Similar to "space", this allows multiple telemetry sources to operate +side-by-side without conflicting. + +# Framework + +Open MCT Web is built on the [AngularJS framework]( http://www.angularjs.org ). A +good understanding of that framework is recommended. + +Open MCT Web adds an extra layer on top of AngularJS to (a) generalize its +dependency injection mechanism slightly, particularly to handle many-to-one +relationships; and (b) handle script loading. Combined, these features become a +plugin mechanism. + +This framework layer operates on two key concepts: + +* __Bundle:__ A bundle is a collection of related functionality that can be +added to the application as a group. More concretely, a bundle is a directory +containing a JSON file declaring its contents, as well as JavaScript sources, +HTML templates, and other resources used to support that functionality. (The +term bundle is borrowed from [OSGi](http://www.osgi.org/) - which has also +inspired many of the concepts used in the framework layer. A familiarity with +OSGi, particularly Declarative Services, may be useful when working with Open +MCT Web.) +* __Extension:__ An extension is an individual unit of functionality. Extensions +are collected together in bundles, and may interact with other extensions. + +The framework layer, loaded and initiated from `index.html`, is the main point +of entry for an application built on Open MCT Web. It is responsible for wiring +together the application at run time (much of this responsibility is actually +delegated to Angular); at a high-level, the framework does this by proceeding +through four stages: + +1. __Loading definitions:__ JSON declarations are loaded for all bundles which +will constitute the application, and wrapped in a useful API for subsequent +stages. +2. __Resolving extensions:__ Any scripts which provide implementations for +extensions exposed by bundles are loaded, using Require. +3. __Registering extensions__ Resolved extensions are registered with Angular, +such that they can be used by the application at run-time. This stage includes +both registration of Angular built-ins (directives, controllers, routes, +constants, and services) as well as registration of non-Angular extensions. +4. __Bootstrapping__ The Angular application is bootstrapped; at that point, +Angular takes over and populates the body of the page using the extensions that +have been registered. + +## Bundles + +The basic configurable unit of Open MCT Web is the _bundle_. This term has been +used a bit already; now we'll get to a more formal definition. + +A bundle is a directory which contains: + +* A bundle definition; a file named `bundle.json`. +* Subdirectories for sources, resources, and tests. +* Optionally, a `README.md` Markdown file describing its contents (this is not +used by Open MCT Web in any way, but it's a helpful convention to follow.) + +The bundle definition is the main point of entry for the bundle. The framework +looks at this to determine which components need to be loaded and how they +interact. + +A plugin in Open MCT Web is a bundle. The platform itself is also decomposed +into bundles, each of which provides some category of functionality. The +difference between a _bundle_ and a _plugin_ is purely a matter of the intended +use; a plugin is just a bundle that is meant to be easily added or removed. When +developing, it is typically more useful to think in terms of bundles. + +### Configuring Active Bundles + +To decide which bundles should be loaded, the framework loads a file named +`bundles.json` (peer to the `index.html` file which serves the application) to +determine which bundles should be loaded. This file should contain a single JSON +array of strings, where each is the path to a bundle. These paths should not +include bundle.json (this is implicit) or a trailing slash. + +For instance, if `bundles.json` contained: + + [ + "example/builtins", + "example/extensions" + ] + +...then the Open MCT Web framework would look for bundle definitions at +`example/builtins/bundle.json` and `example/extensions/bundle.json`, relative +to the path of `index.html`. No other bundles would be loaded. + +### Bundle Definition + +A bundle definition (the `bundle.json` file located within a bundle) contains a +description of the bundle itself, as well as the information exposed by the +bundle. + +This definition is expressed as a single JSON object with the following +properties (all of which are optional, falling back to reasonable defaults): + +* `key`: A machine-readable name for the bundle. (Currently used only in +logging.) +* `name`: A human-readable name for the bundle. (Also only used in logging.) +* `sources`: Names a directory in which source scripts (which will implement +extensions) are located. Defaults to 'src' +* `resources`: Names a directory in which resource files (such as HTML templates, +images, CS files, and other non-JavaScript files needed by this bundle) are +located. Defaults to 'res' +* `libraries`: Names a directory in which third-party libraries are located. +Defaults to 'lib' +* `configuration`: A bundle's configuration object, which should be formatted as +would be passed to require.config (see [RequireJS documentation](http://requirejs.org/docs/api.html ) ); +note that only paths and shim have been tested. +* `extensions`: An object containing key-value pairs, where keys are extension +categories, and values are extension definitions. See the section on Extensions +for more information. + +For example, the bundle definition for example/policy looks like: + + { + "name": "Example Policy", + "description": "Provides an example of using policies.", + "sources": "src", + "extensions": { + "policies": [ + { + "implementation": "ExamplePolicy.js", + "category": "action" + } + ] + } + } + +### Bundle Directory Structure + +In addition to the directories defined in the bundle definition, a bundle will +typically contain other directories not used at run-time. Additionally, some +useful development scripts (such as the command line build and the test suite) +expect this directory structure to be in use, and may ignore options chosen by +`b undle.json`. It is recommended that the directory structure described below be +used for new bundles. + +* `src`: Contains JavaScript sources for this bundle. May contain additional +subdirectories to organize these sources; typically, these subdirectories are +named to correspond to the extension categories they contain and/or support, but +this is only a convention. +* `res`: Contains other files needed by this bundle, such as HTML templates. May +contain additional subdirectories to organize these sources. +* `lib`: Contains JavaScript sources from third-party libraries. These are +separated from bundle sources in order to ignore them during code style checking +from the command line build. +* `test`: Contains JavaScript sources implementing [Jasmine](http://jasmine.github.io/) +tests, as well as a file named `suite.json` describing which files to test. +Should have the same folder structure as the `src` directory; see the section on +automated testing for more information. + +For example, the directory structure for bundle `platform/commonUI/about` looks +like: + + Platform + | + |-commonUI + | + +-about + | + |-res + | + |-src + | + |-test + | + |-bundle.json + | + +-README.md + +## Extensions + +While bundles provide groupings of related behaviors, the individual units of +behavior are called extensions. + +Extensions belong to categories; an extension category is the machine-readable +identifier used to identify groups of extensions. In the `extensions` property +of a bundle definition, the keys are extension categories and the values are +arrays of extension definitions. + +### General Extensions + +Extensions are intended as a general-purpose mechanism for adding new types of +functionality to Open MCT Web. + +An extension category is registered with Angular under the name of the +extension, plus a suffix of two square brackets; so, an Angular service (or, +generally, any other extension) can access the full set of registered +extensions, from all bundles, by including this string (e.g. `types[]` to get +all type definitions) in a dependency declaration. + +As a convention, extension categories are given single-word, plural nouns for +names within Open MCT Web (e.g. `types`.) This convention is not enforced by the +platform in any way. For extension categories introduced by external plugins, it +is recommended to prefix the extension category with a vendor identifier (or +similar) followed by a dot, to avoid collisions. + +### Extension Definitions + +The properties used in extension definitions are typically unique to each +category of extension; a few properties have standard interpretations by the +platform. + +* `implementation`: Identifies a JavaScript source file (in the sources +folder) which implements this extension. This JavaScript file is expected to +contain an AMD module (see http://requirejs.org/docs/whyamd.html#amd ) which +gives as its result a single constructor function. +* `depends`: An array of dependencies needed by this extension; these will be +passed on to Angular's [dependency injector](https://docs.angularjs.org/guide/di ) . +By default, this is treated as an empty array. Note that depends does not make +sense without `implementation` (since these dependencies will be passed to the +implementation when it is instantiated.) +* `priority`: A number or string indicating the priority order (see below) of +this extension instance. Before an extension category is registered with +AngularJS, the extensions of this category from all bundles will be concatenated +into a single array, and then sorted by priority. + +Extensions do not need to have an implementation. If no implementation is +provided, consumers of the extension category will receive the extension +definition as a plain JavaScript object. Otherwise, they will receive the +partialized (see below) constructor for that implementation, which will +additionally have all properties from the extension definition attached. + +#### Partial Construction + +In general, extensions are intended to be implemented as constructor functions, +which will be used elsewhere to instantiate new objects of that type. However, +the Angular-supported method for dependency injection is (effectively) +constructor-style injection; so, both declared dependencies and run-time +arguments are competing for space in a constructor's arguments. + +To resolve this, the Open MCT Web framework registers extension instances in a +partially constructed form. That is, the constructor exposed by the extension's +implementation is effectively decomposed into two calls; the first takes the +dependencies, and returns the constructor in its second form, which takes the +remaining arguments. + +This means that, when writing implementations, the constructor function should +be written to include all declared dependencies, followed by all run-time +arguments. When using extensions, only the run-time arguments need to be +provided. + +#### Priority + +Within each extension category, registration occurs in priority order. An +extension's priority may be specified as a `priority` property in its extension +definition; this may be a number, or a symbolic string. Extensions are +registered in reverse order (highest-priority first), and symbolic strings are +mapped to the numeric values as follows: + +* `fallback`: Negative infinity. Used for extensions that are not intended for +use (that is, they are meant to be overridden) but are present as an option of +last resort. +* `default`: `-100`. Used for extensions that are expected to be overridden, but +need a useful default. +* `none`: `0`. Also used if no priority is specified, or if an unknown or +malformed priority is specified. +* `optional`: `100`. Used for extensions that are meant to be used, but may be +overridden. +* `preferred`: `1000`. Used for extensions that are specifically intended to be +used, but still may be overridden in principle. +* `mandatory`: Positive infinity. Used when an extension should definitely not +be overridden. + +These symbolic names are chosen to support usage where many extensions may +satisfy a given need, but only one may be used; in this case, as a convention it +should be the lowest-ordered (highest-priority) extensions available. In other +cases, a full set (or multi-element subset) of extensions may be desired, with a +specific ordering; in these cases, it is preferable to specify priority +numerically when declaring extensions, and to understand that extensions will be +sorted according to these conventions when using them. + +### Angular Built-ins + +Several entities supported Angular are expressed and managed as extensions in +Open MCT Web. Specifically, these extension categories are _directives_, +_controllers_, _services_, _constants_, _runs_, and _routes_. + +#### Angular Directives + +New [directives]( https://docs.angularjs.org/guide/directive ) may be +registered as extensions of the directives category. Implementations of +directives in this category should take only dependencies as arguments, and +should return a directive definition object. + +The directive's name should be provided as a key property of its extension +definition, in camel-case format. + +#### Angular Controllers + +New [controllers]( https://docs.angularjs.org/guide/controller ) may be registered +as extensions of the controllers category. The implementation is registered +directly as the controller; its only constructor arguments are its declared +dependencies. + +The directive's identifier should be provided as a key property of its extension +definition. + + +#### Angular Services + +New [services](https://docs.angularjs.org/guide/services ) may be registered as +extensions of the services category. The implementation is registered via a +[service call]( https://docs.angularjs.org/api/auto/service/$provide#service ), so +it will be instantiated with the new operator. + +#### Angular Constants + +Constant values may be registered as extensions of the [ constants category](https://docs.angularjs.org/api/ng/type/angular.Module#constant ). +These extensions have no implementation; instead, they should contain a property + key , which is the name under which the constant will be registered, and a +property value , which is the constant value that will be registered. + +#### Angular Runs + +In some cases, you want to register code to run as soon as the application +starts; these can be registered as extensions of the [ runs category](https://docs.angularjs.org/api/ng/type/angular.Module#run ). +Implementations registered in this category will be invoked (with their declared +dependencies) when the Open MCT Web application first starts. (Note that, in +this case, the implementation is better thought of as just a function, as +opposed to a constructor function.) + +#### Angular Routes + +Extensions of category `routes` will be registered with Angular's [route provider](https://docs.angularjs.org/api/ngRoute/provider/$routeProvider ). +Extensions of this category have no implementations, and need only two +properties in their definition: + +* `when`: The value that will be passed as the path argument to `$routeProvider.when`; +specifically, the string that will appear in the trailing +part of the URL corresponding to this route. This property may be omitted, in +which case this extension instance will be treated as the default route. +* `templateUrl`: A path to the template to render for this route. Specified as a +path relative to the bundle's resource directory (`res` by default.) + +### Composite Services + +Composite services are described in the [relevant section](../architecture/Framework.md#Composite-Services) +of the framework guide. + +A component should include the following properties in its extension definition: + +* `provides`: The symbolic identifier for the service that will be composed. The + fully-composed service will be registered with Angular under this name. +* `type`: One of `provider`, `aggregator` or `decorator` (as above) + +In addition to any declared dependencies, _aggregators_ and _decorators_ both +receive one more argument (immediately following declared dependencies) that is +provided by the framework. For an aggregator, this will be an array of all +providers of the same service (that is, with matching `provides` properties); +for a decorator, this will be whichever provider, decorator, or aggregator is +next in the sequence of decorators. + +Services exposed by the Open MCT Web platform are often declared as composite +services, as this form is open for a variety of common modifications. + +# Core API + +Most of Open MCT Web's relevant API is provided and/or mediated by the +framework; that is, much of developing for Open MCT Web is a matter of adding +extensions which access other parts of the platform by means of dependency +injection. + +The core bundle (`platform/core`) introduces a few additional object types meant +to be passed along by other services. + +## Domain Objects + +Domain objects are the most fundamental component of Open MCT Web's information +model. A domain object is some distinct thing relevant to a user's work flow, +such as a telemetry channel, display, or similar. Open MCT Web is a tool for +viewing, browsing, manipulating, and otherwise interacting with a graph of +domain objects. + +A domain object should be conceived of as the union of the following: + +* __Identifier__: A machine-readable string that uniquely identifies the domain +object within this application instance. +* __Model__: The persistent state of the domain object. A domain object's model +is a JavaScript object that can be losslessly converted to JSON. +* __Capabilities__: Dynamic behavior associated with the domain object. +Capabilities are JavaScript objects which provide additional methods for +interacting with the domain objects which expose those capabilities. Not all +domain objects expose all capabilities. + +At run-time, a domain object has the following interface: + +* `getId()`: Get the identifier for this domain object. +* `getModel()`: Get the plain state associated with this domain object. This +will return a JavaScript object that can be losslessly converted to JSON. Note +that the model returned here can be modified directly but should not be; +instead, use the mutation capability. +* `getCapability(key)`: Get the specified capability associated with this domain +object. This will return a JavaScript object whose interface is specific to the +type of capability being requested. If the requested capability is not exposed +by this domain object, this will return undefined . +* `hasCapability(key)`: Shorthand for checking if a domain object exposes the +requested capability. +* `useCapability(key, arguments )`: Shorthand for +`getCapability(key).invoke(arguments)`, with additional checking between calls. +If the provided capability has no invoke method, the return value here functions +as `getCapability` including returning `undefined` if the capability is not +exposed. + +## Domain Object Actions + +An `Action` is behavior that can be performed upon/using a `DomainObject`. An +Action has the following interface: + +* `perform()`: Do this action. For example, if one had an instance of a +`RemoveAction` invoking its perform method would cause the domain object which +exposed it to be removed from its container. +* `getMetadata()`: Get metadata associated with this action. Returns an object +containing: + * `name`: Human-readable name. + * `description`: Human-readable summary of this action. + * `glyph`: Single character to be displayed in Open MCT Web's icon font set. + * `context`: The context in which this action is being performed (see below) + +Action instances are typically obtained via a domain object's `action` +capability. + +### Action Contexts + +An action context is a JavaScript object with the following properties: + +* `domainObject`: The domain object being acted upon. +* `selectedObject`: Optional; the selection at the time of action (e.g. the +dragged object in a drag-and-drop operation.) + +## Telemetry + +Telemetry series data in Open MCT Web is represented by a common interface, and +packaged in a consistent manner to facilitate passing telemetry updates around +multiple visualizations. + +### Telemetry Requests + +A telemetry request is a JavaScript object containing the following properties: + +* `source`: A machine-readable identifier for the source of this telemetry. This +is useful when multiple distinct data sources are in use side-by-side. +* `key`: A machine-readable identifier for a unique series of telemetry within +that source. +* _Note: This API is still under development; additional properties, such as +start and end time, should be present in future versions of Open MCT Web._ + +Additional properties may be included in telemetry requests which have specific +interpretations for specific sources. + +### Telemetry Responses + +When returned from the `telemetryService` (see [Services](#Services) section), +telemetry series data will be packaged in a `source -> key -> TelemetrySeries` +fashion. That is, telemetry is passed in an object containing key-value pairs. +Keys identify telemetry sources; values are objects containing additional +key-value pairs. In this object, keys identify individual telemetry series (and +match they `key` property from corresponding requests) and values are +`TelemetrySeries` objects (see below.) + +### Telemetry Series + +A telemetry series is a specific sequence of data, typically associated with a +specific instrument. Telemetry is modeled as an ordered sequence of domain and +range values, where domain values must be non-decreasing but range values do +not. (Typically, domain values are interpreted as UTC timestamps in milliseconds +relative to the UNIX epoch.) A series must have at least one domain and one +range, and may have more than one. + +Telemetry series data in Open MCT Web is expressed via the following +`TelemetrySeries` interface: + +* `getPointCount()`: Returns the number of unique points/samples in this series. +* `getDomainValue(index, [domain])`: Get the domain value at the specified index . +If a second domain argument is provided, this is taken as a string identifier +indicating which domain option (of, presumably, multiple) should be returned. +* `getRangeValue(index, [range])`: Get the domain value at the specified index . +If a second range argument is provided, this is taken as a string identifier +indicating which range option (of, presumably, multiple) should be returned. + +### Telemetry Metadata + +Domain objects which have associated telemetry also expose metadata about that +telemetry; this is retrievable via the `getMetadata()` of the telemetry +capability. This will return a single JavaScript object containing the following +properties: + +* `source`: The machine-readable identifier for the source of telemetry data for +this object. +* `key`: The machine-readable identifier for the individual telemetry series. +* `domains`: An array of supported domains (see TelemetrySeries above.) Each +domain should be expressed as an object which includes: + * `key`: Machine-readable identifier for this domain, as will be passed into + a getDomainValue(index, domain) call. + * `name`: Human-readable name for this domain. +* `ranges`: An array of supported ranges; same format as domains . + +Note that this metadata is also used as the prototype for telemetry requests +made using this capability. + +## Types +A domain object's type is represented as a Type object, which has the following +interface: + +* `getKey()`: Get the machine-readable identifier for this type. +* `getName()`: Get the human-readable name for this type. +* `getDescription()`: Get a human-readable summary of this type. +* `getGlyph()`: Get the single character to be rendered as an icon for this type +in Open MCT Web's custom font set. +* `getInitialModel()`: Get a domain object model that represents the initial +state (before user specification of properties) for domain objects of this type. +* `getDefinition()`: Get the extension definition for this type, as a JavaScript +object. +* `instanceOf(type)`: Check if this type is (or inherits from) a specified type . +This type can be either a string, in which case it is taken to be that type's + key , or it may be a `Type` instance. +* `hasFeature(feature)`: Returns a boolean value indicating whether or not this +type supports the specified feature, which is a symbolic string. +* `getProperties()`: Get all properties associated with this type, expressed as +an array of `TypeProperty` instances. + +### Type Features + +Features of a domain object type are expressed as symbolic string identifiers. +They are defined in practice by usage; currently, the Open MCT Web platform only +uses the creation feature to determine which domain object types should appear +in the Create menu. + +### Type Properties + +Types declare the user-editable properties of their domain object instances in +order to allow the forms which appear in the __Create__ and __Edit Properties__ +dialogs to be generated by the platform. A `TypeProperty` has the following interface: + +* `getValue(model)`: Get the current value for this property, as it appears in +the provided domain object model. +* `setValue(model, value)`: Set a new value for this property in the provided +domain object model . +* `getDefinition()`: Get the raw definition for this property as a JavaScript +object (as it was declared in this type's extension definition.) + +# Extension Categories + +The information in this section is focused on registering new extensions of +specific types; it does not contain a catalog of the extension instances of +these categories provided by the platform. Relevant summaries there are provided +in subsequent sections. + +## Actions Category + +An action is a thing that can be done to or using a domain object, typically as +initiated by the user. + +An action's implementation: + +* Should take a single `context` argument in its constructor. (See Action +Contexts, under Core API.) +* Should provide a method `perform` which causes the behavior associated with +the action to occur. +* May provide a method `getMetadata` which provides metadata associated with +the action. If omitted, one will be provided by the platform which includes +metadata from the action's extension definition. +* May provide a static method `appliesTo(context)` (that is, a function +available as a property of the implementation's constructor itself), which will +be used by the platform to filter out actions from contexts in which they are +inherently inapplicable. + +An action's bundle definition (and/or `getMetadata()` return value) may include: + +* `category`: A string or array of strings identifying which category or +categories an action falls into; used to determine when an action is displayed. +Categories supported by the platform include: + * `contextual`: Actions in a context menu. + * `view-control`: Actions triggered by buttons in the top-right of Browse + view. +* `key`: A machine-readable identifier for this action. +* `name`: A human-readable name for this action (e.g. to show in a menu) +* `description`: A human-readable summary of the behavior of this action. +* `glyph`: A single character which will be rendered in Open MCT Web's custom +font set as an icon for this action. + +## Capabilities Category + +Capabilities are exposed by domain objects (e.g. via the `getCapability` method) +but most commonly originate as extensions of this category. + +Extension definitions for capabilities should include both an implementation, +and a property named key whose value should be a string used as a +machine-readable identifier for that capability, e.g. when passed as the +argument to a domain object's `getCapability(key)` call. + +A capability's implementation should have methods specific to that capability; +that is, there is no common format for capability implementations, aside from +support for invocation via the `useCapability` shorthand. + +A capability's implementation will take a single argument (in addition to any +declared dependencies), which is the domain object that will expose that +capability. + +A capability's implementation may also expose a static method `appliesTo(model)` +which should return a boolean value, and will be used by the platform to filter +down capabilities to those which should be exposed by specific domain objects, +based on their domain object models. + +## Controls Category + +Controls provide options for the `mct-control` directive. + +Six standard control types are included in the forms bundle: + +* `textfield`: An area to enter plain text. +* `select`: A drop-down list of options. +* `checkbox`: A box which may be checked/unchecked. +* `color`: A color picker. +* `button`: A button. +* `datetime`: An input for UTC date/time entry; gives result as a UNIX +timestamp, in milliseconds since start of 1970, UTC. + +New controls may be added as extensions of the controls category. Extensions of +this category have two properties: + +* `key`: The symbolic name for this control (matched against the control field +in rows of the form structure). +* `templateUrl`: The URL to the control's Angular template, relative to the +resources directory of the bundle which exposes the extension. + +Within the template for a control, the following variables will be included in +scope: + +* `ngModel`: The model where form input will be stored. Notably we also need to +look at field (see below) to determine which field in the model should be +modified. +* `ngRequired`: True if input is required. +* `ngPattern`: The pattern to match against (for text entry) +* `options`: The options for this control, as passed from the `options` property +of an individual row definition. +* `field`: Name of the field in `ngModel` which will hold the value for this +control. + +## Gestures Category + +A _gesture_ is a user action which can be taken upon a representation of a +domain object. + +Examples of gestures included in the platform are: + +* `drag`: For representations that can be used to initiate drag-and-drop +composition. +* `drop`: For representations that can be drop targets for drag-and-drop +composition. +* `menu`: For representations that can be used to pop up a context menu. + +Gesture definitions have a property `key` which is used as a machine-readable +identifier for the gesture (e.g. `drag`, `drop`, `menu` above.) + +A gesture's implementation is instantiated once per representation that uses the +gesture. This class will receive the jqLite-wrapped `mct-representation` element +and the domain object being represented as arguments, and should do any +necessary "wiring" (e.g. listening for events) during its constructor call. The +gesture's implementation may also expose an optional `destroy()` method which +will be called when the gesture should be removed, to avoid memory leaks by way +of unremoved listeners. + +## Indicators Category + +An indicator is an element that should appear in the status area at the bottom +of a running Open MCT Web client instance. + +### Standard Indicators + +Indicators which wish to appear in the common form of an icon-text pair should +provide implementations with the following methods: + +* `getText()`: Provides the human-readable text that will be displayed for this +indicator. +* `getGlyph()`: Provides a single-character string that will be displayed as an +icon in Open MCT Web's custom font set. +* `getDescription()`: Provides a human-readable summary of the current state of +this indicator; will be displayed in a tooltip on hover. +* `getClass()`: Get a CSS class that will be applied to this indicator. +* `getTextClass()`: Get a CSS class that will be applied to this indicator's +text portion. +* `getGlyphClass()`: Get a CSS class that will be applied to this indicator's +icon portion. +* `configure()`: If present, a configuration icon will appear to the right of +this indicator, and clicking it will invoke this method. + +Note that all methods are optional, and are called directly from an Angular +template, so they should be appropriate to run during digest cycles. + +### Custom Indicators + +Indicators which wish to have an arbitrary appearance (instead of following the +icon-text convention commonly used) may specify a `template` property in their +extension definition. The value of this property will be used as the `key` for +an `mct-include` directive (so should refer to an extension of category + templates .) This template will be rendered to the status area. Indicators of +this variety do not need to provide an implementation. + +## Licenses Category + +The extension category `licenses` can be used to add entries into the 'Licensing +information' page, reachable from Open MCT Web's About dialog. + +Licenses may have the following properties, all of which are strings: + +* `name`: Human-readable name of the licensed component. (e.g. 'AngularJS'.) +* `version`: Human-readable version of the licensed component. (e.g. '1.2.26'.) +* `description`: Human-readable summary of the component. +* `author`: Name or names of entities to which authorship should be attributed. +* `copyright`: Copyright text to display for this component. +* `link`: URL to full license text. + +## Policies Category + +Policies are used to handle decisions made using Open MCT Web's `policyService`; +examples of these decisions are determining the applicability of certain +actions, or checking whether or not a domain object of one type can contain a +domain object of a different type. See the section on the Policies for an +overview of Open MCT Web's policy model. + +A policy's extension definition should include: + +* `category`: The machine-readable identifier for the type of policy decision +being supported here. For a list of categories supported by the platform, see +the section on Policies. Plugins may introduce and utilize additional policy +categories not in that list. +* `message`: Optional; a human-readable message describing the policy, intended +for display in situations where this specific policy has disallowed something. + +A policy's implementation should include a single method, `allow(candidate, +context)`. The specific types used for `candidate` and `context` vary by policy +category; in general, what is being asked is 'is this candidate allowed in this +context?' This method should return a boolean value. + +Open MCT Web's policy model requires consensus; a policy decision is allowed +when and only when all policies choose to allow it. As such, policies should +generally be written to reject a certain case, and allow (by returning `true`) +anything else. + +## Representations Category + +A representation is an Angular template used to display a domain object. The +`representations` extension category is used to add options for the +`mct-representation` directive. + +A representation definition should include the following properties: + +* `key`: The machine-readable name which identifies the representation. +* `templateUrl`: The path to the representation's Angular template. This path is +relative to the bundle's resources directory. +* `uses`: Optional; an array of capability names. Indicates that this +representation intends to use those capabilities of a domain object (via a +`useCapability` call), and expects to find the latest results of that +`useCapability` call in the scope of the presented template (under the same name +as the capability itself.) Note that, if `useCapability` returns a promise, this +will be resolved before being placed in the representation's scope. +* `gestures`: An array of keys identifying gestures (see the `gestures` +extension category) which should be available upon this representation. Examples +of gestures include `drag` (for representations that should act as draggable +sources for drag-drop operations) and `menu` (for representations which should +show a domain-object-specific context menu on right-click.) + +### Representation Scope + +While _representations_ do not have implementations, per se, they do refer to +Angular templates which need to interact with information (e.g. the domain +object being represented) provided by the platform. This information is passed +in through the template's scope, such that simple representations may be created +by providing only templates. (More complex representations will need controllers +which are referenced from templates. See [https://docs.angularjs.org/guide/controller ]() +for more information on controllers in Angular.) + +A representation's scope will contain: + +* `domainObject`: The represented domain object. +* `model`: The domain object's model. +* `configuration`: An object containing configuration information for this +representation (an empty object if there is no saved configuration.) The +contents of this object are managed entirely by the view/representation which +receives it. +* `representation`: An empty object, useful as a 'scratch pad' for +representation state. +* `ngModel`: An object passed through the ng-model attribute of the +`mct-representation` , if any. +* `parameters`: An object passed through the parameters attribute of the +`mct-representation`, if any. +* Any capabilities requested by the uses property of the representation +definition. + +## Representers Category + +The `representers` extension category is used to add additional behavior to the +`mct-representation` directive. This extension category is intended primarily +for use internal to the platform. + +Unlike _representations_, which describe specific ways to represent domain +objects, _representers_ are used to modify or augment the process of +representing domain objects in general. For example, support for the _gestures_ +extension category is added by a _representer_. + +A representer needs only provide an implementation. When an `mct-representation` +is linked (see [https://docs.angularjs.org/guide/directive ]() or when the +domain object being represented changes, a new _representer_ of each declared +type is instantiated. The constructor arguments for a _representer_ are the same +as the arguments to the link function in an Angular directive: `scope` the +Angular scope for this representation; `element` the jqLite-wrapped +`mct-representation` element, and `attrs` a set of key-value pairs of that +element's attributes. _Representers_ may wish to populate the scope, attach +event listeners to the element, etc. + +This implementation must provide a single method, `destroy()`, which will be +invoked when the representer is no longer needed. + +## Roots Category + +The extension category `roots` is used to provide root-level domain object +models. Root-level domain objects appear at the top-level of the tree hierarchy. +For example, the _My Items_ folder is added as an extension of this category. + +Extensions of this category should have the following properties: + +* `id`: The machine-readable identifier for the domaiwn object being exposed. +* `model`: The model, as a JSON object, for the domain object being exposed. + +## Stylesheets Category + +The stylesheets extension category is used to add CSS files to style the +application. Extension definitions for this category should include one +property: + +* `stylesheetUrl`: Path and filename, including extension, for the stylesheet to +include. This path is relative to the bundle's resources folder (by default, +`res`) + +To control the order of CSS files, use priority (see the section on Extension +Definitions above.) + +## Templates Category + +The `templates` extension category is used to expose Angular templates under +symbolic identifiers. These can then be utilized using the `mct-include` +directive, which behaves similarly to `ng-include` except that it uses these +symbolic identifiers instead of paths. + +A template's extension definition should include the following properties: + +* `key`: The machine-readable name which identifies this template, matched +against the value given to the key attribute of the `mct-include` directive. +* `templateUrl`: The path to the relevant Angular template. This path is +relative to the bundle's resources directory. + +Note that, when multiple templates are present with the same key , the one with +the highest priority will be used from `mct-include`. This behavior can be used +to override templates exposed by the platform (to change the logo which appears +in the bottom right, for instance.) + +Templates do not have implementations. + +## Types Category + +The types extension category describes types of domain objects which may +appear within Open MCT Web. + +A type's extension definition should have the following properties: + +* `key`: The machine-readable identifier for this domain object type. Will be +stored to and matched against the type property of domain object models. +* `name`: The human-readable name for this domain object type. +* `description`: A human-readable summary of this domain object type. +* `glyph`: A single character to be rendered as an icon in Open MCT Web's custom +font set. +* `model`: A domain object model, used as the initial state for created domain +objects of this type (before any properties are specified.) +* `features`: Optional; an array of strings describing features of this domain +object type. Currently, only creation is recognized by the platform; this is +used to determine that this type should appear in the Create menu. More +generally, this is used to support the `hasFeature(...)` method of the type +capability. +* `properties`: An array describing individual properties of this domain object +(as should appear in the _Create_ or the _Edit Properties_ dialog.) Each +property is described by an object containing the following properties: + * `control`: The key of the control (see `mct-control` and the `controls` + [extension category](#Controls)) to use for editing this property. + * `property`: A string which will be used as the name of the property in the + domain object's model that the value for this property should be stored + under. If this value should be stored in an object nested within the domain + object model, then property should be specified as an array of strings + identifying these nested objects and, finally, the property itself. + * other properties as appropriate for a control of this type (each + property's definition will also be passed in as the structure for its + control.) See documentation of mct-form for more detail on these + properties. + +Types do not have implementations. + +## Versions Category +The versions extension category is used to introduce line items in Open MCT +Web's About dialog. These should have the following properties: + +* `name`: The name of this line item, as should appear in the left-hand side of +the list of version information in the About dialog. +* `value`: The value which should appear to the right of the name in the About +dialog. + +To control the ordering of line items within the About dialog, use `priority`. +(See section on [Extension Definitions](#ExtensionDefinitions) above.) + +This extension category does not have implementations. + +## Views Category + +The views extension category is used to determine which options appear to the +user as available views of domain objects of specific types. A view's extension +definition has the same properties as a representation (and views can be +utilized via `mct-representation`); additionally: + +* `name`: The human-readable name for this view type. +* description : A human-readable summary of this view type. +* `glyph`: A single character to be rendered as an icon in Open MCT Web's custom +font set. +* `type`: Optional; if present, this representation is only applicable for +domain object's of this type. +* `needs`: Optional array of strings; if present, this representation is only +applicable for domain objects which have the capabilities identified by these +strings. +* `delegation`: Optional boolean, intended to be used in conjunction with +`needs`; if present, allow required capabilities to be satisfied by means of +capability delegation. (See [Delegation](#Delegation)) +* `toolbar`: Optional; a definition for the toolbar which may appear in a +toolbar when using this view in Edit mode. This should be specified as a +structure for mct-toolbar , with additional properties available for each item in +that toolbar: + * `property`: A property name. This will refer to a property in the view's + current selection; that property on the selected object will be modifiable + as the `ng-model` of the displayed control in the toolbar. If the value of + the property is a function, it will be used as a getter-setter (called with + no arguments to use as a getter, called with a value to use as a setter.) + * `method`: A method to invoke (again, on the selected object) from the + toolbar control. Useful particularly for buttons (which don't edit a single + property, necessarily.) + +### View Scope + +Views do not have implementations, but do get the same properties in scope that +are provided for `representations`. + +When a view is in Edit mode, this scope will additionally contain: + +* `commit()`: A function which can be invoked to mark any changes to the view's + configuration as ready to persist. +* `selection`: An object representing the current selection state. + +#### Selection State + +A view's selection state is, conceptually, a set of JavaScript objects. The +presence of methods/properties on these objects determine which toolbar controls +are visible, and what state they manage and/or behavior they invoke. + +This set may contain up to two different objects: The _view proxy _, which is +used to make changes to the view as a whole, and the _ selected object _, which is +used to represent some state within the view. (Future versions of Open MCT Web +may support multiple selected objects.) + +The `selection` object made available during Edit mode has the following +methods: + +* `proxy([object])`: Get (or set, if called with an argument) the current view +proxy. +* `select(object)`: Make this object the selected object. +* `deselect()`: Clear the currently selected object. +* `get()`: Get the currently selected object. Returns undefined if there is no +currently selected object. +* `selected(object)`: Check if the JavaScript object is currently in the +selection set. Returns true if the object is either the currently selected +object, or the current view proxy. +* `all()`: Get an array of all objects in the selection state. Will include +either or both of the view proxy and selected object. + +# Directives + +Open MCT Web defines several Angular directives that are intended for use both +internally within the platform, and by plugins. + +## Before Unload + +The `mct-before-unload` directive is used to listen for (and prompt for user +confirmation) of navigation changes in the browser. This includes reloading, +following links out of Open MCT Web, or changing routes. It is used to hook into +both `onbeforeunload` event handling as well as route changes from within +Angular. + +This directive is useable as an attribute. Its value should be an Angular +expression. When an action that would trigger an unload and/or route change +occurs, this Angular expression is evaluated. Its result should be a message to +display to the user to confirm their navigation change; if this expression +evaluates to a falsy value, no message will be displayed. + +## Chart + +The `mct-chart` directive is used to support drawing of simple charts. It is +present to support the Plot view, and its functionality is limited to the +functionality that is relevant for that view. + +This directive is used at the element level and takes one attribute, `draw` +which is an Angular expression which will should evaluate to a drawing object. +This drawing object should contain the following properties: + +* `dimensions`: The size, in logical coordinates, of the chart area. A +two-element array or numbers. +* `origin`: The position, in logical coordinates, of the lower-left corner of +the chart area. A two-element array or numbers. +* `lines`: An array of lines (e.g. as a plot line) to draw, where each line is +expressed as an object containing: + * `buffer`: A Float32Array containing points in the line, in logical + coordinates, in sequential x,y pairs. + * `color`: The color of the line, as a four-element RGBA array, where + each element is a number in the range of 0.0-1.0. + * `points`: The number of points in the line. +* `boxes`: An array of rectangles to draw in the chart area. Each is an object +containing: + * `start`: The first corner of the rectangle, as a two-element array of + numbers, in logical coordinates. + * `end`: The opposite corner of the rectangle, as a two-element array of + numbers, in logical coordinates. color : The color of the line, as a + four-element RGBA array, where each element is a number in the range of + 0.0-1.0. + +While `mct-chart` is intended to support plots specifically, it does perform +some useful management of canvas objects (e.g. choosing between WebGL and Canvas +2D APIs for drawing based on browser support) so its usage is recommended when +its supported drawing primitives are sufficient for other charting tasks. + +## Container + +The `mct-container` is similar to the `mct-include` directive insofar as it allows +templates to be referenced by symbolic keys instead of by URL. Unlike +`mct-include` it supports transclusion. + +Unlike `mct-include` `mct-container` accepts a key as a plain string attribute, +instead of as an Angular expression. + +## Control + +The `mct-control` directive is used to display user input elements. Several +controls are included with the platform to wrap default input types. This +directive is primarily intended for internal use by the `mct-form` and +`mct-toolbar` directives. + +When using `mct-control` the attributes `ng-model` `ng-disabled` +`ng-required` and `ng-pattern` may also be used. These have the usual meaning +(as they would for an input element) except for `ng-model`; when used, it will +actually be `ngModel[field]` (see below) that is two-way bound by this control. +This allows `mct-control` elements to more easily delegate to other +`mct-control` instances, and also facilitates usage for generated forms. + +This directive supports the following additional attributes, all specified as +Angular expressions: + +* `key`: A machine-readable identifier for the specific type of control to +display. +* `options`: A set of options to display in this control. +* `structure`: In practice, contains the definition object which describes this +form row or toolbar item. Used to pass additional control-specific parameters. +* `field`: The field in the `ngModel` under which to read/store the property +associated with this control. + +## Drag + +The `mct-drag` directive is used to support drag-based gestures on HTML +elements. Note that this is not 'drag' in the 'drag-and-drop' sense, but 'drag' +in the more general 'mouse down, mouse move, mouse up' sense. + +This takes the form of three attributes: + +* `mct-drag`: An Angular expression to evaluate during drag movement. +* `mct-drag-down`: An Angular expression to evaluate when the drag starts. +* `mct-drag-up`: An Angular expression to evaluate when the drag ends. + +In each case, a variable `delta` will be provided to the expression; this is a +two-element array or the horizontal and vertical pixel offset of the current +mouse position relative to the mouse position where dragging began. + +## Form + +The `mct-form` directive is used to generate forms using a declarative structure, +and to gather back user input. It is applicable at the element level and +supports the following attributes: + +* `ng-model`: The object which should contain the full form input. Individual +fields in this model are bound to individual controls; the names used for these +fields are provided in the form structure (see below). +* `structure`: The structure of the form; e.g. sections, rows, their names, and +so forth. The value of this attribute should be an Angular expression. +* `name`: The name in the containing scope under which to publish form +"meta-state", e.g. `$valid` `$dirty` etc. This is as the behavior of `ng-form`. +Passed as plain text in the attribute. + +### Form Structure + +Forms in Open MCT Web have a common structure to permit consistent display. A +form is broken down into sections, which will be displayed in groups; each +section is broken down into rows, each of which provides a control for a single +property. Input from this form is two-way bound to the object passed via +`ng-model`. + +A form's structure is represented by a JavaScript object in the following form: + + { + "name": ... title to display for the form, as a string ..., + "sections": [ + { + "name": ... title to display for the section ..., + "rows": [ + { + "name": ... title to display for this row ..., + "control": ... symbolic key for the control ..., + "key": ... field name in ng-model ... + "pattern": ... optional, reg exp to match against ... + "required": ... optional boolean ... + "options": [ + "name": ... name to display (e.g. in a select) ..., + "value": ... value to store in the model ... + ] + }, + ... and other rows ... + ] + }, + ... and other sections ... + ] + } + +Note that `pattern` may be specified as a string, to simplify storing for +structures as JSON when necessary. The string should be given in a form +appropriate to pass to a `RegExp` constructor. + +### Form Controls + +A few standard control types are included in the platform/forms bundle: + +* `textfield`: An area to enter plain text. +* `select`: A drop-down list of options. +* `checkbox`: A box which may be checked/unchecked. +* `color`: A color picker. +* `button`: A button. +* `datetime`: An input for UTC date/time entry; gives result as a UNIX +timestamp, in milliseconds since start of 1970, UTC. + +## Include + +The `mct-include` directive is similar to ng-include , except that it takes a +symbolic identifier for a template instead of a URL. Additionally, templates +included via mct-include will have an isolated scope. + +The directive should be used at the element level and supports the following +attributes, all of which are specified as Angular expressions: + +* `key`: Machine-readable identifier for the template (of extension category +templates ) to be displayed. +* `ng-model`: _Optional_; will be passed into the template's scope as `ngModel`. +Intended usage is for two-way bound user input. +* `parameters`: _Optional_; will be passed into the template's scope as +parameters. Intended usage is for template-specific display parameters. + +## Representation + +The `mct-representation` directive is used to include templates which +specifically represent domain objects. Usage is similar to `mct-include`. + +The directive should be used at the element level and supports the following +attributes, all of which are specified as Angular expressions: + +* `key`: Machine-readable identifier for the representation (of extension +category _representations_ or _views_ ) to be displayed. +* `mct-object`: The domain object being represented. +* `ng-model`: Optional; will be passed into the template's scope as `ngModel`. +Intended usage is for two-way bound user input. +* `parameters`: Optional; will be passed into the template's scope as +parameters . Intended usage is for template-specific display parameters. + +## Resize + +The `mct-resize` directive is used to monitor the size of an HTML element. It is +specified as an attribute whose value is an Angular expression that will be +evaluated when the size of the HTML element changes. This expression will be +provided a single variable, `bounds` which is an object containing two +properties, `width` and `height` describing the size in pixels of the element. + +When using this directive, an attribute `mct-resize-interval` may optionally be +provided. Its value is an Angular expression describing the number of +milliseconds to wait before next checking the size of the HTML element; this +expression is evaluated when the directive is linked and reevaluated whenever +the size is checked. + +## Scroll + +The `mct-scroll-x` and `mct-scroll-y` directives are used to both monitor and +control the horizontal and vertical scroll bar state of an element, +respectively. They are intended to be used as attributes whose values are +assignable Angular expressions which two-way bind to the scroll bar state. + +## Toolbar + +The `mct-toolbar` directive is used to generate toolbars using a declarative +structure, and to gather back user input. It is applicable at the element level +and supports the following attributes: + +* `ng-model`: The object which should contain the full toolbar input. Individual +fields in this model are bound to individual controls; the names used for these +fields are provided in the form structure (see below). +* `structure`: The structure of the toolbar; e.g. sections, rows, their names, and +so forth. The value of this attribute should be an Angular expression. +* `name`: The name in the containing scope under which to publish form +"meta-state", e.g. `$valid`, `$dirty` etc. This is as the behavior of +`ng-form`. Passed as plain text in the attribute. + +Toolbars support the same control options as forms. + +### Toolbar Structure + +A toolbar's structure is defined similarly to forms, except instead of rows +there are items . + + { + "name": ... title to display for the form, as a string ..., + "sections": [ + { + "name": ... title to display for the section ..., + "items": [ + { + "name": ... title to display for this row ..., + "control": ... symbolic key for the control ..., + "key": ... field name in ng-model ... + "pattern": ... optional, reg exp to match against ... + "required": ... optional boolean ... + "options": [ + "name": ... name to display (e.g. in a select) ..., + "value": ... value to store in the model ... + ], + "disabled": ... true if control should be disabled ... + "size": ... size of the control (for textfields) ... + "click": ... function to invoke (for buttons) ... + "glyph": ... glyph to display (for buttons) ... + "text": ... text within control (for buttons) ... + }, + ... and other rows ... + ] + }, + ... and other sections ... + ] + } + +# Services + +The Open MCT Web platform provides a variety of services which can be retrieved +and utilized via dependency injection. These services fall into two categories: + +* _Composite Services_ are defined by a set of components extensions; plugins may +introduce additional components with matching interfaces to extend or augment +the functionality of the composed service. (See the Framework section on +Composite Services.) +* _Other services_ which are defined as standalone service objects; these can be +utilized by plugins but are not intended to be modified or augmented. + +## Composite Type Services + +This section describes the composite services exposed by Open MCT Web, +specifically focusing on their interface and contract. + +In many cases, the platform will include a provider for a service which consumes +a specific extension category; for instance, the `actionService` depends on +`actions[]` and will expose available actions based on the rules defined for +that extension category. + +In these cases, it will usually be simpler to add a new extension of a given +category (e.g. of category `actions`) even when the same behavior could be +introduced by a service component (e.g. an extension of category `components` +where `provides` is `actionService` and `type` is `provider`.) + +Occasionally, the extension category does not provide enough expressive power to +achieve a desired result. For instance, the Create menu is populated with +`create` actions, where one such action exists for each creatable type. Since +the framework does not provide a declarative means to introduce a new action per +type declaratively, the platform implements this explicitly in an `actionService` +component of type `provider`. Plugins may use a similar approach when the normal +extension mechanism is insufficient to achieve a desired result. + +### Action Service + +The [Action Service](../architecture/platform#action-service) (`actionService`) +provides `Action` instances which are applicable in specific contexts. See Core +API for additional notes on the interface for actions. The `actionService` has +the following interface: + +* `getActions(context)`: Returns an array of Action objects which are applicable +in the specified action context. + +### Capability Service + +The [Capability Service](../architecture/platform#capability-service) (`capabilityService`) +provides constructors for capabilities which will be exposed for a given domain +object. + +The capabilityService has the following interface: + +* `getCapabilities(model)`: Returns a an object containing key-value pairs, +representing capabilities which should be exposed by the domain object with this +model. Keys in this object are the capability keys (as used in a +`getCapability(...)` call) and values are either: + * Functions, in which case they will be used as constructors, which will + receive the domain object instance to which the capability applies as their + sole argument.The resulting object will be provided as the result of a + domain object's `getCapability(...)` call. Note that these instances are cached + by each object, but may be recreated when an object is mutated. + * Other objects, which will be used directly as the result of a domain + object's `getCapability(...)` call. + +### Dialog Service + +The `dialogService` provides a means for requesting user input via a modal +dialog. It has the following interface: + +* `getUserInput(formStructure, formState)`: Prompt the user to fill out a form. +The first argument describes the form's structure (as will be passed to + mct-form ) while the second argument contains the initial state of that form. +This returns a Promise for the state of the form after the user has filled it +in; this promise will be rejected if the user cancels input. +* `getUserChoice(dialogStructure)`: Prompt the user to make a single choice from +a set of options, which (in the platform implementation) will be expressed as +buttons in the displayed dialog. Returns a Promise for the user's choice, which +will be rejected if the user cancels input. + +### Dialog Structure + +The object passed as the `dialogStructure` to `getUserChoice` should have the +following properties: + +* `title`: The title to display at the top of the dialog. +* `hint`: Short message to display below the title. +* `template`: Identifying key (as will be passed to mct-include ) for the +template which will be used to populate the inner area of the dialog. +* `model`: Model to pass in the ng-model attribute of mct-include . +* `parameters`: Parameters to pass in the parameters attribute of mct-include . +* `options`: An array of options describing each button at the bottom. Each +option may have the following properties: + * `name`: Human-readable name to display in the button. + * `key`: Machine-readable key, to pass as the result of the resolved promise + when clicked. + * `description`: Description to show in tooltip on hover. + +### Domain Object Service + +The [Object Service](../architecture/platform.md#object-service) (`objectService`) +provides domain object instances. It has the following interface: + +* `getObjects(ids)`: For the provided array of domain object identifiers, +returns a Promise for an object containing key-value pairs, where keys are +domain object identifiers and values are corresponding DomainObject instances. +Note that the result may contain a superset or subset of the objects requested. + +### Gesture Service + +The `gestureService` is used to attach gestures (see extension category gestures) +to representations. It has the following interface: + +* `attachGestures(element, domainObject, keys)`: Attach gestures specified by +the provided gesture keys (an array of strings) to this jqLite-wrapped HTML +element , which represents the specified domainObject . Returns an object with a +single method `destroy()`, to be invoked when it is time to detach these +gestures. + +### Model Service + +The [Model Service](../architecture/platform.md#model-service) (`modelService`) +provides domain object models. It has the following interface: + +* `getModels(ids)`: For the provided array of domain object identifiers, returns +a Promise for an object containing key-value pairs, where keys are domain object +identifiers and values are corresponding domain object models. Note that the +result may contain a superset or subset of the models requested. + +### Persistence Service + +The [Persistence Service](../architecture/platform.md#persistence-service) (`persistenceService`) +provides the ability to load/store JavaScript objects +(presumably serializing/deserializing to JSON in the process.) This is used +primarily to store domain object models. It has the following interface: + +* `listSpaces()`: Returns a Promise for an array of strings identifying the +different persistence spaces this service supports. Spaces are intended to be +used to distinguish between different underlying persistence stores, to allow +these to live side by side. +* `listObjects()`: Returns a Promise for an array of strings identifying all +documents stored in this persistence service. +* `createObject(space, key, value)`: Create a new document in the specified +persistence space , identified by the specified key , the contents of which shall +match the specified value . Returns a promise that will be rejected if creation +fails. +* `readObject(space, key)`: Read an existing document in the specified +persistence space , identified by the specified key . Returns a promise for the +specified document; this promise will resolve to undefined if the document does +not exist. +* `updateObject(space, key, value)`: Update an existing document in the +specified persistence space , identified by the specified key , such that its +contents match the specified value . Returns a promise that will be rejected if +the update fails. +* `deleteObject(space, key)`: Delete an existing document from the specified +persistence space , identified by the specified key . Returns a promise which will +be rejected if deletion fails. + +### Policy Service + +The [Policy Service](../architecture/platform.md#policy-service) (`policyService`) +may be used to determine whether or not certain behaviors are +allowed within the application. It has the following interface: + +* `allow(category, candidate, context, [callback])`: Check if this decision +should be allowed. Returns a boolean. Its arguments are interpreted as: + * `category`: A string identifying which kind of decision is being made. See + the [section on Categories](#PolicyCategories) for categories supported by + the platform; plugins may define and utilize policies of additional + categories, as well. + * `candidate`: An object representing the thing which shall or shall not be + allowed. Usually, this will be an instance of an extension of the category + defined above. This does need to be the case; additional policies which are + not specific to any extension may also be defined and consulted using unique + category identifiers. In this case, the type of the object delivered for the + candidate may be unique to the policy type. + * `context`: An object representing the context in which the decision is + occurring. Its contents are specific to each policy category. + * `callback`: Optional; a function to call if the policy decision is rejected. + This function will be called with the message string (which may be + undefined) of whichever individual policy caused the operation to fail. + +### Telemetry Service + +The [Telemetry Service](../architecture/platform.md#telemetry-service) (`telemetryService`) +is used to acquire telemetry data. See the section on +Telemetry in Core API for more information on how both the arguments and +responses of this service are structured. + +When acquiring telemetry for display, it is recommended that the +`telemetryHandler` service be used instead of this service. The +`telemetryHandler` has additional support for subscribing to and requesting +telemetry data associated with domain objects or groups of domain objects. See +the [Other Services](#Other-Services) section for more information. + +The `telemetryService` has the following interface: + +* `requestTelemetry(requests)`: Issue a request for telemetry, matching the +specified telemetry requests . Returns a _ Promise _ for a telemetry response +object. +* `subscribe(callback, requests)`: Subscribe to real-time updates for telemetry, +matching the specified `requests`. The specified `callback` will be invoked with +telemetry response objects as they become available. This method returns a +function which can be invoked to terminate the subscription. + +### Type Service + +The [Type Service](../architecture/platform.md#type-service) (`typeService`) exposes +domain object types. It has the following interface: + +* `listTypes()`: Returns all domain object types supported in the application, +as an array of `Type` instances. +* `getType(key)`: Returns the `Type` instance identified by the provided key, or +undefined if no such type exists. + +### View Service + +The [View Service](../architecture/platform.md#view-service) (`viewService`) exposes +definitions for views of domain objects. It has the following interface: + +* `getViews(domainObject)`: Get an array of extension definitions of category +`views` which are valid and applicable to the specified `domainObject`. + +## Other Services + +### Drag and Drop + +The `dndService` provides information about the content of an active +drag-and-drop gesture within the application. It is intended to complement the +`DataTransfer` API of HTML5 drag-and-drop, by providing access to non-serialized +JavaScript objects being dragged, as well as by permitting inspection during +drag (which is normally prohibited by browsers for security reasons.) + +The `dndService` has the following methods: + +* `setData(key, value)`: Set drag data associated with a given type, specified +by the `key` argument. +* `getData(key)`: Get drag data associated with a given type, specified by the +`key` argument. +* `removeData(key)`: Clear drag data associated with a given type, specified by +the `key` argument. + +### Navigation + +The _Navigation_ service provides information about the current navigation state +of the application; that is, which object is the user currently viewing? This +service merely tracks this state and notifies listeners; it does not take +immediate action when navigation changes, although its listeners might. + +The `navigationService` has the following methods: + +* `getNavigation()`: Get the current navigation state. Returns a `DomainObject`. +* `setNavigation(domainObject)`: Set the current navigation state. Returns a +`DomainObject`. +* `addListener(callback)`: Listen for changes in navigation state. The provided +`callback` should be a `Function` which takes a single `DomainObject` as an +argument. +* `removeListener(callback)`: Stop listening for changes in navigation state. +The provided `callback` should be a `Function` which has previously been passed +to addListener . + +### Now + +The service now is a function which acts as a simple wrapper for `Date.now()`. +It is present mainly so that this functionality may be more easily mocked in +tests for scripts which use the current time. + +### Telemetry Formatter + +The _Telemetry Formatter_ is a utility for formatting domain and range values +read from a telemetry series. + +`telemetryFormatter` has the following methods: + +* `formatDomainValue(value)`: Format the provided domain value (which will be +assumed to be a timestamp) for display; returns a string. +* `formatRangeValue(value)`: Format the provided range value (a number) for +display; returns a string. + +### Telemetry Handler + +The _Telemetry Handler_ is a utility for retrieving telemetry data associated +with domain objects; it is particularly useful for dealing with cases where the +telemetry capability is delegated to contained objects (as occurs +in _Telemetry Panels_.) + +The `telemetryHandler` has the following methods: + +* `handle(domainObject, callback, [lossless])`: Subscribe to and issue future +requests for telemetry associated with the provided `domainObject`, invoking the +provided callback function when streaming data becomes available. Returns a +`TelemetryHandle` (see below.) + +#### Telemetry Handle + +A TelemetryHandle has the following methods: + +* `getTelemetryObjects()`: Get the domain objects (as a `DomainObject[]`) that +have a telemetry capability and are being handled here. Note that these are +looked up asynchronously, so this method may return an empty array if the +initial lookup is not yet completed. +* `promiseTelemetryObjects()`: As `getTelemetryObjects()`, but returns a Promise +that will be fulfilled when the lookup is complete. +* `unsubscribe()`: Unsubscribe to streaming telemetry updates associated with +this handle. +* `getDomainValue(domainObject)`: Get the most recent domain value received via +a streaming update for the specified `domainObject`. +* `getRangeValue(domainObject)`: Get the most recent range value received via a +streaming update for the specified `domainObject`. +* `getMetadata()`: Get metadata (as reported by the `getMetadata()` method of a +telemetry capability) associated with telemetry-providing domain objects. +Returns an array, which is in the same order as getTelemetryObjects() . +* `request(request, callback)`: Issue a new request for historical telemetry +data. The provided callback will be invoked when new data becomes available, +which may occur multiple times (e.g. if there are multiple domain objects.) It +will be invoked with the DomainObject for which a new series is available, and +the TelemetrySeries itself, in that order. +* `getSeries(domainObject)`: Get the latest `TelemetrySeries` (as resulted from +a previous `request(...)` call) available for this domain object. + + +# Models +Domain object models in Open MCT Web are JavaScript objects describing the +persistent state of the domain objects they describe. Their contents include a +mix of commonly understood metadata attributes; attributes which are recognized +by and/or determine the applicability of specific extensions; and properties +specific to given types. + +## General Metadata + +Some properties of domain object models have a ubiquitous meaning through Open +MCT Web and can be utilized directly: + +* `name`: The human-readable name of the domain object. + +## Extension-specific Properties + +Other properties of domain object models have specific meaning imposed by other +extensions within the Open MCT Web platform. + +### Capability-specific Properties + +Some properties either trigger the presence/absence of certain capabilities, or +are managed by specific capabilities: + +* `composition`: An array of domain object identifiers that represents the +contents of this domain object (e.g. as will appear in the tree hierarchy.) +Understood by the composition capability; the presence or absence of this +property determines the presence or absence of that capability. +* `modified`: The timestamp (in milliseconds since the UNIX epoch) of the last +modification made to this domain object. Managed by the mutation capability. +* `persisted`: The timestamp (in milliseconds since the UNIX epoch) of the last +time when changes to this domain object were persisted. Managed by the + persistence capability. +* `relationships`: An object containing key-value pairs, where keys are symbolic +identifiers for relationship types, and values are arrays of domain object +identifiers. Used by the relationship capability; the presence or absence of +this property determines the presence or absence of that capability. +* `telemetry`: An object which serves as a template for telemetry requests +associated with this domain object (e.g. specifying `source` and `key`; see +Telemetry Requests under Core API.) Used by the telemetry capability; the +presence or absence of this property determines the presence or absence of that +capability. +* `type`: A string identifying the type of this domain object. Used by the `type` +capability. + +### View Configurations + +Persistent configurations for specific views of domain objects are stored in the +domain object model under the property configurations . This is an object +containing key-value pairs, where keys identify the view, and values are objects +containing view-specific (and view-managed) configuration properties. + +## Modifying Models +When interacting with a domain object's model, it is possible to make +modifications to it directly. __Don't!__ These changes may not be properly detected +by the platform, meaning that other representations of the domain object may not +be updated, changes may not be saved at the expected times, and generally, that +unexpected behavior may occur. Instead, use the `mutation` capability. + +# Capabilities + +Dynamic behavior associated with a domain object is expressed as capabilities. A +capability is a JavaScript object with an interface that is specific to the type +of capability in use. + +Often, there is a relationship between capabilities and services. For instance, +there is an action capability and an actionService , and there is a telemetry +capability as well as a `telemetryService`. Typically, the pattern here is that +the capability will utilize the service for the specific domain object. + +When interacting with domain objects, it is generally preferable to use a +capability instead of a service when the option is available. Capability +interfaces are typically easier to use and/or more powerful in these situations. +Additionally, this usage provides a more robust substitutability mechanism; for +instance, one could configure a plugin such that it provided a totally new +implementation of a given capability which might not invoke the underlying +service, while user code which interacts with capabilities remains indifferent +to this detail. + +## Action Capability + +The `action` capability is present for all domain objects. It allows applicable +`Action` instances to be retrieved and performed for specific domain objects. + +For example: + `domainObject.getCapability("action").perform("navigate"); ` + ...will initiate a navigate action upon the domain object, if an action with + key "navigate" is defined. + +This capability has the following interface: +* `getActions(context)`: Get the actions that are applicable in the specified +action `context`; the capability will fill in the `domainObject` field of this +context if necessary. If context is specified as a string, they will instead be +used as the `key` of the action context. Returns an array of `Action` instances. +* `perform(context)`: Perform an action. This will find and perform the first +matching action available for the specified action context , filling in the +`domainObject` field as necessary. If `context` is specified as a string, they +will instead be used as the `key` of the action context. Returns a `Promise` for +the result of the action that was performed, or `undefined` if no matching action +was found. + +## Composition Capability + +The `composition` capability provides access to domain objects that are +contained by this domain object. While the `composition` property of a domain +object's model describes these contents (by their identifiers), the +`composition` capability provides a means to load the corresponding +`DomainObject` instances in the same order. The absence of this property in the +model will result in the absence of this capability in the domain object. + +This capability has the following interface: + +* `invoke()`: Returns a `Promise` for an array of `DomainObject` instances. + +## Delegation Capability + +The delegation capability is used to communicate the intent of a domain object +to delegate responsibilities, which would normally handled by other +capabilities, to the domain objects in its composition. + +This capability has the following interface: + +* `getDelegates(key)`: Returns a Promise for an array of DomainObject instances, +to which this domain object wishes to delegate the capability with the specified +key . +* `invoke(key)`: Alias of getDelegates(key) . +* `doesDelegate(key)`: Returns true if the domain object does delegate the +capability with the specified key . + +The platform implementation of the delegation capability inspects the domain +object's type definition for a property delegates , whose value is an array of +strings describing which capabilities domain objects of that type wish to +delegate. If this property is not present, the delegation capability will not be +present in domain objects of that type. + +## Editor Capability + +The editor capability is meant primarily for internal use by Edit mode, and +helps to manage the behavior associated with exiting _Edit_ mode via _Save_ or +_Cancel_. Its interface is not intended for general use. However, +`domainObject.hasCapability(editor)` is a useful way of determining whether or +not we are looking at an object in _Edit_ mode. + +## Mutation Capability + +The `mutation` capability provides a means by which the contents of a domain +object's model can be modified. This capability is provided by the platform for +all domain objects, and has the following interface: + +* `mutate(mutator, [timestamp])`: Modify the domain object's model using the +specified `mutator` function. After changes are made, the `modified` property of +the model will be updated with the specified `timestamp` if one was provided, +or with the current system time. +* `invoke(...)`: Alias of `mutate`. + +Changes to domain object models should only be made via the `mutation` +capability; other platform behavior is likely to break (either by exhibiting +undesired behavior, or failing to exhibit desired behavior) if models are +modified by other means. + +### Mutator Function + +The mutator argument above is a function which will receive a cloned copy of the +domain object's model as a single argument. It may return: + +* A `Promise` in which case the resolved value of the promise will be used to +determine which of the following forms is used. +* Boolean `false` in which case the mutation is cancelled. +* A JavaScript object, in which case this object will be used as the new model +for this domain object. +* No value (or, equivalently, `undefined`), in which case the cloned copy +(including any changes made in place by the mutator function) will be used as +the new domain object model. + +## Persistence Capability + +The persistence capability provides a mean for interacting with the underlying +persistence service which stores this domain object's model. It has the +following interface: + +* `persist()`: Store the local version of this domain object, including any +changes, to the persistence store. Returns a Promise for a boolean value, which +will be true when the object was successfully persisted. +* `refresh()`: Replace this domain object's model with the most recent version +from persistence. Returns a Promise which will resolve when the change has +completed. +* `getSpace()`: Return the string which identifies the persistence space which +stores this domain object. + +## Relationship Capability + +The relationship capability provides a means for accessing other domain objects +with which this domain object has some typed relationship. It has the following +interface: + +* `listRelationships()`: List all types of relationships exposed by this object. +Returns an array of strings identifying the types of relationships. +* `getRelatedObjects(relationship)`: Get all domain objects to which this domain +object has the specified type of relationship, which is a string identifier +(as above.) Returns a `Promise` for an array of `DomainObject` instances. + +The platform implementation of the `relationship` capability is present for domain +objects which has a `relationships` property in their model, whose value is an +object containing key-value pairs, where keys are strings identifying +relationship types, and values are arrays of domain object identifiers. + +## Telemetry Capability + +The telemetry capability provides a means for accessing telemetry data +associated with a domain object. It has the following interface: + +* `requestData([request])`: Request telemetry data for this specific domain +object, using telemetry request parameters from the specified request if +provided. This capability will fill in telemetry request properties as-needed +for this domain object. Returns a `Promise` for a `TelemetrySeries`. +* `subscribe(callback, [request])`: Subscribe to telemetry data updates for +this specific domain object, using telemetry request parameters from the +specified request if provided. This capability will fill in telemetry request +properties as-needed for this domain object. The specified callback will be +invoked with TelemetrySeries instances as they arrive. Returns a function which +can be invoked to terminate the subscription, or undefined if no subscription +could be obtained. +* `getMetadata()`: Get metadata associated with this domain object's telemetry. + +The platform implementation of the `telemetry` capability is present for domain +objects which has a `telemetry` property in their model and/or type definition; +this object will serve as a template for telemetry requests made using this +object, and will also be returned by `getMetadata()` above. + +## Type Capability +The `type` capability exposes information about the domain object's type. It has +the same interface as `Type`; see Core API. + +## View Capability + +The `view` capability exposes views which are applicable to a given domain +object. It has the following interface: + +* `invoke()`: Returns an array of extension definitions for views which are +applicable for this domain object. + +# Actions + +Actions are reusable processes/behaviors performed by users within the system, +typically upon domain objects. + +## Action Categories + +The platform understands the following action categories (specifiable as the +`category` parameter of an action's extension definition.) + +* `contextual`: Appears in context menus. +* `view-control`: Appears in top-right area of view (as buttons) in Browse mode + +## Platform Actions +The platform defines certain actions which can be utilized by way of a domain +object's `action` capability. Unless otherwise specified, these act upon (and +modify) the object described by the `domainObject` property of the action's +context. + +* `cancel`: Cancel the current editing action (invoked from Edit mode.) +* `compose`: Place an object in another object's composition. The object to be +added should be provided as the `selectedObject` of the action context. +* `edit`: Start editing an object (enter Edit mode.) +* `fullscreen`: Enter full screen mode. +* `navigate`: Make this object the focus of navigation (e.g. highlight it within +the tree, display a view of it to the right.) +* `properties`: Show the 'Edit Properties' dialog. +* `remove`: Remove this domain object from its parent's composition. (The +parent, in this case, is whichever other domain object exposed this object by +way of its `composition` capability.) +* `save`: Save changes (invoked from Edit mode.) +* `window`: Open this object in a new window. + +# Policies + +Policies are consulted to determine when certain behavior in Open MCT Web is +allowed. Policy questions are assigned to certain categories, which broadly +describe the type of decision being made; within each category, policies have a +candidate (the thing which may or may not be allowed) and, optionally, a context +(describing, generally, the context in which the decision is occurring.) + +The types of objects passed for 'candidate' and 'context' vary by category; +these types are documented below. + +## Policy Categories + +The platform understands the following policy categories (specifiable as the +`category` parameter of an policy's extension definition.) + +* `action`: Determines whether or not a given action is allowable. The candidate +argument here is an Action; the context is its action context object. +* `composition`: Determines whether or not domain objects of a given type are +allowed to contain domain objects of another type. The candidate argument here +is the container's `Type`; the context argument is the `Type` of the object to be +contained. +* `view`: Determines whether or not a view is applicable for a domain object. +The candidate argument is the view's extension definition; the context argument +is the `DomainObject` to be viewed. + +# Build-Test-Deploy +Open MCT Web is designed to support a broad variety of build and deployment +options. The sources can be deployed in the same directory structure used during +development. A few utilities are included to support development processes. + +## Command-line Build +Open MCT Web includes a script for building via command line using Maven 3.0.4 +[https://maven.apache.org/](). + +Invoking mvn clean install will: + +* Check code style using JSLint. The build will fail if JSLint raises any warnings. +* Run the test suite (see below.) The build will fail if any tests fail. +* Populate version info (e.g. commit hash, build time.) +* Produce a web archive (`.war`) artifact in the `target` directory. + +The produced artifact contains a subset of the repository's own folder +hierarchy, omitting tests and example bundles. + +Note that an internet connection is required to run this build, in order to +download build dependencies. + +## Test Suite + +Open MCT Web uses Jasmine [http://jasmine.github.io/]() for automated testing. +The file `test.html` included at the top level of the source repository, can be +run from the browser to perform tests for all active bundles, as defined in +`bundle.json`. + +To define tests for a bundle: + +* Include a directory named `test` within that bundle. +* In the `test` directory, include a file named `suite.json`. This will identify +which scripts will be tested. +* The file `suite.json` must contain a JSON array of strings, where each string +is the name of a script to be tested. These names should include any directory +paths to the script after (but not including) the `src` folder, and should not +include the file's `.js` extension. (Note that while Open MCT Web's framework +allows a different name to be chosen for the src directory, the test runner +does not: This directory must be named `src` for the test runner to find it.) +* For each script to be tested, a corresponding test script should be located in +the bundle's `test` directory. This should include the suffix Spec at the end of +the filename (but before the `.js` extension.) This test script should be an AMD +module which uses the Jasmine API to declare its test behavior. It should +declare an AMD dependency on the script to be tested, using a relative path. + +For example, if writing tests for a bundle at example/foo with two scripts: +* `example/foo/src/controllers/FooController.js` +* `example/foo/src/directives/FooDirective.js` + +First, these scripts should be identified in `example/foo/test/suite.json` e.g. +with contents:`[ "controllers/FooController", "directives/FooDirective" ]` + +Then, scripts which describe these tests should be written. For example, test +`example/foo/test/controllers/FooControllerSpec.js` could look like: + + /*global define,Promise,describe,it,expect,beforeEach*/ + + define( + ["../../src/controllers/FooController"], + function (FooController) { + "use strict"; + + + describe("The foo controller", function () { + it("does something", function () { + var controller = new FooController(); + expect(controller.foo()).toEqual("foo"); + }); + }); + } + ); + + +## Code Coverage + +In addition to running tests, the test runner will also capture code coverage +information using [Blanket.JS](http://blanketjs.org/) and display this at the +bottom of the screen. Currently, only statement coverage is displayed. + +## Deployment +Open MCT Web is built to be flexible in terms of the deployment strategies it +supports. In order to run in the browser, Open MCT Web needs: + +1. HTTP access to sources/resources for the framework, platform, and all active +bundles. +2. Access to any external services utilized by active bundles. (This means that +external services need to support HTTP or some other web-accessible interface, +like WebSockets.) + +Any HTTP server capable of serving flat files is sufficient for the first point. +The command-line build also packages Open MCT Web into a `.war` file for easier +deployment on containers such as Apache Tomcat. + +The second point may be less flexible, as it depends upon the specific services +to be utilized by Open MCT Web. Because of this, it is often the set of external +services (and the manner in which they are exposed) that determine how to deploy +Open MCT Web. + +One important constraint to consider in this context is the browser's same +origin policy. If external services are not on the same apparent host and port +as the client (from the perspective of the browser) then access may be +disallowed. There are two workarounds if this occurs: + +* Make the external service appear to be on the same host/port, either by +actually deploying it there, or by proxying requests to it. +* Enable CORS (cross-origin resource sharing) on the external service. This is +only possible if the external service can be configured to support CORS. Care +should be exercised if choosing this option to ensure that the chosen +configuration does not create a security vulnerability. + +Examples of deployment strategies (and the conditions under which they make the +most sense) include: + +* If the external services that Open MCT Web will utilize are all running on +Apache Tomcat [https://tomcat.apache.org/](), then it makes sense to run Open +MCT Web from the same Tomcat instance as a separate web application. The +`.war` artifact produced by the command line build facilitates this deployment +option. (See [https://tomcat.apache.org/tomcat-8.0-doc/deployer-howto.html() for +general information on deploying in Tomcat.) +* If a variety of external services will be running from a variety of +hosts/ports, then it may make sense to use a web server that supports proxying, +such as the Apache HTTP Server [http://httpd.apache.org/](). In this +configuration, the HTTP server would be configured to proxy (or reverse proxy) +requests at specific paths to the various external services, while providing +Open MCT Web as flat files from a different path. +* If a single server component is being developed to handle all server-side +needs of an Open MCT Web instance, it can make sense to serve Open MCT Web (as +flat files) from the same component using an embedded HTTP server such as Nancy +[http://nancyfx.org/](). +* If no external services are needed (or if the 'external services' will just +be generating flat files to read) it makes sense to utilize a lightweight flat +file HTTP server such as Lighttpd [http://www.lighttpd.net/](). In this +configuration, Open MCT Web sources/resources would be placed at one path, while +the files generated by the external service are placed at another path. +* If all external services support CORS, it may make sense to have an HTTP +server that is solely responsible for making Open MCT Web sources/resources +available, and to have Open MCT Web contact these external services directly. +Again, lightweight HTTP servers such as Lighttpd [http://www.lighttpd.net/]() +are useful in this circumstance. The downside of this option is that additional +configuration effort is required, both to enable CORS on the external services, +and to ensure that Open MCT Web can correctly locate these services. + +Another important consideration is authentication. By design, Open MCT Web does +not handle user authentication. Instead, this should typically be treated as a +deployment-time concern, where authentication is handled by the HTTP server +which provides Open MCT Web, or an external access management system. + +### Configuration +In most of the deployment options above, some level of configuration is likely +to be needed or desirable to make sure that bundles can reach the external +services they need to reach. Most commonly this means providing the path or URL +to an external service. + +Configurable parameters within Open MCT Web are specified via constants +(literally, as extensions of the `constants` category) and accessed via +dependency injection by the scripts which need them. Reasonable defaults for +these constants are provided in the bundle where they are used. Plugins are +encouraged to follow the same pattern. + +Constants may be specified in any bundle; if multiple constants are specified +with the same `key` the highest-priority one will be used. This allows default +values to be overridden by specifying constants with higher priority. + +This permits at least three configuration approaches: + +* Modify the constants defined in their original bundles when deploying. This is +generally undesirable due to the amount of manual work required and potential +for error, but is viable if there are a small number of constants to change. +* Add a separate configuration bundle which overrides the values of these +constants. This is particularly appropriate when multiple configurations (e.g. +development, test, production) need to be managed easily; these can be swapped +quickly by changing the set of active bundles in bundles.json. +* Deploy Open MCT Web and its external services in such a fashion that the +default paths to reach external services are all correct. + +### Configuration Constants + +The following configuration constants are recognized by Open MCT Web bundles: +* CouchDB adapter - `platform/persistence/couch` + * `COUCHDB_PATH`: URL or path to the CouchDB database to be used for domain + object persistence. Should not include a trailing slash. +* ElasticSearch adapter - `platform/persistence/elastic` + * `ELASTIC_ROOT`: URL or path to the ElasticSearch instance to be used for + domain object persistence. Should not include a trailing slash. + * `ELASTIC_PATH`: Path relative to the ElasticSearch instance where domain + object models should be persisted. Should take the form `/`. \ No newline at end of file diff --git a/docs/src/index.html b/docs/src/index.html index e84b405234..e80a6138b2 100644 --- a/docs/src/index.html +++ b/docs/src/index.html @@ -29,8 +29,9 @@ Sections: diff --git a/docs/src/tutorials/images/add-task.png b/docs/src/tutorials/images/add-task.png new file mode 100644 index 0000000000..7780365c5a Binary files /dev/null and b/docs/src/tutorials/images/add-task.png differ diff --git a/docs/src/tutorials/images/bar-plot-2.png b/docs/src/tutorials/images/bar-plot-2.png new file mode 100644 index 0000000000..a32c2b76f1 Binary files /dev/null and b/docs/src/tutorials/images/bar-plot-2.png differ diff --git a/docs/src/tutorials/images/bar-plot-3.png b/docs/src/tutorials/images/bar-plot-3.png new file mode 100644 index 0000000000..0899984a33 Binary files /dev/null and b/docs/src/tutorials/images/bar-plot-3.png differ diff --git a/docs/src/tutorials/images/bar-plot-4.png b/docs/src/tutorials/images/bar-plot-4.png new file mode 100644 index 0000000000..50a9091fa7 Binary files /dev/null and b/docs/src/tutorials/images/bar-plot-4.png differ diff --git a/docs/src/tutorials/images/bar-plot.png b/docs/src/tutorials/images/bar-plot.png new file mode 100644 index 0000000000..2f113d4c6d Binary files /dev/null and b/docs/src/tutorials/images/bar-plot.png differ diff --git a/docs/src/tutorials/images/chrome.png b/docs/src/tutorials/images/chrome.png new file mode 100644 index 0000000000..1b9b7b80d2 Binary files /dev/null and b/docs/src/tutorials/images/chrome.png differ diff --git a/docs/src/tutorials/images/remove-task.png b/docs/src/tutorials/images/remove-task.png new file mode 100644 index 0000000000..015ec95ac4 Binary files /dev/null and b/docs/src/tutorials/images/remove-task.png differ diff --git a/docs/src/tutorials/images/telemetry-1.png b/docs/src/tutorials/images/telemetry-1.png new file mode 100644 index 0000000000..2a606e83c7 Binary files /dev/null and b/docs/src/tutorials/images/telemetry-1.png differ diff --git a/docs/src/tutorials/images/telemetry-2.png b/docs/src/tutorials/images/telemetry-2.png new file mode 100644 index 0000000000..0b34dd90f5 Binary files /dev/null and b/docs/src/tutorials/images/telemetry-2.png differ diff --git a/docs/src/tutorials/images/telemetry-3.png b/docs/src/tutorials/images/telemetry-3.png new file mode 100644 index 0000000000..c235b1d543 Binary files /dev/null and b/docs/src/tutorials/images/telemetry-3.png differ diff --git a/docs/src/tutorials/images/todo-edit.png b/docs/src/tutorials/images/todo-edit.png new file mode 100644 index 0000000000..3c1ba3f5cc Binary files /dev/null and b/docs/src/tutorials/images/todo-edit.png differ diff --git a/docs/src/tutorials/images/todo-list.png b/docs/src/tutorials/images/todo-list.png new file mode 100644 index 0000000000..48c84c63e4 Binary files /dev/null and b/docs/src/tutorials/images/todo-list.png differ diff --git a/docs/src/tutorials/images/todo-restyled.png b/docs/src/tutorials/images/todo-restyled.png new file mode 100644 index 0000000000..9fd7008c2f Binary files /dev/null and b/docs/src/tutorials/images/todo-restyled.png differ diff --git a/docs/src/tutorials/images/todo-selection.png b/docs/src/tutorials/images/todo-selection.png new file mode 100644 index 0000000000..a0ff87514d Binary files /dev/null and b/docs/src/tutorials/images/todo-selection.png differ diff --git a/docs/src/tutorials/images/todo.png b/docs/src/tutorials/images/todo.png new file mode 100644 index 0000000000..44a7b7b2ec Binary files /dev/null and b/docs/src/tutorials/images/todo.png differ diff --git a/docs/src/tutorials/index.md b/docs/src/tutorials/index.md new file mode 100644 index 0000000000..d07466abac --- /dev/null +++ b/docs/src/tutorials/index.md @@ -0,0 +1,3055 @@ +# Open MCT Web Tutorials + +Victor Woeltjen +victor.woeltjen@nasa.gov + +October 14, 2015 +Document Version 2.2 + +Date | Version | Summary of Changes | Author +---------------- | ------- | --------------------------------- | --------------- +May 12, 2015 | 0 | Initial Draft | Victor Woeltjen +June 4, 2015 | 1.0 | Name changes | Victor Woeltjen +July 28, 2015 | 2.0 | Telemetry adapter tutorial | Victor Woeltjen +July 31, 2015 | 2.1 | Clarify telemetry adapter details | Victor Woeltjen +October 14, 2015 | 2.2 | Conversion to markdown | Andrew Henry + +# Introduction + +## This document +This document contains a number of code examples in formatted code blocks. In +many cases these code blocks are repeated in order to highlight code that has +been added or removed as part of the tutorial. In these cases, any lines added +will be indicated with a '+' at the start of the line. Any lines removed will +be indicated with a '-'. + +## Setting Up Open MCT Web + +In this section, we will cover the steps necessary to get a minimal Open MCT Web +developer environment up and running. Once we have this, we will be able to +proceed with writing plugins as described in this tutorial. + +### Prerequisites + +This tutorial assumes you have the following software installed. Version numbers +record what was used in writing this tutorial; the same steps should work with +more recent versions, but this cannot be guaranteed. + +* Node.js v0.12.2: https://nodejs.org/ +* git v1.8.3.4: http://git-scm.com/ +* Google Chrome v42: https://www.google.com/chrome/ +* A text editor. + +Open MCT Web can be run without any of these tools, provided suitable +alternatives are taken; see the [Open MCT Web Developer Guide](../guide/index.md) +for a more general overview of how to run and deploy a Open MCT Web application. + +### Check out Open MCT Web Sources + +First step is to check out Open MCT Web from the source repository. + +`git clone https://github.com/nasa/openmctweb.git openmctweb` + +This will create a copy of the Open MCT Web source code repository in the folder +`openmctweb` (relative to the path from which you ran the command.) +If you have a repository URL, use that as the “path to repo” above. Alternately, +if you received Open MCT Web as a git bundle, the path to that bundle on the +local filesystem can be used instead. +At this point, it will also be useful to branch off of Open MCT Web v0.6.2 +(which was used when writing these tutorials) to begin adding plugins. + + cd openmctweb + git branch open-v0.6.2 + git checkout + +### Configuring Persistence + +In its default configuration, Open MCT Web will try to use ElasticSearch +(expected to be deployed at /elastic on the same HTTP server running Open MCT +Web) to persist user-created domain objects. We don’t need that for these +tutorials, so we will replace the ElasticSearch plugin with the example +persistence plugin. This doesn’t actually persist, so anything we create within +Open MCT Web will be lost on reload, but that’s fine for purposes of these +tutorials. + +To change this configuration, edit bundles.json (at the top level of the Open +MCT Web repository) and replace platform/persistence/elastic with +example/persistence. + +#### Bundle Before + + [ + "platform/framework", + "platform/core", + "platform/representation", + "platform/commonUI/about", + "platform/commonUI/browse", + "platform/commonUI/edit", + "platform/commonUI/dialog", + "platform/commonUI/general", + "platform/containment", + "platform/telemetry", + "platform/features/layout", + "platform/features/pages", + "platform/features/plot", + "platform/features/scrolling", + "platform/forms", + "platform/persistence/queue", + -- "platform/persistence/elastic", + "platform/policy", + + "example/generator" + ] +__bundles.json__ + +#### Bundle After + + [ + "platform/framework", + "platform/core", + "platform/representation", + "platform/commonUI/about", + "platform/commonUI/browse", + "platform/commonUI/edit", + "platform/commonUI/dialog", + "platform/commonUI/general", + "platform/containment", + "platform/telemetry", + "platform/features/layout", + "platform/features/pages", + "platform/features/plot", + "platform/features/scrolling", + "platform/forms", + "platform/persistence/queue", + "platform/policy", + + ++ "example/persistence", + "example/generator" + ] +__bundles.json__ + +### Run a Web Server + +The next step is to run a web server so that you can view the Open MCT Web +client (including the plugins you add to it) in browser. Any web server can +be used for hosting OpenMCTWeb, and a trivial web server is provided in this +package for the purposes of running the tutorials. The provided web server +should not be used in a production environment + +To run the tutorial web server + + node app.js + +### Viewing in Browser + +Once running, you should be able to view Open MCT Web from your browser at +[http://localhost:8080/]() (assuming the web server is running on port 8080, +and OpenMCTWeb is installed at the server's root path). +[Google Chrome](https://www.google.com/chrome/) is recommended for these +tutorials, as Chrome is Open MCT Web’s “test-to” browser. The browser cache +can sometimes interfere with development (masking changes by +using older versions of sources); to avoid this, it is easiest to run Chrome +with Developer Tools expanded, and “Disable cache” selected from the Network +tab, as shown below. + +![Chrome Developer Tools](images/chrome.png) + +# Tutorials + +These tutorials cover three of the common tasks in Open MCT Web: + +* The “to-do list” tutorial illustrates how to add a new application feature. +* The “bar graph” tutorial illustrates how to add a new telemetry visualization. +* The “data set reader” tutorial illustrates how to integrate with a telemetry +backend. + +## To-do List + +The goal of this tutorial is to add a new application feature to Open MCT Web: +To-do lists. Users should be able to create and manage these to track items that +they need to do. This is modelled after the to-do lists at [http://todomvc.com/](). + +### Step 1-Create the Plugin + +The first step to adding a new feature to Open MCT Web is to create the plugin +which will expose that feature. A plugin in Open MCT Web is represented by what +is called a bundle; a bundle, in turn, is a directory which contains a file +bundle.json, which in turn describes where other relevant sources & resources +will be. The syntax of this file is described in more detail in the Open MCT Web +Developer Guide. + +We will create this file in the directory tutorials/todo (we can hereafter refer +to this plugin as tutorials/todo as well.) We will start with an “empty bundle”, +one which exposes no extensions - which looks like: + + { + "name": "To-do Plugin", + "description": "Allows creating and editing to-do lists.", + "extensions": { + + } + } + +__tutorials/todo/bundle.json__ + +We will also include this in our list of active bundles. + +#### Before + [ + "platform/framework", + "platform/core", + "platform/representation", + "platform/commonUI/about", + "platform/commonUI/browse", + "platform/commonUI/edit", + "platform/commonUI/dialog", + "platform/commonUI/general", + "platform/containment", + "platform/telemetry", + "platform/features/layout", + "platform/features/pages", + "platform/features/plot", + "platform/features/scrolling", + "platform/forms", + "platform/persistence/queue", + "platform/policy", + + "example/persistence", + "example/generator" + ] +__bundles.json__ + +#### After + [ + "platform/framework", + "platform/core", + "platform/representation", + "platform/commonUI/about", + "platform/commonUI/browse", + "platform/commonUI/edit", + "platform/commonUI/dialog", + "platform/commonUI/general", + "platform/containment", + "platform/telemetry", + "platform/features/layout", + "platform/features/pages", + "platform/features/plot", + "platform/features/scrolling", + "platform/forms", + "platform/persistence/queue", + "platform/policy", + + "example/persistence", + "example/generator", + + ++ "tutorials/todo" + ] + +__bundles.json__ + +At this point, we can reload Open MCT Web. We haven’t introduced any new +functionality, so we don’t see anything different, but if we run with logging +enabled ([http://localhost:8080/?log=info]()) and check the browser console, we +should see: + +`Resolving extensions for bundle tutorials/todo(To-do Plugin)` + +...which shows that our plugin has loaded. + +### Step 2-Add a Domain Object Type + +Features in a Open MCT Web application are most commonly expressed as domain +objects and/or views thereof. A domain object is some thing that is relevant to +the work that the Open MCT Web application is meant to support. Domain objects +can be created, organized, edited, placed in layouts, and so forth. (For a +deeper explanation of domain objects, see the Open MCT Web Developer Guide.) + +In the case of our to-do list feature, the to-do list itself is the thing we’ll +want users to be able to create and edit. So, we will add that as a new type in +our bundle definition: + + { + "name": "To-do Plugin", + "description": "Allows creating and editing to-do lists.", + "extensions": { + ++ "types": [ + ++ { + ++ "key": "example.todo", + ++ "name": "To-Do List", + ++ "glyph": "j", + ++ "description": "A list of things that need to be done.", + ++ "features": ["creation"] + ++ } + ] + } + } +__tutorials/todo/bundle.json__ + +What have we done here? We’ve stated that this bundle includes extensions of the +category _types_, which is used to describe domain object types. Then, we’ve +included a definition for one such extension, which is the to-do list object. + +Going through the properties we’ve defined: + +* The `key` of `example.todo` will be stored as the machine-readable name for +domain objects of this type. +* The `name` of “To-Do List” is the human-readable name for this type, and will +be shown to users. +* The `glyph` refers to a special character in Open MCT Web’s custom font set; +this will be used as an icon. +* The `description` is also human-readable, and will be used whenever a longer +explanation of what this type is should be shown. +* Finally, the `features` property describes some special features of objects of +this type. Including `creation` here means that we want users to be able to +create this (in other cases, we may wish to expose things as domain objects +which aren’t user-created, in which case we would omit this.) + +If we reload Open MCT Web, we see that our new domain object type appears in the +Create menu: + +![To-Do List](images/todo.png) + +At this point, our to-do list doesn’t do much of anything; we can create them +and give them names, but they don’t have any specific functionality attached, +because we haven’t defined any yet. + +### Step 3-Add a View + +In order to allow a to-do list to be used, we need to define and display its +contents. In Open MCT Web, the pattern that the user expects is that they’ll +click on an object in the left-hand tree, and see a visualization of it to the +right; in Open MCT Web, these visualizations are called views. +A view in Open MCT Web is defined by an Angular template. We’ll add that in the +directory `tutorials/todo/res/templates` (`res` is, by default, the directory +where bundle-related resources are kept, and `templates` is where HTML templates +are stored by convention.) + + + +
    +
  • + + {{task.description}} +
  • +
+__tutorials/todo/res/templates/todo.html__ + +A summary of what’s included: + +* At the top, we have some buttons that we will later wire in to allow the user +to filter down to either complete or incomplete tasks. +* After that, we have a list of tasks. The scope variable `model` is the model +of the domain object being viewed; this contains all of the persistent state +associated with that object. This model is effectively just a JSON document, so +we can choose what goes into it (so long as we take care not to collide with +platform-defined properties; see the Open MCT Web Developer Guide.) Here, we +assume that all tasks will be stored in a property `tasks`, and that each will be +an object containing a `description` (the readable summary of the task) and a +boolean `completed` flag. + +To expose this view in Open MCT Web, we need to declare it in our bundle +definition: + + { + "name": "To-do Plugin", + "description": "Allows creating and editing to-do lists.", + "extensions": { + "types": [ + { + "key": "example.todo", + "name": "To-Do List", + "glyph": "j", + "description": "A list of things that need to be done.", + "features": ["creation"] + } + ], + ++ "views": [ + ++ { + ++ "key": "example.todo", + ++ "type": "example.todo", + ++ "glyph": "j", + ++ "name": "List", + ++ "templateUrl": "templates/todo.html" + ++ } + ++ ] + } + } +__tutorials/todo/bundle.json__ + +Here, we’ve added another extension, this time belonging to category `views`. It +contains the following properties: + +* Its `key` is its machine-readable name; we’ve given it the same name here as +the domain object type, but could have chosen any unique name. + +* The `type` property tells Open MCT Web that this view is only applicable to +domain objects of that type. This means that we’ll see this view for To-do Lists +that we create, but not for other domain objects (such as Folders.) + +* The `glyph` and `name` properties describe the icon and human-readable name +for this view to display in the UI where needed (if multiple views are available +for To-do Lists, the user will be able to choose one.) + +* Finally, the `templateUrl` points to the Angular template we wrote; this path is +relative to the bundle’s `res` folder. + +This template looks like it should display tasks, but we don’t have any way for +the user to create these yet. As a temporary workaround to test the view, we +will specify an initial state for To-do List domain object models in the +definition of that type. + + { + "name": "To-do Plugin", + "description": "Allows creating and editing to-do lists.", + "extensions": { + "types": [ + { + "key": "example.todo", + "name": "To-Do List", + "glyph": "j", + "description": "A list of things that need to be done.", + "features": ["creation"], + ++ "model": { + ++ "tasks": [ + ++ { "description": "Add a type", "completed": true }, + ++ { "description": "Add a view" } + ++ ] + } + } + ], + "views": [ + { + "key": "example.todo", + "type": "example.todo", + "glyph": "j", + "name": "List", + "templateUrl": "templates/todo.html" + } + ] + } + } +__tutorials/todo/bundle.json__ + +Now, when To-do List objects are created in Open MCT Web, they will initially +have the state described by that model property. + +If we reload Open MCT Web, create a To-do List, and navigate to it in the tree, +we should now see: + +![To-Do List](images/todo-list.png) + +This looks roughly like what we want. We’ll handle styling later, so let’s work +on adding functionality. Currently, the filter choices do nothing, and while the +checkboxes can be checked/unchecked, we’re not actually making the changes in +the domain object - if we click over to My Items and come back to our +To-Do List, for instance, we’ll see that those check boxes have returned to +their initial state. + +### Step 4-Add a Controller + +We need to do some scripting to add dynamic behavior to that view. In +particular, we want to: + +* Filter by complete/incomplete status. +* Change the completion state of tasks in the model. + +To do this, we will support this by adding an Angular controller. (See +[https://docs.angularjs.org/guide/controller]() for an overview of controllers.) +We will define that in an AMD module (see [http://requirejs.org/docs/whyamd.html]()) +in the directory `tutorials/todo/src/controllers` (`src` is, by default, the +directory where bundle-related source code is kept, and controllers is where +Angular controllers are stored by convention.) + + define(function () { + function TodoController($scope) { + var showAll = true, + showCompleted; + + // Persist changes made to a domain object's model + function persist() { + var persistence = + $scope.domainObject.getCapability('persistence'); + return persistence && persistence.persist(); + } + + // Change which tasks are visible + $scope.setVisibility = function (all, completed) { + showAll = all; + showCompleted = completed; + }; + + // Toggle the completion state of a task + $scope.toggleCompletion = function (taskIndex) { + $scope.domainObject.useCapability('mutation', function (model) { + var task = model.tasks[taskIndex]; + task.completed = !task.completed; + }); + persist(); + }; + + // Check whether a task should be visible + $scope.showTask = function (task) { + return showAll || (showCompleted === !!(task.completed)); + }; + } + + return TodoController; + }); +__tutorials/todo/src/controllers/TodoController.js__ + +Here, we’ve defined three new functions and placed them in our `$scope`, which +will make them available from the template: + +* `setVisibility` changes which tasks are meant to be visible. The first argument +is a boolean, which, if true, means we want to show everything; the second +argument is the completion state we want to show (which is only relevant if the +first argument is falsy.) + +* `toggleCompletion` changes whether or not a task is complete. We make the +change via the domain object’s `mutation` capability, and then persist the +change via its `persistence` capability. See the Open MCT Web Developer Guide +for more information on these capabilities. + +* `showTask` is meant to be used to help decide if a task should be shown, based +on the current visibility settings. It is true when we have decided to show +everything, or when the completion state matches the state we’ve chosen. (Note +the use of the double-not !! to coerce the completed flag to a boolean, for +equality testing.) + +Note that these functions make reference to `$scope.domainObject;` this is the +domain object being viewed, which is passed into the scope by Open MCT Web +prior to our template being utilized. + +On its own, this controller merely exposes these functions; the next step is to +use them from our template: + + ++
+
+ ++ All + ++ Incomplete + ++ Complete +
+ +
    +
  • + + {{task.description}} +
  • +
+ ++
+__tutorials/todo/res/templates/todo.html__ + +Summary of changes here: + +* First, we surround everything in a `div` which we use to utilize our +`TodoController`. This `div` will also come in handy later for styling. +* From our filters at the top, we change the visibility settings when a different +option is clicked. +* When showing tasks, we check with `showTask` to see if the task matches current +filter settings. +* Finally, when the checkbox for a task is clicked, we make the change in the +model via `toggleCompletion`. + +If we were to try to run at this point, we’d run into problems because the +`TodoController` has not been registered with Angular. We need to first declare +it in our bundle definition, as an extension of category `controllers`: + + { + "name": "To-do Plugin", + "description": "Allows creating and editing to-do lists.", + "extensions": { + "types": [ + { + "key": "example.todo", + "name": "To-Do List", + "glyph": "j", + "description": "A list of things that need to be done.", + "features": ["creation"], + "model": { + "tasks": [ + { "description": "Add a type", "completed": true }, + { "description": "Add a view" } + ] + } + } + ], + "views": [ + { + "key": "example.todo", + "type": "example.todo", + "glyph": "j", + "name": "List", + "templateUrl": "templates/todo.html" + } + ], + + "controllers": [ + + { + + "key": "TodoController", + + "implementation": "controllers/TodoController.js", + + "depends": [ "$scope" ] + + } + + ] + } + } +__tutorials/todo/bundle.json__ + +In this extension definition we have: + +* A `key`, which again is a machine-readable identifier. This is the name that +templates will reference. +* An `implementation`, which refers to an AMD module. The path is relative to the +`src` directory within the bundle. +* The `depends` property declares the dependencies of this controller. Here, we +want Angular to inject `$scope`, the current Angular scope (which, going back +to our controller, is expected as our first argument.) + +If we reload the browser now, our To-do List looks much the same, but now we are +able to filter down the visible list, and the changes we make will stick around +if we go to My Items and come back. + + +### Step 5-Support Editing + +We now have a somewhat-functional view of our To-Do List, but we’re still +missing some important functionality: Adding and removing tasks! + +This is a good place to discuss the user interface style of Open MCT Web. Open +MCT Web draws a distinction between “using” and “editing” a domain object; in +general, you can only make changes to a domain object while in Edit mode, which +is reachable from the button with a pencil icon. This distinction helps users +keep these tasks separate. + +The distinction between “using” and “editing” may vary depending on what domain +objects or views are being used. While it may be convenient for a developer to +think of “editing” as “any changes made to a domain object,” in practice some of +these activities will be thought of as “using.” + +For this tutorial we’ll consider checking/unchecking tasks as “using” To-Do +Lists, and adding/removing tasks as “editing.” We’ve already implemented the +“using” part, in this case, so let’s focus on editing. + +There are two new pieces of functionality we’ll want out of this step: + +* The ability to add new tasks. +* The ability to remove existing tasks. + +An Editing user interface is typically handled in a tool bar associated with a +view. The contents of this tool bar are defined declaratively in a view’s +extension definition. + + { + "name": "To-do Plugin", + "description": "Allows creating and editing to-do lists.", + "extensions": { + "types": [ + { + "key": "example.todo", + "name": "To-Do List", + "glyph": "j", + "description": "A list of things that need to be done.", + "features": ["creation"], + "model": { + "tasks": [ + { "description": "Add a type", "completed": true }, + { "description": "Add a view" } + ] + } + } + ], + "views": [ + { + "key": "example.todo", + "type": "example.todo", + "glyph": "j", + "name": "List", + "templateUrl": "templates/todo.html", + + "toolbar": { + + "sections": [ + + { + + "items": [ + + { + + "text": "Add Task", + + "glyph": "+", + + "method": "addTask", + + "control": "button" + + } + + ] + + }, + + { + + "items": [ + + { + + "glyph": "Z", + + "method": "removeTask", + + "control": "button" + + } + + ] + + } + + ] + + } + } + ], + "controllers": [ + { + "key": "TodoController", + "implementation": "controllers/TodoController.js", + "depends": [ "$scope" ] + } + ] + } + } +__tutorials/todo/bundle.json__ + +What we’ve stated here is that the To-Do List’s view will have a toolbar which +contains two sections (which will be visually separated by a divider), each of +which contains one button. The first is a button labelled “Add Task” that will +invoke an `addTask` method; the second is a button with a glyph (which will appear +as a trash can in Open MCT Web’s custom font set) which will invoke a `removeTask` +method. For more information on forms and tool bars in Open MCT Web, see the +Open MCT Web Developer Guide. + +If we reload and run Open MCT Web, we won’t see any tool bar when we switch over +to Edit mode. This is because the aforementioned methods are expected to be +found on currently-selected elements; we haven’t done anything with selections +in our view yet, so the Open MCT Web platform will filter this tool bar down to +all the applicable controls, which means no controls at all. + +To support selection, we will need to make some changes to our controller: + + define(function () { + + // Form to display when adding new tasks + + var NEW_TASK_FORM = { + + name: "Add a Task", + + sections: [{ + + rows: [{ + + name: 'Description', + + key: 'description', + + control: 'textfield', + + required: true + + }] + + }] + + }; + + + function TodoController($scope, dialogService) { + var showAll = true, + showCompleted; + + // Persist changes made to a domain object's model + function persist() { + var persistence = + $scope.domainObject.getCapability('persistence'); + return persistence && persistence.persist(); + } + + + // Remove a task + + function removeTaskAtIndex(taskIndex) { + + $scope.domainObject.useCapability('mutation', function + + (model) { + + model.tasks.splice(taskIndex, 1); + + }); + + persist(); + + } + + + // Add a task + + function addNewTask(task) { + + $scope.domainObject.useCapability('mutation', function + + (model) { + + model.tasks.push(task); + + }); + + persist(); + + } + + // Change which tasks are visible + $scope.setVisibility = function (all, completed) { + showAll = all; + showCompleted = completed; + }; + + // Toggle the completion state of a task + $scope.toggleCompletion = function (taskIndex) { + $scope.domainObject.useCapability('mutation', function (model) { + var task = model.tasks[taskIndex]; + task.completed = !task.completed; + }); + persist(); + }; + + // Check whether a task should be visible + $scope.showTask = function (task) { + return showAll || (showCompleted === !!(task.completed)); + }; + + // Handle selection state in edit mode + + if ($scope.selection) { + + // Expose the ability to select tasks + + $scope.selectTask = function (taskIndex) { + + $scope.selection.select({ + + removeTask: function () { + + removeTaskAtIndex(taskIndex); + + $scope.selection.deselect(); + + } + + }); + + }; + + + // Expose a view-level selection proxy + + $scope.selection.proxy({ + + addTask: function () { + + dialogService.getUserInput(NEW_TASK_FORM, {}) + + .then(addNewTask); + + } + + }); + + } + } + + return TodoController; + }); +__tutorials/todo/src/controllers/TodoController.js__ + +There are a few changes to pay attention to here. Let’s review them: + +* At the top, we describe the form that should be shown to the user when they +click the _Add Task_ button. This form is described declaratively, and populates +an object that has the same format as tasks in the `tasks` array of our +To-Do List’s model. +* We’ve added an argument to the `TodoController`: The `dialogService`, which is +exposed by the Open MCT Web platform to handle showing dialogs. +* Some utility functions for handling the actual adding and removing of tasks. +These use the `mutation` capability to modify the tasks in the To-Do List’s +model. +* Finally, we check for the presence of a `selection` object in our scope. This +object is provided by Edit mode to manage current selections for editing. When +it is present, we expose a `selectTask` function to our scope to allow selecting +individual tasks; when this occurs, we expose an object to `selection` which has +a `removeTask` method, as expected by the tool bar we’ve defined. We additionally +expose a view proxy, to handle view-level changes (e.g. not associated with any +specific selected object); this has an `addTask` method, which again is expected +by the tool bar we’ve defined. + +Additionally, we need to make changes to our template to select specific tasks +in response to some user gesture. Here, we will select tasks when a user clicks +the description. + +
+ + +
    +
  • + + + + {{task.description}} + + +
  • +
+
+__tutorials/todo/res/templates/todo.html__ + +Finally, the `TodoController` uses the `dialogService` now, so we need to +declare that dependency in its extension definition: + + { + "name": "To-do Plugin", + "description": "Allows creating and editing to-do lists.", + "extensions": { + "types": [ + { + "key": "example.todo", + "name": "To-Do List", + "glyph": "j", + "description": "A list of things that need to be done.", + "features": ["creation"], + "model": { + "tasks": [ + { "description": "Add a type", "completed": true }, + { "description": "Add a view" } + ] + } + } + ], + "views": [ + { + "key": "example.todo", + "type": "example.todo", + "glyph": "j", + "name": "List", + "templateUrl": "templates/todo.html", + "toolbar": { + "sections": [ + { + "items": [ + { + "text": "Add Task", + "glyph": "+", + "method": "addTask", + "control": "button" + } + ] + }, + { + "items": [ + { + "glyph": "Z", + "method": "removeTask", + "control": "button" + } + ] + } + ] + } + } + ], + "controllers": [ + { + "key": "TodoController", + "implementation": "controllers/TodoController.js", + + "depends": [ "$scope", "dialogService" ] + } + ] + } + } +__tutorials/todo/bundle.json__ + +If we now reload Open MCT Web, we’ll be able to see the new functionality we’ve +added. If we Create a new To-Do List, navigate to it, and click the button with +the Pencil icon in the top-right, we’ll be in edit mode. We see, first, that our +“Add Task” button appears in the tool bar: + +![Edit](images/todo-edit.png) + +If we click on this, we’ll get a dialog allowing us to add a new task: + +![Add task](images/add-task.png) + +Finally, if we click on the description of a specific task, we’ll see a new +button appear, which we can then click on to remove that task: + +![Remove task](images/remove-task.png) + +As always in Edit mode, the user will be able to Save or Cancel any changes they have made. +In terms of functionality, our To-Do List can do all the things we want, but the appearance is still lacking. In particular, we can’t distinguish our current filter choice or our current selection state. + +### Step 6-Customizing Look and Feel + +In this section, our goal is to: + +* Display the current filter choice. +* Display the current task selection (when in Edit mode.) +* Tweak the general aesthetics to our liking. +* Get rid of those default tasks (we can create our own now.) + +To support the first two, we’ll need to expose some methods for checking these +states in the controller: + + + define(function () { + // Form to display when adding new tasks + var NEW_TASK_FORM = { + name: "Add a Task", + sections: [{ + rows: [{ + name: 'Description', + key: 'description', + control: 'textfield', + required: true + }] + }] + }; + + function TodoController($scope, dialogService) { + var showAll = true, + showCompleted; + + // Persist changes made to a domain object's model + function persist() { + var persistence = + $scope.domainObject.getCapability('persistence'); + return persistence && persistence.persist(); + } + + // Remove a task + function removeTaskAtIndex(taskIndex) { + $scope.domainObject.useCapability('mutation', function (model) { + model.tasks.splice(taskIndex, 1); + }); + persist(); + } + + // Add a task + function addNewTask(task) { + $scope.domainObject.useCapability('mutation', function (model) { + model.tasks.push(task); + }); + persist(); + } + + // Change which tasks are visible + $scope.setVisibility = function (all, completed) { + showAll = all; + showCompleted = completed; + }; + + + // Check if current visibility settings match + + $scope.checkVisibility = function (all, completed) { + + return showAll ? all : (completed === showCompleted); + + }; + + // Toggle the completion state of a task + $scope.toggleCompletion = function (taskIndex) { + $scope.domainObject.useCapability('mutation', function (model) { + var task = model.tasks[taskIndex]; + task.completed = !task.completed; + }); + persist(); + }; + + // Check whether a task should be visible + $scope.showTask = function (task) { + return showAll || (showCompleted === !!(task.completed)); + }; + + // Handle selection state in edit mode + if ($scope.selection) { + // Expose the ability to select tasks + $scope.selectTask = function (taskIndex) { + $scope.selection.select({ + removeTask: function () { + removeTaskAtIndex(taskIndex); + $scope.selection.deselect(); + }, + + taskIndex: taskIndex + }); + }; + + + // Expose a check for current selection state + + $scope.isSelected = function (taskIndex) { + + return ($scope.selection.get() || {}).taskIndex === + + taskIndex; + + }; + + // Expose a view-level selection proxy + $scope.selection.proxy({ + addTask: function () { + dialogService.getUserInput(NEW_TASK_FORM, {}) + .then(addNewTask); + } + }); + } + } + + return TodoController; + }); +__tutorials/todo/src/controllers/TodoController.js__ + +A summary of these changes: + +* `checkVisibility` has the same arguments as `setVisibility`, but instead of +making a change, it simply returns a boolean true/false indicating whether those +settings are in effect. The logic reflects the fact that the second parameter is +ignored when showing all. +* To support checking for selection, the index of the currently-selected task is +tracked as part of the selection object. +* Finally, an isSelected function is exposed which checks if the indicated task +is currently selected, using the index from above. + +Additionally, we will want to define some CSS rules in order to reflect these +states visually, and to generally improve the appearance of our view. We add +another file to the res directory of our bundle; this time, it is `css/todo.css` +(with the `css` directory again being a convention.) + + .example-todo div.example-button-group { + margin-top: 12px; + margin-bottom: 12px; + } + + .example-todo .example-button-group a { + padding: 3px; + margin: 3px; + } + + .example-todo .example-button-group a.selected { + border: 1px gray solid; + border-radius: 3px; + background: #444; + } + + .example-todo .example-task-completed .example-task-description { + text-decoration: line-through; + opacity: 0.75; + } + + .example-todo .example-task-description.selected { + background: #46A; + border-radius: 3px; + } + + .example-todo .example-message { + font-style: italic; + } +__tutorials/todo/res/css/todo.css__ + +Here, we have defined classes and appearances for: + +* Our filter choosers (`example-button-group`). +* Our selected and/or completed tasks (`example-task-description`). +* A message, which we will add next, to display when there are no tasks +(`example-message`). + +To include this CSS file in our running instance of Open MCT Web, we need to +declare it in our bundle definition, this time as an extension of category +`stylesheets`: + + { + "name": "To-do Plugin", + "description": "Allows creating and editing to-do lists.", + "extensions": { + "types": [ + { + "key": "example.todo", + "name": "To-Do List", + "glyph": "j", + "description": "A list of things that need to be done.", + "features": ["creation"], + "model": { + "tasks": [] + } + } + ], + "views": [ + { + "key": "example.todo", + "type": "example.todo", + "glyph": "j", + "name": "List", + "templateUrl": "templates/todo.html", + "toolbar": { + "sections": [ + { + "items": [ + { + "text": "Add Task", + "glyph": "+", + "method": "addTask", + "control": "button" + } + ] + }, + { + "items": [ + { + "glyph": "Z", + "method": "removeTask", + "control": "button" + } + ] + } + ] + } + } + ], + "controllers": [ + { + "key": "TodoController", + "implementation": "controllers/TodoController.js", + "depends": [ "$scope", "dialogService" ] + } + ], + + "stylesheets": [ + + { + + "stylesheetUrl": "css/todo.css" + + } + + ] + } + } +__tutorials/todo/bundle.json__ + +Note that we’ve also removed our placeholder tasks from the `model` of the +To-Do List’s type above; now To-Do Lists will start off empty. + +Finally, let’s utilize these changes from our view’s template: + + +
+ +
+ + All + + Incomplete + + Complete +
+ +
    +
  • + + + {{task.description}} + +
  • +
+ +
+ + There are no tasks to show. + +
+ +
+__tutorials/todo/res/templates/todo.html__ + +Now, if we reload our page and create a new To-Do List, we will initially see: + +![Todo Restyled](images/todo-restyled.png) + +If we then go into Edit mode, add some tasks, and select one, it will now be +much clearer what the current selection is (e.g. before we hit the remove button +in the toolbar): + +![Todo Restyled](images/todo-selection.png) + +## Bar Graph + +In this tutorial, we will look at creating a bar graph plugin for visualizing +telemetry data. Specifically, we want some bars that raise and lower to match +the observed state of real-time telemetry; this is particularly useful for +monitoring things like battery charge levels. +It is recommended that the reader completes (or is familiar with) the To-Do +List tutorial before completing this tutorial, as certain concepts discussed +there will be addressed in more brevity here. + +### Step 1-Define the View + +Since the goal is to introduce a new view and expose it from a plugin, we will +want to create a new bundle which declares an extension of category `views`. +We’ll also be defining some custom styles, so we’ll include that extension as +well. We’ll be creating this plugin in `tutorials/bargraph`, so our initial +bundle definition looks like: + + { + "name": "Bar Graph", + "description": "Provides the Bar Graph view of telemetry elements.", + "extensions": { + "views": [ + { + "name": "Bar Graph", + "key": "example.bargraph", + "glyph": "H", + "templateUrl": "templates/bargraph.html", + "needs": [ "telemetry" ], + "delegation": true + } + ], + "stylesheets": [ + { + "stylesheetUrl": "css/bargraph.css" + } + ] + } + } +__tutorials/bargraph/bundle.json__ + +The view definition should look familiar after the To-Do List tutorial, with +some additions: + +* The `needs` property indicates that this view is only applicable to domain +objects with a `telemetry` capability. This ensures that this view is available +for telemetry points, but not for other objects (like folders.) +* The `delegation` property indicates that the above constraint can be satisfied +via capability delegation; that is, by domain objects which delegate the +`telemetry` capability to their contained objects. This allows this view to be +used for Telemetry Panel objects as well as for individual telemetry-providing +domain objects. + +For this tutorial, we’ll assume that we’ve sketched out our template and CSS +file ahead of time to describe the general look we want for the view. These +look like: + +
+
+
High
+
Middle
+
Low
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+ Label A +
+
+ Label B +
+
+ Label C +
+
+
+__tutorials/bargraph/res/templates/bargraph.html__ + +Here, three regions are defined. The first will be for tick labels along the +vertical axis, showing the numeric value that certain heights correspond to. The +second will be for the actual bar graphs themselves; three are included here. +The third is for labels along the horizontal axis, which will indicate which +bar corresponds to which telemetry point. Inline `style` attributes are used +wherever dynamic positioning (handled by a script) is anticipated. +The corresponding CSS file which styles and positions these elements: + + .example-bargraph { + position: absolute; + top: 0; + bottom: 0; + right: 0; + left: 0; + mid-width: 160px; + min-height: 160px; + } + + .example-bargraph .example-tick-labels { + position: absolute; + left: 0; + top: 24px; + bottom: 32px; + width: 72px; + font-size: 75%; + } + + .example-bargraph .example-tick-label { + position: absolute; + right: 0; + height: 1em; + margin-bottom: -0.5em; + padding-right: 6px; + text-align: right; + } + + .example-bargraph .example-graph-area { + position: absolute; + border: 1px gray solid; + left: 72px; + top: 24px; + bottom: 32px; + right: 0; + } + + .example-bargraph .example-bar-labels { + position: absolute; + left: 72px; + bottom: 0; + right: 0; + height: 32px; + } + + .example-bargraph .example-bar-holder { + position: absolute; + top: 0; + bottom: 0; + } + + .example-bargraph .example-graph-tick { + position: absolute; + width: 100%; + height: 1px; + border-bottom: 1px gray dashed; + } + + .example-bargraph .example-bar { + position: absolute; + background: darkcyan; + right: 4px; + left: 4px; + } + + .example-bargraph .example-label { + text-align: center; + font-size: 85%; + padding-top: 6px; + } +__tutorials/bargraph/res/css/bargraph.css__ + +This is already enough that, if we add `“tutorials/bargraph”` to `bundles.json`, +we should be able to run Open MCT Web and see our Bar Graph as an available view +for domain objects which provide telemetry (such as the example +_Sine Wave Generator_) as well as for _Telemetry Panel_ objects: + +![Bar Plot](images/bar-plot.png) + +This means that our remaining work will be to populate and position these +elements based on the actual contents of the domain object. + +### Step 2-Add a Controller + +Our next step will be to begin dynamically populating this template’s contents. +Specifically, our goals for this step will be to: + +* Show one bar per telemetry-providing domain object (for which we’ll be getting +actual telemetry data in subsequent steps.) +* Show correct labels for these objects at the bottom. +* Show numeric labels on the left-hand side. + +Notably, we will not try to show telemetry data after this step. + +To support this, we will add a new controller which supports our Bar Graph view: + + define(function () { + function BarGraphController($scope, telemetryHandler) { + var handle; + + // Add min/max defaults + $scope.low = -1; + $scope.middle = 0; + $scope.high = 1; + + // Convert value to a percent between 0-100, keeping values in points + $scope.toPercent = function (value) { + var pct = 100 * (value - $scope.low) / ($scope.high - $scope.low); + return Math.min(100, Math.max(0, pct)); + }; + + // Use the telemetryHandler to get telemetry objects here + handle = telemetryHandler.handle($scope.domainObject, function () { + $scope.telemetryObjects = handle.getTelemetryObjects(); + $scope.barWidth = + 100 / Math.max(($scope.telemetryObjects).length, 1); + }); + + // Release subscriptions when scope is destroyed + $scope.$on('$destroy', handle.unsubscribe); + } + + return BarGraphController; + }); +__tutorials/bargraph/src/controllers/BarGraphController.js__ + +A summary of what we’ve done here: + +* We’re exposing some numeric values that will correspond to the _low_, _middle_, +and _high_ end of the graph. (The `medium` attribute will be useful for +positioning the middle line, which are graphs will ultimately descend down or +push up from.) +* Add a utility function which converts from numeric values to percentages. This +will help support some positioning in the template. +* Utilize the `telemetryHandler`, provided by the platform, to start listening +to real-time telemetry updates. This will deal with most of the complexity of +dealing with telemetry (e.g. differentiating between individual telemetry points +and telemetry panels, monitoring latest values) and provide us with a useful +interface for populating our view. The the Open MCT Web Developer Guide for more +information on dealing with telemetry. + +Whenever the telemetry handler invokes its callbacks, we update the set of +telemetry objects in view, as well as the width for each bar. + +We will also utilize this from our template: + +
+
+ +
+ + {{value}} + +
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+ + + + + +
+
+
+__tutorials/bargraph/res/templates/bargraph.html__ + +Summarizing these changes: + +* Utilize the exposed `low`, `middle`, and `high` values to populate our labels +along the vertical axis. Additionally, use the `toPercent` function to position +these from the bottom. +* Replace our three hard-coded bars with a repeater that looks at the +`telemetryObjects` exposed by the controller and adds one bar each. +* Position the dashed tick-line using the `middle` value and the `toPercent` +function, lining it up with its label to the left. +* At the bottom, repeat a set of labels for the telemetry-providing domain +objects, with matching alignment to the bars above. We use an existing +representation, `label`, to make this easier. + +Finally, we expose our controller from our bundle definition. Note that the +depends declaration includes both `$scope` as well as the `telemetryHandler` +service we made use of. + + { + "name": "Bar Graph", + "description": "Provides the Bar Graph view of telemetry elements.", + "extensions": { + "views": [ + { + "name": "Bar Graph", + "key": "example.bargraph", + "glyph": "H", + "templateUrl": "templates/bargraph.html", + "needs": [ "telemetry" ], + "delegation": true + } + ], + "stylesheets": [ + { + "stylesheetUrl": "css/bargraph.css" + } + ], + + "controllers": [ + + { + + "key": "BarGraphController", + + "implementation": "controllers/BarGraphController.js", + + "depends": [ "$scope", "telemetryHandler" ] + + } + + ] + } + } +__tutorials/bargraph/bundle.json__ + +When we reload Open MCT Web, we are now able to see that our bar graph view +correctly labels one bar per telemetry-providing domain object, as shown for +this Telemetry Panel containing four Sine Wave Generators. + +![Bar Plot](images/bar-plot-2.png) + +### Step 3-Using Telemetry Data + +Now that our bar graph is labeled correctly, it’s time to start putting data +into the view. + +First, let’s add expose some more functionality from our controller. To make it +simple, we’ll expose the top and bottom for a bar graph for a given +telemetry-providing domain object, as percentages. + + + define(function () { + function BarGraphController($scope, telemetryHandler) { + var handle; + + // Add min/max defaults + $scope.low = -1; + $scope.middle = 0; + $scope.high = 1; + + // Convert value to a percent between 0-100, keeping values in points + $scope.toPercent = function (value) { + var pct = 100 * (value - $scope.low) / ($scope.high - $scope.low); + return Math.min(100, Math.max(0, pct)); + }; + + // Get bottom and top (as percentages) for current value + + $scope.getBottom = function (telemetryObject) { + + var value = handle.getRangeValue(telemetryObject); + + return $scope.toPercent(Math.min($scope.middle, value)); + + } + + $scope.getTop = function (telemetryObject) { + + var value = handle.getRangeValue(telemetryObject); + + return 100 - $scope.toPercent(Math.max($scope.middle, value)); + + } + + // Use the telemetryHandler to get telemetry objects here + handle = telemetryHandler.handle($scope.domainObject, function () { + $scope.telemetryObjects = handle.getTelemetryObjects(); + $scope.barWidth = + 100 / Math.max(($scope.telemetryObjects).length, 1); + }); + + // Release subscriptions when scope is destroyed + $scope.$on('$destroy', handle.unsubscribe); + } + + return BarGraphController; + }); +__tutorials/bargraph/src/controllers/BarGraphController.js__ + +The `telemetryHandler` exposes a method to provide us with our latest data value +(the `getRangeValue` method), and we already have a function to convert from a +numeric value to a percentage within the view, so we just use those. The only +slight complication is that we want our bar to move up or down from the middle +value, so either of our top or bottom position for the bar itself could be +either the middle line, or the data value. We let `Math.min` and `Math.max` +decide this. + +Next, we utilize this functionality from the template: + +
+
+
+ {{value}} +
+
+ +
+
+
+
+
+
+
+
+ +
+
+ + +
+
+
+__tutorials/bargraph/res/templates/bargraph.html__ + +Here, we utilize the functions we just provided from the controller to position +the bar, using an ng-style attribute. + +When we reload Open MCT Web, our bar graph view now looks like: + +![Bar Plot](images/bar-plot-3.png) + +### Step 4-View Configuration + +The default minimum and maximum values we’ve provided happen to make sense for +sine waves, but what about other values? We want to provide the user with a +means of configuring these boundaries. + +This is normally done via Edit mode. Since view configuration is a common +problem, the Open MCT Web platform exposes a configuration object - called +`configuration` - into our view’s scope. We can populate it as we please, and +when we return to our view later, those changes will be persisted. + +First, let’s add a tool bar for changing these three values in Edit mode: + + { + "name": "Bar Graph", + "description": "Provides the Bar Graph view of telemetry elements.", + "extensions": { + "views": [ + { + "name": "Bar Graph", + "key": "example.bargraph", + "glyph": "H", + "templateUrl": "templates/bargraph.html", + "needs": [ "telemetry" ], + "delegation": true, + + "toolbar": { + + "sections": [ + + { + + "items": [ + + { + + "name": "Low", + + "property": "low", + + "required": true, + + "control": "textfield", + + "size": 4 + + }, + + { + + "name": "Middle", + + "property": "middle", + + "required": true, + + "control": "textfield", + + "size": 4 + + }, + + { + + "name": "High", + + "property": "high", + + "required": true, + + "control": "textfield", + + "size": 4 + + } + + ] + + } + ] + } + } + ], + "stylesheets": [ + { + "stylesheetUrl": "css/bargraph.css" + } + ], + "controllers": [ + { + "key": "BarGraphController", + "implementation": "controllers/BarGraphController.js", + "depends": [ "$scope", "telemetryHandler" ] + } + ] + } + } +__tutorials/bargraph/bundle.json__ + +As we saw in to To-Do List plugin, a tool bar needs either a selected object or +a view proxy to work from. We will add this to our controller, and additionally +will start reading/writing those properties to the view’s `configuration` +object. + + define(function () { + function BarGraphController($scope, telemetryHandler) { + var handle; + + + // Expose configuration constants directly in scope + + function exposeConfiguration() { + + $scope.low = $scope.configuration.low; + + $scope.middle = $scope.configuration.middle; + + $scope.high = $scope.configuration.high; + + } + + + // Populate a default value in the configuration + + function setDefault(key, value) { + + if ($scope.configuration[key] === undefined) { + + $scope.configuration[key] = value; + + } + + } + + + // Getter-setter for configuration properties (for view proxy) + + function getterSetter(property) { + + return function (value) { + + value = parseFloat(value); + + if (!isNaN(value)) { + + $scope.configuration[property] = value; + + exposeConfiguration(); + + } + + return $scope.configuration[property]; + + }; + } + + + // Add min/max defaults + + setDefault('low', -1); + + setDefault('middle', 0); + + setDefault('high', 1); + + exposeConfiguration($scope.configuration); + + + // Expose view configuration options + + if ($scope.selection) { + + $scope.selection.proxy({ + + low: getterSetter('low'), + + middle: getterSetter('middle'), + + high: getterSetter('high') + + }); + + } + + // Convert value to a percent between 0-100 + $scope.toPercent = function (value) { + var pct = 100 * (value - $scope.low) / + ($scope.high - $scope.low); + return Math.min(100, Math.max(0, pct)); + }; + + // Get bottom and top (as percentages) for current value + $scope.getBottom = function (telemetryObject) { + var value = handle.getRangeValue(telemetryObject); + return $scope.toPercent(Math.min($scope.middle, value)); + } + $scope.getTop = function (telemetryObject) { + var value = handle.getRangeValue(telemetryObject); + return 100 - $scope.toPercent(Math.max($scope.middle, value)); + } + + // Use the telemetryHandler to get telemetry objects here + handle = telemetryHandler.handle($scope.domainObject, function () { + $scope.telemetryObjects = handle.getTelemetryObjects(); + $scope.barWidth = + 100 / Math.max(($scope.telemetryObjects).length, 1); + }); + + // Release subscriptions when scope is destroyed + $scope.$on('$destroy', handle.unsubscribe); + } + + return BarGraphController; + }); +__tutorials/bargraph/src/controllers/BarGraphController.js__ + +A summary of these changes: + +* First, read `low`, `middle`, and `high` from the view configuration instead of +initializing them to explicit values. This is placed into its own function, +since it will be called a lot. +* The function `setDefault` is included; it will be used to set the default +values for `low`, `middle`, and `high` in the view configuration, but only if +they aren’t present. +* The tool bar will treat properties in a view proxy as getter-setters if +they are functions; that is, they will be called with an argument to be used +as a setter, and with no argument to use as a getter. We provide ourselves a +function for making these getter-setters (since we’ll need three) that +additionally handles some checking to ensure that these are actually numbers. +* After that, we actually initialize both the view `configuration` object with +defaults (if needed), and expose its state into the scope. +* Finally, we expose a view proxy which will handle changes to `low`, `middle`, +and `high` as entered by the user from the tool bar. This uses the +getter-setters we defined previously. + +If we reload Open MCT Web and go to a Bar Graph view in Edit mode, we now see +that we can change these bounds from the tool bar. + +![Bar plot](images/bar-plot-4.png) + +## Telemetry Adapter + +The goal of this tutorial is to demonstrate how to integrate Open MCT Web +with an existing telemetry system. + +A summary of the steps we will take: + +* Expose the telemetry dictionary within the user interface. +* Support subscription/unsubscription to real-time streaming data. +* Support historical retrieval of telemetry data. + +### Step 0-Expose Your Telemetry + +As a precondition to integrating telemetry data into Open MCT Web, this +information needs to be available over web-based interfaces. In practice, +this will most likely mean exposing data over HTTP, or over WebSockets. +For purposes of this tutorial, a simple node server is provided to stand +in place of this existing telemetry system. It generates real-time data +and exposes it over a WebSocket connection. + + + /*global require,process,console*/ + + var CONFIG = { + port: 8081, + dictionary: "dictionary.json", + interval: 1000 + }; + + (function () { + "use strict"; + + var WebSocketServer = require('ws').Server, + fs = require('fs'), + wss = new WebSocketServer({ port: CONFIG.port }), + dictionary = JSON.parse(fs.readFileSync(CONFIG.dictionary, "utf8")), + spacecraft = { + "prop.fuel": 77, + "prop.thrusters": "OFF", + "comms.recd": 0, + "comms.sent": 0, + "pwr.temp": 245, + "pwr.c": 8.15, + "pwr.v": 30 + }, + histories = {}, + listeners = []; + + function updateSpacecraft() { + spacecraft["prop.fuel"] = Math.max( + 0, + spacecraft["prop.fuel"] - + (spacecraft["prop.thrusters"] === "ON" ? 0.5 : 0) + ); + spacecraft["pwr.temp"] = spacecraft["pwr.temp"] * 0.985 + + Math.random() * 0.25 + Math.sin(Date.now()); + spacecraft["pwr.c"] = spacecraft["pwr.c"] * 0.985; + spacecraft["pwr.v"] = 30 + Math.pow(Math.random(), 3); + } + + function generateTelemetry() { + var timestamp = Date.now(), sent = 0; + Object.keys(spacecraft).forEach(function (id) { + var state = { timestamp: timestamp, value: spacecraft[id] }; + histories[id] = histories[id] || []; // Initialize + histories[id].push(state); + spacecraft["comms.sent"] += JSON.stringify(state).length; + }); + listeners.forEach(function (listener) { + listener(); + }); + } + + function update() { + updateSpacecraft(); + generateTelemetry(); + } + + function handleConnection(ws) { + var subscriptions = {}, // Active subscriptions for this connection + handlers = { // Handlers for specific requests + dictionary: function () { + ws.send(JSON.stringify({ + type: "dictionary", + value: dictionary + })); + }, + subscribe: function (id) { + subscriptions[id] = true; + }, + unsubscribe: function (id) { + delete subscriptions[id]; + }, + history: function (id) { + ws.send(JSON.stringify({ + type: "history", + id: id, + value: histories[id] + })); + } + }; + + function notifySubscribers() { + Object.keys(subscriptions).forEach(function (id) { + var history = histories[id]; + if (history) { + ws.send(JSON.stringify({ + type: "data", + id: id, + value: history[history.length - 1] + })); + } + }); + } + + // Listen for requests + ws.on('message', function (message) { + var parts = message.split(' '), + handler = handlers[parts[0]]; + if (handler) { + handler.apply(handlers, parts.slice(1)); + } + }); + + // Stop sending telemetry updates for this connection when closed + ws.on('close', function () { + listeners = listeners.filter(function (listener) { + return listener !== notifySubscribers; + }); + }); + + // Notify subscribers when telemetry is updated + listeners.push(notifySubscribers); + } + + update(); + setInterval(update, CONFIG.interval); + + wss.on('connection', handleConnection); + + console.log("Example spacecraft running on port "); + console.log("Press Enter to toggle thruster state."); + process.stdin.on('data', function (data) { + spacecraft['prop.thrusters'] = + (spacecraft['prop.thrusters'] === "OFF") ? "ON" : "OFF"; + console.log("Thrusters " + spacecraft["prop.thrusters"]); + }); + }()); +__tutorial-server/app.js__ + +For purposes of this tutorial, how this server has been implemented is +not important; it has just enough functionality to resemble a WebSocket +interface to a real telemetry system, and niceties such as error-handling +have been omitted. (For more information on using WebSockets, both in the +client and on the server, +[https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API]() is an +excellent starting point.) + +What does matter for this tutorial is the interfaces that are exposed. Once a +WebSocket connection has been established to this server, it accepts plain text +messages in the following formats, and issues JSON-formatted responses. + +The requests it handles are: + +* `dictionary`: Responds with a JSON response with the following fields: + * `type`: “dictionary” + * `value`: … the telemetry dictionary (see below) … +* `subscribe `: Subscribe to new telemetry data for the measurement with +the provided identifier. The server will begin sending messages of the +following form: + * `type`: “data” + * `id`: The identifier for the measurement. + * `value`: An object containing the actual measurement, in two fields: + * `timestamp`: A UNIX timestamp (in milliseconds) for the “measurement” + * `value`: The data value for the measurement (either a number, or a + string) +* `unsubscribe `: Stop receiving new data for the identified measurement. +* `history `: Request a history of all telemetry data for the identified +measurement. + * `type`: “history” + * `id`: The identifier for the measurement. + * `value`: An array of objects containing the actual measurement, each of + which having two fields: + * `timestamp`: A UNIX timestamp (in milliseconds) for the “measurement” + * `value`: The data value for the measurement (either a number, or + a string) + +(Note that the term “measurement” is used to describe a distinct data series +within this system; in other systems, these have been called channels, +mnemonics, telemetry points, or other names. No preference is made here; +Open MCT Web is easily adapted to use the terminology appropriate to your +system.) +Additionally, while running the server from the terminal we can toggle the +state of the “spacecraft” by hitting enter; this will turn the “thrusters” +on and off, having observable changes in telemetry. + +The telemetry dictionary referenced previously is contained in a separate file, +used by the server. It uses a custom format and, for purposes of example, +contains three “subsystems” containing a mix of numeric and string-based +telemetry. + + { + "name": "Example Spacecraft", + "identifier": "sc", + "subsystems": [ + { + "name": "Propulsion", + "identifier": "prop", + "measurements": [ + { + "name": "Fuel", + "identifier": "prop.fuel", + "units": "kilograms", + "type": "float" + }, + { + "name": "Thrusters", + "identifier": "prop.thrusters", + "units": "None", + "type": "string" + } + ] + }, + { + "name": "Communications", + "identifier": "comms", + "measurements": [ + { + "name": "Received", + "identifier": "comms.recd", + "units": "bytes", + "type": "integer" + }, + { + "name": "Sent", + "identifier": "comms.sent", + "units": "bytes", + "type": "integer" + } + ] + }, + { + "name": "Power", + "identifier": "pwr", + "measurements": [ + { + "name": "Generator Temperature", + "identifier": "pwr.temp", + "units": "\u0080C", + "type": "float" + }, + { + "name": "Generator Current", + "identifier": "pwr.c", + "units": "A", + "type": "float" + }, + { + "name": "Generator Voltage", + "identifier": "pwr.v", + "units": "V", + "type": "float" + } + ] + } + ] + } +__tutorial-server/dictionary.json__ + +It should be noted that neither the interface for the example server nor the +dictionary format are expected by Open MCT Web; rather, these are intended to +stand in for some existing source of telemetry data to which we wish to adapt +Open MCT Web. + +We can run this example server by: + + cd tutorial-server + npm install ws + node app.js + +To verify that this is running and try out its interface, we can use a tool +like [https://www.npmjs.com/package/wscat](): + + wscat -c ws://localhost:8081 + connected (press CTRL+C to quit) + > dictionary + < {"type":"dictionary","value":{"name":"Example Spacecraft","identifier":"sc","subsystems":[{"name":"Propulsion","identifier":"prop","measurements":[{"name":"Fuel","identifier":"prop.fuel","units":"kilograms","type":"float"},{"name":"Thrusters","identifier":"prop.thrusters","units":"None","type":"string"}]},{"name":"Communications","identifier":"comms","measurements":[{"name":"Received","identifier":"comms.recd","units":"bytes","type":"integer"},{"name":"Sent","identifier":"comms.sent","units":"bytes","type":"integer"}]},{"name":"Power","identifier":"pwr","measurements":[{"name":"Generator Temperature","identifier":"pwr.temp","units":"€C","type":"float"},{"name":"Generator Current","identifier":"pwr.c","units":"A","type":"float"},{"name":"Generator Voltage","identifier":"pwr.v","units":"V","type":"float"}]}]}} + +Now that the example server’s interface is reasonably well-understood, a plugin +can be written to adapt Open MCT Web to utilize it. + +### Step 1-Add a Top-level Object + +Since Open MCT Web uses an “object-first” approach to accessing data, before +we’ll be able to do anything with this new data source, we’ll need to have a +way to explore the available measurements in the tree. In this step, we will +add a top-level object which will serve as a container; in the next step, we +will populate this with the contents of the telemetry dictionary (which we +will retrieve from the server.) + + { + "name": "Example Telemetry Adapter", + "extensions": { + "types": [ + { + "name": "Spacecraft", + "key": "example.spacecraft", + "glyph": "o" + } + ], + "roots": [ + { + "id": "example:sc", + "priority": "preferred", + "model": { + "type": "example.spacecraft", + "name": "My Spacecraft", + "composition": [] + } + } + ] + } + } +__tutorials/telemetry/bundle.json__ + +Here, we’ve created our initial telemetry plugin. This exposes a new domain +object type (the “Spacecraft”, which will be represented by the contents of the +telemetry dictionary) and also adds one instance of it as a root-level object +(by declaring an extension of category roots.) We have also set priority to +preferred so that this shows up near the top, instead of below My Items. + +If we include this in our set of active bundles: + + [ + "platform/framework", + "platform/core", + "platform/representation", + "platform/commonUI/about", + "platform/commonUI/browse", + "platform/commonUI/edit", + "platform/commonUI/dialog", + "platform/commonUI/general", + "platform/containment", + "platform/telemetry", + "platform/features/layout", + "platform/features/pages", + "platform/features/plot", + "platform/features/scrolling", + "platform/forms", + "platform/persistence/queue", + "platform/policy", + + "example/persistence", + "example/generator" + ] + [ + "platform/framework", + "platform/core", + "platform/representation", + "platform/commonUI/about", + "platform/commonUI/browse", + "platform/commonUI/edit", + "platform/commonUI/dialog", + "platform/commonUI/general", + "platform/containment", + "platform/telemetry", + "platform/features/layout", + "platform/features/pages", + "platform/features/plot", + "platform/features/scrolling", + "platform/forms", + "platform/persistence/queue", + "platform/policy", + + "example/persistence", + "example/generator", + + + "tutorials/telemetry" + ] +__bundles.json__ + +...we will be able to reload Open MCT Web and see that it is present: + +![Telemetry](images/telemetry-1.png) + +Now, we have somewhere in the UI to put the contents of our telemetry +dictionary. + +### Step 2-Expose the Telemetry Dictionary + +In order to expose the telemetry dictionary, we first need to read it from the +server. Our first step will be to add a service that will handle interactions +with the server; this will not be used by Open MCT Web directly, but will be +used by subsequent components we add. + + /*global define,WebSocket*/ + + define( + [], + function () { + "use strict"; + + function ExampleTelemetryServerAdapter($q, wsUrl) { + var ws = new WebSocket(wsUrl), + dictionary = $q.defer(); + + // Handle an incoming message from the server + ws.onmessage = function (event) { + var message = JSON.parse(event.data); + + switch (message.type) { + case "dictionary": + dictionary.resolve(message.value); + break; + } + }; + + // Request dictionary once connection is established + ws.onopen = function () { + ws.send("dictionary"); + }; + + return { + dictionary: function () { + return dictionary.promise; + } + }; + } + + return ExampleTelemetryServerAdapter; + } + ); +__tutorials/telemetry/src/ExampleTelemetryServerAdapter.js__ + +When created, this service initiates a connection to the server, and begins +loading the dictionary. This will occur asynchronously, so the `dictionary()` +method it exposes returns a `Promise` for the loaded dictionary +(`dictionary.json` from above), using Angular’s `$q` +(see [https://docs.angularjs.org/api/ng/service/$q]().) Note that error- and +close-handling for this WebSocket connection have been omitted for brevity. + +Once the dictionary has been loaded, we will want to represent its contents +as domain objects. Specifically, we want subsystems to appear as objects +under My Spacecraft, and measurements to appear as objects within those +subsystems. This means that we need to convert the data from the dictionary +into domain object models, and expose these to Open MCT Web via a +`modelService`. + + /*global define*/ + + define( + function () { + "use strict"; + + var PREFIX = "example_tlm:", + FORMAT_MAPPINGS = { + float: "number", + integer: "number", + string: "string" + }; + + function ExampleTelemetryModelProvider(adapter, $q) { + var modelPromise, empty = $q.when({}); + + // Check if this model is in our dictionary (by prefix) + function isRelevant(id) { + return id.indexOf(PREFIX) === 0; + } + + // Build a domain object identifier by adding a prefix + function makeId(element) { + return PREFIX + element.identifier; + } + + // Create domain object models from this dictionary + function buildTaxonomy(dictionary) { + var models = {}; + + // Create & store a domain object model for a measurement + function addMeasurement(measurement) { + var format = FORMAT_MAPPINGS[measurement.type]; + models[makeId(measurement)] = { + type: "example.measurement", + name: measurement.name, + telemetry: { + key: measurement.identifier, + ranges: [{ + key: "value", + name: "Value", + units: measurement.units, + format: format + }] + } + }; + } + + // Create & store a domain object model for a subsystem + function addSubsystem(subsystem) { + var measurements = + (subsystem.measurements || []); + models[makeId(subsystem)] = { + type: "example.subsystem", + name: subsystem.name, + composition: measurements.map(makeId) + }; + measurements.forEach(addMeasurement); + } + + (dictionary.subsystems || []).forEach(addSubsystem); + + return models; + } + + // Begin generating models once the dictionary is available + modelPromise = adapter.dictionary().then(buildTaxonomy); + + return { + getModels: function (ids) { + // Return models for the dictionary only when they + // are relevant to the request. + return ids.some(isRelevant) ? modelPromise : empty; + } + }; + } + + return ExampleTelemetryModelProvider; + } + ); +__tutorials/telemetry/src/ExampleTelemetryModelProvider.js__ + +This script implements a `provider` for `modelService`; the `modelService` is a +composite service, meaning that multiple such services can exist side by side. +(For example, there is another `provider` for `modelService` that reads domain +object models from the persistence store.) + +Here, we read the dictionary using the server adapter from above; since this +will be loaded asynchronously, we use promise-chaining (see +[https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then#Chaining]()) +to take that result and build up an object mapping identifiers to new domain +object models. This is returned from our `modelService`, but only when the +request actually calls for identifiers that look like they’re from the +dictionary. This means that loading other models is not blocked by loading the +dictionary. (Note that the `modelService` contract allows us to return either a +sub- or superset of the requested models, so it is fine to always return the +whole dictionary.) + +Some notable points to call out here: + +* Every subsystem and every measurement from the dictionary has an `identifier` +field declared. We use this as part of the domain object identifier, but we +also prefix it with `example_tlm`:. This accomplishes a few things: + * We can easily tell whether an identifier is expected to be in the + dictionary or not. + * We avoid naming collisions with other model providers. + * Finally, Open MCT Web uses the colon prefix as a hint that this domain + object will not be in the persistence store. +* A couple of new types are introduced here (in the `type` field of the domain +object models we create); we will need to define these as extensions as well in +order for them to display correctly. +* The `composition` field of each subsystem contained the Open MCT Web +identifiers of all the measurements in that subsystem. This `composition` field +will be used by Open MCT Web to determine what domain objects contain other +domain objects (e.g. to populate the tree.) +* The `telemetry` field of each measurement will be used by Open MCT Web to +understand how to request and interpret telemetry data for this object. The +`key` is the machine-readable identifier for this measurement within the +telemetry system; the `ranges` provide metadata about the values for this data. +(A separate field, `domains`, provides metadata about timestamps or other +ordering properties of the data, but this will be the same for all +measurements, so we will define that later at the type level.) + * This field (whose contents will be merged atop the telemetry property we +define at the type-level) will serve as a template for later `telemetry` +requests to the `telemetryService`, so we’ll see the properties we define here +again later in Steps 3 and 4. + +This allows our telemetry dictionary to be expressed as domain object models +(and, in turn, as domain objects), but these objects still aren’t reachable. To +fix this, we will need another script which will add these subsystems to the +root-level object we added in Step 1. + + /*global define*/ + + define( + function () { + "use strict"; + + var TAXONOMY_ID = "example:sc", + PREFIX = "example_tlm:"; + + function ExampleTelemetryInitializer(adapter, objectService) { + // Generate a domain object identifier for a dictionary element + function makeId(element) { + return PREFIX + element.identifier; + } + + // When the dictionary is available, add all subsystems + // to the composition of My Spacecraft + function initializeTaxonomy(dictionary) { + // Get the top-level container for dictionary objects + // from a group of domain objects. + function getTaxonomyObject(domainObjects) { + return domainObjects[TAXONOMY_ID]; + } + + // Populate + function populateModel(taxonomyObject) { + return taxonomyObject.useCapability( + "mutation", + function (model) { + model.name = + dictionary.name; + model.composition = + dictionary.subsystems.map(makeId); + } + ); + } + + // Look up My Spacecraft, and populate it accordingly. + objectService.getObjects([TAXONOMY_ID]) + .then(getTaxonomyObject) + .then(populateModel); + } + + adapter.dictionary().then(initializeTaxonomy); + } + + return ExampleTelemetryInitializer; + } + ); +__tutorials/telemetry/src/ExampleTelemetryInitializer.js__ + +At the conclusion of Step 1, the top-level My Spacecraft object was empty. This +script will wait for the dictionary to be loaded, then load My Spacecraft (by +its identifier), and “mutate” it. The `mutation` capability allows changes to be +made to a domain object’s model. Here, we take this top-level object, update its +name to match what was in the dictionary, and set its `composition` to an array +of domain object identifiers for all subsystems contained in the dictionary +(using the same identifier prefix as before.) + +Finally, we wire in these changes by modifying our plugin’s `bundle.json` to +provide metadata about how these pieces interact (both with each other, and +with the platform): + + { + "name": "Example Telemetry Adapter", + "extensions": { + "types": [ + { + "name": "Spacecraft", + "key": "example.spacecraft", + "glyph": "o" + }, + { + + "name": "Subsystem", + + "key": "example.subsystem", + + "glyph": "o", + + "model": { "composition": [] } + + }, + + { + + "name": "Measurement", + + "key": "example.measurement", + + "glyph": "T", + + "model": { "telemetry": {} }, + + "telemetry": { + + "source": "example.source", + + "domains": [ + + { + + "name": "Time", + + "key": "timestamp" + + } + + ] + + } + + } + ], + "roots": [ + { + "id": "example:sc", + "priority": "preferred", + "model": { + "type": "example.spacecraft", + "name": "My Spacecraft", + "composition": [] + } + } + ], + + "services": [ + + { + + "key": "example.adapter", + + "implementation": "ExampleTelemetryServerAdapter.js", + + "depends": [ "$q", "EXAMPLE_WS_URL" ] + + } + + ], + + "constants": [ + + { + + "key": "EXAMPLE_WS_URL", + + "priority": "fallback", + + "value": "ws://localhost:8081" + + } + + ], + + "runs": [ + + { + + "implementation": "ExampleTelemetryInitializer.js", + + "depends": [ "example.adapter", "objectService" ] + + } + + ], + + "components": [ + + { + + "provides": "modelService", + + "type": "provider", + + "implementation": "ExampleTelemetryModelProvider.js", + + "depends": [ "example.adapter", "$q" ] + + } + + ] + } + } +__tutorials/telemetry/bundle.json__ + +A summary of what we’ve added here: + +* New type definitions have been added to represent Subsystems and Measurements, +respectively. + * Measurements have a `telemetry` field; this is similar to the `telemetry` + field added in the model, but contains properties that will be common among + all Measurements. In particular, the `source` field will be used later as a + symbolic identifier for the telemetry data source. + * We have also added some “initial models” for these two types using the + `model` field. While domain objects of these types cannot be created via the + Create menu, some policies will look at initial models to predict what + capabilities domain objects of certain types would have, so we want to + ensure that Subsystems and Measurements will be recognized as having + `composition` and `telemetry` capabilities, respectively. +* The adapter to the WebSocket server has been added as a service with the +symbolic name `example.adapter`; it is depended-upon elsewhere within this +plugin. +* A constant, `EXAMPLE_WS_URL`, is defined, and depended-upon by +`example.server`. Setting `priority` to `fallback` means this constant will be +overridden if defined anywhere else, allowing configuration bundles to specify +different URLs for the WebSocket connection. +* The initializer script is registered using the `runs` category of extension, +to ensure that this executes (and populates the contents of the top-level My +Spacecraft object) once Open MCT Web is started. + * This depends upon the `example.adapter` service we exposed above, as well + as Angular’s `$q`; these services will be made available in the constructor + call. +* Finally, the `modelService` provider which presents dictionary elements as +domain object models is exposed. Since `modelService` is a composite service, +this is registered under the extension category `components`. + * As with the initializer, this depends upon the `example.adapter` service + we exposed above, as well as Angular’s `$q`; these services will be made + available in the constructor call. + +Now if we run Open MCT Web (assuming our example telemetry server is also +running) and expand our top-level node completely, we see the contents of our +dictionary: + +![Telemetry 2](images/telemetry-2.png) + + +Note that “My Spacecraft” has changed its name to “Example Spacecraft”, which +is the name it had in the dictionary. + +### Step 3-Historical Telemetry + +After Step 2, we are able to see our dictionary in the user interface and click +around our different measurements, but we don’t see any data. We need to give +ourselves the ability to retrieve this data from the server. In this step, we +will do so for the server’s historical telemetry. + +Our first step will be to add a method to our server adapter which allows us to +send history requests to the server: + + /*global define,WebSocket*/ + + define( + [], + function () { + "use strict"; + + function ExampleTelemetryServerAdapter($q, wsUrl) { + var ws = new WebSocket(wsUrl), + + histories = {}, + dictionary = $q.defer(); + + // Handle an incoming message from the server + ws.onmessage = function (event) { + var message = JSON.parse(event.data); + + switch (message.type) { + case "dictionary": + dictionary.resolve(message.value); + break; + + case "history": + + histories[message.id].resolve(message); + + delete histories[message.id]; + + break; + } + }; + + // Request dictionary once connection is established + ws.onopen = function () { + ws.send("dictionary"); + }; + + return { + dictionary: function () { + return dictionary.promise; + }, + + history: function (id) { + + histories[id] = histories[id] || $q.defer(); + + ws.send("history " + id); + + return histories[id].promise; + + } + }; + } + + return ExampleTelemetryServerAdapter; + } + ); + +__tutorials/telemetry/src/ExampleTelemetryServerAdapter.js__ + +When the `history` method is called, a new request is issued to the server for +historical telemetry, _unless_ a request for the same historical telemetry is +still pending. Similarly, when historical telemetry arrives for a given +identifier, the pending promise is resolved. + +This `history` method will be used by a `telemetryService` provider which we +will implement: + + /*global define*/ + + define( + ['./ExampleTelemetrySeries'], + function (ExampleTelemetrySeries) { + "use strict"; + + var SOURCE = "example.source"; + + function ExampleTelemetryProvider(adapter, $q) { + // Used to filter out requests for telemetry + // from some other source + function matchesSource(request) { + return (request.source === SOURCE); + } + + return { + requestTelemetry: function (requests) { + var packaged = {}, + relevantReqs = requests.filter(matchesSource); + + // Package historical telemetry that has been received + function addToPackage(history) { + packaged[SOURCE][history.id] = + new ExampleTelemetrySeries(history.value); + } + + // Retrieve telemetry for a specific measurement + function handleRequest(request) { + var key = request.key; + return adapter.history(key).then(addToPackage); + } + + packaged[SOURCE] = {}; + return $q.all(relevantReqs.map(handleRequest)) + .then(function () { return packaged; }); + }, + subscribe: function (callback, requests) { + return function () {}; + } + }; + } + + return ExampleTelemetryProvider; + } + ); +__tutorials/telemetry/src/ExampleTelemetryProvider.js__ + +The `requestTelemetry` method of a `telemetryService` is expected to take an +array of requests (each with `source` and `key` parameters, identifying the +general source of data and the specific element within that source, respectively) and +return a Promise for any telemetry data it knows of which satisfies those +requests, packaged in a specific way. This packaging is as an object containing +key-value pairs, where keys correspond to `source` properties of requests and +values are key-value pairs, where keys correspond to `key` properties of requests +and values are `TelemetrySeries` objects. (We will see our implementation +below.) + +To do this, we create a container for our telemetry source, and consult the +adapter to get telemetry histories for any relevant requests, then package +them as they come in. The `$q.all` method is used to return a single Promise +that will resolve only when all histories have been packaged. Promise-chaining +is used to ensure that the resolved value will be the fully-packaged data. + +It is worth mentioning here that the `requests` we receive should look a little +familiar. When Open MCT Web generates a `request` object associated with a +domain object, it does so by merging together three JavaScript objects: + +* First, the `telemetry` property from that domain object’s type definition. +* Second, the `telemetry` property from that domain object’s model. +* Finally, the `request` object that was passed in via that domain object’s +`telemetry` capability. + +As such, the `source` and `key` properties we observe here will come from the +type definition and domain object model, respectively, as we specified them +during Step 2. (Or, they might come from somewhere else entirely, if we have +other telemetry-providing domain objects in our system; that is something we +check for using the `source` property.) + +Finally, note that we also have a `subscribe` method, to satisfy the interface of +`telemetryService`, but this `subscribe` method currently does nothing. + +This script uses an `ExampleTelemetrySeries` class, which looks like: + + /*global define*/ + + define( + function () { + "use strict"; + + function ExampleTelemetrySeries(data) { + return { + getPointCount: function () { + return data.length; + }, + getDomainValue: function (index) { + return (data[index] || {}).timestamp; + }, + getRangeValue: function (index) { + return (data[index] || {}).value; + } + }; + } + + return ExampleTelemetrySeries; + } + ); +__tutorials/telemetry/src/ExampleTelemetrySeries.js__ + +This takes the array of telemetry values (as returned by the server) and wraps +it with the interface expected by the platform (the methods shown.) + +Finally, we expose this `telemetryService` provider declaratively: + + { + "name": "Example Telemetry Adapter", + "extensions": { + "types": [ + { + "name": "Spacecraft", + "key": "example.spacecraft", + "glyph": "o" + }, + { + "name": "Subsystem", + "key": "example.subsystem", + "glyph": "o", + "model": { "composition": [] } + }, + { + "name": "Measurement", + "key": "example.measurement", + "glyph": "T", + "model": { "telemetry": {} }, + "telemetry": { + "source": "example.source", + "domains": [ + { + "name": "Time", + "key": "timestamp" + } + ] + } + } + ], + "roots": [ + { + "id": "example:sc", + "priority": "preferred", + "model": { + "type": "example.spacecraft", + "name": "My Spacecraft", + "composition": [] + } + } + ], + "services": [ + { + "key": "example.adapter", + "implementation": "ExampleTelemetryServerAdapter.js", + "depends": [ "$q", "EXAMPLE_WS_URL" ] + } + ], + "constants": [ + { + "key": "EXAMPLE_WS_URL", + "priority": "fallback", + "value": "ws://localhost:8081" + } + ], + "runs": [ + { + "implementation": "ExampleTelemetryInitializer.js", + "depends": [ "example.adapter", "objectService" ] + } + ], + "components": [ + { + "provides": "modelService", + "type": "provider", + "implementation": "ExampleTelemetryModelProvider.js", + "depends": [ "example.adapter", "$q" ] + }, + + { + + "provides": "telemetryService", + + "type": "provider", + + "implementation": "ExampleTelemetryProvider.js", + + "depends": [ "example.adapter", "$q" ] + + } + ] + } + } +__tutorials/telemetry/bundle.json__ + +Now, if we navigate to one of our numeric measurements, we should see a plot of +its historical telemetry: + +![Telemetry](images/telemetry-3.png) + +We can now visualize our data, but it doesn’t update over time - we know the +server is continually producing new data, but we have to click away and come +back to see it. We can fix this by adding support for telemetry subscriptions. + +### Step 4-Real-time Telemetry + +Finally, we want to utilize the server’s ability to subscribe to telemetry +from Open MCT Web. To do this, first we want to expose some new methods for +this from our server adapter: + + /*global define,WebSocket*/ + + define( + [], + function () { + "use strict"; + + function ExampleTelemetryServerAdapter($q, wsUrl) { + var ws = new WebSocket(wsUrl), + histories = {}, + + listeners = [], + dictionary = $q.defer(); + + // Handle an incoming message from the server + ws.onmessage = function (event) { + var message = JSON.parse(event.data); + + switch (message.type) { + case "dictionary": + dictionary.resolve(message.value); + break; + case "history": + histories[message.id].resolve(message); + delete histories[message.id]; + break; + + case "data": + + listeners.forEach(function (listener) { + + listener(message); + + }); + + break; + } + }; + + // Request dictionary once connection is established + ws.onopen = function () { + ws.send("dictionary"); + }; + + return { + dictionary: function () { + return dictionary.promise; + }, + history: function (id) { + histories[id] = histories[id] || $q.defer(); + ws.send("history " + id); + return histories[id].promise; + }, + + subscribe: function (id) { + + ws.send("subscribe " + id); + + }, + + unsubscribe: function (id) { + + ws.send("unsubscribe " + id); + + }, + + listen: function (callback) { + + listeners.push(callback); + + } + }; + } + + return ExampleTelemetryServerAdapter; + } + ); +__tutorials/telemetry/src/ExampleTelemetryServerAdapter.js__ + +Here, we have added `subscribe` and `unsubscribe` methods which issue the +corresponding requests to the server. Seperately, we introduce the ability to +listen for `data` messages as they come in: These will contain the data associated +with these subscriptions. + +We then need only to utilize these methods from our `telemetryService`: + + /*global define*/ + + define( + ['./ExampleTelemetrySeries'], + function (ExampleTelemetrySeries) { + "use strict"; + + var SOURCE = "example.source"; + + function ExampleTelemetryProvider(adapter, $q) { + + var subscribers = {}; + + // Used to filter out requests for telemetry + // from some other source + function matchesSource(request) { + return (request.source === SOURCE); + } + + + // Listen for data, notify subscribers + + adapter.listen(function (message) { + + var packaged = {}; + + packaged[SOURCE] = {}; + + packaged[SOURCE][message.id] = + + new ExampleTelemetrySeries([message.value]); + + (subscribers[message.id] || []).forEach(function (cb) { + + cb(packaged); + + }); + + }); + + return { + requestTelemetry: function (requests) { + var packaged = {}, + relevantReqs = requests.filter(matchesSource); + + // Package historical telemetry that has been received + function addToPackage(history) { + packaged[SOURCE][history.id] = + new ExampleTelemetrySeries(history.value); + } + + // Retrieve telemetry for a specific measurement + function handleRequest(request) { + var key = request.key; + return adapter.history(key).then(addToPackage); + } + + packaged[SOURCE] = {}; + return $q.all(relevantReqs.map(handleRequest)) + .then(function () { return packaged; }); + }, + subscribe: function (callback, requests) { + + var keys = requests.filter(matchesSource) + + .map(function (req) { return req.key; }); + + + + function notCallback(cb) { + + return cb !== callback; + + } + + + + function unsubscribe(key) { + + subscribers[key] = + + (subscribers[key] || []).filter(notCallback); + + if (subscribers[key].length < 1) { + + adapter.unsubscribe(key); + + } + + } + + + + keys.forEach(function (key) { + + subscribers[key] = subscribers[key] || []; + + adapter.subscribe(key); + + subscribers[key].push(callback); + + }); + + + + return function () { + + keys.forEach(unsubscribe); + + }; + } + }; + } + + return ExampleTelemetryProvider; + } + ); +__tutorials/telemetry/src/ExampleTelemetryProvider.js__ + +A quick summary of these changes: + +* First, we maintain current subscribers (callbacks) in an object containing +key-value pairs, where keys are request key properties, and values are callback +arrays. +* We listen to new data coming in from the server adapter, and invoke any +relevant callbacks when this happens. We package the data in the same manner +that historical telemetry is packaged (even though in this case we are +providing single-element series objects.) +* Finally, in our `subscribe` method we add callbacks to the lists of active +subscribers. This method is expected to return a function which terminates the +subscription when called, so we do some work to remove subscribers in this +situations. When our subscriber count for a given measurement drops to zero, +we issue an unsubscribe request. (We don’t take any care to avoid issuing +multiple subscribe requests to the server, because we happen to know that the +server can handle this.) + +Running Open MCT Web again, we can still plot our historical telemetry - but +now we also see that it updates in real-time as more data comes in from the +server. diff --git a/example/generator/bundle.json b/example/generator/bundle.json index a13bbdc8f8..cdb4736957 100644 --- a/example/generator/bundle.json +++ b/example/generator/bundle.json @@ -34,6 +34,10 @@ { "key": "time", "name": "Time" + }, + { + "key": "yesterday", + "name": "Yesterday" } ], "ranges": [ @@ -61,4 +65,4 @@ } ] } -} \ No newline at end of file +} diff --git a/example/generator/src/SinewaveLimitCapability.js b/example/generator/src/SinewaveLimitCapability.js index 30d222b0c7..ac4f4718a2 100644 --- a/example/generator/src/SinewaveLimitCapability.js +++ b/example/generator/src/SinewaveLimitCapability.js @@ -30,25 +30,25 @@ define( YELLOW = 0.5, LIMITS = { rh: { - cssClass: "s-limit-upr-red", + cssClass: "s-limit-upr s-limit-red", low: RED, high: Number.POSITIVE_INFINITY, name: "Red High" }, rl: { - cssClass: "s-limit-lwr-red", + cssClass: "s-limit-lwr s-limit-red", high: -RED, low: Number.NEGATIVE_INFINITY, name: "Red Low" }, yh: { - cssClass: "s-limit-upr-yellow", + cssClass: "s-limit-upr s-limit-yellow", low: YELLOW, high: RED, name: "Yellow High" }, yl: { - cssClass: "s-limit-lwr-yellow", + cssClass: "s-limit-lwr s-limit-yellow", low: -RED, high: -YELLOW, name: "Yellow Low" diff --git a/example/generator/src/SinewaveTelemetryProvider.js b/example/generator/src/SinewaveTelemetryProvider.js index 014510f67c..c4062e659c 100644 --- a/example/generator/src/SinewaveTelemetryProvider.js +++ b/example/generator/src/SinewaveTelemetryProvider.js @@ -25,8 +25,8 @@ * Module defining SinewaveTelemetryProvider. Created by vwoeltje on 11/12/14. */ define( - ["./SinewaveTelemetry"], - function (SinewaveTelemetry) { + ["./SinewaveTelemetrySeries"], + function (SinewaveTelemetrySeries) { "use strict"; /** @@ -45,7 +45,7 @@ define( function generateData(request) { return { key: request.key, - telemetry: new SinewaveTelemetry(request) + telemetry: new SinewaveTelemetrySeries(request) }; } @@ -112,4 +112,4 @@ define( return SinewaveTelemetryProvider; } -); \ No newline at end of file +); diff --git a/example/generator/src/SinewaveTelemetry.js b/example/generator/src/SinewaveTelemetrySeries.js similarity index 58% rename from example/generator/src/SinewaveTelemetry.js rename to example/generator/src/SinewaveTelemetrySeries.js index 6c255bf56a..1e84034766 100644 --- a/example/generator/src/SinewaveTelemetry.js +++ b/example/generator/src/SinewaveTelemetrySeries.js @@ -29,35 +29,47 @@ define( function () { "use strict"; - var firstObservedTime = Date.now(); + var ONE_DAY = 60 * 60 * 24, + firstObservedTime = Math.floor(Date.now() / 1000) - ONE_DAY; /** * * @constructor */ - function SinewaveTelemetry(request) { - var latestObservedTime = Date.now(), - count = Math.floor((latestObservedTime - firstObservedTime) / 1000), - period = request.period || 30, - generatorData = {}; + function SinewaveTelemetrySeries(request) { + var timeOffset = (request.domain === 'yesterday') ? ONE_DAY : 0, + latestTime = Math.floor(Date.now() / 1000) - timeOffset, + firstTime = firstObservedTime - timeOffset, + endTime = (request.end !== undefined) ? + Math.floor(request.end / 1000) : latestTime, + count = Math.min(endTime, latestTime) - firstTime, + period = +request.period || 30, + generatorData = {}, + requestStart = (request.start === undefined) ? firstTime : + Math.max(Math.floor(request.start / 1000), firstTime), + offset = requestStart - firstTime; + + if (request.size !== undefined) { + offset = Math.max(offset, count - request.size); + } generatorData.getPointCount = function () { - return count; + return count - offset; }; generatorData.getDomainValue = function (i, domain) { - return i * 1000 + - (domain !== 'delta' ? firstObservedTime : 0); + return (i + offset) * 1000 + firstTime * 1000 - + (domain === 'yesterday' ? ONE_DAY : 0); }; generatorData.getRangeValue = function (i, range) { range = range || "sin"; - return Math[range](i * Math.PI * 2 / period); + return Math[range]((i + offset) * Math.PI * 2 / period); }; return generatorData; } - return SinewaveTelemetry; + return SinewaveTelemetrySeries; } -); \ No newline at end of file +); diff --git a/example/profiling/bundle.json b/example/profiling/bundle.json index b6090717d2..25c1b10749 100644 --- a/example/profiling/bundle.json +++ b/example/profiling/bundle.json @@ -4,7 +4,11 @@ { "implementation": "WatchIndicator.js", "depends": ["$interval", "$rootScope"] + }, + { + "implementation": "DigestIndicator.js", + "depends": ["$interval", "$rootScope"] } ] } -} \ No newline at end of file +} diff --git a/example/profiling/src/DigestIndicator.js b/example/profiling/src/DigestIndicator.js new file mode 100644 index 0000000000..02fbc7a08b --- /dev/null +++ b/example/profiling/src/DigestIndicator.js @@ -0,0 +1,77 @@ +/***************************************************************************** + * 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. + *****************************************************************************/ +/*global define*/ + +define( + [], + function () { + "use strict"; + + /** + * Displays the number of digests that have occurred since the + * indicator was first instantiated. + * @constructor + * @param $interval Angular's $interval + * @implements {Indicator} + */ + function DigestIndicator($interval, $rootScope) { + var digests = 0, + displayed = 0, + start = Date.now(); + + function update() { + var secs = (Date.now() - start) / 1000; + displayed = Math.round(digests / secs); + } + + function increment() { + digests += 1; + } + + $rootScope.$watch(increment); + + // Update state every second + $interval(update, 1000); + + // Provide initial state, too + update(); + + return { + getGlyph: function () { + return "."; + }, + getGlyphClass: function () { + return undefined; + }, + getText: function () { + return displayed + " digests/sec"; + }, + getDescription: function () { + return ""; + } + }; + } + + return DigestIndicator; + + } +); diff --git a/package.json b/package.json index a1ac01f437..c96642129c 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,8 @@ "split": "^1.0.0", "mkdirp": "^0.5.1", "nomnoml": "^0.0.3", - "canvas": "^1.2.7" + "canvas": "^1.2.7", + "markdown-toc": "^0.11.7" }, "scripts": { "start": "node app.js", diff --git a/platform/commonUI/browse/bundle.json b/platform/commonUI/browse/bundle.json index e3284d99b5..bc42a33c1f 100644 --- a/platform/commonUI/browse/bundle.json +++ b/platform/commonUI/browse/bundle.json @@ -1,4 +1,9 @@ { + "configuration": { + "paths": { + "uuid": "uuid" + } + }, "extensions": { "routes": [ { diff --git a/platform/commonUI/browse/res/templates/browse-object.html b/platform/commonUI/browse/res/templates/browse-object.html index ebe53b368f..3bd6138da6 100644 --- a/platform/commonUI/browse/res/templates/browse-object.html +++ b/platform/commonUI/browse/res/templates/browse-object.html @@ -42,9 +42,8 @@ -
- - -
+ + diff --git a/platform/commonUI/browse/res/templates/browse.html b/platform/commonUI/browse/res/templates/browse.html index e6255bcb55..9a1be7e771 100644 --- a/platform/commonUI/browse/res/templates/browse.html +++ b/platform/commonUI/browse/res/templates/browse.html @@ -28,7 +28,9 @@
- +
diff --git a/platform/commonUI/browse/res/templates/create/create-button.html b/platform/commonUI/browse/res/templates/create/create-button.html index 67a0294111..a7b4ad96e5 100644 --- a/platform/commonUI/browse/res/templates/create/create-button.html +++ b/platform/commonUI/browse/res/templates/create/create-button.html @@ -20,7 +20,7 @@ at runtime from the About dialog for additional information. -->