Compare commits

..

1 Commits

Author SHA1 Message Date
Henry
9553e0c64f Tutorial packages 2017-02-27 19:28:21 -08:00
15 changed files with 363 additions and 662 deletions

568
API.md
View File

@@ -1,150 +1,76 @@
# Building Applications With Open MCT
# Open MCT API
## Scope and purpose of this document
The Open MCT framework public api can be utilized by building the application
(`gulp install`) and then copying the file from `dist/main.js` to your
directory of choice.
This document is intended to serve as a reference for developing an application
based on Open MCT. It will provide details of the API functions necessary to extend the
Open MCT platform meet common use cases such as integrating with a telemetry source.
Open MCT supports AMD, CommonJS, and loading via a script tag; it's easy to use
in your project. The [`openmct`]{@link module:openmct} module is exported
via AMD and CommonJS, and is also exposed as `openmct` in the global scope
if loaded via a script tag.
The best place to start is with the [Open MCT Tutorials](https://github.com/nasa/openmct-tutorial).
These will walk you through the process of getting up and running with Open MCT,
as well as addressing some common developer use cases.
## Overview
## Building From Source
Open MCT's goal is to allow you to browse, create, edit, and visualize all of
the domain knowledge you need on a daily basis.
The latest version of Open MCT is available from [our GitHub repository](https://github.com/nasa/openmct).
If you have `git`, and `node` installed, you can build Open MCT with the commands
```
git clone https://github.com/nasa/openmct.git
cd openmct
npm install
```
These commands will fetch the Open MCT source from our GitHub repository, and build
a minified version that can be included in your application. The output of the
build process is placed in a `dist` folder under the openmct source directory,
which can be copied out to another location as needed. The contents of this
folder will include a minified javascript file named `openmct.js` as well as
assets such as html, css, and images necessary for the UI.
## Starting an Open MCT application
To start a minimally functional Open MCT application, it is necessary to include
the Open MCT distributable, enable some basic plugins, and bootstrap the application.
The tutorials walk through the process of getting Open MCT up and running from scratch,
but provided below is a minimal HTML template that includes Open MCT, installs
some basic plugins, and bootstraps the application. It assumes that Open MCT is
installed under an `openmct` subdirectory, as described in [Building From Source](#building-from-source).
This approach includes openmct using a simple script tag, resulting in a global
variable named `openmct`. This `openmct` object is used subsequently to make API
calls.
Open MCT is packaged as a UMD (Universal Module Definition) module, so common
script loaders are also supported.
```html
<!DOCTYPE html>
<html>
<head>
<title>Open MCT</title>
<script src="openmct.js"></script>
</head>
<body>
<script>
openmct.setAssetPath('openmct/dist');
openmct.install(openmct.plugins.LocalStorage());
openmct.install(openmct.plugins.MyItems());
openmct.install(openmct.plugins.UTCTimeSystem());
openmct.install(openmct.plugins.Espresso());
openmct.start();
</script>
</body>
</html>
```
The Open MCT library included above requires certain assets such as html templates,
images, and css. If you installed Open MCT from GitHub as described in the section
on [Building from Source](#building-from-source) then these assets will have been
downloaded along with the Open MCT javascript library. You can specify the
location of these assets by calling `openmct.setAssetPath()`. Typically this will
be the same location as the `openmct.js` library is included from.
There are some plugins bundled with the application that provide UI, persistence,
and other default configuration which are necessary to be able to do anything with
the application initially. Any of these plugins can, in principle, be replaced with a custom
plugin. The included plugins are documented in the [Included Plugins](#included-plugins)
section.
## Plugins
### Defining and Installing a New Plugin
```javascript
openmct.install(function install(openmctAPI) {
// Do things here
// ...
});
```
New plugins are installed in Open MCT by calling `openmct.install`, and providing
a plugin installation function. This function will be invoked on application
startup with one parameter - the openmct API object. A common approach used in
the Open MCT codebase is to define a plugin as a function that returns this
installation function. This allows configuration to be specified when the plugin is included.
eg.
```javascript
openmct.install(openmct.plugins.Elasticsearch("http://localhost:8002/openmct"));
```
This approach can be seen in all of the [plugins provided with Open MCT](https://github.com/nasa/openmct/blob/master/src/plugins/plugins.js).
## Domain Objects and Identifiers
_Domain Objects_ are the basic entities that represent domain knowledge in Open MCT.
The temperature sensor on a solar panel, an overlay plot comparing
the results of all temperature sensors, the command dictionary for a spacecraft,
the individual commands in that dictionary, the "My Items" folder:
To do this, the main building block provided by Open MCT is the _domain object_.
The temperature sensor on the starboard solar panel,
an overlay plot comparing the results of all temperature sensor,
the command dictionary for a spacecraft,
the individual commands in that dictionary, your "my documents" folder:
All of these things are domain objects.
A _Domain Object_ is simply a javascript object with some standard attributes.
An example of a _Domain Object_ is the "My Items" object which is a folder in
which a user can persist any objects that they create. The My Items object
looks like this:
Domain objects have Types, so a specific instrument temperature sensor is a
"Telemetry Point," and turning on a drill for a certain duration of time is
an "Activity". Types allow you to form an ontology of knowledge and provide
an abstraction for grouping, visualizing, and interpreting data.
```javascript
{
identifier: {
namespace: ""
key: "mine"
}
name:"My Items",
type:"folder",
location:"ROOT",
composition: []
}
And then we have Views. Views allow you to visualize domain objects. Views can
apply to specific domain objects; they may also apply to certain types of
domain objects, or they may apply to everything. Views are simply a method
of visualizing domain objects.
Regions allow you to specify what views are displayed for specific types of
domain objects in response to different user actions. For instance, you may
want to display a different view while editing, or you may want to update the
toolbar display when objects are selected. Regions allow you to map views to
specific user actions.
Domain objects can be mutated and persisted, developers can create custom
actions and apply them to domain objects, and many more things can be done.
For more information, read on!
## Running Open MCT
Once the [`openmct`](@link module:openmct) module has been loaded, you can
simply invoke [`start`]{@link module:openmct.MCT#start} to run Open MCT:
```
openmct.start();
```
### Object Attributes
Generally, however, you will want to configure Open MCT by adding plugins
before starting it. It is important to install plugins and configure Open MCT
_before_ calling [`start`]{@link module:openmct.MCT#start}; Open MCT is not
designed to be reconfigured once started.
The main attributes to note are the `identifier`, and `type` attributes.
* `identifier`: A composite key that provides a universally unique identifier for
this object. The `namespace` and `key` are used to identify the object. The `key`
must be unique within the namespace.
* `type`: All objects in Open MCT have a type. Types allow you to form an
ontology of knowledge and provide an abstraction for grouping, visualizing, and
interpreting data. Details on how to define a new object type are provided below.
## Configuring Open MCT
Open MCT uses a number of builtin types. Typically you are going to want to
define your own if extending Open MCT.
The [`openmct`]{@link module:openmct} module (more specifically, the
[`MCT`]{@link module:openmct.MCT} class, of which `openmct` is an instance)
exposes a variety of methods to allow the application to be configured,
extended, and customized before running.
### Domain Object Types
Short examples follow; see the linked documentation for further details.
Custom types may be registered via the `addType` function on the opencmt Type
registry.
### Adding Domain Object Types
eg.
```javascript
Custom types may be registered via
[`openmct.types`]{@link module:openmct.MCT#types}:
```
openmct.types.addType('my-type', {
label: "My Type",
description: "This is a type that I added!",
@@ -152,98 +78,66 @@ openmct.types.addType('my-type', {
});
```
The `addType` function accepts two arguments:
* A `string` key identifying the type. This key is used when specifying a type
for an object.
* An object type specification. An object type definition supports the following
attributes
* `label`: a `string` naming this object type
* `description`: a `string` specifying a longer-form description of this type
* `initialize`: a `function` which initializes the model for new domain objects
of this type. This can be used for setting default values on an object when
it is instantiated.
* `creatable`: A `boolean` indicating whether users should be allowed to create
this type (default: `false`). This will determine whether the type appears
in the `Create` menu.
* `cssClass`: A `string` specifying a CSS class to apply to each representation
of this object. This is used for specifying an icon to appear next to each
object of this type.
### Adding Views
The [Open MCT Tutorials](https://github.com/openmct/openmct-tutorial) provide a
step-by-step examples of writing code for Open MCT that includes a [section on
defining a new object type](https://github.com/nasa/openmct-tutorial#step-3---providing-objects).
Custom views may be registered based on the region in the application
where they should appear:
## Root Objects
* [`openmct.mainViews`]{@link module:openmct.MCT#mainViews} is a registry
of views of domain objects which should appear in the main viewing area.
* [`openmct.inspectors`]{@link module:openmct.MCT#inspectors} is a registry
of views of domain objects and/or active selections, which should appear in
the Inspector.
* [`openmct.toolbars`]{@link module:openmct.MCT#toolbars} is a registry
of views of domain objects and/or active selections, which should appear in
the toolbar area while editing.
* [`openmct.indicators`]{@link module:openmct.MCT#inspectors} is a registry
of views which should appear in the status area of the application.
In many cases, you'd like a certain object (or a certain hierarchy of objects)
to be accessible from the top level of the application (the tree on the left-hand
side of Open MCT.) For example, it is typical to expose a telemetry dictionary
as a hierarchy of telemetry-providing domain objects in this fashion.
Example:
To do so, use the `addRoot` method of the object API.
eg.
```javascript
openmct.objects.addRoot({
namespace: "my-namespace",
key: "my-key"
});
```
openmct.mainViews.addProvider({
canView: function (domainObject) {
return domainObject.type === 'my-type';
},
view: function (domainObject) {
return new MyView(domainObject);
}
});
```
The `addRoot` function takes a single [object identifier](#domain-objects-and-identifiers)
as an argument.
### Adding a Root-level Object
In many cases, you'd like a certain object (or a certain hierarchy of
objects) to be accessible from the top level of the application (the
tree on the left-hand side of Open MCT.) It is typical to expose a telemetry
dictionary as a hierarchy of telemetry-providing domain objects in this
fashion.
To do so, use the [`addRoot`]{@link module:openmct.ObjectAPI#addRoot} method
of the [object API]{@link module:openmct.ObjectAPI}:
```
openmct.objects.addRoot({ key: "my-key", namespace: "my-namespace" });
```
Root objects are loaded just like any other objects, i.e. via an object
provider.
## Object Providers
### Adding Composition Providers
An Object Provider is used to build _Domain Objects_, typically retrieved from
some source such as a persistence store or telemetry dictionary. In order to
integrate telemetry from a new source an object provider will need to be created
that can build objects representing telemetry points exposed by the telemetry
source. The API call to define a new object provider is fairly straightforward.
Here's a very simple example:
```javascript
openmct.objects.addProvider('example.namespace', {
get: function (identifier) {
return Promise.resolve({
identifier: identifier,
name: 'Example Object',
type: 'example-object-type'
});
}
});
```
The `addProvider` function takes two arguments:
* `namespace`: A `string` representing the namespace that this object provider
will provide objects for.
* `provider`: An `object` with a single function, `get`. This function accepts an
[Identifier](#domain-objects-and-identifiers) for the object to be provided.
It is expected that the `get` function will return a
[Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)
that resolves with the object being requested.
In future, object providers will support other methods to enable other operations
with persistence stores, such as creating, updating, and deleting objects.
## Composition Providers
The _composition_ of a domain object is the list of objects it contains, as shown
(for example) in the tree for browsing. Open MCT provides a
The "composition" of a domain object is the list of objects it contains,
as shown (for example) in the tree for browsing. Open MCT provides a
[default solution](#default-composition-provider) for composition, but there
may be cases where you want to provide the composition of a certain object
(or type of object) dynamically.
### Adding Composition Providers
For instance, you may want to populate a hierarchy under a custom root-level
object based on the contents of a telemetry dictionary.
To do this, you can add a new CompositionProvider:
You may want to populate a hierarchy under a custom root-level object based on
the contents of a telemetry dictionary. To do this, you can add a new
Composition Provider:
```javascript
```
openmct.composition.addProvider({
appliesTo: function (domainObject) {
return domainObject.type === 'my-type';
@@ -253,27 +147,20 @@ openmct.composition.addProvider({
}
});
```
The `addProvider` function accepts a Composition Provider object as its sole
argument. A Composition Provider is a javascript object exposing two functions:
* `appliesTo`: A `function` that accepts a `domainObject` argument, and returns
a `boolean` value indicating whether this composition provider applies to the
given object.
* `load`: A `function` that accepts a `domainObject` as an argument, and returns
a `Promise` that resolves with an array of [Identifier](#domain-objects-and-identifiers).
These identifiers will be used to fetch Domain Objects from an [Object Provider](#object-provider)
### Default Composition Provider
#### Default Composition Provider
The default composition provider applies to any domain object with a `composition`
property. The value of `composition` should be an array of identifiers, e.g.:
The default composition provider applies to any domain object with
a `composition` property. The value of `composition` should be an
array of identifiers, e.g.:
```javascript
```js
var domainObject = {
name: "My Object",
type: 'folder',
composition: [
{
id: '412229c3-922c-444b-8624-736d85516247',
key: '412229c3-922c-444b-8624-736d85516247',
namespace: 'foo'
},
{
@@ -284,146 +171,169 @@ var domainObject = {
};
```
## Telemetry Providers
### Adding Telemetry Providers
When connecting to a new telemetry source, you will need to register a new
_Telemetry Provider_. A _Telemetry Provider_ retrieves telemetry data from some telemetry
source, and exposes them in a way that can be used by Open MCT. A telemetry
provider typically can support a one off __request__ for a batch of telemetry data,
or it can provide the ability to __subscribe__ to receive new telemetry data when
it becomes available, or both.
When connecting to a new telemetry source, you will want to register a new
[telemetry provider]{@link module:openmct.TelemetryAPI~TelemetryProvider}
with the [telemetry API]{@link module:openmct.TelemetryAPI#addProvider}:
```javascript
openmct.telemetry.addProvider({
supportsRequest: function (domainObject) {
//...
},
supportsSubscribe: function (domainObject) {
//...
},
request: function (domainObject, options) {
//...
},
subscribe: function (domainObject, callback, options) {
//...
}
})
```
A telemetry provider is an object with the following functions defined:
* `supportsRequest`: An __optional__ `function` that accepts a
[Domain Object](#domain-objects-and-identifiers) and returns a `boolean` value
indicating whether or not this provider supports telemetry requests for the
given object. If this returns `true` then a `request` function must be defined.
* `supportsSubscribe`: An __optional__ `function` that accepts a
[Domain Object](#domain-objects-and-identifiers) and returns a `boolean` value
indicating whether or not this provider supports telemetry subscriptions. If this
returns `true` then a `subscribe` function must also be defined. As with `request`,
the return value will typically be conditional, and based on attributes of
`domainObject` such as its identifier.
* `request`: A `function` that returns a `Promise` that will resolve with an `Array`
of telemetry in a single query. This function accepts as arguments a
[Domain Object](#domain-objects-and-identifiers) and an object containing some
[request options](#telemetry-requests).
* `subscribe`: A `function` that accepts a [Domain Object](#domain-objects-and-identifiers),
a callback `function`, and a [telemetry request](#telemetry-requests). The
callback is invoked whenever telemetry is available, and
openmct.telemetry.addProvider({
canProvideTelemetry: function (domainObject) {
return domainObject.type === 'my-type';
},
properties: function (domainObject) {
return [
{ key: 'value', name: "Temperature", units: "degC" },
{ key: 'time', name: "UTC" }
];
},
request: function (domainObject, options) {
var telemetryId = domainObject.myTelemetryId;
return myAdapter.request(telemetryId, options.start, options.end);
},
subscribe: function (domainObject, callback) {
var telemetryId = domainObject.myTelemetryId;
myAdapter.subscribe(telemetryId, callback);
return myAdapter.unsubscribe.bind(myAdapter, telemetryId, callback);
}
});
```
The implementations for `request` and `subscribe` can vary depending on the
nature of the endpoint which will provide telemetry. In the example above,
it is assumed that `myAdapter` contains the implementation details
(such as HTTP requests, WebSocket connections, etc.) associated with some telemetry
it is assumed that `myAdapter` contains the specific implementations
(HTTP requests, WebSocket connections, etc.) associated with some telemetry
source.
For a step-by-step guide to building a telemetry adapter, please see the
[Open MCT Tutorials](https://github.com/larkin/openmct-tutorial).
## Using Open MCT
### Telemetry Requests
Telemetry requests support time bounded queries. A call to a _Telemetry Provider_'s
`request` function will include an `options` argument. These are simply javascript
objects with attributes for the request parameters. An example of a telemetry
request object with a start and end time is included below:
```javascript
{
start: 1487981997240,
end: 1487982897240
}
When implementing new features, it is useful and sometimes necessary to
utilize functionality exposed by Open MCT.
### Retrieving Composition
To limit which objects are loaded at any given time, the composition of
a domain object must be requested asynchronously:
```
openmct.composition(myObject).load().then(function (childObjects) {
childObjects.forEach(doSomething);
});
```
### Telemetry Data
### Support Common Gestures
Telemetry data is provided to Open MCT by _[Telemetry Providers](#telemetry-providers)_
in the form of javascript objects. A collection of telemetry values (for example,
retrieved in response to a `request`) is represented by an `Array` of javascript
objects. These telemetry javascript objects are simply key value pairs.
Custom views may also want to support common gestures using the
[gesture API]{@link module:openmct.GestureAPI}. For instance, to make
a view (or part of a view) selectable:
Typically a telemetry datum will have some timestamp associated with it. This
time stamp should have a key that corresponds to some time system supported by
Open MCT. If the `UTCTimeSystem` plugin is installed, then the key `utc` can be used.
An example of a telemetry provider request function that returns a collection of
mock telemtry data is below:
```javascript
openmct.telemetry.addProvider({
supportsRequest: function (domainObject) {
return true
},
request: function (domainObject, options) {
return Promise.resolve([
{
'utc': Date.now() - 2000,
'value': 1,
},
{
'utc': Date.now() - 1000,
'value': 2,
},
{
'utc': Date.now(),
'value': 3,
}
]);
}
})
```
openmct.gestures.selectable(myHtmlElement, myDomainObject);
```
## Included Plugins
### Working with Domain Objects
The [object API]{@link module:openmct.ObjectAPI} provides useful methods
for working with domain objects.
To make changes to a domain object, use the
[`mutate`]{@link module:openmct.ObjectAPI#mutate} method:
```
openmct.objects.mutate(myDomainObject, "name", "New name!");
```
Making modifications in this fashion allows other usages of the domain
object to remain up to date using the
[`observe`]{@link module:openmct.ObjectAPI#observe} method:
```
openmct.objects.observe(myDomainObject, "name", function (newName) {
myLabel.textContent = newName;
});
```
### Using Telemetry
Very often in Open MCT, you wish to work with telemetry data (for instance,
to display it in a custom visualization.)
### Synchronizing with the Time Conductor
Views which wish to remain synchronized with the state of Open MCT's
time conductor should utilize
[`openmct.conductor`]{@link module:openmct.TimeConductor}:
```
openmct.conductor.on('bounds', function (newBounds) {
requestTelemetry(newBounds.start, newBounds.end).then(displayTelemetry);
});
```
## Plugins
While you can register new features with Open MCT directly, it is generally
more useful to package these as a plugin. A plugin is a function that takes
[`openmct`]{@link module:openmct} as an argument, and performs configuration
upon `openmct` when invoked.
### Installing Plugins
To install plugins, use the [`install`]{@link module:openmct.MCT#install}
method:
```
openmct.install(myPlugin);
```
The plugin will be invoked to configure Open MCT before it is started.
### Included Plugins
Open MCT is packaged along with a few general-purpose plugins:
* `openmct.plugins.CouchDB` is an adapter for using CouchDB for persistence
of user-created objects. This is a constructor that takes the URL for the
CouchDB database as a parameter, e.g.
```javascript
openmct.install(openmct.plugins.CouchDB('http://localhost:5984/openmct'))
```
`openmct.install(new openmct.plugins.CouchDB('http://localhost:5984/openmct'))`
* `openmct.plugins.Elasticsearch` is an adapter for using Elasticsearch for
persistence of user-created objects. This is a
constructor that takes the URL for the Elasticsearch instance as a
parameter. eg.
```javascript
openmct.install(openmct.plugins.CouchDB('http://localhost:9200'))
```
* `openmct.plugins.Espresso` and `openmct.plugins.Snow` are two different
parameter, e.g.
`openmct.install(new openmct.plugins.CouchDB('http://localhost:9200'))`.
Domain objects will be indexed at `/mct/domain_object`.
* `openmct.plugins.espresso` and `openmct.plugins.snow` are two different
themes (dark and light) available for Open MCT. Note that at least one
of these themes must be installed for Open MCT to appear correctly.
* `openmct.plugins.LocalStorage` provides persistence of user-created
* `openmct.plugins.localStorage` provides persistence of user-created
objects in browser-local storage. This is particularly useful in
development environments.
* `openmct.plugins.MyItems` adds a top-level folder named "My Items"
* `openmct.plugins.myItems` adds a top-level folder named "My Items"
when the application is first started, providing a place for a
user to store created items.
* `openmct.plugins.UTCTimeSystem` provides a default time system for Open MCT.
* `openmct.plugins.utcTimeSystem` provides support for using the time
conductor with UTC time.
Generally, you will want to either install these plugins, or install
different plugins that provide persistence and an initial folder
hierarchy.
hierarchy. Installation is as described [above](#installing-plugins):
eg.
```javascript
openmct.install(openmct.plugins.LocalStorage());
openmct.install(openmct.plugins.MyItems());
```
openmct.install(openmct.plugins.localStorage);
openmct.install(openmct.plugins.myItems);
```
### Writing Plugins
Plugins configure Open MCT, and should utilize the
[`openmct`]{@link module:openmct} module to do so, as summarized above in
"Configuring Open MCT" and "Using Open MCT" above.
### Distributing Plugins
Hosting or downloading plugins is outside of the scope of this documentation.
We recommend distributing plugins as UMD modules which export a single
function.

View File

@@ -31,25 +31,10 @@ define(
var firstObservedTime = Date.now(),
images = [
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18731.jpg",
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18732.jpg",
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18733.jpg",
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18734.jpg",
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18735.jpg",
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18736.jpg",
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18737.jpg",
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18738.jpg",
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18739.jpg",
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18740.jpg",
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18741.jpg",
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18742.jpg",
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18743.jpg",
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18744.jpg",
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18745.jpg",
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18746.jpg",
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18747.jpg",
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18748.jpg"
"http://www.nasa.gov/393811main_Palomar_ao_bouchez_10s_after_impact_4x3_946-710.png",
"http://www.nasa.gov/393821main_Palomar_ao_bouchez_15s_after_impact_4x3_946-710.png",
"http://www.nasa.gov/images/content/393801main_CfhtVeillet2_4x3_516-387.jpg",
"http://www.nasa.gov/images/content/392790main_1024_768_GeminiNorth_NightBeforeImpact_946-710.jpg"
].map(function (url, index) {
return {
timestamp: firstObservedTime + 1000 * index,

View File

@@ -25,8 +25,16 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title></title>
<script src="openmct-tutorial/lib/http.js">
</script>
<script src="bower_components/requirejs/require.js">
</script>
<script src="openmct-tutorial/dictionary-plugin.js">
</script>
<script src="openmct-tutorial/realtime-telemetry-plugin.js">
</script>
<script src="openmct-tutorial/historical-telemetry-plugin.js">
</script>
<script>
require(['openmct'], function (openmct) {
[
@@ -40,6 +48,9 @@
openmct.install(openmct.plugins.Espresso());
openmct.install(openmct.plugins.Generator());
openmct.install(openmct.plugins.UTCTimeSystem());
openmct.install(DictionaryPlugin());
openmct.install(RealtimeTelemetryPlugin());
openmct.install(HistoricalTelemetryPlugin());
openmct.start();
});
</script>

1
openmct-tutorial Submodule

Submodule openmct-tutorial added at 7076a15d3a

View File

@@ -76,18 +76,17 @@
&:not(.first) {
border-top: 1px solid $colorFormLines;
}
}
.form-row {
@include align-items(center);
border: none !important;
margin-bottom: 0 !important;
padding: $interiorMarginSm 0;
.label {
min-width: 80px;
}
input[type='text'],
input[type='search'] {
width: 100%;
.form-row {
@include align-items(center);
border: none;
padding: $interiorMarginSm 0;
.label {
min-width: 80px;
}
input[type='text'],
input[type='search'] {
width: 100%;
}
}
}
}

View File

@@ -47,6 +47,7 @@ define([
"key": "url",
"name": "URL",
"control": "textfield",
"pattern": "^(ftp|https?)\\:\\/\\/\\w+(\\.\\w+)*(\\:\\d+)?(\\/\\S*)*$",
"required": true,
"cssClass": "l-input-lg"
}

View File

@@ -112,6 +112,22 @@ define(
this.lastBounds = bounds;
};
/**
* Determines is a given telemetry datum is within the bounds currently
* defined for this telemetry collection.
* @private
* @param datum
* @returns {boolean}
*/
TelemetryCollection.prototype.inBounds = function (datum) {
var noBoundsDefined = !this.lastBounds || (this.lastBounds.start === undefined && this.lastBounds.end === undefined);
var withinBounds =
_.get(datum, this.sortField) >= this.lastBounds.start &&
_.get(datum, this.sortField) <= this.lastBounds.end;
return noBoundsDefined || withinBounds;
};
/**
* Adds an individual item to the collection. Used internally only
* @private
@@ -157,10 +173,9 @@ define(
// based on time stamp because the array is guaranteed ordered due
// to sorted insertion.
var startIx = _.sortedIndex(array, item, this.sortField);
var endIx;
if (startIx !== array.length) {
endIx = _.sortedLastIndex(array, item, this.sortField);
var endIx = _.sortedLastIndex(array, item, this.sortField);
// Create an array of potential dupes, based on having the
// same time stamp
@@ -170,7 +185,7 @@ define(
}
if (!isDuplicate) {
array.splice(endIx || startIx, 0, item);
array.splice(startIx, 0, item);
//Return true if it was added and in bounds
return array === this.telemetry;

View File

@@ -425,38 +425,6 @@ define(
}
};
/**
* Finds the correct insertion point for a new row, which takes into
* account duplicates to make sure new rows are inserted in a way that
* maintains arrival order.
*
* @private
* @param {Array} searchArray
* @param {Object} searchElement Object to find the insertion point for
*/
MCTTableController.prototype.findInsertionPoint = function (searchArray, searchElement) {
//First, use a binary search to find the correct insertion point
var index = this.binarySearch(searchArray, searchElement, 0, searchArray.length - 1);
var testIndex = index;
//It's possible that the insertion point is a duplicate of the element to be inserted
var isDupe = function () {
return this.sortComparator(searchElement,
searchArray[testIndex][this.$scope.sortColumn].text) === 0;
}.bind(this);
// In the event of a duplicate, scan left or right (depending on
// sort order) to find an insertion point that maintains order received
while (testIndex >= 0 && testIndex < searchArray.length && isDupe()) {
if (this.$scope.sortDirection === 'asc') {
index = ++testIndex;
} else {
index = testIndex--;
}
}
return index;
};
/**
* @private
*/
@@ -471,9 +439,9 @@ define(
case -1:
return this.binarySearch(searchArray, searchElement, min,
sampleAt - 1);
case 0:
case 0 :
return sampleAt;
case 1:
case 1 :
return this.binarySearch(searchArray, searchElement,
sampleAt + 1, max);
}
@@ -490,7 +458,7 @@ define(
index = array.length;
} else {
//Sort is enabled, perform binary search to find insertion point
index = this.findInsertionPoint(array, element[this.$scope.sortColumn].text);
index = this.binarySearch(array, element[this.$scope.sortColumn].text, 0, array.length - 1);
}
if (index === -1) {
array.unshift(element);

View File

@@ -72,7 +72,7 @@ define(
* Create a new format object from legacy object, and replace it
* when it changes
*/
this.domainObject = objectUtils.toNewFormat($scope.domainObject.getModel(),
this.newObject = objectUtils.toNewFormat($scope.domainObject.getModel(),
$scope.domainObject.getId());
_.bindAll(this, [
@@ -144,9 +144,9 @@ define(
this.unobserveObject();
}
this.unobserveObject = this.openmct.objects.observe(this.domainObject, "*",
this.unobserveObject = this.openmct.objects.observe(this.newObject, "*",
function (domainObject) {
this.domainObject = domainObject;
this.newObject = domainObject;
this.getData();
}.bind(this)
);
@@ -364,8 +364,11 @@ define(
var telemetryApi = this.openmct.telemetry;
var telemetryCollection = this.telemetry;
//Set table max length to avoid unbounded growth.
//var maxRows = 100000;
var maxRows = Number.MAX_VALUE;
var limitEvaluator;
var added = false;
var scope = this.$scope;
var table = this.table;
this.subscriptions.forEach(function (subscription) {
@@ -376,6 +379,16 @@ define(
function newData(domainObject, datum) {
limitEvaluator = telemetryApi.limitEvaluator(domainObject);
added = telemetryCollection.add([table.getRowValues(limitEvaluator, datum)]);
//Inform table that a new row has been added
if (scope.rows.length > maxRows) {
scope.$broadcast('remove:rows', scope.rows[0]);
scope.rows.shift();
}
if (!scope.loading && added) {
scope.$broadcast('add:row',
scope.rows.length - 1);
}
}
objects.forEach(function (object) {
@@ -386,42 +399,6 @@ define(
return objects;
};
/**
* Return an array of telemetry objects in this view that should be
* subscribed to.
* @private
* @returns {Promise<Array>} a promise that resolves with an array of
* telemetry objects in this view.
*/
TelemetryTableController.prototype.getTelemetryObjects = function () {
var telemetryApi = this.openmct.telemetry;
var compositionApi = this.openmct.composition;
function filterForTelemetry(objects) {
return objects.filter(telemetryApi.canProvideTelemetry.bind(telemetryApi));
}
/*
* If parent object is a telemetry object, subscribe to it. Do not
* test composees.
*/
if (telemetryApi.canProvideTelemetry(this.domainObject)) {
return Promise.resolve([this.domainObject]);
} else {
/*
* If parent object is not a telemetry object, subscribe to all
* composees that are telemetry producing objects.
*/
var composition = compositionApi.get(this.domainObject);
if (composition) {
return composition
.load()
.then(filterForTelemetry);
}
}
};
/**
* Request historical data, and subscribe to for real-time data.
* @private
@@ -429,7 +406,10 @@ define(
* established, and historical telemetry is received and processed.
*/
TelemetryTableController.prototype.getData = function () {
var telemetryApi = this.openmct.telemetry;
var compositionApi = this.openmct.composition;
var scope = this.$scope;
var newObject = this.newObject;
this.telemetry.clear();
this.telemetry.bounds(this.openmct.conductor.bounds());
@@ -441,9 +421,28 @@ define(
console.error(e.stack);
}
function filterForTelemetry(objects) {
return objects.filter(telemetryApi.canProvideTelemetry.bind(telemetryApi));
}
function getDomainObjects() {
var objects = [newObject];
var composition = compositionApi.get(newObject);
if (composition) {
return composition
.load()
.then(function (children) {
return objects.concat(children);
});
} else {
return Promise.resolve(objects);
}
}
scope.rows = [];
return this.getTelemetryObjects()
return getDomainObjects()
.then(filterForTelemetry)
.then(this.loadColumns)
.then(this.subscribeToNewData)
.then(this.getHistoricalData)

View File

@@ -138,27 +138,6 @@ define(
};
collection.add([addedObjectB, addedObjectA]);
expect(collection.telemetry[11]).toBe(addedObjectB);
}
);
it("maintains insertion order in the case of duplicate time stamps",
function () {
var addedObjectA = {
timestamp: 10000,
value: {
integer: 10,
text: integerTextMap[10]
}
};
var addedObjectB = {
timestamp: 10000,
value: {
integer: 11,
text: integerTextMap[11]
}
};
collection.add([addedObjectA, addedObjectB]);
expect(collection.telemetry[11]).toBe(addedObjectB);
}
);

View File

@@ -459,14 +459,14 @@ define(
beforeEach(function () {
row4 = {
'col1': {'text': 'row4 col1'},
'col1': {'text': 'row5 col1'},
'col2': {'text': 'xyz'},
'col3': {'text': 'row4 col3'}
'col3': {'text': 'row5 col3'}
};
row5 = {
'col1': {'text': 'row5 col1'},
'col1': {'text': 'row6 col1'},
'col2': {'text': 'aaa'},
'col3': {'text': 'row5 col3'}
'col3': {'text': 'row6 col3'}
};
row6 = {
'col1': {'text': 'row6 col1'},
@@ -490,71 +490,6 @@ define(
expect(mockScope.displayRows[3].col2.text).toEqual('ggg');
});
it('Inserts duplicate values for sort column in order received when sorted descending', function () {
mockScope.sortColumn = 'col2';
mockScope.sortDirection = 'desc';
mockScope.displayRows = controller.sortRows(testRows.slice(0));
var row6b = {
'col1': {'text': 'row6b col1'},
'col2': {'text': 'ggg'},
'col3': {'text': 'row6b col3'}
};
var row6c = {
'col1': {'text': 'row6c col1'},
'col2': {'text': 'ggg'},
'col3': {'text': 'row6c col3'}
};
controller.addRows(undefined, [row4, row5]);
controller.addRows(undefined, [row6, row6b, row6c]);
expect(mockScope.displayRows[0].col2.text).toEqual('xyz');
expect(mockScope.displayRows[7].col2.text).toEqual('aaa');
// Added duplicate rows
expect(mockScope.displayRows[2].col2.text).toEqual('ggg');
expect(mockScope.displayRows[3].col2.text).toEqual('ggg');
expect(mockScope.displayRows[4].col2.text).toEqual('ggg');
// Check that original order is maintained with dupes
expect(mockScope.displayRows[2].col3.text).toEqual('row6c col3');
expect(mockScope.displayRows[3].col3.text).toEqual('row6b col3');
expect(mockScope.displayRows[4].col3.text).toEqual('row6 col3');
});
it('Inserts duplicate values for sort column in order received when sorted ascending', function () {
mockScope.sortColumn = 'col2';
mockScope.sortDirection = 'asc';
mockScope.displayRows = controller.sortRows(testRows.slice(0));
var row6b = {
'col1': {'text': 'row6b col1'},
'col2': {'text': 'ggg'},
'col3': {'text': 'row6b col3'}
};
var row6c = {
'col1': {'text': 'row6c col1'},
'col2': {'text': 'ggg'},
'col3': {'text': 'row6c col3'}
};
controller.addRows(undefined, [row4, row5, row6]);
controller.addRows(undefined, [row6b, row6c]);
expect(mockScope.displayRows[0].col2.text).toEqual('aaa');
expect(mockScope.displayRows[7].col2.text).toEqual('xyz');
// Added duplicate rows
expect(mockScope.displayRows[3].col2.text).toEqual('ggg');
expect(mockScope.displayRows[4].col2.text).toEqual('ggg');
expect(mockScope.displayRows[5].col2.text).toEqual('ggg');
// Check that original order is maintained with dupes
expect(mockScope.displayRows[3].col3.text).toEqual('row6 col3');
expect(mockScope.displayRows[4].col3.text).toEqual('row6b col3');
expect(mockScope.displayRows[5].col3.text).toEqual('row6c col3');
});
it('Adds new rows at the correct sort position when' +
' sorted and filtered', function () {
mockScope.sortColumn = 'col2';

View File

@@ -161,7 +161,7 @@ define(
});
});
describe ('when getting telemetry', function () {
describe ('Subscribes to new data', function () {
var mockComposition,
mockTelemetryObject,
mockChildren,
@@ -173,7 +173,9 @@ define(
"load"
]);
mockTelemetryObject = {};
mockTelemetryObject = jasmine.createSpyObj("mockTelemetryObject", [
"something"
]);
mockTelemetryObject.identifier = {
key: "mockTelemetryObject"
};
@@ -190,12 +192,22 @@ define(
});
done = false;
});
it('fetches historical data for the time period specified by the conductor bounds', function () {
controller.getData().then(function () {
done = true;
});
});
it('fetches historical data', function () {
waitsFor(function () {
return done;
}, "getData to return", 100);
runs(function () {
expect(mockTelemetryAPI.request).toHaveBeenCalledWith(mockTelemetryObject, jasmine.any(Object));
});
});
it('fetches historical data for the time period specified by the conductor bounds', function () {
waitsFor(function () {
return done;
}, "getData to return", 100);
@@ -205,11 +217,17 @@ define(
});
});
it('unsubscribes on view destruction', function () {
controller.getData().then(function () {
done = true;
it('subscribes to new data', function () {
waitsFor(function () {
return done;
}, "getData to return", 100);
runs(function () {
expect(mockTelemetryAPI.subscribe).toHaveBeenCalledWith(mockTelemetryObject, jasmine.any(Function), {});
});
});
it('and unsubscribes on view destruction', function () {
waitsFor(function () {
return done;
}, "getData to return", 100);
@@ -221,87 +239,6 @@ define(
expect(unsubscribe).toHaveBeenCalled();
});
});
it('fetches historical data for the time period specified by the conductor bounds', function () {
controller.getData().then(function () {
done = true;
});
waitsFor(function () {
return done;
}, "getData to return", 100);
runs(function () {
expect(mockTelemetryAPI.request).toHaveBeenCalledWith(mockTelemetryObject, mockBounds);
});
});
it('fetches data for, and subscribes to parent object if it is a telemetry object', function () {
controller.getData().then(function () {
done = true;
});
waitsFor(function () {
return done;
}, "getData to return", 100);
runs(function () {
expect(mockTelemetryAPI.subscribe).toHaveBeenCalledWith(mockTelemetryObject, jasmine.any(Function), {});
expect(mockTelemetryAPI.request).toHaveBeenCalledWith(mockTelemetryObject, jasmine.any(Object));
});
});
it('fetches data for, and subscribes to parent object if it is a telemetry object', function () {
controller.getData().then(function () {
done = true;
});
waitsFor(function () {
return done;
}, "getData to return", 100);
runs(function () {
expect(mockTelemetryAPI.subscribe).toHaveBeenCalledWith(mockTelemetryObject, jasmine.any(Function), {});
expect(mockTelemetryAPI.request).toHaveBeenCalledWith(mockTelemetryObject, jasmine.any(Object));
});
});
it('fetches data for, and subscribes to any composees that are telemetry objects if parent is not', function () {
mockChildren = [
{name: "child 1"}
];
var mockTelemetryChildren = [
{name: "child 2"},
{name: "child 3"},
{name: "child 4"}
];
mockChildren = mockChildren.concat(mockTelemetryChildren);
mockComposition.load.andReturn(Promise.resolve(mockChildren));
mockTelemetryAPI.canProvideTelemetry.andCallFake(function (object) {
if (object === mockTelemetryObject) {
return false;
} else {
return mockTelemetryChildren.indexOf(object) !== -1;
}
});
controller.getData().then(function () {
done = true;
});
waitsFor(function () {
return done;
}, "getData to return", 100);
runs(function () {
mockTelemetryChildren.forEach(function (child) {
expect(mockTelemetryAPI.subscribe).toHaveBeenCalledWith(child, jasmine.any(Function), {});
});
mockTelemetryChildren.forEach(function (child) {
expect(mockTelemetryAPI.request).toHaveBeenCalledWith(child, jasmine.any(Object));
});
expect(mockTelemetryAPI.subscribe).not.toHaveBeenCalledWith(mockChildren[0], jasmine.any(Function), {});
expect(mockTelemetryAPI.subscribe).not.toHaveBeenCalledWith(mockTelemetryObject[0], jasmine.any(Function), {});
});
});
});
it('When in real-time mode, enables auto-scroll', function () {

View File

@@ -140,8 +140,7 @@ define(
typeRequest = (type && type.getDefinition().telemetry) || {},
modelTelemetry = domainObject.getModel().telemetry,
fullRequest = Object.create(typeRequest),
bounds,
timeSystem;
bounds;
// Add properties from the telemetry field of this
// specific domain object.
@@ -168,13 +167,6 @@ define(
fullRequest.end = bounds.end;
}
if (request.domain === undefined) {
timeSystem = this.openmct.conductor.timeSystem();
if (timeSystem !== undefined) {
fullRequest.domain = timeSystem.metadata.key;
}
}
return fullRequest;
};

View File

@@ -104,13 +104,6 @@ define(
start: 0,
end: 1
};
},
timeSystem: function () {
return {
metadata: {
key: 'mockTimeSystem'
}
};
}
}
};
@@ -148,8 +141,7 @@ define(
id: "testId", // from domain object
source: "testSource", // from model
key: "testKey", // from model
start: 42, // from argument
domain: 'mockTimeSystem'
start: 42 // from argument
}]);
});
@@ -168,8 +160,7 @@ define(
source: "testSource",
key: "testKey",
start: 0,
end: 1,
domain: 'mockTimeSystem'
end: 1
});
});
@@ -185,8 +176,7 @@ define(
source: "testSource", // from model
key: "testId", // from domain object
start: 0,
end: 1,
domain: 'mockTimeSystem'
end: 1
});
});
@@ -267,8 +257,7 @@ define(
source: "testSource",
key: "testKey",
start: 0,
end: 1,
domain: 'mockTimeSystem'
end: 1
}]
);
@@ -285,28 +274,8 @@ define(
expect(mockUnsubscribe).not.toHaveBeenCalled();
subscription(); // should be an unsubscribe function
expect(mockUnsubscribe).toHaveBeenCalled();
});
it("applies time conductor bounds if request bounds not defined", function () {
var fullRequest = telemetry.buildRequest({});
var mockBounds = mockAPI.conductor.bounds();
expect(fullRequest.start).toBe(mockBounds.start);
expect(fullRequest.end).toBe(mockBounds.end);
fullRequest = telemetry.buildRequest({start: 10, end: 20});
expect(fullRequest.start).toBe(10);
expect(fullRequest.end).toBe(20);
});
it("applies domain from time system if none defined", function () {
var fullRequest = telemetry.buildRequest({});
var mockTimeSystem = mockAPI.conductor.timeSystem();
expect(fullRequest.domain).toBe(mockTimeSystem.metadata.key);
fullRequest = telemetry.buildRequest({domain: 'someOtherDomain'});
expect(fullRequest.domain).toBe('someOtherDomain');
});
});
}

View File

@@ -44,7 +44,7 @@ define(['./Type'], function (Type) {
}
/**
* Register a new object type.
* Register a new type of view.
*
* @param {string} typeKey a string identifier for this type
* @param {module:openmct.Type} type the type to add