mirror of
https://github.com/nasa/openmct.git
synced 2025-06-28 11:51:10 +00:00
Compare commits
47 Commits
new-tutori
...
time-api
Author | SHA1 | Date | |
---|---|---|---|
be476aff43 | |||
d62a3ca494 | |||
d77ea6c024 | |||
fae7cd69da | |||
80432710e0 | |||
7dd5da8993 | |||
95ac304afb | |||
80dc5a13b8 | |||
b5abe6527b | |||
616aab4a2d | |||
12e693941c | |||
59c61e72b8 | |||
3ce954c68c | |||
df0d4dff6f | |||
a867cd6be0 | |||
b48dd4b63b | |||
35c457b7fb | |||
6870055033 | |||
5f968b50f8 | |||
251438eefd | |||
c07a372c6e | |||
f666a7ca09 | |||
c27b37918a | |||
67eab82bd1 | |||
8d86ca05da | |||
4d6a0d4931 | |||
4ae35576a5 | |||
af622599a5 | |||
12d1302138 | |||
09419398e9 | |||
b69e03368a | |||
4e457f1cf0 | |||
4196da9f52 | |||
a59177447b | |||
529abcc4b0 | |||
1cb5dd021f | |||
399b745084 | |||
f09a76e23b | |||
16b6a70e22 | |||
1a7260cc14 | |||
8a75381a3d | |||
c394fe9287 | |||
06f4a955b5 | |||
a2bf92db97 | |||
784114e256 | |||
d1e7e7894e | |||
65bf38d5e6 |
558
API.md
558
API.md
@ -1,76 +1,150 @@
|
||||
# Open MCT API
|
||||
# Building Applications With Open MCT
|
||||
|
||||
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.
|
||||
## Scope and purpose of this document
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
## Overview
|
||||
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.
|
||||
|
||||
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.
|
||||
## Building From Source
|
||||
|
||||
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:
|
||||
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:
|
||||
All of these things are domain objects.
|
||||
|
||||
Domain objects have Types, so a specific instrument temperature sensor is a
|
||||
"Telemetry Point," and turning on a drill for a certain duration of time is
|
||||
an "Activity". Types allow you to form an ontology of knowledge and provide
|
||||
an abstraction for grouping, visualizing, and interpreting data.
|
||||
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:
|
||||
|
||||
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();
|
||||
```javascript
|
||||
{
|
||||
identifier: {
|
||||
namespace: ""
|
||||
key: "mine"
|
||||
}
|
||||
name:"My Items",
|
||||
type:"folder",
|
||||
location:"ROOT",
|
||||
composition: []
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
### Object Attributes
|
||||
|
||||
## Configuring Open MCT
|
||||
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.
|
||||
|
||||
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.
|
||||
Open MCT uses a number of builtin types. Typically you are going to want to
|
||||
define your own if extending Open MCT.
|
||||
|
||||
Short examples follow; see the linked documentation for further details.
|
||||
### Domain Object Types
|
||||
|
||||
### Adding Domain Object Types
|
||||
Custom types may be registered via the `addType` function on the opencmt Type
|
||||
registry.
|
||||
|
||||
Custom types may be registered via
|
||||
[`openmct.types`]{@link module:openmct.MCT#types}:
|
||||
|
||||
```
|
||||
eg.
|
||||
```javascript
|
||||
openmct.types.addType('my-type', {
|
||||
label: "My Type",
|
||||
description: "This is a type that I added!",
|
||||
@ -78,66 +152,98 @@ openmct.types.addType('my-type', {
|
||||
});
|
||||
```
|
||||
|
||||
### Adding Views
|
||||
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.
|
||||
|
||||
Custom views may be registered based on the region in the application
|
||||
where they should appear:
|
||||
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).
|
||||
|
||||
* [`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.
|
||||
## Root Objects
|
||||
|
||||
Example:
|
||||
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.
|
||||
|
||||
```
|
||||
openmct.mainViews.addProvider({
|
||||
canView: function (domainObject) {
|
||||
return domainObject.type === 'my-type';
|
||||
},
|
||||
view: function (domainObject) {
|
||||
return new MyView(domainObject);
|
||||
}
|
||||
});
|
||||
To do so, use the `addRoot` method of the object API.
|
||||
|
||||
eg.
|
||||
```javascript
|
||||
openmct.objects.addRoot({
|
||||
namespace: "my-namespace",
|
||||
key: "my-key"
|
||||
});
|
||||
```
|
||||
|
||||
### 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" });
|
||||
```
|
||||
The `addRoot` function takes a single [object identifier](#domain-objects-and-identifiers)
|
||||
as an argument.
|
||||
|
||||
Root objects are loaded just like any other objects, i.e. via an object
|
||||
provider.
|
||||
|
||||
### Adding Composition Providers
|
||||
## Object 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
|
||||
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
|
||||
[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.
|
||||
|
||||
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:
|
||||
### Adding Composition Providers
|
||||
|
||||
```
|
||||
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';
|
||||
@ -147,20 +253,27 @@ 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.:
|
||||
|
||||
```js
|
||||
```javascript
|
||||
var domainObject = {
|
||||
name: "My Object",
|
||||
type: 'folder',
|
||||
composition: [
|
||||
{
|
||||
key: '412229c3-922c-444b-8624-736d85516247',
|
||||
id: '412229c3-922c-444b-8624-736d85516247',
|
||||
namespace: 'foo'
|
||||
},
|
||||
{
|
||||
@ -171,169 +284,146 @@ var domainObject = {
|
||||
};
|
||||
```
|
||||
|
||||
### Adding Telemetry Providers
|
||||
## Telemetry Providers
|
||||
|
||||
When connecting to a new telemetry source, you will want to register a new
|
||||
[telemetry provider]{@link module:openmct.TelemetryAPI~TelemetryProvider}
|
||||
with the [telemetry API]{@link module:openmct.TelemetryAPI#addProvider}:
|
||||
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.
|
||||
|
||||
```
|
||||
```javascript
|
||||
openmct.telemetry.addProvider({
|
||||
canProvideTelemetry: function (domainObject) {
|
||||
return domainObject.type === 'my-type';
|
||||
supportsRequest: function (domainObject) {
|
||||
//...
|
||||
},
|
||||
properties: function (domainObject) {
|
||||
return [
|
||||
{ key: 'value', name: "Temperature", units: "degC" },
|
||||
{ key: 'time', name: "UTC" }
|
||||
];
|
||||
supportsSubscribe: function (domainObject) {
|
||||
//...
|
||||
},
|
||||
request: function (domainObject, options) {
|
||||
var telemetryId = domainObject.myTelemetryId;
|
||||
return myAdapter.request(telemetryId, options.start, options.end);
|
||||
request: function (domainObject, options) {
|
||||
//...
|
||||
},
|
||||
subscribe: function (domainObject, callback) {
|
||||
var telemetryId = domainObject.myTelemetryId;
|
||||
myAdapter.subscribe(telemetryId, callback);
|
||||
return myAdapter.unsubscribe.bind(myAdapter, telemetryId, callback);
|
||||
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
|
||||
|
||||
|
||||
The implementations for `request` and `subscribe` can vary depending on the
|
||||
nature of the endpoint which will provide telemetry. In the example above,
|
||||
it is assumed that `myAdapter` contains the specific implementations
|
||||
(HTTP requests, WebSocket connections, etc.) associated with some telemetry
|
||||
it is assumed that `myAdapter` contains the implementation details
|
||||
(such as HTTP requests, WebSocket connections, etc.) associated with some telemetry
|
||||
source.
|
||||
|
||||
## Using Open MCT
|
||||
For a step-by-step guide to building a telemetry adapter, please see the
|
||||
[Open MCT Tutorials](https://github.com/larkin/openmct-tutorial).
|
||||
|
||||
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 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
|
||||
}
|
||||
```
|
||||
|
||||
### Support Common Gestures
|
||||
### Telemetry Data
|
||||
|
||||
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:
|
||||
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.
|
||||
|
||||
```
|
||||
openmct.gestures.selectable(myHtmlElement, myDomainObject);
|
||||
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,
|
||||
}
|
||||
]);
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### 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
|
||||
## 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.
|
||||
`openmct.install(new openmct.plugins.CouchDB('http://localhost:5984/openmct'))`
|
||||
```javascript
|
||||
openmct.install(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, 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
|
||||
parameter. eg.
|
||||
```javascript
|
||||
openmct.install(openmct.plugins.CouchDB('http://localhost:9200'))
|
||||
```
|
||||
* `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 support for using the time
|
||||
conductor with UTC time.
|
||||
* `openmct.plugins.UTCTimeSystem` provides a default time system for Open MCT.
|
||||
|
||||
Generally, you will want to either install these plugins, or install
|
||||
different plugins that provide persistence and an initial folder
|
||||
hierarchy. Installation is as described [above](#installing-plugins):
|
||||
hierarchy.
|
||||
|
||||
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.
|
||||
|
||||
|
@ -2261,7 +2261,7 @@ The platform understands the following policy categories (specifiable as the
|
||||
|
||||
* `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 (first argument, `parentType`) can contain a given object (second argument, `child`).
|
||||
* `composition`: Determines whether or not a given domain object(first argument, `parent`) can contain a candidate child object (second argument, `child`).
|
||||
* `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.
|
||||
|
@ -31,10 +31,25 @@ define(
|
||||
|
||||
var firstObservedTime = Date.now(),
|
||||
images = [
|
||||
"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"
|
||||
"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"
|
||||
|
||||
].map(function (url, index) {
|
||||
return {
|
||||
timestamp: firstObservedTime + 1000 * index,
|
||||
|
@ -20,7 +20,7 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define(['../../../platform/features/conductor/core/src/timeSystems/LocalClock'], function (LocalClock) {
|
||||
define(['../../../src/plugins/utcTimeSystem/LocalClock'], function (LocalClock) {
|
||||
/**
|
||||
* @implements TickSource
|
||||
* @constructor
|
||||
@ -28,14 +28,12 @@ define(['../../../platform/features/conductor/core/src/timeSystems/LocalClock'],
|
||||
function LADTickSource ($timeout, period) {
|
||||
LocalClock.call(this, $timeout, period);
|
||||
|
||||
this.metadata = {
|
||||
key: 'test-lad',
|
||||
mode: 'lad',
|
||||
cssClass: 'icon-clock',
|
||||
label: 'Latest Available Data',
|
||||
name: 'Latest available data',
|
||||
description: 'Monitor real-time streaming data as it comes in. The Time Conductor and displays will automatically advance themselves based on a UTC clock.'
|
||||
};
|
||||
this.key = 'test-lad';
|
||||
this.mode = 'lad';
|
||||
this.cssClass = 'icon-clock';
|
||||
this.label = 'Latest Available Data';
|
||||
this.name = 'Latest available data';
|
||||
this.description = 'Monitor real-time streaming data as it comes in. The Time Conductor and displays will automatically advance themselves based on a UTC clock.';
|
||||
}
|
||||
LADTickSource.prototype = Object.create(LocalClock.prototype);
|
||||
|
||||
|
@ -21,10 +21,9 @@
|
||||
*****************************************************************************/
|
||||
|
||||
define([
|
||||
'../../../platform/features/conductor/core/src/timeSystems/TimeSystem',
|
||||
'../../../platform/features/conductor/core/src/timeSystems/LocalClock',
|
||||
'../../../src/plugins/utcTimeSystem/LocalClock',
|
||||
'./LADTickSource'
|
||||
], function (TimeSystem, LocalClock, LADTickSource) {
|
||||
], function (LocalClock, LADTickSource) {
|
||||
var THIRTY_MINUTES = 30 * 60 * 1000,
|
||||
DEFAULT_PERIOD = 1000;
|
||||
|
||||
@ -34,25 +33,20 @@ define([
|
||||
* @constructor
|
||||
*/
|
||||
function LocalTimeSystem ($timeout) {
|
||||
TimeSystem.call(this);
|
||||
|
||||
/**
|
||||
* Some metadata, which will be used to identify the time system in
|
||||
* the UI
|
||||
* @type {{key: string, name: string, glyph: string}}
|
||||
*/
|
||||
this.metadata = {
|
||||
'key': 'local',
|
||||
'name': 'Local',
|
||||
'glyph': '\u0043'
|
||||
};
|
||||
this.key = 'local';
|
||||
this.name = 'Local';
|
||||
this.cssClass = '\u0043';
|
||||
|
||||
this.fmts = ['local-format'];
|
||||
this.sources = [new LocalClock($timeout, DEFAULT_PERIOD), new LADTickSource($timeout, DEFAULT_PERIOD)];
|
||||
}
|
||||
|
||||
LocalTimeSystem.prototype = Object.create(TimeSystem.prototype);
|
||||
|
||||
LocalTimeSystem.prototype.formats = function () {
|
||||
return this.fmts;
|
||||
};
|
||||
@ -65,6 +59,10 @@ define([
|
||||
return this.sources;
|
||||
};
|
||||
|
||||
LocalTimeSystem.prototype.isUTCBased = function () {
|
||||
return true;
|
||||
};
|
||||
|
||||
LocalTimeSystem.prototype.defaults = function (key) {
|
||||
var now = Math.ceil(Date.now() / 1000) * 1000;
|
||||
return {
|
||||
|
@ -92,7 +92,7 @@ define([
|
||||
{
|
||||
"key":"rems.adapter",
|
||||
"implementation": RemsTelemetryServerAdapter,
|
||||
"depends": ["$q", "$http", "$log", "REMS_WS_URL"]
|
||||
"depends": ["$http", "$log", "REMS_WS_URL"]
|
||||
}
|
||||
],
|
||||
"components": [
|
||||
|
@ -42,14 +42,12 @@ define(
|
||||
* @param REMS_WS_URL The location of the REMS telemetry data.
|
||||
* @constructor
|
||||
*/
|
||||
function RemsTelemetryServerAdapter($q, $http, $log, REMS_WS_URL) {
|
||||
function RemsTelemetryServerAdapter($http, $log, REMS_WS_URL) {
|
||||
this.localDataURI = module.uri.substring(0, module.uri.lastIndexOf('/') + 1) + LOCAL_DATA;
|
||||
this.deferreds = {};
|
||||
this.REMS_WS_URL = REMS_WS_URL;
|
||||
this.$q = $q;
|
||||
this.$http = $http;
|
||||
this.$log = $log;
|
||||
this.cache = undefined;
|
||||
this.promise = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -65,15 +63,10 @@ define(
|
||||
*/
|
||||
RemsTelemetryServerAdapter.prototype.requestHistory = function(request) {
|
||||
var self = this,
|
||||
id = request.key,
|
||||
deferred = this.$q.defer();
|
||||
id = request.key;
|
||||
|
||||
function processResponse(response){
|
||||
var data = [];
|
||||
/*
|
||||
* Currently all data is returned for entire history of the mission. Cache response to avoid unnecessary re-queries.
|
||||
*/
|
||||
self.cache = response;
|
||||
/*
|
||||
* History data is organised by Sol. Iterate over sols...
|
||||
*/
|
||||
@ -110,17 +103,15 @@ define(
|
||||
}
|
||||
|
||||
function packageAndResolve(results){
|
||||
deferred.resolve({id: id, values: results});
|
||||
return {id: id, values: results};
|
||||
}
|
||||
|
||||
|
||||
this.$q.when(this.cache || this.$http.get(this.REMS_WS_URL))
|
||||
return (this.promise = this.promise || this.$http.get(this.REMS_WS_URL))
|
||||
.catch(fallbackToLocal)
|
||||
.then(processResponse)
|
||||
.then(filterResults)
|
||||
.then(packageAndResolve);
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -132,7 +123,6 @@ define(
|
||||
* @returns {Promise | Array<RemsTelemetryValue>} that resolves with an Array of {@link RemsTelemetryValue} objects for the request data key.
|
||||
*/
|
||||
RemsTelemetryServerAdapter.prototype.history = function(request) {
|
||||
var id = request.key;
|
||||
return this.requestHistory(request);
|
||||
};
|
||||
|
||||
|
@ -53,7 +53,7 @@ module.exports = function(config) {
|
||||
// Preprocess matching files before serving them to the browser.
|
||||
// https://npmjs.org/browse/keyword/karma-preprocessor
|
||||
preprocessors: {
|
||||
'src/**/src/**/!(*Spec).js': [ 'coverage' ],
|
||||
'src/**/!(*Spec).js': [ 'coverage' ],
|
||||
'platform/**/src/**/!(*Spec).js': [ 'coverage' ]
|
||||
},
|
||||
|
||||
|
@ -84,11 +84,5 @@ define([
|
||||
return new Main().run(defaultRegistry);
|
||||
});
|
||||
|
||||
// For now, install conductor by default
|
||||
openmct.install(openmct.plugins.Conductor({
|
||||
showConductor: false
|
||||
}));
|
||||
|
||||
|
||||
return openmct;
|
||||
});
|
||||
|
@ -34,6 +34,7 @@ define([
|
||||
"./src/actions/SaveAsAction",
|
||||
"./src/actions/CancelAction",
|
||||
"./src/policies/EditActionPolicy",
|
||||
"./src/policies/EditPersistableObjectsPolicy",
|
||||
"./src/policies/EditableLinkPolicy",
|
||||
"./src/policies/EditableMovePolicy",
|
||||
"./src/policies/EditContextualActionPolicy",
|
||||
@ -72,6 +73,7 @@ define([
|
||||
SaveAsAction,
|
||||
CancelAction,
|
||||
EditActionPolicy,
|
||||
EditPersistableObjectsPolicy,
|
||||
EditableLinkPolicy,
|
||||
EditableMovePolicy,
|
||||
EditContextualActionPolicy,
|
||||
@ -247,6 +249,11 @@ define([
|
||||
"category": "action",
|
||||
"implementation": EditActionPolicy
|
||||
},
|
||||
{
|
||||
"category": "action",
|
||||
"implementation": EditPersistableObjectsPolicy,
|
||||
"depends": ["openmct"]
|
||||
},
|
||||
{
|
||||
"category": "action",
|
||||
"implementation": EditContextualActionPolicy,
|
||||
|
@ -60,11 +60,9 @@ define(
|
||||
policyService = this.policyService;
|
||||
|
||||
function validateLocation(parent) {
|
||||
var parentType = parent &&
|
||||
parent.getCapability('type');
|
||||
return parentType && policyService.allow(
|
||||
return parent && policyService.allow(
|
||||
"composition",
|
||||
parentType,
|
||||
parent,
|
||||
domainObject
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,59 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2016, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define(
|
||||
['../../../../../src/api/objects/object-utils'],
|
||||
function (objectUtils) {
|
||||
|
||||
/**
|
||||
* Policy that prevents editing of any object from a provider that does not
|
||||
* support persistence (ie. the 'save' operation). Editing is prevented
|
||||
* as a subsequent save would fail, causing the loss of a user's changes.
|
||||
* @param openmct
|
||||
* @constructor
|
||||
*/
|
||||
function EditPersistableObjectsPolicy(openmct) {
|
||||
this.openmct = openmct;
|
||||
}
|
||||
|
||||
EditPersistableObjectsPolicy.prototype.allow = function (action, context) {
|
||||
var identifier;
|
||||
var provider;
|
||||
var domainObject = context.domainObject;
|
||||
var key = action.getMetadata().key;
|
||||
var category = (context || {}).category;
|
||||
|
||||
// Use category to selectively block edit from the view. Edit action
|
||||
// is also invoked during the create process which should be allowed,
|
||||
// because it may be saved elsewhere
|
||||
if ((key === 'edit' && category === 'view-control') || key === 'properties') {
|
||||
identifier = objectUtils.parseKeyString(domainObject.getId());
|
||||
provider = this.openmct.objects.getProvider(identifier);
|
||||
return provider.save !== undefined;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
return EditPersistableObjectsPolicy;
|
||||
}
|
||||
);
|
@ -161,6 +161,7 @@ define(
|
||||
'otherType',
|
||||
['getKey']
|
||||
),
|
||||
|
||||
//Create a form structure with location
|
||||
structure = wizard.getFormStructure(true),
|
||||
sections = structure.sections,
|
||||
@ -174,7 +175,7 @@ define(
|
||||
// can actually contain objects of this type
|
||||
expect(mockPolicyService.allow).toHaveBeenCalledWith(
|
||||
'composition',
|
||||
mockOtherType,
|
||||
mockDomainObj,
|
||||
mockDomainObject
|
||||
);
|
||||
});
|
||||
|
@ -0,0 +1,104 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2016, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define(
|
||||
["../../src/policies/EditPersistableObjectsPolicy"],
|
||||
function (EditPersistableObjectsPolicy) {
|
||||
|
||||
describe("The Edit persistable objects policy", function () {
|
||||
var mockDomainObject,
|
||||
mockEditAction,
|
||||
mockPropertiesAction,
|
||||
mockOtherAction,
|
||||
mockAPI,
|
||||
mockObjectAPI,
|
||||
testContext,
|
||||
policy;
|
||||
|
||||
beforeEach(function () {
|
||||
mockDomainObject = jasmine.createSpyObj(
|
||||
'domainObject',
|
||||
[
|
||||
'getId'
|
||||
]
|
||||
);
|
||||
|
||||
mockObjectAPI = jasmine.createSpyObj('objectAPI', [
|
||||
'getProvider'
|
||||
]);
|
||||
|
||||
mockAPI = {
|
||||
objects: mockObjectAPI
|
||||
};
|
||||
|
||||
mockEditAction = jasmine.createSpyObj('edit', ['getMetadata']);
|
||||
mockPropertiesAction = jasmine.createSpyObj('properties', ['getMetadata']);
|
||||
mockOtherAction = jasmine.createSpyObj('other', ['getMetadata']);
|
||||
|
||||
mockEditAction.getMetadata.andReturn({ key: 'edit' });
|
||||
mockPropertiesAction.getMetadata.andReturn({ key: 'properties' });
|
||||
mockOtherAction.getMetadata.andReturn({key: 'other'});
|
||||
|
||||
mockDomainObject.getId.andReturn('test:testId');
|
||||
|
||||
testContext = {
|
||||
domainObject: mockDomainObject,
|
||||
category: 'view-control'
|
||||
};
|
||||
|
||||
policy = new EditPersistableObjectsPolicy(mockAPI);
|
||||
});
|
||||
|
||||
it("Applies to edit action", function () {
|
||||
mockObjectAPI.getProvider.andReturn({});
|
||||
expect(mockObjectAPI.getProvider).not.toHaveBeenCalled();
|
||||
|
||||
policy.allow(mockEditAction, testContext);
|
||||
expect(mockObjectAPI.getProvider).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("Applies to properties action", function () {
|
||||
mockObjectAPI.getProvider.andReturn({});
|
||||
expect(mockObjectAPI.getProvider).not.toHaveBeenCalled();
|
||||
|
||||
policy.allow(mockPropertiesAction, testContext);
|
||||
expect(mockObjectAPI.getProvider).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not apply to other actions", function () {
|
||||
mockObjectAPI.getProvider.andReturn({});
|
||||
expect(mockObjectAPI.getProvider).not.toHaveBeenCalled();
|
||||
|
||||
policy.allow(mockOtherAction, testContext);
|
||||
expect(mockObjectAPI.getProvider).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("Tests object provider for editability", function () {
|
||||
mockObjectAPI.getProvider.andReturn({});
|
||||
expect(policy.allow(mockEditAction, testContext)).toBe(false);
|
||||
expect(mockObjectAPI.getProvider).toHaveBeenCalled();
|
||||
mockObjectAPI.getProvider.andReturn({save: function () {}});
|
||||
expect(policy.allow(mockEditAction, testContext)).toBe(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
@ -76,17 +76,18 @@
|
||||
&:not(.first) {
|
||||
border-top: 1px solid $colorFormLines;
|
||||
}
|
||||
.form-row {
|
||||
@include align-items(center);
|
||||
border: none;
|
||||
padding: $interiorMarginSm 0;
|
||||
.label {
|
||||
min-width: 80px;
|
||||
}
|
||||
input[type='text'],
|
||||
input[type='search'] {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -25,12 +25,14 @@ define([
|
||||
"./src/CompositionMutabilityPolicy",
|
||||
"./src/CompositionModelPolicy",
|
||||
"./src/ComposeActionPolicy",
|
||||
"./src/PersistableCompositionPolicy",
|
||||
'legacyRegistry'
|
||||
], function (
|
||||
CompositionPolicy,
|
||||
CompositionMutabilityPolicy,
|
||||
CompositionModelPolicy,
|
||||
ComposeActionPolicy,
|
||||
PersistableCompositionPolicy,
|
||||
legacyRegistry
|
||||
) {
|
||||
|
||||
@ -59,6 +61,12 @@ define([
|
||||
"$injector"
|
||||
],
|
||||
"message": "Objects of this type cannot contain objects of that type."
|
||||
},
|
||||
{
|
||||
"category": "composition",
|
||||
"implementation": PersistableCompositionPolicy,
|
||||
"depends": ["openmct"],
|
||||
"message": "Change cannot be made to composition of non-persistable object"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -43,9 +43,6 @@ define(
|
||||
}
|
||||
|
||||
ComposeActionPolicy.prototype.allowComposition = function (containerObject, selectedObject) {
|
||||
// Get the object types involved in the compose action
|
||||
var containerType = containerObject &&
|
||||
containerObject.getCapability('type');
|
||||
|
||||
// Get a reference to the policy service if needed...
|
||||
this.policyService = this.policyService || this.getPolicyService();
|
||||
@ -54,7 +51,7 @@ define(
|
||||
return containerObject.getId() !== selectedObject.getId() &&
|
||||
this.policyService.allow(
|
||||
'composition',
|
||||
containerType,
|
||||
containerObject,
|
||||
selectedObject
|
||||
);
|
||||
};
|
||||
|
@ -14,8 +14,9 @@ define(
|
||||
}
|
||||
|
||||
CompositionModelPolicy.prototype.allow = function (candidate) {
|
||||
var candidateType = candidate.getCapability('type');
|
||||
return Array.isArray(
|
||||
(candidate.getInitialModel() || {}).composition
|
||||
(candidateType.getInitialModel() || {}).composition
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -37,7 +37,7 @@ define(
|
||||
// Equate creatability with mutability; that is, users
|
||||
// can only modify objects of types they can create, and
|
||||
// vice versa.
|
||||
return candidate.hasFeature('creation');
|
||||
return candidate.getCapability('type').hasFeature('creation');
|
||||
};
|
||||
|
||||
return CompositionMutabilityPolicy;
|
||||
|
@ -30,16 +30,16 @@ define(
|
||||
function () {
|
||||
|
||||
/**
|
||||
* Defines composition policy as driven by type metadata.
|
||||
* Determines whether a given object can contain a candidate child object.
|
||||
* @constructor
|
||||
* @memberof platform/containment
|
||||
* @implements {Policy.<Type, Type>}
|
||||
* @implements {Policy.<DomainObjectImpl, DomainObjectImpl>}
|
||||
*/
|
||||
function CompositionPolicy() {
|
||||
}
|
||||
|
||||
CompositionPolicy.prototype.allow = function (parentType, child) {
|
||||
var parentDef = parentType.getDefinition();
|
||||
CompositionPolicy.prototype.allow = function (parent, child) {
|
||||
var parentDef = parent.getCapability('type').getDefinition();
|
||||
|
||||
// A parent without containment rules can contain anything.
|
||||
if (!parentDef.contains) {
|
||||
|
60
platform/containment/src/PersistableCompositionPolicy.js
Normal file
60
platform/containment/src/PersistableCompositionPolicy.js
Normal file
@ -0,0 +1,60 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2016, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* This bundle implements "containment" rules, which determine which objects
|
||||
* can be contained within which other objects.
|
||||
* @namespace platform/containment
|
||||
*/
|
||||
define(
|
||||
['../../../src/api/objects/object-utils'],
|
||||
function (objectUtils) {
|
||||
|
||||
function PersistableCompositionPolicy(openmct) {
|
||||
this.openmct = openmct;
|
||||
}
|
||||
|
||||
/**
|
||||
* Only allow changes to composition if the changes can be saved. This in
|
||||
* effect prevents selection of objects from the locator that do not
|
||||
* support persistence.
|
||||
* @param parent
|
||||
* @param child
|
||||
* @returns {boolean}
|
||||
*/
|
||||
PersistableCompositionPolicy.prototype.allow = function (parent) {
|
||||
// If object is in edit mode, allow composition because it is
|
||||
// part of object creation, and the object may be saved to another
|
||||
// namespace that does support persistence. The EditPersistableObjectsPolicy
|
||||
// prevents editing of objects that cannot be persisted, so we can assume that this
|
||||
// is a new object.
|
||||
if (!(parent.hasCapability('editor') && parent.getCapability('editor').isEditContextRoot())) {
|
||||
var identifier = objectUtils.parseKeyString(parent.getId());
|
||||
var provider = this.openmct.objects.getProvider(identifier);
|
||||
return provider.save !== undefined;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
return PersistableCompositionPolicy;
|
||||
}
|
||||
);
|
@ -78,7 +78,7 @@ define(
|
||||
|
||||
expect(mockPolicyService.allow).toHaveBeenCalledWith(
|
||||
'composition',
|
||||
mockTypes[0],
|
||||
mockDomainObjects[0],
|
||||
mockDomainObjects[1]
|
||||
);
|
||||
});
|
||||
|
@ -4,19 +4,25 @@ define(
|
||||
function (CompositionModelPolicy) {
|
||||
|
||||
describe("The composition model policy", function () {
|
||||
var mockType,
|
||||
var mockObject,
|
||||
mockType,
|
||||
policy;
|
||||
|
||||
beforeEach(function () {
|
||||
mockType = jasmine.createSpyObj('type', ['getInitialModel']);
|
||||
mockObject = {
|
||||
getCapability: function () {
|
||||
return mockType;
|
||||
}
|
||||
};
|
||||
policy = new CompositionModelPolicy();
|
||||
});
|
||||
|
||||
it("only allows composition for types which will have a composition property", function () {
|
||||
mockType.getInitialModel.andReturn({});
|
||||
expect(policy.allow(mockType)).toBeFalsy();
|
||||
expect(policy.allow(mockObject)).toBeFalsy();
|
||||
mockType.getInitialModel.andReturn({ composition: [] });
|
||||
expect(policy.allow(mockType)).toBeTruthy();
|
||||
expect(policy.allow(mockObject)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -25,18 +25,24 @@ define(
|
||||
function (CompositionMutabilityPolicy) {
|
||||
|
||||
describe("The composition mutability policy", function () {
|
||||
var mockType,
|
||||
var mockObject,
|
||||
mockType,
|
||||
policy;
|
||||
|
||||
beforeEach(function () {
|
||||
mockType = jasmine.createSpyObj('type', ['hasFeature']);
|
||||
mockObject = {
|
||||
getCapability: function () {
|
||||
return mockType;
|
||||
}
|
||||
};
|
||||
policy = new CompositionMutabilityPolicy();
|
||||
});
|
||||
|
||||
it("only allows composition for types which can be created/modified", function () {
|
||||
expect(policy.allow(mockType)).toBeFalsy();
|
||||
expect(policy.allow(mockObject)).toBeFalsy();
|
||||
mockType.hasFeature.andReturn(true);
|
||||
expect(policy.allow(mockType)).toBeTruthy();
|
||||
expect(policy.allow(mockObject)).toBeTruthy();
|
||||
expect(mockType.hasFeature).toHaveBeenCalledWith('creation');
|
||||
});
|
||||
});
|
||||
|
@ -24,13 +24,18 @@ define(
|
||||
["../src/CompositionPolicy"],
|
||||
function (CompositionPolicy) {
|
||||
describe("Composition policy", function () {
|
||||
var typeA,
|
||||
var mockParentObject,
|
||||
typeA,
|
||||
typeB,
|
||||
typeC,
|
||||
mockChildObject,
|
||||
policy;
|
||||
|
||||
beforeEach(function () {
|
||||
mockParentObject = jasmine.createSpyObj('domainObject', [
|
||||
'getCapability'
|
||||
]);
|
||||
|
||||
typeA = jasmine.createSpyObj(
|
||||
'type A-- the particular kind',
|
||||
['getKey', 'getDefinition']
|
||||
@ -70,27 +75,31 @@ define(
|
||||
describe('enforces simple containment rules', function () {
|
||||
|
||||
it('allows when type matches', function () {
|
||||
mockParentObject.getCapability.andReturn(typeA);
|
||||
|
||||
mockChildObject.getCapability.andReturn(typeA);
|
||||
expect(policy.allow(typeA, mockChildObject))
|
||||
expect(policy.allow(mockParentObject, mockChildObject))
|
||||
.toBeTruthy();
|
||||
|
||||
expect(policy.allow(typeB, mockChildObject))
|
||||
mockParentObject.getCapability.andReturn(typeB);
|
||||
expect(policy.allow(mockParentObject, mockChildObject))
|
||||
.toBeTruthy();
|
||||
|
||||
mockChildObject.getCapability.andReturn(typeB);
|
||||
expect(policy.allow(typeB, mockChildObject))
|
||||
expect(policy.allow(mockParentObject, mockChildObject))
|
||||
.toBeTruthy();
|
||||
});
|
||||
|
||||
|
||||
it('disallows when type doesn\'t match', function () {
|
||||
|
||||
mockParentObject.getCapability.andReturn(typeA);
|
||||
mockChildObject.getCapability.andReturn(typeB);
|
||||
expect(policy.allow(typeA, mockChildObject))
|
||||
expect(policy.allow(mockParentObject, mockChildObject))
|
||||
.toBeFalsy();
|
||||
|
||||
mockChildObject.getCapability.andReturn(typeC);
|
||||
expect(policy.allow(typeA, mockChildObject))
|
||||
expect(policy.allow(mockParentObject, mockChildObject))
|
||||
.toBeFalsy();
|
||||
});
|
||||
|
||||
@ -98,8 +107,10 @@ define(
|
||||
|
||||
describe('enforces capability-based containment rules', function () {
|
||||
it('allows when object has capability', function () {
|
||||
mockParentObject.getCapability.andReturn(typeC);
|
||||
|
||||
mockChildObject.hasCapability.andReturn(true);
|
||||
expect(policy.allow(typeC, mockChildObject))
|
||||
expect(policy.allow(mockParentObject, mockChildObject))
|
||||
.toBeTruthy();
|
||||
expect(mockChildObject.hasCapability)
|
||||
.toHaveBeenCalledWith('telemetry');
|
||||
@ -107,7 +118,10 @@ define(
|
||||
|
||||
it('skips when object doesn\'t have capability', function () {
|
||||
mockChildObject.hasCapability.andReturn(false);
|
||||
expect(policy.allow(typeC, mockChildObject))
|
||||
|
||||
mockParentObject.getCapability.andReturn(typeC);
|
||||
|
||||
expect(policy.allow(mockParentObject, mockChildObject))
|
||||
.toBeFalsy();
|
||||
expect(mockChildObject.hasCapability)
|
||||
.toHaveBeenCalledWith('telemetry');
|
||||
|
@ -0,0 +1,85 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2016, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define(
|
||||
["../src/PersistableCompositionPolicy"],
|
||||
function (PersistableCompositionPolicy) {
|
||||
describe("Persistable Composition policy", function () {
|
||||
var objectAPI;
|
||||
var mockOpenMCT;
|
||||
var persistableCompositionPolicy;
|
||||
var mockParent;
|
||||
var mockChild;
|
||||
var mockEditorCapability;
|
||||
|
||||
beforeEach(function () {
|
||||
objectAPI = jasmine.createSpyObj('objectsAPI', [
|
||||
'getProvider'
|
||||
]);
|
||||
|
||||
mockOpenMCT = {
|
||||
objects: objectAPI
|
||||
};
|
||||
mockParent = jasmine.createSpyObj('domainObject', [
|
||||
'hasCapability',
|
||||
'getCapability',
|
||||
'getId'
|
||||
]);
|
||||
mockParent.hasCapability.andReturn(true);
|
||||
mockParent.getId.andReturn('someNamespace:someId');
|
||||
mockChild = {};
|
||||
mockEditorCapability = jasmine.createSpyObj('domainObject', [
|
||||
'isEditContextRoot'
|
||||
]);
|
||||
mockParent.getCapability.andReturn(mockEditorCapability);
|
||||
|
||||
objectAPI.getProvider.andReturn({
|
||||
save: function () {}
|
||||
});
|
||||
persistableCompositionPolicy = new PersistableCompositionPolicy(mockOpenMCT);
|
||||
});
|
||||
|
||||
//Parent
|
||||
// - getCapability ('editor')
|
||||
// - isEditContextRoot
|
||||
// - openMct.objects.getProvider
|
||||
|
||||
it("Does not allow composition for objects that are not persistable", function () {
|
||||
mockEditorCapability.isEditContextRoot.andReturn(false);
|
||||
expect(persistableCompositionPolicy.allow(mockParent, mockChild)).toBe(true);
|
||||
objectAPI.getProvider.andReturn({});
|
||||
expect(persistableCompositionPolicy.allow(mockParent, mockChild)).toBe(false);
|
||||
});
|
||||
|
||||
it("Always allows composition of objects in edit mode to support object creation", function () {
|
||||
mockEditorCapability.isEditContextRoot.andReturn(true);
|
||||
expect(persistableCompositionPolicy.allow(mockParent, mockChild)).toBe(true);
|
||||
expect(objectAPI.getProvider).not.toHaveBeenCalled();
|
||||
|
||||
mockEditorCapability.isEditContextRoot.andReturn(false);
|
||||
expect(persistableCompositionPolicy.allow(mockParent, mockChild)).toBe(true);
|
||||
expect(objectAPI.getProvider).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
@ -49,7 +49,6 @@ define([
|
||||
"./src/services/Now",
|
||||
"./src/services/Throttle",
|
||||
"./src/services/Topic",
|
||||
"./src/services/Contextualize",
|
||||
"./src/services/Instantiate",
|
||||
'legacyRegistry'
|
||||
], function (
|
||||
@ -81,7 +80,6 @@ define([
|
||||
Now,
|
||||
Throttle,
|
||||
Topic,
|
||||
Contextualize,
|
||||
Instantiate,
|
||||
legacyRegistry
|
||||
) {
|
||||
@ -284,8 +282,7 @@ define([
|
||||
"key": "composition",
|
||||
"implementation": CompositionCapability,
|
||||
"depends": [
|
||||
"$injector",
|
||||
"contextualize"
|
||||
"$injector"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -380,13 +377,6 @@ define([
|
||||
"$log"
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "contextualize",
|
||||
"implementation": Contextualize,
|
||||
"depends": [
|
||||
"$log"
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "instantiate",
|
||||
"implementation": Instantiate,
|
||||
|
@ -24,7 +24,8 @@
|
||||
* Module defining CompositionCapability. Created by vwoeltje on 11/7/14.
|
||||
*/
|
||||
define(
|
||||
function () {
|
||||
['./ContextualDomainObject'],
|
||||
function (ContextualDomainObject) {
|
||||
|
||||
/**
|
||||
* Composition capability. A domain object's composition is the set of
|
||||
@ -38,13 +39,12 @@ define(
|
||||
* @constructor
|
||||
* @implements {Capability}
|
||||
*/
|
||||
function CompositionCapability($injector, contextualize, domainObject) {
|
||||
function CompositionCapability($injector, domainObject) {
|
||||
// Get a reference to the object service from $injector
|
||||
this.injectObjectService = function () {
|
||||
this.objectService = $injector.get("objectService");
|
||||
};
|
||||
|
||||
this.contextualize = contextualize;
|
||||
this.domainObject = domainObject;
|
||||
}
|
||||
|
||||
@ -115,7 +115,6 @@ define(
|
||||
CompositionCapability.prototype.invoke = function () {
|
||||
var domainObject = this.domainObject,
|
||||
model = domainObject.getModel(),
|
||||
contextualize = this.contextualize,
|
||||
ids;
|
||||
|
||||
// Then filter out non-existent objects,
|
||||
@ -125,7 +124,7 @@ define(
|
||||
return ids.filter(function (id) {
|
||||
return objects[id];
|
||||
}).map(function (id) {
|
||||
return contextualize(objects[id], domainObject);
|
||||
return new ContextualDomainObject(objects[id], domainObject);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -21,8 +21,8 @@
|
||||
*****************************************************************************/
|
||||
|
||||
define(
|
||||
[],
|
||||
function () {
|
||||
['./ContextualDomainObject'],
|
||||
function (ContextualDomainObject) {
|
||||
|
||||
/**
|
||||
* Implements the `instantiation` capability. This allows new domain
|
||||
@ -70,11 +70,7 @@ define(
|
||||
|
||||
var newObject = this.instantiateFn(model, id);
|
||||
|
||||
this.contextualizeFn = this.contextualizeFn ||
|
||||
this.$injector.get("contextualize");
|
||||
|
||||
|
||||
return this.contextualizeFn(newObject, this.domainObject);
|
||||
return new ContextualDomainObject(newObject, this.domainObject);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -1,82 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2016, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define(
|
||||
['../capabilities/ContextualDomainObject'],
|
||||
function (ContextualDomainObject) {
|
||||
|
||||
/**
|
||||
* Wrap a domain object such that it has a `context` capability
|
||||
* referring to a specific parent.
|
||||
*
|
||||
* Usage:
|
||||
*
|
||||
* contextualize(domainObject, parentObject)
|
||||
*
|
||||
* Attempting to contextualize an object with a parent that does
|
||||
* not include that object in its composition may have
|
||||
* unpredictable results; a warning will be logged if this occurs.
|
||||
*
|
||||
* @returns {Function}
|
||||
* @memberof platform/core
|
||||
*/
|
||||
function Contextualize($log) {
|
||||
function validate(id, parentObject) {
|
||||
var model = parentObject && parentObject.getModel(),
|
||||
composition = (model || {}).composition || [];
|
||||
if (composition.indexOf(id) === -1) {
|
||||
$log.warn([
|
||||
"Attempted to contextualize",
|
||||
id,
|
||||
"in",
|
||||
parentObject && parentObject.getId(),
|
||||
"but that object does not contain",
|
||||
id,
|
||||
"in its composition.",
|
||||
"Unexpected behavior may follow."
|
||||
].join(" "));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Contextualize this domain object.
|
||||
* @param {DomainObject} domainObject the domain object
|
||||
* to wrap with a context
|
||||
* @param {DomainObject} parentObject the domain object
|
||||
* which should appear as the contextual parent
|
||||
*/
|
||||
return function (domainObject, parentObject) {
|
||||
// Don't validate while editing; consistency is not
|
||||
// necessarily expected due to unsaved changes.
|
||||
var editor = domainObject.getCapability('editor');
|
||||
if (!editor || !editor.inEditContext()) {
|
||||
validate(domainObject.getId(), parentObject);
|
||||
}
|
||||
|
||||
return new ContextualDomainObject(domainObject, parentObject);
|
||||
};
|
||||
}
|
||||
|
||||
return Contextualize;
|
||||
}
|
||||
);
|
||||
|
@ -41,7 +41,6 @@ define(
|
||||
describe("The composition capability", function () {
|
||||
var mockDomainObject,
|
||||
mockInjector,
|
||||
mockContextualize,
|
||||
mockObjectService,
|
||||
composition;
|
||||
|
||||
@ -72,19 +71,11 @@ define(
|
||||
return (name === "objectService") && mockObjectService;
|
||||
}
|
||||
};
|
||||
mockContextualize = jasmine.createSpy('contextualize');
|
||||
|
||||
// Provide a minimal (e.g. no error-checking) implementation
|
||||
// of contextualize for simplicity
|
||||
mockContextualize.andCallFake(function (domainObject, parentObject) {
|
||||
return new ContextualDomainObject(domainObject, parentObject);
|
||||
});
|
||||
|
||||
mockObjectService.getObjects.andReturn(mockPromise([]));
|
||||
|
||||
composition = new CompositionCapability(
|
||||
mockInjector,
|
||||
mockContextualize,
|
||||
mockDomainObject
|
||||
);
|
||||
});
|
||||
|
@ -28,7 +28,6 @@ define(
|
||||
var mockInjector,
|
||||
mockIdentifierService,
|
||||
mockInstantiate,
|
||||
mockContextualize,
|
||||
mockIdentifier,
|
||||
mockNow,
|
||||
mockDomainObject,
|
||||
@ -37,7 +36,6 @@ define(
|
||||
beforeEach(function () {
|
||||
mockInjector = jasmine.createSpyObj("$injector", ["get"]);
|
||||
mockInstantiate = jasmine.createSpy("instantiate");
|
||||
mockContextualize = jasmine.createSpy("contextualize");
|
||||
mockIdentifierService = jasmine.createSpyObj(
|
||||
'identifierService',
|
||||
['parse', 'generate']
|
||||
@ -53,8 +51,7 @@ define(
|
||||
|
||||
mockInjector.get.andCallFake(function (key) {
|
||||
return {
|
||||
'instantiate': mockInstantiate,
|
||||
'contextualize': mockContextualize
|
||||
'instantiate': mockInstantiate
|
||||
}[key];
|
||||
});
|
||||
mockIdentifierService.parse.andReturn(mockIdentifier);
|
||||
@ -85,18 +82,12 @@ define(
|
||||
'hasCapability'
|
||||
]), testModel = { someKey: "some value" };
|
||||
mockInstantiate.andReturn(mockDomainObj);
|
||||
mockContextualize.andCallFake(function (x) {
|
||||
return x;
|
||||
});
|
||||
expect(instantiation.instantiate(testModel))
|
||||
.toBe(mockDomainObj);
|
||||
instantiation.instantiate(testModel);
|
||||
expect(mockInstantiate)
|
||||
.toHaveBeenCalledWith({
|
||||
someKey: "some value",
|
||||
modified: mockNow()
|
||||
}, jasmine.any(String));
|
||||
expect(mockContextualize)
|
||||
.toHaveBeenCalledWith(mockDomainObj, mockDomainObject);
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -1,103 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2016, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define(
|
||||
["../../src/services/Contextualize"],
|
||||
function (Contextualize) {
|
||||
|
||||
var DOMAIN_OBJECT_METHODS = [
|
||||
'getId',
|
||||
'getModel',
|
||||
'getCapability',
|
||||
'hasCapability',
|
||||
'useCapability'
|
||||
];
|
||||
|
||||
describe("The 'contextualize' service", function () {
|
||||
var mockLog,
|
||||
mockDomainObject,
|
||||
mockParentObject,
|
||||
mockEditor,
|
||||
testParentModel,
|
||||
contextualize;
|
||||
|
||||
beforeEach(function () {
|
||||
testParentModel = { composition: ["abc"] };
|
||||
|
||||
mockLog = jasmine.createSpyObj(
|
||||
"$log",
|
||||
["error", "warn", "info", "debug"]
|
||||
);
|
||||
|
||||
mockDomainObject =
|
||||
jasmine.createSpyObj('domainObject', DOMAIN_OBJECT_METHODS);
|
||||
mockParentObject =
|
||||
jasmine.createSpyObj('parentObject', DOMAIN_OBJECT_METHODS);
|
||||
|
||||
mockEditor =
|
||||
jasmine.createSpyObj('editor', ['inEditContext']);
|
||||
|
||||
mockDomainObject.getId.andReturn("abc");
|
||||
mockDomainObject.getModel.andReturn({});
|
||||
mockParentObject.getId.andReturn("parent");
|
||||
mockParentObject.getModel.andReturn(testParentModel);
|
||||
|
||||
mockEditor.inEditContext.andReturn(false);
|
||||
mockDomainObject.getCapability.andCallFake(function (c) {
|
||||
return c === 'editor' && mockEditor;
|
||||
});
|
||||
|
||||
contextualize = new Contextualize(mockLog);
|
||||
});
|
||||
|
||||
it("attaches a context capability", function () {
|
||||
var contextualizedObject =
|
||||
contextualize(mockDomainObject, mockParentObject);
|
||||
|
||||
expect(contextualizedObject.getId()).toEqual("abc");
|
||||
expect(contextualizedObject.getCapability("context"))
|
||||
.toBeDefined();
|
||||
expect(contextualizedObject.getCapability("context").getParent())
|
||||
.toBe(mockParentObject);
|
||||
});
|
||||
|
||||
it("issues a warning if composition does not match", function () {
|
||||
// Precondition - normally it should not issue a warning
|
||||
contextualize(mockDomainObject, mockParentObject);
|
||||
expect(mockLog.warn).not.toHaveBeenCalled();
|
||||
|
||||
testParentModel.composition = ["xyz"];
|
||||
|
||||
contextualize(mockDomainObject, mockParentObject);
|
||||
expect(mockLog.warn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not issue warnings for objects being edited", function () {
|
||||
mockEditor.inEditContext.andReturn(true);
|
||||
testParentModel.composition = ["xyz"];
|
||||
contextualize(mockDomainObject, mockParentObject);
|
||||
expect(mockLog.warn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
@ -132,7 +132,6 @@ define([
|
||||
"provides": "objectService",
|
||||
"implementation": LocatingObjectDecorator,
|
||||
"depends": [
|
||||
"contextualize",
|
||||
"$q",
|
||||
"$log"
|
||||
]
|
||||
|
@ -47,7 +47,7 @@ define(
|
||||
}
|
||||
return this.policyService.allow(
|
||||
"composition",
|
||||
parentCandidate.getCapability('type'),
|
||||
parentCandidate,
|
||||
object
|
||||
);
|
||||
};
|
||||
|
@ -51,7 +51,7 @@ define(
|
||||
}
|
||||
return this.policyService.allow(
|
||||
"composition",
|
||||
parentCandidate.getCapability('type'),
|
||||
parentCandidate,
|
||||
object
|
||||
);
|
||||
};
|
||||
|
@ -22,7 +22,8 @@
|
||||
|
||||
|
||||
define(
|
||||
function () {
|
||||
['../../../core/src/capabilities/ContextualDomainObject'],
|
||||
function (ContextualDomainObject) {
|
||||
|
||||
/**
|
||||
* Ensures that domain objects are loaded with a context capability
|
||||
@ -31,8 +32,7 @@ define(
|
||||
* @implements {ObjectService}
|
||||
* @memberof platform/entanglement
|
||||
*/
|
||||
function LocatingObjectDecorator(contextualize, $q, $log, objectService) {
|
||||
this.contextualize = contextualize;
|
||||
function LocatingObjectDecorator($q, $log, objectService) {
|
||||
this.$log = $log;
|
||||
this.objectService = objectService;
|
||||
this.$q = $q;
|
||||
@ -41,7 +41,6 @@ define(
|
||||
LocatingObjectDecorator.prototype.getObjects = function (ids) {
|
||||
var $q = this.$q,
|
||||
$log = this.$log,
|
||||
contextualize = this.contextualize,
|
||||
objectService = this.objectService,
|
||||
result = {};
|
||||
|
||||
@ -76,7 +75,7 @@ define(
|
||||
return loadObjectInContext(location, exclude)
|
||||
.then(function (parent) {
|
||||
// ...and then contextualize with it!
|
||||
return contextualize(domainObject, parent);
|
||||
return new ContextualDomainObject(domainObject, parent);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -55,7 +55,7 @@ define(
|
||||
}
|
||||
return this.policyService.allow(
|
||||
"composition",
|
||||
parentCandidate.getCapability('type'),
|
||||
parentCandidate,
|
||||
object
|
||||
);
|
||||
};
|
||||
|
@ -103,7 +103,7 @@ define(
|
||||
validate();
|
||||
expect(policyService.allow).toHaveBeenCalledWith(
|
||||
"composition",
|
||||
parentCandidate.capabilities.type,
|
||||
parentCandidate,
|
||||
object
|
||||
);
|
||||
});
|
||||
|
@ -113,7 +113,7 @@ define(
|
||||
validate();
|
||||
expect(mockPolicyService.allow).toHaveBeenCalledWith(
|
||||
"composition",
|
||||
parentCandidate.capabilities.type,
|
||||
parentCandidate,
|
||||
object
|
||||
);
|
||||
});
|
||||
|
@ -23,13 +23,13 @@
|
||||
|
||||
define(
|
||||
[
|
||||
'../../src/services/LocatingObjectDecorator'
|
||||
'../../src/services/LocatingObjectDecorator',
|
||||
'../../../core/src/capabilities/ContextualDomainObject'
|
||||
],
|
||||
function (LocatingObjectDecorator) {
|
||||
function (LocatingObjectDecorator, ContextualDomainObject) {
|
||||
|
||||
describe("LocatingObjectDecorator", function () {
|
||||
var mockContextualize,
|
||||
mockQ,
|
||||
var mockQ,
|
||||
mockLog,
|
||||
mockObjectService,
|
||||
mockCallback,
|
||||
@ -57,21 +57,12 @@ define(
|
||||
};
|
||||
testObjects = {};
|
||||
|
||||
mockContextualize = jasmine.createSpy("contextualize");
|
||||
mockQ = jasmine.createSpyObj("$q", ["when", "all"]);
|
||||
mockLog =
|
||||
jasmine.createSpyObj("$log", ["error", "warn", "info", "debug"]);
|
||||
mockObjectService =
|
||||
jasmine.createSpyObj("objectService", ["getObjects"]);
|
||||
|
||||
mockContextualize.andCallFake(function (domainObject, parentObject) {
|
||||
// Not really what contextualize does, but easy to test!
|
||||
return {
|
||||
testObject: domainObject,
|
||||
testParent: parentObject
|
||||
};
|
||||
});
|
||||
|
||||
mockQ.when.andCallFake(testPromise);
|
||||
mockQ.all.andCallFake(function (promises) {
|
||||
var result = {};
|
||||
@ -97,28 +88,21 @@ define(
|
||||
});
|
||||
|
||||
decorator = new LocatingObjectDecorator(
|
||||
mockContextualize,
|
||||
mockQ,
|
||||
mockLog,
|
||||
mockObjectService
|
||||
);
|
||||
});
|
||||
|
||||
it("contextualizes domain objects by location", function () {
|
||||
it("contextualizes domain objects", function () {
|
||||
decorator.getObjects(['b', 'c']).then(mockCallback);
|
||||
expect(mockCallback).toHaveBeenCalledWith({
|
||||
b: {
|
||||
testObject: testObjects.b,
|
||||
testParent: testObjects.a
|
||||
},
|
||||
c: {
|
||||
testObject: testObjects.c,
|
||||
testParent: {
|
||||
testObject: testObjects.b,
|
||||
testParent: testObjects.a
|
||||
}
|
||||
}
|
||||
});
|
||||
expect(mockCallback).toHaveBeenCalled();
|
||||
|
||||
var callbackObj = mockCallback.mostRecentCall.args[0];
|
||||
expect(testObjects.b.getCapability('context')).not.toBeDefined();
|
||||
expect(testObjects.c.getCapability('context')).not.toBeDefined();
|
||||
expect(callbackObj.b.getCapability('context')).toBeDefined();
|
||||
expect(callbackObj.c.getCapability('context')).toBeDefined();
|
||||
});
|
||||
|
||||
it("warns on cycle detection", function () {
|
||||
|
@ -123,7 +123,7 @@ define(
|
||||
validate();
|
||||
expect(policyService.allow).toHaveBeenCalledWith(
|
||||
"composition",
|
||||
parentCandidate.capabilities.type,
|
||||
parentCandidate,
|
||||
object
|
||||
);
|
||||
});
|
||||
|
54
platform/features/autoflow/plugin.js
Executable file
54
platform/features/autoflow/plugin.js
Executable file
@ -0,0 +1,54 @@
|
||||
define([
|
||||
'text!./res/templates/autoflow-tabular.html',
|
||||
'./src/AutoflowTabularController',
|
||||
'./src/MCTAutoflowTable'
|
||||
], function (
|
||||
autoflowTabularTemplate,
|
||||
AutoflowTabularController,
|
||||
MCTAutoflowTable
|
||||
) {
|
||||
return function (options) {
|
||||
return function (openmct) {
|
||||
openmct.legacyRegistry.register("platform/features/autoflow", {
|
||||
"name": "WARP Telemetry Adapter",
|
||||
"description": "Retrieves telemetry from the WARP Server and provides related types and views.",
|
||||
"resources": "res",
|
||||
"extensions": {
|
||||
"views": [
|
||||
{
|
||||
"key": "autoflow",
|
||||
"name": "Autoflow Tabular",
|
||||
"cssClass": "icon-packet",
|
||||
"description": "A tabular view of packet contents.",
|
||||
"template": autoflowTabularTemplate,
|
||||
"type": options && options.type,
|
||||
"needs": [
|
||||
"telemetry"
|
||||
],
|
||||
"delegation": true
|
||||
}
|
||||
],
|
||||
"controllers": [
|
||||
{
|
||||
"key": "AutoflowTabularController",
|
||||
"implementation": AutoflowTabularController,
|
||||
"depends": [
|
||||
"$scope",
|
||||
"$timeout",
|
||||
"telemetrySubscriber"
|
||||
]
|
||||
}
|
||||
],
|
||||
"directives": [
|
||||
{
|
||||
"key": "mctAutoflowTable",
|
||||
"implementation": MCTAutoflowTable
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
openmct.legacyRegistry.enable("platform/features/autoflow");
|
||||
};
|
||||
};
|
||||
});
|
||||
|
26
platform/features/autoflow/res/templates/autoflow-tabular.html
Executable file
26
platform/features/autoflow/res/templates/autoflow-tabular.html
Executable file
@ -0,0 +1,26 @@
|
||||
<div class="items-holder abs contents autoflow obj-value-format"
|
||||
ng-controller="AutoflowTabularController as autoflow">
|
||||
<div class="abs l-flex-row holder t-autoflow-header l-autoflow-header">
|
||||
<mct-include key="'input-filter'"
|
||||
ng-model="autoflow.filter"
|
||||
class="flex-elem">
|
||||
</mct-include>
|
||||
<div class="flex-elem grows t-last-update" title="Last Update">{{autoflow.updated()}}</div>
|
||||
<a title="Change column width"
|
||||
class="s-button flex-elem icon-arrows-right-left change-column-width"
|
||||
ng-click="autoflow.increaseColumnWidth()"></a>
|
||||
</div>
|
||||
<div class="abs t-autoflow-items l-autoflow-items"
|
||||
mct-resize="autoflow.setBounds(bounds)"
|
||||
mct-resize-interval="50">
|
||||
<mct-autoflow-table values="autoflow.rangeValues()"
|
||||
objects="autoflow.getTelemetryObjects()"
|
||||
rows="autoflow.getRows()"
|
||||
classes="autoflow.classes()"
|
||||
updated="autoflow.updated()"
|
||||
column-width="autoflow.columnWidth()"
|
||||
counter="autoflow.counter()"
|
||||
>
|
||||
</mct-autoflow-table>
|
||||
</div>
|
||||
</div>
|
169
platform/features/autoflow/src/AutoflowTableLinker.js
Executable file
169
platform/features/autoflow/src/AutoflowTableLinker.js
Executable file
@ -0,0 +1,169 @@
|
||||
/*global angular*/
|
||||
define(
|
||||
[],
|
||||
function () {
|
||||
|
||||
/**
|
||||
* The link step for the `mct-autoflow-table` directive;
|
||||
* watches scope and updates the DOM appropriately.
|
||||
* See documentation in `MCTAutoflowTable.js` for the rationale
|
||||
* for including this directive, as well as for an explanation
|
||||
* of which values are placed in scope.
|
||||
*
|
||||
* @constructor
|
||||
* @param {Scope} scope the scope for this usage of the directive
|
||||
* @param element the jqLite-wrapped element which used this directive
|
||||
*/
|
||||
function AutoflowTableLinker(scope, element) {
|
||||
var objects, // Domain objects at last structure refresh
|
||||
rows, // Number of rows from last structure refresh
|
||||
priorClasses = {},
|
||||
valueSpans = {}; // Span elements to put data values in
|
||||
|
||||
// Create a new name-value pair in the specified column
|
||||
function createListItem(domainObject, ul) {
|
||||
// Create a new li, and spans to go in it.
|
||||
var li = angular.element('<li>'),
|
||||
titleSpan = angular.element('<span>'),
|
||||
valueSpan = angular.element('<span>');
|
||||
|
||||
// Place spans in the li, and li into the column.
|
||||
// valueSpan must precede titleSpan in the DOM due to new CSS float approach
|
||||
li.append(valueSpan).append(titleSpan);
|
||||
ul.append(li);
|
||||
|
||||
// Style appropriately
|
||||
li.addClass('l-autoflow-row');
|
||||
titleSpan.addClass('l-autoflow-item l');
|
||||
valueSpan.addClass('l-autoflow-item r l-obj-val-format');
|
||||
|
||||
// Set text/tooltip for the name-value row
|
||||
titleSpan.text(domainObject.getModel().name);
|
||||
titleSpan.attr("title", domainObject.getModel().name);
|
||||
|
||||
// Keep a reference to the span which will hold the
|
||||
// data value, to populate in the next refreshValues call
|
||||
valueSpans[domainObject.getId()] = valueSpan;
|
||||
|
||||
return li;
|
||||
}
|
||||
|
||||
// Create a new column of name-value pairs in this table.
|
||||
function createColumn(el) {
|
||||
// Create a ul
|
||||
var ul = angular.element('<ul>');
|
||||
|
||||
// Add it into the mct-autoflow-table
|
||||
el.append(ul);
|
||||
|
||||
// Style appropriately
|
||||
ul.addClass('l-autoflow-col');
|
||||
|
||||
// Get the current col width and apply at time of column creation
|
||||
// Important to do this here, as new columns could be created after
|
||||
// the user has changed the width.
|
||||
ul.css('width', scope.columnWidth + 'px');
|
||||
|
||||
// Return it, so some li elements can be added
|
||||
return ul;
|
||||
}
|
||||
|
||||
// Change the width of the columns when user clicks the resize button.
|
||||
function resizeColumn() {
|
||||
element.find('ul').css('width', scope.columnWidth + 'px');
|
||||
}
|
||||
|
||||
// Rebuild the DOM associated with this table.
|
||||
function rebuild(domainObjects, rowCount) {
|
||||
var activeColumn;
|
||||
|
||||
// Empty out our cached span elements
|
||||
valueSpans = {};
|
||||
|
||||
// Start with an empty DOM beneath this directive
|
||||
element.html("");
|
||||
|
||||
// Add DOM elements for each domain object being displayed
|
||||
// in this table.
|
||||
domainObjects.forEach(function (object, index) {
|
||||
// Start a new column if we'd run out of room
|
||||
if (index % rowCount === 0) {
|
||||
activeColumn = createColumn(element);
|
||||
}
|
||||
// Add the DOM elements for that object to whichever
|
||||
// column (a `ul` element) is current.
|
||||
createListItem(object, activeColumn);
|
||||
});
|
||||
}
|
||||
|
||||
// Update spans with values, as made available via the
|
||||
// `values` attribute of this directive.
|
||||
function refreshValues() {
|
||||
// Get the available values
|
||||
var values = scope.values || {},
|
||||
classes = scope.classes || {};
|
||||
|
||||
// Populate all spans with those values (or clear
|
||||
// those spans if no value is available)
|
||||
(objects || []).forEach(function (object) {
|
||||
var id = object.getId(),
|
||||
span = valueSpans[id],
|
||||
value;
|
||||
|
||||
if (span) {
|
||||
// Look up the value...
|
||||
value = values[id];
|
||||
// ...and convert to empty string if it's undefined
|
||||
value = value === undefined ? "" : value;
|
||||
span.attr("data-value", value);
|
||||
|
||||
// Update the span
|
||||
span.text(value);
|
||||
span.attr("title", value);
|
||||
span.removeClass(priorClasses[id]);
|
||||
span.addClass(classes[id]);
|
||||
priorClasses[id] = classes[id];
|
||||
}
|
||||
// Also need stale/alert/ok class
|
||||
// on span
|
||||
});
|
||||
}
|
||||
|
||||
// Refresh the DOM for this table, if necessary
|
||||
function refreshStructure() {
|
||||
// Only rebuild if number of rows or set of objects
|
||||
// has changed; otherwise, our structure is still valid.
|
||||
if (scope.objects !== objects ||
|
||||
scope.rows !== rows) {
|
||||
|
||||
// Track those values to support future refresh checks
|
||||
objects = scope.objects;
|
||||
rows = scope.rows;
|
||||
|
||||
// Rebuild the DOM
|
||||
rebuild(objects || [], rows || 1);
|
||||
|
||||
// Refresh all data values shown
|
||||
refreshValues();
|
||||
}
|
||||
}
|
||||
|
||||
// Changing the domain objects in use or the number
|
||||
// of rows should trigger a structure change (DOM rebuild)
|
||||
scope.$watch("objects", refreshStructure);
|
||||
scope.$watch("rows", refreshStructure);
|
||||
|
||||
// When the current column width has been changed, resize the column
|
||||
scope.$watch('columnWidth', resizeColumn);
|
||||
|
||||
// When the last-updated time ticks,
|
||||
scope.$watch("updated", refreshValues);
|
||||
|
||||
// Update displayed values when the counter changes.
|
||||
scope.$watch("counter", refreshValues);
|
||||
|
||||
}
|
||||
|
||||
return AutoflowTableLinker;
|
||||
}
|
||||
);
|
324
platform/features/autoflow/src/AutoflowTabularController.js
Executable file
324
platform/features/autoflow/src/AutoflowTabularController.js
Executable file
@ -0,0 +1,324 @@
|
||||
|
||||
define(
|
||||
['moment'],
|
||||
function (moment) {
|
||||
|
||||
var ROW_HEIGHT = 16,
|
||||
SLIDER_HEIGHT = 10,
|
||||
INITIAL_COLUMN_WIDTH = 225,
|
||||
MAX_COLUMN_WIDTH = 525,
|
||||
COLUMN_WIDTH_STEP = 25,
|
||||
DEBOUNCE_INTERVAL = 100,
|
||||
DATE_FORMAT = "YYYY-DDD HH:mm:ss.SSS\\Z",
|
||||
NOT_UPDATED = "No updates",
|
||||
EMPTY_ARRAY = [];
|
||||
|
||||
/**
|
||||
* Responsible for supporting the autoflow tabular view.
|
||||
* Implements the all-over logic which drives that view,
|
||||
* mediating between template-provided areas, the included
|
||||
* `mct-autoflow-table` directive, and the underlying
|
||||
* domain object model.
|
||||
* @constructor
|
||||
*/
|
||||
function AutflowTabularController(
|
||||
$scope,
|
||||
$timeout,
|
||||
telemetrySubscriber
|
||||
) {
|
||||
var filterValue = "",
|
||||
filterValueLowercase = "",
|
||||
subscription,
|
||||
filteredObjects = [],
|
||||
lastUpdated = {},
|
||||
updateText = NOT_UPDATED,
|
||||
rangeValues = {},
|
||||
classes = {},
|
||||
limits = {},
|
||||
updatePending = false,
|
||||
lastBounce = Number.NEGATIVE_INFINITY,
|
||||
columnWidth = INITIAL_COLUMN_WIDTH,
|
||||
rows = 1,
|
||||
counter = 0;
|
||||
|
||||
// Trigger an update of the displayed table by incrementing
|
||||
// the counter that it watches.
|
||||
function triggerDisplayUpdate() {
|
||||
counter += 1;
|
||||
}
|
||||
|
||||
// Check whether or not an object's name matches the
|
||||
// user-entered filter value.
|
||||
function filterObject(domainObject) {
|
||||
return (domainObject.getModel().name || "")
|
||||
.toLowerCase()
|
||||
.indexOf(filterValueLowercase) !== -1;
|
||||
}
|
||||
|
||||
// Comparator for sorting points back into packet order
|
||||
function compareObject(objectA, objectB) {
|
||||
var indexA = objectA.getModel().index || 0,
|
||||
indexB = objectB.getModel().index || 0;
|
||||
return indexA - indexB;
|
||||
}
|
||||
|
||||
// Update the list of currently-displayed objects; these
|
||||
// will be the subset of currently subscribed-to objects
|
||||
// which match a user-entered filter.
|
||||
function doUpdateFilteredObjects() {
|
||||
// Generate the list
|
||||
filteredObjects = (
|
||||
subscription ?
|
||||
subscription.getTelemetryObjects() :
|
||||
[]
|
||||
).filter(filterObject).sort(compareObject);
|
||||
|
||||
// Clear the pending flag
|
||||
updatePending = false;
|
||||
|
||||
// Track when this occurred, so that we can wait
|
||||
// a whole before updating again.
|
||||
lastBounce = Date.now();
|
||||
|
||||
triggerDisplayUpdate();
|
||||
}
|
||||
|
||||
// Request an update to the list of current objects; this may
|
||||
// run on a timeout to avoid excessive calls, e.g. while the user
|
||||
// is typing a filter.
|
||||
function updateFilteredObjects() {
|
||||
// Don't do anything if an update is already scheduled
|
||||
if (!updatePending) {
|
||||
if (Date.now() > lastBounce + DEBOUNCE_INTERVAL) {
|
||||
// Update immediately if it's been long enough
|
||||
doUpdateFilteredObjects();
|
||||
} else {
|
||||
// Otherwise, update later, and track that we have
|
||||
// an update pending so that subsequent calls can
|
||||
// be ignored.
|
||||
updatePending = true;
|
||||
$timeout(doUpdateFilteredObjects, DEBOUNCE_INTERVAL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Track the latest data values for this domain object
|
||||
function recordData(telemetryObject) {
|
||||
// Get latest domain/range values for this object.
|
||||
var id = telemetryObject.getId(),
|
||||
domainValue = subscription.getDomainValue(telemetryObject),
|
||||
rangeValue = subscription.getRangeValue(telemetryObject);
|
||||
|
||||
// Track the most recent timestamp change observed...
|
||||
if (domainValue !== undefined && domainValue !== lastUpdated[id]) {
|
||||
lastUpdated[id] = domainValue;
|
||||
// ... and update the displayable text for that timestamp
|
||||
updateText = isNaN(domainValue) ? "" :
|
||||
moment.utc(domainValue).format(DATE_FORMAT);
|
||||
}
|
||||
|
||||
// Store data values into the rangeValues structure, which
|
||||
// will be used to populate the table itself.
|
||||
// Note that we want full precision here.
|
||||
rangeValues[id] = rangeValue;
|
||||
|
||||
// Update limit states as well
|
||||
classes[id] = limits[id] && (limits[id].evaluate({
|
||||
// This relies on external knowledge that the
|
||||
// range value of a telemetry point is encoded
|
||||
// in its datum as "value."
|
||||
value: rangeValue
|
||||
}) || {}).cssClass;
|
||||
}
|
||||
|
||||
|
||||
// Look at telemetry objects from the subscription; this is watched
|
||||
// to detect changes from the subscription.
|
||||
function subscribedTelemetry() {
|
||||
return subscription ?
|
||||
subscription.getTelemetryObjects() : EMPTY_ARRAY;
|
||||
}
|
||||
|
||||
// Update the data values which will be used to populate the table
|
||||
function updateValues() {
|
||||
subscribedTelemetry().forEach(recordData);
|
||||
triggerDisplayUpdate();
|
||||
}
|
||||
|
||||
// Getter-setter function for user-entered filter text.
|
||||
function filter(value) {
|
||||
// If value was specified, we're a setter
|
||||
if (value !== undefined) {
|
||||
// Store the new value
|
||||
filterValue = value;
|
||||
filterValueLowercase = value.toLowerCase();
|
||||
// Change which objects appear in the table
|
||||
updateFilteredObjects();
|
||||
}
|
||||
|
||||
// Always act as a getter
|
||||
return filterValue;
|
||||
}
|
||||
|
||||
// Update the bounds (width and height) of this view;
|
||||
// called from the mct-resize directive. Recalculates how
|
||||
// many rows should appear in the contained table.
|
||||
function setBounds(bounds) {
|
||||
var availableSpace = bounds.height - SLIDER_HEIGHT;
|
||||
rows = Math.max(1, Math.floor(availableSpace / ROW_HEIGHT));
|
||||
}
|
||||
|
||||
// Increment the current column width, up to the defined maximum.
|
||||
// When the max is hit, roll back to the default.
|
||||
function increaseColumnWidth() {
|
||||
columnWidth += COLUMN_WIDTH_STEP;
|
||||
// Cycle down to the initial width instead of exceeding max
|
||||
columnWidth = columnWidth > MAX_COLUMN_WIDTH ?
|
||||
INITIAL_COLUMN_WIDTH : columnWidth;
|
||||
}
|
||||
|
||||
// Get displayable text for last-updated value
|
||||
function updated() {
|
||||
return updateText;
|
||||
}
|
||||
|
||||
// Unsubscribe, if a subscription is active.
|
||||
function releaseSubscription() {
|
||||
if (subscription) {
|
||||
subscription.unsubscribe();
|
||||
subscription = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// Update set of telemetry objects managed by this view
|
||||
function updateTelemetryObjects(telemetryObjects) {
|
||||
updateFilteredObjects();
|
||||
limits = {};
|
||||
telemetryObjects.forEach(function (telemetryObject) {
|
||||
var id = telemetryObject.getId();
|
||||
limits[id] = telemetryObject.getCapability('limit');
|
||||
});
|
||||
}
|
||||
|
||||
// Create a subscription for the represented domain object.
|
||||
// This will resolve capability delegation as necessary.
|
||||
function makeSubscription(domainObject) {
|
||||
// Unsubscribe, if there is an existing subscription
|
||||
releaseSubscription();
|
||||
|
||||
// Clear updated timestamp
|
||||
lastUpdated = {};
|
||||
updateText = NOT_UPDATED;
|
||||
|
||||
// Create a new subscription; telemetrySubscriber gets
|
||||
// to do the meaningful work here.
|
||||
subscription = domainObject && telemetrySubscriber.subscribe(
|
||||
domainObject,
|
||||
updateValues
|
||||
);
|
||||
|
||||
// Our set of in-view telemetry objects may have changed,
|
||||
// so update the set that is being passed down to the table.
|
||||
updateFilteredObjects();
|
||||
}
|
||||
|
||||
// Watch for changes to the set of objects which have telemetry
|
||||
$scope.$watch(subscribedTelemetry, updateTelemetryObjects);
|
||||
|
||||
// Watch for the represented domainObject (this field will
|
||||
// be populated by mct-representation)
|
||||
$scope.$watch("domainObject", makeSubscription);
|
||||
|
||||
// Make sure we unsubscribe when this view is destroyed.
|
||||
$scope.$on("$destroy", releaseSubscription);
|
||||
|
||||
return {
|
||||
/**
|
||||
* Get the number of rows which should be shown in this table.
|
||||
* @return {number} the number of rows to show
|
||||
*/
|
||||
getRows: function () {
|
||||
return rows;
|
||||
},
|
||||
/**
|
||||
* Get the objects which should currently be displayed in
|
||||
* this table. This will be watched, so the return value
|
||||
* should be stable when this list is unchanging. Only
|
||||
* objects which match the user-entered filter value should
|
||||
* be returned here.
|
||||
* @return {DomainObject[]} the domain objects to include in
|
||||
* this table.
|
||||
*/
|
||||
getTelemetryObjects: function () {
|
||||
return filteredObjects;
|
||||
},
|
||||
/**
|
||||
* Set the bounds (width/height) of this autoflow tabular view.
|
||||
* The template must ensure that these bounds are tracked on
|
||||
* the table area only.
|
||||
* @param bounds the bounds; and object with `width` and
|
||||
* `height` properties, both as numbers, in pixels.
|
||||
*/
|
||||
setBounds: setBounds,
|
||||
/**
|
||||
* Increments the width of the autoflow column.
|
||||
* Setting does not yet persist.
|
||||
*/
|
||||
increaseColumnWidth: increaseColumnWidth,
|
||||
/**
|
||||
* Get-or-set the user-supplied filter value.
|
||||
* @param {string} [value] the new filter value; omit to use
|
||||
* as a getter
|
||||
* @returns {string} the user-supplied filter value
|
||||
*/
|
||||
filter: filter,
|
||||
/**
|
||||
* Get all range values for use in this table. These will be
|
||||
* returned as an object of key-value pairs, where keys are
|
||||
* domain object IDs, and values are the most recently observed
|
||||
* data values associated with those objects, formatted for
|
||||
* display.
|
||||
* @returns {object.<string,string>} most recent values
|
||||
*/
|
||||
rangeValues: function () {
|
||||
return rangeValues;
|
||||
},
|
||||
/**
|
||||
* Get CSS classes to apply to specific rows, representing limit
|
||||
* states and/or stale states. These are returned as key-value
|
||||
* pairs where keys are domain object IDs, and values are CSS
|
||||
* classes to display for domain objects with those IDs.
|
||||
* @returns {object.<string,string>} CSS classes
|
||||
*/
|
||||
classes: function () {
|
||||
return classes;
|
||||
},
|
||||
/**
|
||||
* Get the "last updated" text for this view; this will be
|
||||
* the most recent timestamp observed for any telemetry-
|
||||
* providing object, formatted for display.
|
||||
* @returns {string} the time of the most recent update
|
||||
*/
|
||||
updated: updated,
|
||||
/**
|
||||
* Get the current column width, in pixels.
|
||||
* @returns {number} column width
|
||||
*/
|
||||
columnWidth: function () {
|
||||
return columnWidth;
|
||||
},
|
||||
/**
|
||||
* Keep a counter and increment this whenever the display
|
||||
* should be updated; this will be watched by the
|
||||
* `mct-autoflow-table`.
|
||||
* @returns {number} a counter value
|
||||
*/
|
||||
counter: function () {
|
||||
return counter;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return AutflowTabularController;
|
||||
}
|
||||
);
|
60
platform/features/autoflow/src/MCTAutoflowTable.js
Executable file
60
platform/features/autoflow/src/MCTAutoflowTable.js
Executable file
@ -0,0 +1,60 @@
|
||||
|
||||
define(
|
||||
["./AutoflowTableLinker"],
|
||||
function (AutoflowTableLinker) {
|
||||
|
||||
/**
|
||||
* The `mct-autoflow-table` directive specifically supports
|
||||
* autoflow tabular views; it is not intended for use outside
|
||||
* of that view.
|
||||
*
|
||||
* This directive is responsible for creating the structure
|
||||
* of the table in this view, and for updating its values.
|
||||
* While this is achievable using a regular Angular template,
|
||||
* this is undesirable from the perspective of performance
|
||||
* due to the number of watches that can be involved for large
|
||||
* tables. Instead, this directive will maintain a small number
|
||||
* of watches, rebuilding table structure only when necessary,
|
||||
* and updating displayed values in the more common case of
|
||||
* new data arriving.
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
function MCTAutoflowTable() {
|
||||
return {
|
||||
// Only applicable at the element level
|
||||
restrict: "E",
|
||||
|
||||
// The link function; handles DOM update/manipulation
|
||||
link: AutoflowTableLinker,
|
||||
|
||||
// Parameters to pass from attributes into scope
|
||||
scope: {
|
||||
// Set of domain objects to show in the table
|
||||
objects: "=",
|
||||
|
||||
// Values for those objects, by ID
|
||||
values: "=",
|
||||
|
||||
// CSS classes to show for objects, by ID
|
||||
classes: "=",
|
||||
|
||||
// Number of rows to show before autoflowing
|
||||
rows: "=",
|
||||
|
||||
// Time of last update; watched to refresh values
|
||||
updated: "=",
|
||||
|
||||
// Current width of the autoflow column
|
||||
columnWidth: "=",
|
||||
|
||||
// A counter used to trigger display updates
|
||||
counter: "="
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return MCTAutoflowTable;
|
||||
|
||||
}
|
||||
);
|
178
platform/features/autoflow/test/AutoflowTableLinkerSpec.js
Executable file
178
platform/features/autoflow/test/AutoflowTableLinkerSpec.js
Executable file
@ -0,0 +1,178 @@
|
||||
|
||||
define(
|
||||
["../src/AutoflowTableLinker"],
|
||||
function (AutoflowTableLinker) {
|
||||
|
||||
describe("The mct-autoflow-table linker", function () {
|
||||
var cachedAngular,
|
||||
mockAngular,
|
||||
mockScope,
|
||||
mockElement,
|
||||
mockElements,
|
||||
linker;
|
||||
|
||||
// Utility function to generate more mock elements
|
||||
function createMockElement(html) {
|
||||
var mockEl = jasmine.createSpyObj(
|
||||
"element-" + html,
|
||||
[
|
||||
"append",
|
||||
"addClass",
|
||||
"removeClass",
|
||||
"text",
|
||||
"attr",
|
||||
"html",
|
||||
"css",
|
||||
"find"
|
||||
]
|
||||
);
|
||||
mockEl.testHtml = html;
|
||||
mockEl.append.andReturn(mockEl);
|
||||
mockElements.push(mockEl);
|
||||
return mockEl;
|
||||
}
|
||||
|
||||
function createMockDomainObject(id) {
|
||||
var mockDomainObject = jasmine.createSpyObj(
|
||||
"domainObject-" + id,
|
||||
["getId", "getModel"]
|
||||
);
|
||||
mockDomainObject.getId.andReturn(id);
|
||||
mockDomainObject.getModel.andReturn({name: id.toUpperCase()});
|
||||
return mockDomainObject;
|
||||
}
|
||||
|
||||
function fireWatch(watchExpression, value) {
|
||||
mockScope.$watch.calls.forEach(function (call) {
|
||||
if (call.args[0] === watchExpression) {
|
||||
call.args[1](value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// AutoflowTableLinker accesses Angular in the global
|
||||
// scope, since it is not injectable; we simulate that
|
||||
// here by adding/removing it to/from the window object.
|
||||
beforeEach(function () {
|
||||
mockElements = [];
|
||||
|
||||
mockAngular = jasmine.createSpyObj("angular", ["element"]);
|
||||
mockScope = jasmine.createSpyObj("scope", ["$watch"]);
|
||||
mockElement = createMockElement('<div>');
|
||||
|
||||
mockAngular.element.andCallFake(createMockElement);
|
||||
|
||||
if (window.angular !== undefined) {
|
||||
cachedAngular = window.angular;
|
||||
}
|
||||
window.angular = mockAngular;
|
||||
|
||||
linker = new AutoflowTableLinker(mockScope, mockElement);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
if (cachedAngular !== undefined) {
|
||||
window.angular = cachedAngular;
|
||||
} else {
|
||||
delete window.angular;
|
||||
}
|
||||
});
|
||||
|
||||
it("watches for changes in inputs", function () {
|
||||
expect(mockScope.$watch).toHaveBeenCalledWith(
|
||||
"objects",
|
||||
jasmine.any(Function)
|
||||
);
|
||||
expect(mockScope.$watch).toHaveBeenCalledWith(
|
||||
"rows",
|
||||
jasmine.any(Function)
|
||||
);
|
||||
expect(mockScope.$watch).toHaveBeenCalledWith(
|
||||
"counter",
|
||||
jasmine.any(Function)
|
||||
);
|
||||
});
|
||||
|
||||
it("changes structure when domain objects change", function () {
|
||||
// Set up scope
|
||||
mockScope.rows = 4;
|
||||
mockScope.objects = ['a', 'b', 'c', 'd', 'e', 'f']
|
||||
.map(createMockDomainObject);
|
||||
|
||||
// Fire an update to the set of objects
|
||||
fireWatch("objects");
|
||||
|
||||
// Should have rebuilt with two columns of
|
||||
// four and two rows each; first, by clearing...
|
||||
expect(mockElement.html).toHaveBeenCalledWith("");
|
||||
|
||||
// Should have appended two columns...
|
||||
expect(mockElement.append.calls.length).toEqual(2);
|
||||
|
||||
// ...which should have received two and four rows each
|
||||
expect(mockElement.append.calls[0].args[0].append.calls.length)
|
||||
.toEqual(4);
|
||||
expect(mockElement.append.calls[1].args[0].append.calls.length)
|
||||
.toEqual(2);
|
||||
});
|
||||
|
||||
it("updates values", function () {
|
||||
var mockSpans;
|
||||
|
||||
mockScope.objects = ['a', 'b', 'c', 'd', 'e', 'f']
|
||||
.map(createMockDomainObject);
|
||||
mockScope.values = { a: 0 };
|
||||
|
||||
// Fire an update to the set of values
|
||||
fireWatch("objects");
|
||||
fireWatch("updated");
|
||||
|
||||
// Get all created spans
|
||||
mockSpans = mockElements.filter(function (mockElem) {
|
||||
return mockElem.testHtml === '<span>';
|
||||
});
|
||||
|
||||
// First span should be a, should have gotten this value.
|
||||
// This test detects, in particular, WTD-749
|
||||
expect(mockSpans[0].text).toHaveBeenCalledWith('A');
|
||||
expect(mockSpans[1].text).toHaveBeenCalledWith(0);
|
||||
});
|
||||
|
||||
it("listens for changes in column width", function () {
|
||||
var mockUL = createMockElement("<ul>");
|
||||
mockElement.find.andReturn(mockUL);
|
||||
mockScope.columnWidth = 200;
|
||||
fireWatch("columnWidth", mockScope.columnWidth);
|
||||
expect(mockUL.css).toHaveBeenCalledWith("width", "200px");
|
||||
});
|
||||
|
||||
it("updates CSS classes", function () {
|
||||
var mockSpans;
|
||||
|
||||
mockScope.objects = ['a', 'b', 'c', 'd', 'e', 'f']
|
||||
.map(createMockDomainObject);
|
||||
mockScope.values = { a: "a value to find" };
|
||||
mockScope.classes = { a: 'class-a' };
|
||||
|
||||
// Fire an update to the set of values
|
||||
fireWatch("objects");
|
||||
fireWatch("updated");
|
||||
|
||||
// Figure out which span holds the relevant value...
|
||||
mockSpans = mockElements.filter(function (mockElem) {
|
||||
return mockElem.testHtml === '<span>';
|
||||
}).filter(function (mockSpan) {
|
||||
var attrCalls = mockSpan.attr.calls;
|
||||
return attrCalls.some(function (call) {
|
||||
return call.args[0] === 'title' &&
|
||||
call.args[1] === mockScope.values.a;
|
||||
});
|
||||
});
|
||||
|
||||
// ...and make sure it also has had its class applied
|
||||
expect(mockSpans[0].addClass)
|
||||
.toHaveBeenCalledWith(mockScope.classes.a);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
341
platform/features/autoflow/test/AutoflowTabularControllerSpec.js
Executable file
341
platform/features/autoflow/test/AutoflowTabularControllerSpec.js
Executable file
@ -0,0 +1,341 @@
|
||||
|
||||
define(
|
||||
["../src/AutoflowTabularController"],
|
||||
function (AutoflowTabularController) {
|
||||
|
||||
describe("The autoflow tabular controller", function () {
|
||||
var mockScope,
|
||||
mockTimeout,
|
||||
mockSubscriber,
|
||||
mockDomainObject,
|
||||
mockSubscription,
|
||||
controller;
|
||||
|
||||
// Fire watches that are registered as functions.
|
||||
function fireFnWatches() {
|
||||
mockScope.$watch.calls.forEach(function (call) {
|
||||
if (typeof call.args[0] === 'function') {
|
||||
call.args[1](call.args[0]());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
mockScope = jasmine.createSpyObj(
|
||||
"$scope",
|
||||
["$on", "$watch"]
|
||||
);
|
||||
mockTimeout = jasmine.createSpy("$timeout");
|
||||
mockSubscriber = jasmine.createSpyObj(
|
||||
"telemetrySubscriber",
|
||||
["subscribe"]
|
||||
);
|
||||
mockDomainObject = jasmine.createSpyObj(
|
||||
"domainObject",
|
||||
["getId", "getModel", "getCapability"]
|
||||
);
|
||||
mockSubscription = jasmine.createSpyObj(
|
||||
"subscription",
|
||||
[
|
||||
"unsubscribe",
|
||||
"getTelemetryObjects",
|
||||
"getDomainValue",
|
||||
"getRangeValue"
|
||||
]
|
||||
);
|
||||
|
||||
mockSubscriber.subscribe.andReturn(mockSubscription);
|
||||
mockDomainObject.getModel.andReturn({name: "something"});
|
||||
|
||||
controller = new AutoflowTabularController(
|
||||
mockScope,
|
||||
mockTimeout,
|
||||
mockSubscriber
|
||||
);
|
||||
});
|
||||
|
||||
it("listens for the represented domain object", function () {
|
||||
expect(mockScope.$watch).toHaveBeenCalledWith(
|
||||
"domainObject",
|
||||
jasmine.any(Function)
|
||||
);
|
||||
});
|
||||
|
||||
it("provides a getter-setter function for filtering", function () {
|
||||
expect(controller.filter()).toEqual("");
|
||||
controller.filter("something");
|
||||
expect(controller.filter()).toEqual("something");
|
||||
});
|
||||
|
||||
it("tracks bounds and adjust number of rows accordingly", function () {
|
||||
// Rows are 15px high, and need room for an 10px slider
|
||||
controller.setBounds({ width: 700, height: 120 });
|
||||
expect(controller.getRows()).toEqual(6); // 110 usable height / 16px
|
||||
controller.setBounds({ width: 700, height: 240 });
|
||||
expect(controller.getRows()).toEqual(14); // 230 usable height / 16px
|
||||
});
|
||||
|
||||
it("subscribes to a represented object's telemetry", function () {
|
||||
// Set up subscription, scope
|
||||
mockSubscription.getTelemetryObjects
|
||||
.andReturn([mockDomainObject]);
|
||||
mockScope.domainObject = mockDomainObject;
|
||||
|
||||
// Invoke the watcher with represented domain object
|
||||
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
|
||||
|
||||
// Should have subscribed to it
|
||||
expect(mockSubscriber.subscribe).toHaveBeenCalledWith(
|
||||
mockDomainObject,
|
||||
jasmine.any(Function)
|
||||
);
|
||||
|
||||
// Should report objects as reported from subscription
|
||||
expect(controller.getTelemetryObjects())
|
||||
.toEqual([mockDomainObject]);
|
||||
});
|
||||
|
||||
it("releases subscriptions on destroy", function () {
|
||||
// Set up subscription...
|
||||
mockSubscription.getTelemetryObjects
|
||||
.andReturn([mockDomainObject]);
|
||||
mockScope.domainObject = mockDomainObject;
|
||||
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
|
||||
|
||||
// Verify precondition
|
||||
expect(mockSubscription.unsubscribe).not.toHaveBeenCalled();
|
||||
|
||||
// Make sure we're listening for $destroy
|
||||
expect(mockScope.$on).toHaveBeenCalledWith(
|
||||
"$destroy",
|
||||
jasmine.any(Function)
|
||||
);
|
||||
|
||||
// Fire a destroy event
|
||||
mockScope.$on.mostRecentCall.args[1]();
|
||||
|
||||
// Should have unsubscribed
|
||||
expect(mockSubscription.unsubscribe).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("presents latest values and latest update state", function () {
|
||||
// Make sure values are available
|
||||
mockSubscription.getDomainValue.andReturn(402654321123);
|
||||
mockSubscription.getRangeValue.andReturn(789);
|
||||
mockDomainObject.getId.andReturn('testId');
|
||||
|
||||
// Set up subscription...
|
||||
mockSubscription.getTelemetryObjects
|
||||
.andReturn([mockDomainObject]);
|
||||
mockScope.domainObject = mockDomainObject;
|
||||
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
|
||||
|
||||
// Fire subscription callback
|
||||
mockSubscriber.subscribe.mostRecentCall.args[1]();
|
||||
|
||||
// ...and exposed the results for template to consume
|
||||
expect(controller.updated()).toEqual("1982-278 08:25:21.123Z");
|
||||
expect(controller.rangeValues().testId).toEqual(789);
|
||||
});
|
||||
|
||||
it("sorts domain objects by index", function () {
|
||||
var testIndexes = { a: 2, b: 1, c: 3, d: 0 },
|
||||
mockDomainObjects = Object.keys(testIndexes).sort().map(function (id) {
|
||||
var mockDomainObj = jasmine.createSpyObj(
|
||||
"domainObject",
|
||||
["getId", "getModel"]
|
||||
);
|
||||
|
||||
mockDomainObj.getId.andReturn(id);
|
||||
mockDomainObj.getModel.andReturn({ index: testIndexes[id] });
|
||||
|
||||
return mockDomainObj;
|
||||
});
|
||||
|
||||
// Expose those domain objects...
|
||||
mockSubscription.getTelemetryObjects.andReturn(mockDomainObjects);
|
||||
mockScope.domainObject = mockDomainObject;
|
||||
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
|
||||
|
||||
// Fire subscription callback
|
||||
mockSubscriber.subscribe.mostRecentCall.args[1]();
|
||||
|
||||
// Controller should expose same objects, but sorted by index from model
|
||||
expect(controller.getTelemetryObjects()).toEqual([
|
||||
mockDomainObjects[3], // d, index=0
|
||||
mockDomainObjects[1], // b, index=1
|
||||
mockDomainObjects[0], // a, index=2
|
||||
mockDomainObjects[2] // c, index=3
|
||||
]);
|
||||
});
|
||||
|
||||
it("uses a timeout to throttle update", function () {
|
||||
// Set up subscription...
|
||||
mockSubscription.getTelemetryObjects
|
||||
.andReturn([mockDomainObject]);
|
||||
mockScope.domainObject = mockDomainObject;
|
||||
|
||||
// Set the object in view; should not need a timeout
|
||||
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
|
||||
expect(mockTimeout.calls.length).toEqual(0);
|
||||
|
||||
// Next call should schedule an update on a timeout
|
||||
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
|
||||
expect(mockTimeout.calls.length).toEqual(1);
|
||||
|
||||
// ...but this last one should not, since existing
|
||||
// timeout will cover it
|
||||
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
|
||||
expect(mockTimeout.calls.length).toEqual(1);
|
||||
});
|
||||
|
||||
it("allows changing column width", function () {
|
||||
var initialWidth = controller.columnWidth();
|
||||
controller.increaseColumnWidth();
|
||||
expect(controller.columnWidth()).toBeGreaterThan(initialWidth);
|
||||
});
|
||||
|
||||
describe("filter", function () {
|
||||
var doFilter,
|
||||
filteredObjects,
|
||||
filteredObjectNames;
|
||||
|
||||
beforeEach(function () {
|
||||
var telemetryObjects,
|
||||
updateFilteredObjects;
|
||||
|
||||
telemetryObjects = [
|
||||
'DEF123',
|
||||
'abc789',
|
||||
'456abc',
|
||||
'4ab3cdef',
|
||||
'hjs[12].*(){}^\\'
|
||||
].map(function (objectName, index) {
|
||||
var mockTelemetryObject = jasmine.createSpyObj(
|
||||
objectName,
|
||||
["getId", "getModel"]
|
||||
);
|
||||
|
||||
mockTelemetryObject.getId.andReturn(objectName);
|
||||
mockTelemetryObject.getModel.andReturn({
|
||||
name: objectName,
|
||||
index: index
|
||||
});
|
||||
|
||||
return mockTelemetryObject;
|
||||
});
|
||||
|
||||
mockSubscription
|
||||
.getTelemetryObjects
|
||||
.andReturn(telemetryObjects);
|
||||
|
||||
// Trigger domainObject change to create subscription.
|
||||
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
|
||||
|
||||
updateFilteredObjects = function () {
|
||||
filteredObjects = controller.getTelemetryObjects();
|
||||
filteredObjectNames = filteredObjects.map(function (o) {
|
||||
return o.getModel().name;
|
||||
});
|
||||
};
|
||||
|
||||
doFilter = function (term) {
|
||||
controller.filter(term);
|
||||
// Filter is debounced so we have to force it to occur.
|
||||
mockTimeout.mostRecentCall.args[0]();
|
||||
updateFilteredObjects();
|
||||
};
|
||||
|
||||
updateFilteredObjects();
|
||||
});
|
||||
|
||||
it("initially shows all objects", function () {
|
||||
expect(filteredObjectNames).toEqual([
|
||||
'DEF123',
|
||||
'abc789',
|
||||
'456abc',
|
||||
'4ab3cdef',
|
||||
'hjs[12].*(){}^\\'
|
||||
]);
|
||||
});
|
||||
|
||||
it("by blank string matches all objects", function () {
|
||||
doFilter('');
|
||||
expect(filteredObjectNames).toEqual([
|
||||
'DEF123',
|
||||
'abc789',
|
||||
'456abc',
|
||||
'4ab3cdef',
|
||||
'hjs[12].*(){}^\\'
|
||||
]);
|
||||
});
|
||||
|
||||
it("exactly matches an object name", function () {
|
||||
doFilter('4ab3cdef');
|
||||
expect(filteredObjectNames).toEqual(['4ab3cdef']);
|
||||
});
|
||||
|
||||
it("partially matches object names", function () {
|
||||
doFilter('abc');
|
||||
expect(filteredObjectNames).toEqual([
|
||||
'abc789',
|
||||
'456abc'
|
||||
]);
|
||||
});
|
||||
|
||||
it("matches case insensitive names", function () {
|
||||
doFilter('def');
|
||||
expect(filteredObjectNames).toEqual([
|
||||
'DEF123',
|
||||
'4ab3cdef'
|
||||
]);
|
||||
});
|
||||
|
||||
it("works as expected with special characters", function () {
|
||||
doFilter('[12]');
|
||||
expect(filteredObjectNames).toEqual(['hjs[12].*(){}^\\']);
|
||||
doFilter('.*');
|
||||
expect(filteredObjectNames).toEqual(['hjs[12].*(){}^\\']);
|
||||
doFilter('.*()');
|
||||
expect(filteredObjectNames).toEqual(['hjs[12].*(){}^\\']);
|
||||
doFilter('.*?');
|
||||
expect(filteredObjectNames).toEqual([]);
|
||||
doFilter('.+');
|
||||
expect(filteredObjectNames).toEqual([]);
|
||||
});
|
||||
|
||||
it("exposes CSS classes from limits", function () {
|
||||
var id = mockDomainObject.getId(),
|
||||
testClass = "some-css-class",
|
||||
mockLimitCapability =
|
||||
jasmine.createSpyObj('limit', ['evaluate']);
|
||||
|
||||
mockDomainObject.getCapability.andCallFake(function (key) {
|
||||
return key === 'limit' && mockLimitCapability;
|
||||
});
|
||||
mockLimitCapability.evaluate
|
||||
.andReturn({ cssClass: testClass });
|
||||
|
||||
mockSubscription.getTelemetryObjects
|
||||
.andReturn([mockDomainObject]);
|
||||
|
||||
fireFnWatches();
|
||||
mockSubscriber.subscribe.mostRecentCall.args[1]();
|
||||
|
||||
expect(controller.classes()[id]).toEqual(testClass);
|
||||
});
|
||||
|
||||
it("exposes a counter that changes with each update", function () {
|
||||
var i, prior;
|
||||
|
||||
for (i = 0; i < 10; i += 1) {
|
||||
prior = controller.counter();
|
||||
expect(controller.counter()).toEqual(prior);
|
||||
mockSubscriber.subscribe.mostRecentCall.args[1]();
|
||||
expect(controller.counter()).not.toEqual(prior);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
39
platform/features/autoflow/test/MCTAutoflowTableSpec.js
Executable file
39
platform/features/autoflow/test/MCTAutoflowTableSpec.js
Executable file
@ -0,0 +1,39 @@
|
||||
|
||||
define(
|
||||
["../src/MCTAutoflowTable"],
|
||||
function (MCTAutoflowTable) {
|
||||
|
||||
describe("The mct-autoflow-table directive", function () {
|
||||
var mctAutoflowTable;
|
||||
|
||||
beforeEach(function () {
|
||||
mctAutoflowTable = new MCTAutoflowTable();
|
||||
});
|
||||
|
||||
// Real functionality is contained/tested in the linker,
|
||||
// so just check to make sure we're exposing the directive
|
||||
// appropriately.
|
||||
it("is applicable at the element level", function () {
|
||||
expect(mctAutoflowTable.restrict).toEqual("E");
|
||||
});
|
||||
|
||||
it("two-ways binds needed scope variables", function () {
|
||||
expect(mctAutoflowTable.scope).toEqual({
|
||||
objects: "=",
|
||||
values: "=",
|
||||
rows: "=",
|
||||
updated: "=",
|
||||
classes: "=",
|
||||
columnWidth: "=",
|
||||
counter: "="
|
||||
});
|
||||
});
|
||||
|
||||
it("provides a link function", function () {
|
||||
expect(mctAutoflowTable.link).toEqual(jasmine.any(Function));
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
}
|
||||
);
|
@ -41,7 +41,7 @@ define(
|
||||
scope,
|
||||
element
|
||||
) {
|
||||
this.conductor = openmct.conductor;
|
||||
this.timeAPI = openmct.time;
|
||||
this.scope = scope;
|
||||
this.element = element;
|
||||
|
||||
@ -51,24 +51,26 @@ define(
|
||||
}
|
||||
|
||||
ConductorRepresenter.prototype.boundsListener = function (bounds) {
|
||||
var timeSystem = this.timeAPI.getTimeSystem(this.timeAPI.timeSystem());
|
||||
this.scope.$broadcast('telemetry:display:bounds', {
|
||||
start: bounds.start,
|
||||
end: bounds.end,
|
||||
domain: this.conductor.timeSystem().metadata.key
|
||||
}, this.conductor.follow());
|
||||
domain: timeSystem
|
||||
}, this.timeAPI.follow());
|
||||
};
|
||||
|
||||
ConductorRepresenter.prototype.timeSystemListener = function (timeSystem) {
|
||||
var bounds = this.conductor.bounds();
|
||||
ConductorRepresenter.prototype.timeSystemListener = function (key) {
|
||||
var bounds = this.timeAPI.bounds();
|
||||
var timeSystem = this.timeAPI.getTimeSystem(key);
|
||||
this.scope.$broadcast('telemetry:display:bounds', {
|
||||
start: bounds.start,
|
||||
end: bounds.end,
|
||||
domain: timeSystem.metadata.key
|
||||
}, this.conductor.follow());
|
||||
domain: timeSystem
|
||||
}, this.timeAPI.follow());
|
||||
};
|
||||
|
||||
ConductorRepresenter.prototype.followListener = function () {
|
||||
this.boundsListener(this.conductor.bounds());
|
||||
this.boundsListener(this.timeAPI.bounds());
|
||||
};
|
||||
|
||||
// Handle a specific representation of a specific domain object
|
||||
@ -76,16 +78,16 @@ define(
|
||||
if (representation.key === 'browse-object') {
|
||||
this.destroy();
|
||||
|
||||
this.conductor.on("bounds", this.boundsListener);
|
||||
this.conductor.on("timeSystem", this.timeSystemListener);
|
||||
this.conductor.on("follow", this.followListener);
|
||||
this.timeAPI.on("bounds", this.boundsListener);
|
||||
this.timeAPI.on("timeSystem", this.timeSystemListener);
|
||||
this.timeAPI.on("follow", this.followListener);
|
||||
}
|
||||
};
|
||||
|
||||
ConductorRepresenter.prototype.destroy = function destroy() {
|
||||
this.conductor.off("bounds", this.boundsListener);
|
||||
this.conductor.off("timeSystem", this.timeSystemListener);
|
||||
this.conductor.off("follow", this.followListener);
|
||||
this.timeAPI.off("bounds", this.boundsListener);
|
||||
this.timeAPI.off("timeSystem", this.timeSystemListener);
|
||||
this.timeAPI.off("follow", this.followListener);
|
||||
};
|
||||
|
||||
return ConductorRepresenter;
|
||||
|
@ -71,8 +71,7 @@ define([
|
||||
"openmct",
|
||||
"timeConductorViewService",
|
||||
"formatService",
|
||||
"DEFAULT_TIMECONDUCTOR_MODE",
|
||||
"SHOW_TIMECONDUCTOR"
|
||||
"CONDUCTOR_CONFIG"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -151,13 +150,6 @@ define([
|
||||
"link": "https://github.com/d3/d3/blob/master/LICENSE"
|
||||
}
|
||||
],
|
||||
"constants": [
|
||||
{
|
||||
"key": "DEFAULT_TIMECONDUCTOR_MODE",
|
||||
"value": "realtime",
|
||||
"priority": "fallback"
|
||||
}
|
||||
],
|
||||
"formats": [
|
||||
{
|
||||
"key": "number",
|
||||
|
@ -346,7 +346,28 @@
|
||||
content: $i;
|
||||
}
|
||||
.l-axis-holder {
|
||||
$c0: rgba($colorBodyFg, 0.1);
|
||||
$c2: transparent;
|
||||
$grabTicksH: 3px;
|
||||
$grabTicksXSpace: 4px;
|
||||
$grabTicksYOffset: 0;
|
||||
@include cursorGrab();
|
||||
svg {
|
||||
$c1: rgba($colorBodyFg, 0.2);
|
||||
$angle: 90deg;
|
||||
@include background-image(linear-gradient($angle,
|
||||
$c1 1px, $c2 1px,
|
||||
$c2 100%
|
||||
));
|
||||
background-position: center $grabTicksYOffset;
|
||||
background-repeat: repeat-x;
|
||||
background-size: $grabTicksXSpace $grabTicksH;
|
||||
}
|
||||
&:hover {
|
||||
@include background-image(linear-gradient(
|
||||
$c0 70%, $c2 100%
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -22,8 +22,8 @@
|
||||
<div class="contents">
|
||||
<div class="pane left menu-items">
|
||||
<ul>
|
||||
<li ng-repeat="(key, metadata) in ngModel.options"
|
||||
ng-click="ngModel.selectedKey=key">
|
||||
<li ng-repeat="metadata in ngModel.options"
|
||||
ng-click="ngModel.selected = metadata">
|
||||
<a ng-mouseover="ngModel.activeMetadata = metadata"
|
||||
ng-mouseleave="ngModel.activeMetadata = undefined"
|
||||
class="menu-item-a {{metadata.cssClass}}">
|
||||
@ -33,8 +33,7 @@
|
||||
</ul>
|
||||
</div>
|
||||
<div class="pane right menu-item-description">
|
||||
<div
|
||||
class="desc-area ui-symbol icon type-icon {{ngModel.activeMetadata.cssClass}}"></div>
|
||||
<div class="desc-area ui-symbol icon type-icon {{ngModel.activeMetadata.cssClass}}"></div>
|
||||
<div class="desc-area title">
|
||||
{{ngModel.activeMetadata.name}}
|
||||
</div>
|
||||
|
@ -22,8 +22,7 @@
|
||||
<span ng-controller="ClickAwayController as modeController">
|
||||
<div class="s-menu-button"
|
||||
ng-click="modeController.toggle()">
|
||||
<span class="title-label">{{ngModel.options[ngModel.selectedKey]
|
||||
.label}}</span>
|
||||
<span class="title-label">{{ngModel.selected.name}}</span>
|
||||
</div>
|
||||
<div class="menu super-menu mini mode-selector-menu"
|
||||
ng-show="modeController.isActive()">
|
||||
|
@ -1,7 +1,7 @@
|
||||
<!-- Parent holder for time conductor. follow-mode | fixed-mode -->
|
||||
<div ng-controller="TimeConductorController as tcController"
|
||||
class="holder grows flex-elem l-flex-row l-time-conductor {{modeModel.selectedKey}}-mode {{timeSystemModel.selected.metadata.key}}-time-system"
|
||||
ng-class="{'status-panning': tcController.panning}" ng-show="showTimeConductor">
|
||||
ng-class="{'status-panning': tcController.panning}">
|
||||
<div class="flex-elem holder time-conductor-icon">
|
||||
<div class="hand-little"></div>
|
||||
<div class="hand-big"></div>
|
||||
@ -99,14 +99,14 @@
|
||||
<div class="l-time-conductor-controls l-row-elem l-flex-row flex-elem">
|
||||
<mct-include
|
||||
key="'mode-selector'"
|
||||
ng-model="modeModel"
|
||||
ng-model="tcController.menu"
|
||||
class="holder flex-elem menus-up mode-selector">
|
||||
</mct-include>
|
||||
<mct-control
|
||||
key="'menu-button'"
|
||||
class="holder flex-elem menus-up time-system"
|
||||
structure="{
|
||||
text: timeSystemModel.selected.metadata.name,
|
||||
text: timeSystemModel.selected.name,
|
||||
click: tcController.selectTimeSystemByKey,
|
||||
options: timeSystemModel.options
|
||||
}">
|
||||
|
@ -1,47 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT Web, Copyright (c) 2014-2015, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT Web includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define([], function () {
|
||||
/**
|
||||
* A tick source is an event generator such as a timing signal, or
|
||||
* indicator of data availability, which can be used to advance the Time
|
||||
* Conductor. Usage is simple, a listener registers a callback which is
|
||||
* invoked when this source 'ticks'.
|
||||
*
|
||||
* @interface
|
||||
* @constructor
|
||||
*/
|
||||
function TickSource() {
|
||||
this.listeners = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callback Function to be called when this tick source ticks.
|
||||
* @returns an 'unlisten' function that will remove the callback from
|
||||
* the registered listeners
|
||||
*/
|
||||
TickSource.prototype.listen = function (callback) {
|
||||
throw new Error('Not implemented');
|
||||
};
|
||||
|
||||
return TickSource;
|
||||
});
|
@ -1,107 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT Web, Copyright (c) 2014-2015, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT Web includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define([], function () {
|
||||
/**
|
||||
* @interface
|
||||
* @constructor
|
||||
*/
|
||||
function TimeSystem() {
|
||||
/**
|
||||
* @typedef TimeSystemMetadata
|
||||
* @property {string} key
|
||||
* @property {string} name
|
||||
* @property {string} description
|
||||
*
|
||||
* @type {TimeSystemMetadata}
|
||||
*/
|
||||
this.metadata = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Time formats are defined as extensions. Time systems that implement
|
||||
* this interface should provide an array of format keys supported by them.
|
||||
*
|
||||
* @returns {string[]} An array of time format keys
|
||||
*/
|
||||
TimeSystem.prototype.formats = function () {
|
||||
throw new Error('Not implemented');
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef DeltaFormat
|
||||
* @property {string} type the type of MctControl used to represent this
|
||||
* field. Typically 'datetime-field' for UTC based dates, or 'textfield'
|
||||
* otherwise
|
||||
* @property {string} [format] An optional field specifying the
|
||||
* Format to use for delta fields in this time system.
|
||||
*/
|
||||
/**
|
||||
* Specifies a format for deltas in this time system.
|
||||
*
|
||||
* @returns {DeltaFormat} a delta format specifier
|
||||
*/
|
||||
TimeSystem.prototype.deltaFormat = function () {
|
||||
throw new Error('Not implemented');
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the tick sources supported by this time system. Tick sources
|
||||
* are event generators that can be used to advance the time conductor
|
||||
* @returns {TickSource[]} The tick sources supported by this time system.
|
||||
*/
|
||||
TimeSystem.prototype.tickSources = function () {
|
||||
throw new Error('Not implemented');
|
||||
};
|
||||
|
||||
/***
|
||||
*
|
||||
* @typedef {object} TimeConductorZoom
|
||||
* @property {number} min The largest time span that the time
|
||||
* conductor can display in this time system. ie. the span of the time
|
||||
* conductor in its most zoomed out state.
|
||||
* @property {number} max The smallest time span that the time
|
||||
* conductor can display in this time system. ie. the span of the time
|
||||
* conductor bounds in its most zoomed in state.
|
||||
*
|
||||
* @typedef {object} TimeSystemDefault
|
||||
* @property {TimeConductorDeltas} deltas The deltas to apply by default
|
||||
* when this time system is active. Applies to real-time modes only
|
||||
* @property {TimeConductorBounds} bounds The bounds to apply by default
|
||||
* when this time system is active
|
||||
* @property {TimeConductorZoom} zoom Default min and max zoom levels
|
||||
* @returns {TimeSystemDefault[]} At least one set of default values for
|
||||
* this time system.
|
||||
*/
|
||||
TimeSystem.prototype.defaults = function () {
|
||||
throw new Error('Not implemented');
|
||||
};
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
TimeSystem.prototype.isUTCBased = function () {
|
||||
return true;
|
||||
};
|
||||
|
||||
return TimeSystem;
|
||||
});
|
@ -35,14 +35,14 @@ define(
|
||||
function ConductorAxisController(openmct, formatService, conductorViewService, scope, element) {
|
||||
// Dependencies
|
||||
this.formatService = formatService;
|
||||
this.conductor = openmct.conductor;
|
||||
this.timeAPI = openmct.time;
|
||||
this.conductorViewService = conductorViewService;
|
||||
|
||||
this.scope = scope;
|
||||
this.initialized = false;
|
||||
|
||||
this.bounds = this.conductor.bounds();
|
||||
this.timeSystem = this.conductor.timeSystem();
|
||||
this.bounds = this.timeAPI.bounds();
|
||||
this.timeSystem = this.timeAPI.timeSystem();
|
||||
|
||||
//Bind all class functions to 'this'
|
||||
Object.keys(ConductorAxisController.prototype).filter(function (key) {
|
||||
@ -58,8 +58,8 @@ define(
|
||||
* @private
|
||||
*/
|
||||
ConductorAxisController.prototype.destroy = function () {
|
||||
this.conductor.off('timeSystem', this.changeTimeSystem);
|
||||
this.conductor.off('bounds', this.changeBounds);
|
||||
this.timeAPI.off('timeSystem', this.changeTimeSystem);
|
||||
this.timeAPI.off('bounds', this.changeBounds);
|
||||
this.conductorViewService.off("zoom", this.onZoom);
|
||||
this.conductorViewService.off("zoom-stop", this.onZoomStop);
|
||||
};
|
||||
@ -87,8 +87,8 @@ define(
|
||||
}
|
||||
|
||||
//Respond to changes in conductor
|
||||
this.conductor.on("timeSystem", this.changeTimeSystem);
|
||||
this.conductor.on("bounds", this.changeBounds);
|
||||
this.timeAPI.on("timeSystem", this.changeTimeSystem);
|
||||
this.timeAPI.on("bounds", this.changeBounds);
|
||||
|
||||
this.scope.$on("$destroy", this.destroy);
|
||||
|
||||
@ -111,7 +111,7 @@ define(
|
||||
*/
|
||||
ConductorAxisController.prototype.setScale = function () {
|
||||
var width = this.target.offsetWidth;
|
||||
var timeSystem = this.conductor.timeSystem();
|
||||
var timeSystem = this.timeAPI.getTimeSystem(this.timeAPI.timeSystem());
|
||||
var bounds = this.bounds;
|
||||
|
||||
if (timeSystem.isUTCBased()) {
|
||||
@ -134,13 +134,15 @@ define(
|
||||
* When the time system changes, update the scale and formatter used for showing times.
|
||||
* @param timeSystem
|
||||
*/
|
||||
ConductorAxisController.prototype.changeTimeSystem = function (timeSystem) {
|
||||
this.timeSystem = timeSystem;
|
||||
ConductorAxisController.prototype.changeTimeSystem = function (key) {
|
||||
var timeSystem = this.timeAPI.getTimeSystem(key);
|
||||
|
||||
this.timeSystem = key;
|
||||
|
||||
var key = timeSystem.formats()[0];
|
||||
if (key !== undefined) {
|
||||
var format = this.formatService.getFormat(key);
|
||||
var bounds = this.conductor.bounds();
|
||||
var bounds = this.timeAPI.bounds();
|
||||
|
||||
//The D3 scale used depends on the type of time system as d3
|
||||
// supports UTC out of the box.
|
||||
@ -178,7 +180,7 @@ define(
|
||||
ConductorAxisController.prototype.panStop = function () {
|
||||
//resync view bounds with time conductor bounds
|
||||
this.conductorViewService.emit("pan-stop");
|
||||
this.conductor.bounds(this.bounds);
|
||||
this.timeAPI.bounds(this.bounds);
|
||||
};
|
||||
|
||||
/**
|
||||
@ -214,9 +216,9 @@ define(
|
||||
* @fires platform.features.conductor.ConductorAxisController~pan
|
||||
*/
|
||||
ConductorAxisController.prototype.pan = function (delta) {
|
||||
if (!this.conductor.follow()) {
|
||||
if (!this.timeAPI.follow()) {
|
||||
var deltaInMs = delta[0] * this.msPerPixel;
|
||||
var bounds = this.conductor.bounds();
|
||||
var bounds = this.timeAPI.bounds();
|
||||
var start = Math.floor((bounds.start - deltaInMs) / 1000) * 1000;
|
||||
var end = Math.floor((bounds.end - deltaInMs) / 1000) * 1000;
|
||||
this.bounds = {
|
||||
|
@ -30,7 +30,7 @@ define(
|
||||
* @memberof platform.features.conductor
|
||||
*/
|
||||
function ConductorTOIController($scope, openmct, conductorViewService) {
|
||||
this.conductor = openmct.conductor;
|
||||
this.timeAPI = openmct.time;
|
||||
this.conductorViewService = conductorViewService;
|
||||
|
||||
//Bind all class functions to 'this'
|
||||
@ -40,11 +40,11 @@ define(
|
||||
this[key] = ConductorTOIController.prototype[key].bind(this);
|
||||
}.bind(this));
|
||||
|
||||
this.conductor.on('timeOfInterest', this.changeTimeOfInterest);
|
||||
this.timeAPI.on('timeOfInterest', this.changeTimeOfInterest);
|
||||
this.conductorViewService.on('zoom', this.setOffsetFromZoom);
|
||||
this.conductorViewService.on('pan', this.setOffsetFromBounds);
|
||||
|
||||
var timeOfInterest = this.conductor.timeOfInterest();
|
||||
var timeOfInterest = this.timeAPI.timeOfInterest();
|
||||
if (timeOfInterest) {
|
||||
this.changeTimeOfInterest(timeOfInterest);
|
||||
}
|
||||
@ -56,7 +56,7 @@ define(
|
||||
* @private
|
||||
*/
|
||||
ConductorTOIController.prototype.destroy = function () {
|
||||
this.conductor.off('timeOfInterest', this.changeTimeOfInterest);
|
||||
this.timeAPI.off('timeOfInterest', this.changeTimeOfInterest);
|
||||
this.conductorViewService.off('zoom', this.setOffsetFromZoom);
|
||||
this.conductorViewService.off('pan', this.setOffsetFromBounds);
|
||||
};
|
||||
@ -70,7 +70,7 @@ define(
|
||||
* @param {TimeConductorBounds} bounds
|
||||
*/
|
||||
ConductorTOIController.prototype.setOffsetFromBounds = function (bounds) {
|
||||
var toi = this.conductor.timeOfInterest();
|
||||
var toi = this.timeAPI.timeOfInterest();
|
||||
if (toi !== undefined) {
|
||||
var offset = toi - bounds.start;
|
||||
var duration = bounds.end - bounds.start;
|
||||
@ -94,7 +94,7 @@ define(
|
||||
* @private
|
||||
*/
|
||||
ConductorTOIController.prototype.changeTimeOfInterest = function () {
|
||||
var bounds = this.conductor.bounds();
|
||||
var bounds = this.timeAPI.bounds();
|
||||
if (bounds) {
|
||||
this.setOffsetFromBounds(bounds);
|
||||
}
|
||||
@ -112,10 +112,10 @@ define(
|
||||
var width = element.width();
|
||||
var relativeX = e.pageX - element.offset().left;
|
||||
var percX = relativeX / width;
|
||||
var bounds = this.conductor.bounds();
|
||||
var bounds = this.timeAPI.bounds();
|
||||
var timeRange = bounds.end - bounds.start;
|
||||
|
||||
this.conductor.timeOfInterest(timeRange * percX + bounds.start);
|
||||
this.timeAPI.timeOfInterest(timeRange * percX + bounds.start);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -47,8 +47,7 @@ define(
|
||||
openmct,
|
||||
conductorViewService,
|
||||
formatService,
|
||||
DEFAULT_MODE,
|
||||
SHOW_TIMECONDUCTOR
|
||||
config
|
||||
) {
|
||||
|
||||
var self = this;
|
||||
@ -64,19 +63,22 @@ define(
|
||||
this.$window = $window;
|
||||
this.$location = $location;
|
||||
this.conductorViewService = conductorViewService;
|
||||
this.conductor = openmct.conductor;
|
||||
this.timeAPI = openmct.time;
|
||||
this.modes = conductorViewService.availableModes();
|
||||
this.validation = new TimeConductorValidation(this.conductor);
|
||||
this.validation = new TimeConductorValidation(this.timeAPI);
|
||||
this.formatService = formatService;
|
||||
this.config = config;
|
||||
|
||||
//Check if the default mode defined is actually available
|
||||
if (this.modes[DEFAULT_MODE] === undefined) {
|
||||
DEFAULT_MODE = 'fixed';
|
||||
}
|
||||
this.DEFAULT_MODE = DEFAULT_MODE;
|
||||
var options = this.optionsFromConfig(config);
|
||||
this.menu = {
|
||||
selected: options[0],
|
||||
options: options
|
||||
};
|
||||
|
||||
// Construct the provided time system definitions
|
||||
this.timeSystems = conductorViewService.systems;
|
||||
this.timeSystems = config.menuOptions.map(function (menuOption){
|
||||
return openmct.time.getTimeSystem(menuOption.timeSystem);
|
||||
});
|
||||
|
||||
this.initializeScope();
|
||||
var searchParams = JSON.parse(JSON.stringify(this.$location.search()));
|
||||
@ -84,9 +86,9 @@ define(
|
||||
this.setStateFromSearchParams(searchParams);
|
||||
|
||||
//Set the initial state of the UI from the conductor state
|
||||
var timeSystem = this.conductor.timeSystem();
|
||||
var timeSystem = this.timeAPI.timeSystem();
|
||||
if (timeSystem) {
|
||||
this.changeTimeSystem(this.conductor.timeSystem());
|
||||
this.changeTimeSystem(timeSystem);
|
||||
}
|
||||
|
||||
var deltas = this.conductorViewService.deltas();
|
||||
@ -94,7 +96,7 @@ define(
|
||||
this.setFormFromDeltas(deltas);
|
||||
}
|
||||
|
||||
var bounds = this.conductor.bounds();
|
||||
var bounds = this.timeAPI.bounds();
|
||||
if (bounds && bounds.start !== undefined && bounds.end !== undefined) {
|
||||
this.changeBounds(bounds);
|
||||
}
|
||||
@ -105,12 +107,31 @@ define(
|
||||
}.bind(this));
|
||||
|
||||
//Respond to any subsequent conductor changes
|
||||
this.conductor.on('bounds', this.changeBounds);
|
||||
this.conductor.on('timeSystem', this.changeTimeSystem);
|
||||
|
||||
this.$scope.showTimeConductor = SHOW_TIMECONDUCTOR;
|
||||
this.timeAPI.on('bounds', this.changeBounds);
|
||||
this.timeAPI.on('timeSystem', this.changeTimeSystem);
|
||||
}
|
||||
|
||||
TimeConductorController.prototype.optionsFromConfig = function (config) {
|
||||
var options = [{
|
||||
name: 'Fixed Timespan Mode',
|
||||
description: 'Query and explore data that falls between two fixed datetimes',
|
||||
cssClass: 'icon-calendar',
|
||||
clock: undefined
|
||||
}];
|
||||
var timeAPI = this.timeAPI;
|
||||
|
||||
(config.menuOptions || []).forEach(function (menuOption) {
|
||||
options.push({
|
||||
name: menuOption.name,
|
||||
description: menuOption.description,
|
||||
cssClass: menuOption.cssClass || '',
|
||||
clock: timeAPI.getClock(menuOption.clock)
|
||||
});
|
||||
}.bind(this));
|
||||
|
||||
return options;
|
||||
};
|
||||
|
||||
/**
|
||||
* Used as a url search param setter in place of $location.search(...)
|
||||
*
|
||||
@ -129,7 +150,7 @@ define(
|
||||
*/
|
||||
TimeConductorController.prototype.initializeScope = function () {
|
||||
//Set time Conductor bounds in the form
|
||||
this.$scope.boundsModel = this.conductor.bounds();
|
||||
this.$scope.boundsModel = this.timeAPI.bounds();
|
||||
|
||||
//If conductor has a time system selected already, populate the
|
||||
//form from it
|
||||
@ -154,11 +175,11 @@ define(
|
||||
//Set mode from url if changed
|
||||
if (searchParams[SEARCH.MODE] === undefined ||
|
||||
searchParams[SEARCH.MODE] !== this.$scope.modeModel.selectedKey) {
|
||||
this.setMode(searchParams[SEARCH.MODE] || this.DEFAULT_MODE);
|
||||
this.setMode(searchParams[SEARCH.MODE] || 'fixed');
|
||||
}
|
||||
|
||||
if (searchParams[SEARCH.TIME_SYSTEM] &&
|
||||
searchParams[SEARCH.TIME_SYSTEM] !== this.conductor.timeSystem().metadata.key) {
|
||||
searchParams[SEARCH.TIME_SYSTEM] !== this.timeAPI.timeSystem()) {
|
||||
//Will select the specified time system on the conductor
|
||||
this.selectTimeSystemByKey(searchParams[SEARCH.TIME_SYSTEM]);
|
||||
}
|
||||
@ -184,7 +205,7 @@ define(
|
||||
!isNaN(searchParams[SEARCH.END_BOUND]);
|
||||
|
||||
if (validBounds) {
|
||||
this.conductor.bounds({
|
||||
this.timeAPI.bounds({
|
||||
start: parseInt(searchParams[SEARCH.START_BOUND]),
|
||||
end: parseInt(searchParams[SEARCH.END_BOUND])
|
||||
});
|
||||
@ -195,8 +216,8 @@ define(
|
||||
* @private
|
||||
*/
|
||||
TimeConductorController.prototype.destroy = function () {
|
||||
this.conductor.off('bounds', this.changeBounds);
|
||||
this.conductor.off('timeSystem', this.changeTimeSystem);
|
||||
this.timeAPI.off('bounds', this.changeBounds);
|
||||
this.timeAPI.off('timeSystem', this.changeTimeSystem);
|
||||
|
||||
this.conductorViewService.off('pan', this.onPan);
|
||||
this.conductorViewService.off('pan-stop', this.onPanStop);
|
||||
@ -253,10 +274,7 @@ define(
|
||||
this.$scope.modeModel.selectedKey = mode;
|
||||
//Synchronize scope with time system on mode
|
||||
this.$scope.timeSystemModel.options =
|
||||
this.conductorViewService.availableTimeSystems()
|
||||
.map(function (t) {
|
||||
return t.metadata;
|
||||
});
|
||||
this.conductorViewService.availableTimeSystems();
|
||||
};
|
||||
|
||||
/**
|
||||
@ -289,7 +307,7 @@ define(
|
||||
* @param formModel
|
||||
*/
|
||||
TimeConductorController.prototype.setBounds = function (boundsModel) {
|
||||
this.conductor.bounds({
|
||||
this.timeAPI.bounds({
|
||||
start: boundsModel.start,
|
||||
end: boundsModel.end
|
||||
});
|
||||
@ -364,7 +382,7 @@ define(
|
||||
})[0];
|
||||
if (selected) {
|
||||
this.supportsZoom = !!(selected.defaults() && selected.defaults().zoom);
|
||||
this.conductor.timeSystem(selected, selected.defaults().bounds);
|
||||
this.timeAPI.timeSystem(selected, selected.defaults().bounds);
|
||||
}
|
||||
};
|
||||
|
||||
@ -376,9 +394,11 @@ define(
|
||||
*
|
||||
* @param newTimeSystem
|
||||
*/
|
||||
TimeConductorController.prototype.changeTimeSystem = function (newTimeSystem) {
|
||||
TimeConductorController.prototype.changeTimeSystem = function (key) {
|
||||
var newTimeSystem = this.timeAPI.getTimeSystem(key);
|
||||
|
||||
//Set time system in URL on change
|
||||
this.setParam(SEARCH.TIME_SYSTEM, newTimeSystem.metadata.key);
|
||||
this.setParam(SEARCH.TIME_SYSTEM, key);
|
||||
|
||||
if (newTimeSystem && (newTimeSystem !== this.$scope.timeSystemModel.selected)) {
|
||||
this.supportsZoom = !!(newTimeSystem.defaults() && newTimeSystem.defaults().zoom);
|
||||
@ -401,9 +421,9 @@ define(
|
||||
* @returns {number} a value between 0.01 and 0.99, in increments of .01
|
||||
*/
|
||||
TimeConductorController.prototype.toSliderValue = function (timeSpan) {
|
||||
var timeSystem = this.conductor.timeSystem();
|
||||
var timeSystem = this.timeAPI.getTimeSystem(this.timeAPI.timeSystem());
|
||||
if (timeSystem) {
|
||||
var zoomDefaults = this.conductor.timeSystem().defaults().zoom;
|
||||
var zoomDefaults = timeSystem.defaults().zoom;
|
||||
var perc = timeSpan / (zoomDefaults.min - zoomDefaults.max);
|
||||
return 1 - Math.pow(perc, 1 / 4);
|
||||
}
|
||||
@ -415,8 +435,9 @@ define(
|
||||
* @param {TimeSpan} timeSpan
|
||||
*/
|
||||
TimeConductorController.prototype.toTimeUnits = function (timeSpan) {
|
||||
if (this.conductor.timeSystem()) {
|
||||
var timeFormat = this.formatService.getFormat(this.conductor.timeSystem().formats()[0]);
|
||||
var timeSystem = this.timeAPI.getTimeSystem(this.timeAPI.timeSystem());
|
||||
if (this.timeAPI.timeSystem()) {
|
||||
var timeFormat = this.formatService.getFormat(timeSystem.formats()[0]);
|
||||
this.$scope.timeUnits = timeFormat.timeUnits && timeFormat.timeUnits(timeSpan);
|
||||
}
|
||||
};
|
||||
@ -429,7 +450,8 @@ define(
|
||||
* @param bounds
|
||||
*/
|
||||
TimeConductorController.prototype.onZoom = function (sliderValue) {
|
||||
var zoomDefaults = this.conductor.timeSystem().defaults().zoom;
|
||||
var timeSystem = this.timeAPI.getTimeSystem(this.timeAPI.timeSystem());
|
||||
var zoomDefaults = timeSystem.defaults().zoom;
|
||||
var timeSpan = Math.pow((1 - sliderValue), 4) * (zoomDefaults.min - zoomDefaults.max);
|
||||
|
||||
var zoom = this.conductorViewService.zoom(timeSpan);
|
||||
|
@ -31,38 +31,23 @@ define(
|
||||
* @memberof platform.features.conductor
|
||||
* @param {TimeConductorMetadata} metadata
|
||||
*/
|
||||
function TimeConductorMode(metadata, conductor, timeSystems) {
|
||||
this.conductor = conductor;
|
||||
function TimeConductorMode(metadata, timeAPI) {
|
||||
this.timeAPI = timeAPI;
|
||||
|
||||
this.mdata = metadata;
|
||||
this.deltasVal = undefined;
|
||||
this.source = undefined;
|
||||
this.sourceUnlisten = undefined;
|
||||
this.systems = timeSystems;
|
||||
this.availableSources = undefined;
|
||||
this.changeTimeSystem = this.changeTimeSystem.bind(this);
|
||||
this.tick = this.tick.bind(this);
|
||||
|
||||
var timeSystem = this.timeAPI.timeSystem();
|
||||
|
||||
//Set the time system initially
|
||||
if (conductor.timeSystem()) {
|
||||
this.changeTimeSystem(conductor.timeSystem());
|
||||
if (timeSystem) {
|
||||
this.changeTimeSystem(timeSystem);
|
||||
}
|
||||
|
||||
//Listen for subsequent changes to time system
|
||||
conductor.on('timeSystem', this.changeTimeSystem);
|
||||
timeAPI.on('timeSystem', this.changeTimeSystem);
|
||||
|
||||
if (metadata.key === 'fixed') {
|
||||
//Fixed automatically supports all time systems
|
||||
this.availableSystems = timeSystems;
|
||||
} else {
|
||||
this.availableSystems = timeSystems.filter(function (timeSystem) {
|
||||
//Only include time systems that have tick sources that
|
||||
// support the current mode
|
||||
return timeSystem.tickSources().some(function (tickSource) {
|
||||
return metadata.key === tickSource.metadata.mode;
|
||||
});
|
||||
});
|
||||
}
|
||||
this.availableSystems = timeAPI.availableTimeSystems();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -70,7 +55,8 @@ define(
|
||||
* @param timeSystem
|
||||
* @returns {TimeSystem} the currently selected time system
|
||||
*/
|
||||
TimeConductorMode.prototype.changeTimeSystem = function (timeSystem) {
|
||||
TimeConductorMode.prototype.changeTimeSystem = function (key) {
|
||||
var timeSystem = this.timeAPI.getTimeSystem(key);
|
||||
// On time system change, apply default deltas
|
||||
var defaults = timeSystem.defaults() || {
|
||||
bounds: {
|
||||
@ -83,20 +69,8 @@ define(
|
||||
}
|
||||
};
|
||||
|
||||
this.conductor.bounds(defaults.bounds);
|
||||
this.timeAPI.bounds(defaults.bounds);
|
||||
this.deltas(defaults.deltas);
|
||||
|
||||
// Tick sources are mode-specific, so restrict tick sources to only those supported by the current mode.
|
||||
var key = this.mdata.key;
|
||||
var tickSources = timeSystem.tickSources();
|
||||
if (tickSources) {
|
||||
this.availableSources = tickSources.filter(function (source) {
|
||||
return source.metadata.mode === key;
|
||||
});
|
||||
}
|
||||
|
||||
// Set an appropriate tick source from the new time system
|
||||
this.tickSource(this.availableTickSources(timeSystem)[0]);
|
||||
};
|
||||
|
||||
/**
|
||||
@ -110,15 +84,6 @@ define(
|
||||
return this.availableSystems;
|
||||
};
|
||||
|
||||
/**
|
||||
* Tick sources are mode-specific. This returns a filtered list of the tick sources available in the currently selected mode
|
||||
* @param timeSystem
|
||||
* @returns {Array.<T>}
|
||||
*/
|
||||
TimeConductorMode.prototype.availableTickSources = function (timeSystem) {
|
||||
return this.availableSources;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get or set tick source. Setting tick source will also start
|
||||
* listening to it and unlisten from any existing tick source
|
||||
@ -127,49 +92,28 @@ define(
|
||||
*/
|
||||
TimeConductorMode.prototype.tickSource = function (tickSource) {
|
||||
if (arguments.length > 0) {
|
||||
if (this.sourceUnlisten) {
|
||||
this.sourceUnlisten();
|
||||
}
|
||||
this.source = tickSource;
|
||||
if (tickSource) {
|
||||
this.sourceUnlisten = tickSource.listen(this.tick);
|
||||
//Now following a tick source
|
||||
this.conductor.follow(true);
|
||||
} else {
|
||||
this.conductor.follow(false);
|
||||
}
|
||||
var timeSystem = this.timeAPI.getTimeSystem(this.timeAPI.timeSystem());
|
||||
var defaults = timeSystem.defaults() || {
|
||||
bounds: {
|
||||
start: 0,
|
||||
end: 0
|
||||
},
|
||||
deltas: {
|
||||
start: 0,
|
||||
end: 0
|
||||
}
|
||||
};
|
||||
|
||||
this.timeAPI.tickSource(tickSource, defaults.deltas);
|
||||
}
|
||||
return this.source;
|
||||
return this.timeAPI.tickSource();
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
TimeConductorMode.prototype.destroy = function () {
|
||||
this.conductor.off('timeSystem', this.changeTimeSystem);
|
||||
|
||||
if (this.sourceUnlisten) {
|
||||
this.sourceUnlisten();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {number} time some value that is valid in the current TimeSystem
|
||||
*/
|
||||
TimeConductorMode.prototype.tick = function (time) {
|
||||
var deltas = this.deltas();
|
||||
var startTime = time;
|
||||
var endTime = time;
|
||||
|
||||
if (deltas) {
|
||||
startTime = time - deltas.start;
|
||||
endTime = time + deltas.end;
|
||||
}
|
||||
this.conductor.bounds({
|
||||
start: startTime,
|
||||
end: endTime
|
||||
});
|
||||
this.timeAPI.off('timeSystem', this.changeTimeSystem);
|
||||
};
|
||||
|
||||
/**
|
||||
@ -182,12 +126,13 @@ define(
|
||||
TimeConductorMode.prototype.deltas = function (deltas) {
|
||||
if (arguments.length !== 0) {
|
||||
var bounds = this.calculateBoundsFromDeltas(deltas);
|
||||
this.deltasVal = deltas;
|
||||
this.timeAPI.clockOffsets(deltas);
|
||||
|
||||
if (this.metadata().key !== 'fixed') {
|
||||
this.conductor.bounds(bounds);
|
||||
this.timeAPI.bounds(bounds);
|
||||
}
|
||||
}
|
||||
return this.deltasVal;
|
||||
return this.timeAPI.clockOffsets();
|
||||
};
|
||||
|
||||
/**
|
||||
@ -195,11 +140,12 @@ define(
|
||||
* @returns {TimeConductorBounds}
|
||||
*/
|
||||
TimeConductorMode.prototype.calculateBoundsFromDeltas = function (deltas) {
|
||||
var oldEnd = this.conductor.bounds().end;
|
||||
var oldEnd = this.timeAPI.bounds().end;
|
||||
var offsets = this.timeAPI.clockOffsets();
|
||||
|
||||
if (this.deltasVal && this.deltasVal.end !== undefined) {
|
||||
if (offsets && offsets.end !== undefined) {
|
||||
//Calculate the previous raw end value (without delta)
|
||||
oldEnd = oldEnd - this.deltasVal.end;
|
||||
oldEnd = oldEnd - offsets.end;
|
||||
}
|
||||
|
||||
var bounds = {
|
||||
@ -222,18 +168,19 @@ define(
|
||||
*/
|
||||
TimeConductorMode.prototype.calculateZoom = function (timeSpan) {
|
||||
var zoom = {};
|
||||
var offsets;
|
||||
|
||||
// If a tick source is defined, then the concept of 'now' is
|
||||
// important. Calculate zoom based on 'now'.
|
||||
if (this.tickSource()) {
|
||||
if (this.timeAPI.follow()) {
|
||||
offsets = this.timeAPI.clockOffsets();
|
||||
zoom.deltas = {
|
||||
start: timeSpan,
|
||||
end: this.deltasVal.end
|
||||
end: offsets.end
|
||||
};
|
||||
zoom.bounds = this.calculateBoundsFromDeltas(zoom.deltas);
|
||||
// Calculate bounds based on deltas;
|
||||
} else {
|
||||
var bounds = this.conductor.bounds();
|
||||
var bounds = this.timeAPI.bounds();
|
||||
var center = bounds.start + ((bounds.end - bounds.start)) / 2;
|
||||
bounds.start = center - timeSpan / 2;
|
||||
bounds.end = center + timeSpan / 2;
|
||||
|
@ -29,9 +29,9 @@ define(
|
||||
* @param conductor
|
||||
* @constructor
|
||||
*/
|
||||
function TimeConductorValidation(conductor) {
|
||||
function TimeConductorValidation(timeAPI) {
|
||||
var self = this;
|
||||
this.conductor = conductor;
|
||||
this.timeAPI = timeAPI;
|
||||
|
||||
/*
|
||||
* Bind all class functions to 'this'
|
||||
@ -47,13 +47,13 @@ define(
|
||||
* Validation methods below are invoked directly from controls in the TimeConductor form
|
||||
*/
|
||||
TimeConductorValidation.prototype.validateStart = function (start) {
|
||||
var bounds = this.conductor.bounds();
|
||||
return this.conductor.validateBounds({start: start, end: bounds.end}) === true;
|
||||
var bounds = this.timeAPI.bounds();
|
||||
return this.timeAPI.validateBounds({start: start, end: bounds.end}) === true;
|
||||
};
|
||||
|
||||
TimeConductorValidation.prototype.validateEnd = function (end) {
|
||||
var bounds = this.conductor.bounds();
|
||||
return this.conductor.validateBounds({start: bounds.start, end: end}) === true;
|
||||
var bounds = this.timeAPI.bounds();
|
||||
return this.timeAPI.validateBounds({start: bounds.start, end: end}) === true;
|
||||
};
|
||||
|
||||
TimeConductorValidation.prototype.validateStartDelta = function (startDelta) {
|
||||
|
@ -41,11 +41,7 @@ define(
|
||||
|
||||
EventEmitter.call(this);
|
||||
|
||||
this.systems = timeSystems.map(function (timeSystemConstructor) {
|
||||
return timeSystemConstructor();
|
||||
});
|
||||
|
||||
this.conductor = openmct.conductor;
|
||||
this.timeAPI = openmct.time;
|
||||
this.currentMode = undefined;
|
||||
|
||||
/**
|
||||
@ -67,39 +63,25 @@ define(
|
||||
}
|
||||
};
|
||||
|
||||
function hasTickSource(sourceType, timeSystem) {
|
||||
return timeSystem.tickSources().some(function (tickSource) {
|
||||
return tickSource.metadata.mode === sourceType;
|
||||
});
|
||||
}
|
||||
|
||||
var timeSystemsForMode = function (sourceType) {
|
||||
return this.systems.filter(hasTickSource.bind(this, sourceType));
|
||||
}.bind(this);
|
||||
|
||||
//Only show 'real-time mode' if appropriate time systems available
|
||||
if (timeSystemsForMode('realtime').length > 0) {
|
||||
var realtimeMode = {
|
||||
key: 'realtime',
|
||||
cssClass: 'icon-clock',
|
||||
label: 'Real-time',
|
||||
name: 'Real-time Mode',
|
||||
description: 'Monitor real-time streaming data as it comes in. The Time Conductor and displays will automatically advance themselves based on a UTC clock.'
|
||||
};
|
||||
this.availModes[realtimeMode.key] = realtimeMode;
|
||||
}
|
||||
var realtimeMode = {
|
||||
key: 'realtime',
|
||||
cssClass: 'icon-clock',
|
||||
label: 'Real-time',
|
||||
name: 'Real-time Mode',
|
||||
description: 'Monitor real-time streaming data as it comes in. The Time Conductor and displays will automatically advance themselves based on a UTC clock.'
|
||||
};
|
||||
this.availModes[realtimeMode.key] = realtimeMode;
|
||||
|
||||
//Only show 'LAD mode' if appropriate time systems available
|
||||
if (timeSystemsForMode('lad').length > 0) {
|
||||
var ladMode = {
|
||||
key: 'lad',
|
||||
cssClass: 'icon-database',
|
||||
label: 'LAD',
|
||||
name: 'LAD Mode',
|
||||
description: 'Latest Available Data mode monitors real-time streaming data as it comes in. The Time Conductor and displays will only advance when data becomes available.'
|
||||
};
|
||||
this.availModes[ladMode.key] = ladMode;
|
||||
}
|
||||
var ladMode = {
|
||||
key: 'lad',
|
||||
cssClass: 'icon-database',
|
||||
label: 'LAD',
|
||||
name: 'LAD Mode',
|
||||
description: 'Latest Available Data mode monitors real-time streaming data as it comes in. The Time Conductor and displays will only advance when data becomes available.'
|
||||
};
|
||||
this.availModes[ladMode.key] = ladMode;
|
||||
}
|
||||
|
||||
TimeConductorViewService.prototype = Object.create(EventEmitter.prototype);
|
||||
@ -126,25 +108,25 @@ define(
|
||||
TimeConductorViewService.prototype.mode = function (newModeKey) {
|
||||
function contains(timeSystems, ts) {
|
||||
return timeSystems.filter(function (t) {
|
||||
return t.metadata.key === ts.metadata.key;
|
||||
return t.key === ts.key;
|
||||
}).length > 0;
|
||||
}
|
||||
|
||||
if (arguments.length === 1) {
|
||||
var timeSystem = this.conductor.timeSystem();
|
||||
var timeSystem = this.timeAPI.getTimeSystem(this.timeAPI.timeSystem());
|
||||
var modes = this.availableModes();
|
||||
var modeMetaData = modes[newModeKey];
|
||||
|
||||
if (this.currentMode) {
|
||||
this.currentMode.destroy();
|
||||
}
|
||||
this.currentMode = new TimeConductorMode(modeMetaData, this.conductor, this.systems);
|
||||
this.currentMode = new TimeConductorMode(modeMetaData, this.timeAPI);
|
||||
|
||||
// If no time system set on time conductor, or the currently selected time system is not available in
|
||||
// the new mode, default to first available time system
|
||||
if (!timeSystem || !contains(this.currentMode.availableTimeSystems(), timeSystem)) {
|
||||
timeSystem = this.currentMode.availableTimeSystems()[0];
|
||||
this.conductor.timeSystem(timeSystem, timeSystem.defaults().bounds);
|
||||
this.timeAPI.timeSystem(timeSystem.key, timeSystem.defaults().bounds);
|
||||
}
|
||||
}
|
||||
return this.currentMode ? this.currentMode.metadata().key : undefined;
|
||||
@ -201,7 +183,7 @@ define(
|
||||
* mode. Time systems and tick sources are mode dependent
|
||||
*/
|
||||
TimeConductorViewService.prototype.availableTimeSystems = function () {
|
||||
return this.currentMode.availableTimeSystems();
|
||||
return this.timeAPI.availableTimeSystems();
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -31,7 +31,7 @@ define(
|
||||
* @constructor
|
||||
*/
|
||||
function TimeOfInterestController($scope, openmct, formatService) {
|
||||
this.conductor = openmct.conductor;
|
||||
this.timeAPI = openmct.time;
|
||||
this.formatService = formatService;
|
||||
this.format = undefined;
|
||||
this.toiText = undefined;
|
||||
@ -44,11 +44,11 @@ define(
|
||||
this[key] = TimeOfInterestController.prototype[key].bind(this);
|
||||
}.bind(this));
|
||||
|
||||
this.conductor.on('timeOfInterest', this.changeTimeOfInterest);
|
||||
this.conductor.on('timeSystem', this.changeTimeSystem);
|
||||
if (this.conductor.timeSystem()) {
|
||||
this.changeTimeSystem(this.conductor.timeSystem());
|
||||
var toi = this.conductor.timeOfInterest();
|
||||
this.timeAPI.on('timeOfInterest', this.changeTimeOfInterest);
|
||||
this.timeAPI.on('timeSystem', this.changeTimeSystem);
|
||||
if (this.timeAPI.timeSystem()) {
|
||||
this.changeTimeSystem(this.timeAPI.timeSystem());
|
||||
var toi = this.timeAPI.timeOfInterest();
|
||||
if (toi) {
|
||||
this.changeTimeOfInterest(toi);
|
||||
}
|
||||
@ -76,7 +76,8 @@ define(
|
||||
* When time system is changed, update the formatter used to
|
||||
* display the current TOI label
|
||||
*/
|
||||
TimeOfInterestController.prototype.changeTimeSystem = function (timeSystem) {
|
||||
TimeOfInterestController.prototype.changeTimeSystem = function (key) {
|
||||
var timeSystem = this.timeAPI.getTimeSystem(key);
|
||||
this.format = this.formatService.getFormat(timeSystem.formats()[0]);
|
||||
};
|
||||
|
||||
@ -84,8 +85,8 @@ define(
|
||||
* @private
|
||||
*/
|
||||
TimeOfInterestController.prototype.destroy = function () {
|
||||
this.conductor.off('timeOfInterest', this.changeTimeOfInterest);
|
||||
this.conductor.off('timeSystem', this.changeTimeSystem);
|
||||
this.timeAPI.off('timeOfInterest', this.changeTimeOfInterest);
|
||||
this.timeAPI.off('timeSystem', this.changeTimeSystem);
|
||||
};
|
||||
|
||||
/**
|
||||
@ -93,7 +94,7 @@ define(
|
||||
* Time Conductor
|
||||
*/
|
||||
TimeOfInterestController.prototype.dismiss = function () {
|
||||
this.conductor.timeOfInterest(undefined);
|
||||
this.timeAPI.timeOfInterest(undefined);
|
||||
};
|
||||
|
||||
/**
|
||||
@ -101,7 +102,7 @@ define(
|
||||
* the TOI displayed in views.
|
||||
*/
|
||||
TimeOfInterestController.prototype.resync = function () {
|
||||
this.conductor.timeOfInterest(this.conductor.timeOfInterest());
|
||||
this.timeAPI.timeOfInterest(this.timeAPI.timeOfInterest());
|
||||
};
|
||||
|
||||
return TimeOfInterestController;
|
||||
|
@ -34,7 +34,8 @@ define(
|
||||
function LayoutCompositionPolicy() {
|
||||
}
|
||||
|
||||
LayoutCompositionPolicy.prototype.allow = function (parentType, child) {
|
||||
LayoutCompositionPolicy.prototype.allow = function (parent, child) {
|
||||
var parentType = parent.getCapability('type');
|
||||
if (parentType.instanceOf('layout') &&
|
||||
child.getCapability('type').instanceOf('folder')) {
|
||||
|
||||
|
@ -25,6 +25,7 @@ define(
|
||||
function (LayoutCompositionPolicy) {
|
||||
describe("Layout's composition policy", function () {
|
||||
var mockChild,
|
||||
mockCandidateObj,
|
||||
mockCandidate,
|
||||
mockContext,
|
||||
candidateType,
|
||||
@ -41,6 +42,11 @@ define(
|
||||
mockContext =
|
||||
jasmine.createSpyObj('contextType', ['instanceOf']);
|
||||
|
||||
mockCandidateObj = jasmine.createSpyObj('domainObj', [
|
||||
'getCapability'
|
||||
]);
|
||||
mockCandidateObj.getCapability.andReturn(mockCandidate);
|
||||
|
||||
mockChild.getCapability.andReturn(mockContext);
|
||||
|
||||
mockCandidate.instanceOf.andCallFake(function (t) {
|
||||
@ -56,19 +62,19 @@ define(
|
||||
it("disallows folders in layouts", function () {
|
||||
candidateType = 'layout';
|
||||
contextType = 'folder';
|
||||
expect(policy.allow(mockCandidate, mockChild)).toBe(false);
|
||||
expect(policy.allow(mockCandidateObj, mockChild)).toBe(false);
|
||||
});
|
||||
|
||||
it("does not disallow folders elsewhere", function () {
|
||||
candidateType = 'nonlayout';
|
||||
contextType = 'folder';
|
||||
expect(policy.allow(mockCandidate, mockChild)).toBe(true);
|
||||
expect(policy.allow(mockCandidateObj, mockChild)).toBe(true);
|
||||
});
|
||||
|
||||
it("allows things other than folders in layouts", function () {
|
||||
candidateType = 'layout';
|
||||
contextType = 'nonfolder';
|
||||
expect(policy.allow(mockCandidate, mockChild)).toBe(true);
|
||||
expect(policy.allow(mockCandidateObj, mockChild)).toBe(true);
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -47,7 +47,7 @@ define([
|
||||
"key": "url",
|
||||
"name": "URL",
|
||||
"control": "textfield",
|
||||
"pattern": "^(ftp|https?)\\:\\/\\/\\w+(\\.\\w+)*(\\:\\d+)?(\\/\\S*)*$",
|
||||
"pattern": "^(ftp|https?)\\:\\/\\/",
|
||||
"required": true,
|
||||
"cssClass": "l-input-lg"
|
||||
}
|
||||
|
@ -82,7 +82,7 @@ define(
|
||||
lastRange,
|
||||
lastDomain,
|
||||
handle;
|
||||
var conductor = openmct.conductor;
|
||||
var timeAPI = openmct.time;
|
||||
|
||||
// Populate the scope with axis information (specifically, options
|
||||
// available for each axis.)
|
||||
@ -185,7 +185,7 @@ define(
|
||||
|
||||
function changeTimeOfInterest(timeOfInterest) {
|
||||
if (timeOfInterest !== undefined) {
|
||||
var bounds = conductor.bounds();
|
||||
var bounds = timeAPI.bounds();
|
||||
var range = bounds.end - bounds.start;
|
||||
$scope.toiPerc = ((timeOfInterest - bounds.start) / range) * 100;
|
||||
$scope.toiPinned = true;
|
||||
@ -208,8 +208,8 @@ define(
|
||||
);
|
||||
replot();
|
||||
|
||||
changeTimeOfInterest(conductor.timeOfInterest());
|
||||
conductor.on("timeOfInterest", changeTimeOfInterest);
|
||||
changeTimeOfInterest(timeAPI.timeOfInterest());
|
||||
timeAPI.on("timeOfInterest", changeTimeOfInterest);
|
||||
}
|
||||
|
||||
// Release the current subscription (called when scope is destroyed)
|
||||
@ -218,7 +218,7 @@ define(
|
||||
handle.unsubscribe();
|
||||
handle = undefined;
|
||||
}
|
||||
conductor.off("timeOfInterest", changeTimeOfInterest);
|
||||
timeAPI.off("timeOfInterest", changeTimeOfInterest);
|
||||
}
|
||||
|
||||
function requery() {
|
||||
@ -262,7 +262,7 @@ define(
|
||||
requery();
|
||||
}
|
||||
self.setUnsynchedStatus($scope.domainObject, follow && self.isZoomed());
|
||||
changeTimeOfInterest(conductor.timeOfInterest());
|
||||
changeTimeOfInterest(timeAPI.timeOfInterest());
|
||||
}
|
||||
|
||||
this.modeOptions = new PlotModeOptions([], subPlotFactory);
|
||||
@ -286,11 +286,11 @@ define(
|
||||
];
|
||||
|
||||
//Are some initialized bounds defined?
|
||||
var bounds = conductor.bounds();
|
||||
var bounds = timeAPI.bounds();
|
||||
if (bounds &&
|
||||
bounds.start !== undefined &&
|
||||
bounds.end !== undefined) {
|
||||
changeDisplayBounds(undefined, conductor.bounds(), conductor.follow());
|
||||
changeDisplayBounds(undefined, timeAPI.bounds(), timeAPI.follow());
|
||||
}
|
||||
|
||||
// Watch for changes to the selected axis
|
||||
|
@ -112,22 +112,6 @@ 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
|
||||
@ -173,9 +157,10 @@ 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) {
|
||||
var endIx = _.sortedLastIndex(array, item, this.sortField);
|
||||
endIx = _.sortedLastIndex(array, item, this.sortField);
|
||||
|
||||
// Create an array of potential dupes, based on having the
|
||||
// same time stamp
|
||||
@ -185,7 +170,7 @@ define(
|
||||
}
|
||||
|
||||
if (!isDuplicate) {
|
||||
array.splice(startIx, 0, item);
|
||||
array.splice(endIx || startIx, 0, item);
|
||||
|
||||
//Return true if it was added and in bounds
|
||||
return array === this.telemetry;
|
||||
|
@ -27,7 +27,7 @@ define(
|
||||
this.resultsHeader = this.element.find('.mct-table>thead').first();
|
||||
this.sizingTableBody = this.element.find('.sizing-table>tbody').first();
|
||||
this.$scope.sizingRow = {};
|
||||
this.conductor = openmct.conductor;
|
||||
this.timeApi = openmct.time;
|
||||
this.toiFormatter = undefined;
|
||||
this.formatService = formatService;
|
||||
this.callbacks = {};
|
||||
@ -65,6 +65,7 @@ define(
|
||||
this.scrollable.on('scroll', this.onScroll);
|
||||
|
||||
$scope.visibleRows = [];
|
||||
$scope.displayRows = [];
|
||||
|
||||
/**
|
||||
* Set default values for optional parameters on a given scope
|
||||
@ -113,7 +114,7 @@ define(
|
||||
$scope.sortDirection = 'asc';
|
||||
}
|
||||
self.setRows($scope.rows);
|
||||
self.setTimeOfInterestRow(self.conductor.timeOfInterest());
|
||||
self.setTimeOfInterestRow(self.timeApi.timeOfInterest());
|
||||
};
|
||||
|
||||
/*
|
||||
@ -159,13 +160,13 @@ define(
|
||||
if (timeColumns) {
|
||||
this.destroyConductorListeners();
|
||||
|
||||
this.conductor.on('timeSystem', this.changeTimeSystem);
|
||||
this.conductor.on('timeOfInterest', this.changeTimeOfInterest);
|
||||
this.conductor.on('bounds', this.changeBounds);
|
||||
this.timeApi.on('timeSystem', this.changeTimeSystem);
|
||||
this.timeApi.on('timeOfInterest', this.changeTimeOfInterest);
|
||||
this.timeApi.on('bounds', this.changeBounds);
|
||||
|
||||
// If time system defined, set initially
|
||||
if (this.conductor.timeSystem()) {
|
||||
this.changeTimeSystem(this.conductor.timeSystem());
|
||||
if (this.timeApi.timeSystem()) {
|
||||
this.changeTimeSystem(this.timeApi.timeSystem());
|
||||
}
|
||||
}
|
||||
}.bind(this));
|
||||
@ -182,13 +183,14 @@ define(
|
||||
}
|
||||
|
||||
MCTTableController.prototype.destroyConductorListeners = function () {
|
||||
this.conductor.off('timeSystem', this.changeTimeSystem);
|
||||
this.conductor.off('timeOfInterest', this.changeTimeOfInterest);
|
||||
this.conductor.off('bounds', this.changeBounds);
|
||||
this.timeApi.off('timeSystem', this.changeTimeSystem);
|
||||
this.timeApi.off('timeOfInterest', this.changeTimeOfInterest);
|
||||
this.timeApi.off('bounds', this.changeBounds);
|
||||
};
|
||||
|
||||
MCTTableController.prototype.changeTimeSystem = function () {
|
||||
var format = this.conductor.timeSystem().formats()[0];
|
||||
var timeSystem = this.timeApi.getTimeSystem(this.timeApi.timeSystem());
|
||||
var format = timeSystem.formats()[0];
|
||||
this.toiFormatter = this.formatService.getFormat(format);
|
||||
};
|
||||
|
||||
@ -220,7 +222,7 @@ define(
|
||||
}
|
||||
}.bind(this));
|
||||
|
||||
var toi = this.conductor.timeOfInterest();
|
||||
var toi = this.timeApi.timeOfInterest();
|
||||
if (toi !== -1) {
|
||||
this.setTimeOfInterestRow(toi);
|
||||
}
|
||||
@ -425,6 +427,38 @@ 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
|
||||
*/
|
||||
@ -439,9 +473,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);
|
||||
}
|
||||
@ -458,7 +492,7 @@ define(
|
||||
index = array.length;
|
||||
} else {
|
||||
//Sort is enabled, perform binary search to find insertion point
|
||||
index = this.binarySearch(array, element[this.$scope.sortColumn].text, 0, array.length - 1);
|
||||
index = this.findInsertionPoint(array, element[this.$scope.sortColumn].text);
|
||||
}
|
||||
if (index === -1) {
|
||||
array.unshift(element);
|
||||
@ -649,7 +683,7 @@ define(
|
||||
// perform DOM changes, otherwise scrollTo won't work.
|
||||
.then(function () {
|
||||
//If TOI specified, scroll to it
|
||||
var timeOfInterest = this.conductor.timeOfInterest();
|
||||
var timeOfInterest = this.timeApi.timeOfInterest();
|
||||
if (timeOfInterest) {
|
||||
this.setTimeOfInterestRow(timeOfInterest);
|
||||
this.scrollToRow(this.$scope.toiRowIndex);
|
||||
@ -747,7 +781,7 @@ define(
|
||||
* @param bounds
|
||||
*/
|
||||
MCTTableController.prototype.changeBounds = function (bounds) {
|
||||
this.setTimeOfInterestRow(this.conductor.timeOfInterest());
|
||||
this.setTimeOfInterestRow(this.timeApi.timeOfInterest());
|
||||
if (this.$scope.toiRowIndex !== -1) {
|
||||
this.scrollToRow(this.$scope.toiRowIndex);
|
||||
}
|
||||
@ -762,7 +796,7 @@ define(
|
||||
if (selectedTime &&
|
||||
this.toiFormatter.validate(selectedTime) &&
|
||||
event.altKey) {
|
||||
this.conductor.timeOfInterest(this.toiFormatter.parse(selectedTime));
|
||||
this.timeApi.timeOfInterest(this.toiFormatter.parse(selectedTime));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -64,7 +64,7 @@ define(
|
||||
$scope.rows = [];
|
||||
this.table = new TableConfiguration($scope.domainObject,
|
||||
openmct);
|
||||
this.lastBounds = this.openmct.conductor.bounds();
|
||||
this.lastBounds = this.openmct.time.bounds();
|
||||
this.lastRequestTime = 0;
|
||||
this.telemetry = new TelemetryCollection();
|
||||
|
||||
@ -72,7 +72,7 @@ define(
|
||||
* Create a new format object from legacy object, and replace it
|
||||
* when it changes
|
||||
*/
|
||||
this.newObject = objectUtils.toNewFormat($scope.domainObject.getModel(),
|
||||
this.domainObject = objectUtils.toNewFormat($scope.domainObject.getModel(),
|
||||
$scope.domainObject.getId());
|
||||
|
||||
_.bindAll(this, [
|
||||
@ -95,7 +95,7 @@ define(
|
||||
this.registerChangeListeners();
|
||||
}.bind(this));
|
||||
|
||||
this.setScroll(this.openmct.conductor.follow());
|
||||
this.setScroll(this.openmct.time.follow());
|
||||
|
||||
this.$scope.$on("$destroy", this.destroy);
|
||||
}
|
||||
@ -122,7 +122,7 @@ define(
|
||||
|
||||
if (timeSystem) {
|
||||
this.table.columns.forEach(function (column) {
|
||||
if (column.getKey() === timeSystem.metadata.key) {
|
||||
if (column.getKey() === timeSystem) {
|
||||
sortColumn = column;
|
||||
}
|
||||
});
|
||||
@ -144,16 +144,16 @@ define(
|
||||
this.unobserveObject();
|
||||
}
|
||||
|
||||
this.unobserveObject = this.openmct.objects.observe(this.newObject, "*",
|
||||
this.unobserveObject = this.openmct.objects.observe(this.domainObject, "*",
|
||||
function (domainObject) {
|
||||
this.newObject = domainObject;
|
||||
this.domainObject = domainObject;
|
||||
this.getData();
|
||||
}.bind(this)
|
||||
);
|
||||
|
||||
this.openmct.conductor.on('timeSystem', this.sortByTimeSystem);
|
||||
this.openmct.conductor.on('bounds', this.changeBounds);
|
||||
this.openmct.conductor.on('follow', this.setScroll);
|
||||
this.openmct.time.on('timeSystem', this.sortByTimeSystem);
|
||||
this.openmct.time.on('bounds', this.changeBounds);
|
||||
this.openmct.time.on('follow', this.setScroll);
|
||||
|
||||
this.telemetry.on('added', this.addRowsToTable);
|
||||
this.telemetry.on('discarded', this.removeRowsFromTable);
|
||||
@ -188,7 +188,7 @@ define(
|
||||
* @param {openmct.TimeConductorBounds~TimeConductorBounds} bounds
|
||||
*/
|
||||
TelemetryTableController.prototype.changeBounds = function (bounds) {
|
||||
var follow = this.openmct.conductor.follow();
|
||||
var follow = this.openmct.time.follow();
|
||||
var isTick = follow &&
|
||||
bounds.start !== this.lastBounds.start &&
|
||||
bounds.end !== this.lastBounds.end;
|
||||
@ -207,9 +207,9 @@ define(
|
||||
*/
|
||||
TelemetryTableController.prototype.destroy = function () {
|
||||
|
||||
this.openmct.conductor.off('timeSystem', this.sortByTimeSystem);
|
||||
this.openmct.conductor.off('bounds', this.changeBounds);
|
||||
this.openmct.conductor.off('follow', this.setScroll);
|
||||
this.openmct.time.off('timeSystem', this.sortByTimeSystem);
|
||||
this.openmct.time.off('bounds', this.changeBounds);
|
||||
this.openmct.time.off('follow', this.setScroll);
|
||||
|
||||
this.subscriptions.forEach(function (subscription) {
|
||||
subscription();
|
||||
@ -260,7 +260,7 @@ define(
|
||||
// if data matches selected time system
|
||||
this.telemetry.sort(undefined);
|
||||
|
||||
var timeSystem = this.openmct.conductor.timeSystem();
|
||||
var timeSystem = this.openmct.time.timeSystem();
|
||||
if (timeSystem) {
|
||||
this.sortByTimeSystem(timeSystem);
|
||||
}
|
||||
@ -278,7 +278,7 @@ define(
|
||||
TelemetryTableController.prototype.getHistoricalData = function (objects) {
|
||||
var self = this;
|
||||
var openmct = this.openmct;
|
||||
var bounds = openmct.conductor.bounds();
|
||||
var bounds = openmct.time.bounds();
|
||||
var scope = this.$scope;
|
||||
var rowData = [];
|
||||
var processedObjects = 0;
|
||||
@ -364,11 +364,8 @@ 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) {
|
||||
@ -379,16 +376,6 @@ 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) {
|
||||
@ -399,6 +386,42 @@ 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
|
||||
@ -406,13 +429,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());
|
||||
this.telemetry.bounds(this.openmct.time.bounds());
|
||||
|
||||
this.$scope.loading = true;
|
||||
|
||||
@ -421,28 +441,9 @@ 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 getDomainObjects()
|
||||
.then(filterForTelemetry)
|
||||
return this.getTelemetryObjects()
|
||||
.then(this.loadColumns)
|
||||
.then(this.subscribeToNewData)
|
||||
.then(this.getHistoricalData)
|
||||
|
@ -138,6 +138,27 @@ 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);
|
||||
}
|
||||
);
|
||||
|
@ -459,14 +459,14 @@ define(
|
||||
|
||||
beforeEach(function () {
|
||||
row4 = {
|
||||
'col1': {'text': 'row5 col1'},
|
||||
'col1': {'text': 'row4 col1'},
|
||||
'col2': {'text': 'xyz'},
|
||||
'col3': {'text': 'row5 col3'}
|
||||
'col3': {'text': 'row4 col3'}
|
||||
};
|
||||
row5 = {
|
||||
'col1': {'text': 'row6 col1'},
|
||||
'col1': {'text': 'row5 col1'},
|
||||
'col2': {'text': 'aaa'},
|
||||
'col3': {'text': 'row6 col3'}
|
||||
'col3': {'text': 'row5 col3'}
|
||||
};
|
||||
row6 = {
|
||||
'col1': {'text': 'row6 col1'},
|
||||
@ -490,6 +490,71 @@ 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';
|
||||
|
@ -161,7 +161,7 @@ define(
|
||||
});
|
||||
});
|
||||
|
||||
describe ('Subscribes to new data', function () {
|
||||
describe ('when getting telemetry', function () {
|
||||
var mockComposition,
|
||||
mockTelemetryObject,
|
||||
mockChildren,
|
||||
@ -173,9 +173,7 @@ define(
|
||||
"load"
|
||||
]);
|
||||
|
||||
mockTelemetryObject = jasmine.createSpyObj("mockTelemetryObject", [
|
||||
"something"
|
||||
]);
|
||||
mockTelemetryObject = {};
|
||||
mockTelemetryObject.identifier = {
|
||||
key: "mockTelemetryObject"
|
||||
};
|
||||
@ -192,22 +190,12 @@ define(
|
||||
});
|
||||
|
||||
done = false;
|
||||
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 () {
|
||||
controller.getData().then(function () {
|
||||
done = true;
|
||||
});
|
||||
waitsFor(function () {
|
||||
return done;
|
||||
}, "getData to return", 100);
|
||||
@ -217,17 +205,11 @@ define(
|
||||
});
|
||||
});
|
||||
|
||||
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('unsubscribes on view destruction', function () {
|
||||
controller.getData().then(function () {
|
||||
done = true;
|
||||
});
|
||||
|
||||
});
|
||||
it('and unsubscribes on view destruction', function () {
|
||||
waitsFor(function () {
|
||||
return done;
|
||||
}, "getData to return", 100);
|
||||
@ -239,6 +221,87 @@ 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 () {
|
||||
|
@ -140,7 +140,8 @@ define(
|
||||
typeRequest = (type && type.getDefinition().telemetry) || {},
|
||||
modelTelemetry = domainObject.getModel().telemetry,
|
||||
fullRequest = Object.create(typeRequest),
|
||||
bounds;
|
||||
bounds,
|
||||
timeSystem;
|
||||
|
||||
// Add properties from the telemetry field of this
|
||||
// specific domain object.
|
||||
@ -162,11 +163,18 @@ define(
|
||||
}
|
||||
|
||||
if (request.start === undefined && request.end === undefined) {
|
||||
bounds = this.openmct.conductor.bounds();
|
||||
bounds = this.openmct.time.bounds();
|
||||
fullRequest.start = bounds.start;
|
||||
fullRequest.end = bounds.end;
|
||||
}
|
||||
|
||||
if (request.domain === undefined) {
|
||||
timeSystem = this.openmct.time.timeSystem();
|
||||
if (timeSystem !== undefined) {
|
||||
fullRequest.domain = timeSystem;
|
||||
}
|
||||
}
|
||||
|
||||
return fullRequest;
|
||||
};
|
||||
|
||||
|
@ -104,6 +104,13 @@ define(
|
||||
start: 0,
|
||||
end: 1
|
||||
};
|
||||
},
|
||||
timeSystem: function () {
|
||||
return {
|
||||
metadata: {
|
||||
key: 'mockTimeSystem'
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -141,7 +148,8 @@ define(
|
||||
id: "testId", // from domain object
|
||||
source: "testSource", // from model
|
||||
key: "testKey", // from model
|
||||
start: 42 // from argument
|
||||
start: 42, // from argument
|
||||
domain: 'mockTimeSystem'
|
||||
}]);
|
||||
});
|
||||
|
||||
@ -160,7 +168,8 @@ define(
|
||||
source: "testSource",
|
||||
key: "testKey",
|
||||
start: 0,
|
||||
end: 1
|
||||
end: 1,
|
||||
domain: 'mockTimeSystem'
|
||||
});
|
||||
});
|
||||
|
||||
@ -176,7 +185,8 @@ define(
|
||||
source: "testSource", // from model
|
||||
key: "testId", // from domain object
|
||||
start: 0,
|
||||
end: 1
|
||||
end: 1,
|
||||
domain: 'mockTimeSystem'
|
||||
});
|
||||
});
|
||||
|
||||
@ -257,7 +267,8 @@ define(
|
||||
source: "testSource",
|
||||
key: "testKey",
|
||||
start: 0,
|
||||
end: 1
|
||||
end: 1,
|
||||
domain: 'mockTimeSystem'
|
||||
}]
|
||||
);
|
||||
|
||||
@ -274,8 +285,28 @@ 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');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -83,7 +83,7 @@ define([
|
||||
* @memberof module:openmct.MCT#
|
||||
* @name conductor
|
||||
*/
|
||||
this.conductor = new api.TimeConductor();
|
||||
this.time = new api.TimeAPI();
|
||||
|
||||
/**
|
||||
* An interface for interacting with the composition of domain objects.
|
||||
@ -194,15 +194,13 @@ define([
|
||||
*/
|
||||
this.telemetry = new api.TelemetryAPI(this);
|
||||
|
||||
this.TimeConductor = this.conductor; // compatibility for prototype
|
||||
this.Dialog = api.Dialog;
|
||||
|
||||
this.on('navigation', this.selection.clear.bind(this.selection));
|
||||
}
|
||||
|
||||
MCT.prototype = Object.create(EventEmitter.prototype);
|
||||
|
||||
Object.keys(api).forEach(function (k) {
|
||||
MCT.prototype[k] = api[k];
|
||||
});
|
||||
MCT.prototype.MCT = MCT;
|
||||
|
||||
MCT.prototype.legacyExtension = function (category, extension) {
|
||||
|
100
src/MCTSpec.js
Normal file
100
src/MCTSpec.js
Normal file
@ -0,0 +1,100 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2016, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define([
|
||||
'./MCT',
|
||||
'./plugins/plugins',
|
||||
'legacyRegistry'
|
||||
], function (MCT, plugins, legacyRegistry) {
|
||||
describe("MCT", function () {
|
||||
var openmct;
|
||||
var mockPlugin;
|
||||
var mockPlugin2;
|
||||
var mockListener;
|
||||
var oldBundles;
|
||||
|
||||
beforeEach(function () {
|
||||
mockPlugin = jasmine.createSpy('plugin');
|
||||
mockPlugin2 = jasmine.createSpy('plugin2');
|
||||
mockListener = jasmine.createSpy('listener');
|
||||
oldBundles = legacyRegistry.list();
|
||||
|
||||
openmct = new MCT();
|
||||
|
||||
openmct.install(mockPlugin);
|
||||
openmct.install(mockPlugin2);
|
||||
openmct.on('start', mockListener);
|
||||
});
|
||||
|
||||
// Clean up the dirty singleton.
|
||||
afterEach(function () {
|
||||
legacyRegistry.list().forEach(function (bundle) {
|
||||
if (oldBundles.indexOf(bundle) === -1) {
|
||||
legacyRegistry.delete(bundle);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("exposes plugins", function () {
|
||||
expect(openmct.plugins).toEqual(plugins);
|
||||
});
|
||||
|
||||
it("does not issue a start event before started", function () {
|
||||
expect(mockListener).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("start", function () {
|
||||
beforeEach(function () {
|
||||
openmct.start();
|
||||
});
|
||||
|
||||
it("calls plugins for configuration", function () {
|
||||
expect(mockPlugin).toHaveBeenCalledWith(openmct);
|
||||
expect(mockPlugin2).toHaveBeenCalledWith(openmct);
|
||||
});
|
||||
|
||||
it("emits a start event", function () {
|
||||
expect(mockListener).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("setAssetPath", function () {
|
||||
var testAssetPath;
|
||||
|
||||
beforeEach(function () {
|
||||
testAssetPath = "some/path";
|
||||
openmct.legacyExtension = jasmine.createSpy('legacyExtension');
|
||||
openmct.setAssetPath(testAssetPath);
|
||||
});
|
||||
|
||||
it("internally configures the path for assets", function () {
|
||||
expect(openmct.legacyExtension).toHaveBeenCalledWith(
|
||||
'constants',
|
||||
{
|
||||
key: "ASSETS_PATH",
|
||||
value: testAssetPath
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -24,13 +24,13 @@
|
||||
* Module defining AlternateCompositionCapability. Created by vwoeltje on 11/7/14.
|
||||
*/
|
||||
define([
|
||||
'../../api/objects/object-utils'
|
||||
], function (objectUtils) {
|
||||
'../../api/objects/object-utils',
|
||||
'../../../platform/core/src/capabilities/ContextualDomainObject'
|
||||
], function (objectUtils, ContextualDomainObject) {
|
||||
function AlternateCompositionCapability($injector, domainObject) {
|
||||
this.domainObject = domainObject;
|
||||
this.getDependencies = function () {
|
||||
this.instantiate = $injector.get("instantiate");
|
||||
this.contextualize = $injector.get("contextualize");
|
||||
this.getDependencies = undefined;
|
||||
this.openmct = $injector.get("openmct");
|
||||
}.bind(this);
|
||||
@ -74,7 +74,7 @@ define([
|
||||
var keyString = objectUtils.makeKeyString(child.identifier);
|
||||
var oldModel = objectUtils.toOldFormat(child);
|
||||
var newDO = this.instantiate(oldModel, keyString);
|
||||
return this.contextualize(newDO, this.domainObject);
|
||||
return new ContextualDomainObject(newDO, this.domainObject);
|
||||
|
||||
};
|
||||
|
||||
|
@ -26,13 +26,11 @@ define([], function () {
|
||||
}
|
||||
|
||||
AdapterCompositionPolicy.prototype.allow = function (
|
||||
parentType,
|
||||
parent,
|
||||
child
|
||||
) {
|
||||
var container = parentType.getInitialModel();
|
||||
|
||||
return this.openmct.composition.checkPolicy(
|
||||
container,
|
||||
parent,
|
||||
child
|
||||
);
|
||||
};
|
||||
|
@ -1,122 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2016, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define(['./TimeConductor'], function (TimeConductor) {
|
||||
describe("The Time Conductor", function () {
|
||||
var tc,
|
||||
timeSystem,
|
||||
bounds,
|
||||
eventListener,
|
||||
toi,
|
||||
follow;
|
||||
|
||||
beforeEach(function () {
|
||||
tc = new TimeConductor();
|
||||
timeSystem = {};
|
||||
bounds = {start: 0, end: 0};
|
||||
eventListener = jasmine.createSpy("eventListener");
|
||||
toi = 111;
|
||||
follow = true;
|
||||
});
|
||||
|
||||
it("Supports setting and querying of time of interest and and follow mode", function () {
|
||||
expect(tc.timeOfInterest()).not.toBe(toi);
|
||||
tc.timeOfInterest(toi);
|
||||
expect(tc.timeOfInterest()).toBe(toi);
|
||||
|
||||
expect(tc.follow()).not.toBe(follow);
|
||||
tc.follow(follow);
|
||||
expect(tc.follow()).toBe(follow);
|
||||
});
|
||||
|
||||
it("Allows setting of valid bounds", function () {
|
||||
bounds = {start: 0, end: 1};
|
||||
expect(tc.bounds()).not.toBe(bounds);
|
||||
expect(tc.bounds.bind(tc, bounds)).not.toThrow();
|
||||
expect(tc.bounds()).toEqual(bounds);
|
||||
});
|
||||
|
||||
it("Disallows setting of invalid bounds", function () {
|
||||
bounds = {start: 1, end: 0};
|
||||
expect(tc.bounds()).not.toEqual(bounds);
|
||||
expect(tc.bounds.bind(tc, bounds)).toThrow();
|
||||
expect(tc.bounds()).not.toEqual(bounds);
|
||||
|
||||
bounds = {start: 1};
|
||||
expect(tc.bounds()).not.toEqual(bounds);
|
||||
expect(tc.bounds.bind(tc, bounds)).toThrow();
|
||||
expect(tc.bounds()).not.toEqual(bounds);
|
||||
});
|
||||
|
||||
it("Allows setting of time system with bounds", function () {
|
||||
expect(tc.timeSystem()).not.toBe(timeSystem);
|
||||
expect(tc.timeSystem.bind(tc, timeSystem, bounds)).not.toThrow();
|
||||
expect(tc.timeSystem()).toBe(timeSystem);
|
||||
});
|
||||
|
||||
it("Disallows setting of time system without bounds", function () {
|
||||
expect(tc.timeSystem()).not.toBe(timeSystem);
|
||||
expect(tc.timeSystem.bind(tc, timeSystem)).toThrow();
|
||||
expect(tc.timeSystem()).not.toBe(timeSystem);
|
||||
});
|
||||
|
||||
it("Emits an event when time system changes", function () {
|
||||
expect(eventListener).not.toHaveBeenCalled();
|
||||
tc.on("timeSystem", eventListener);
|
||||
tc.timeSystem(timeSystem, bounds);
|
||||
expect(eventListener).toHaveBeenCalledWith(timeSystem);
|
||||
});
|
||||
|
||||
it("Emits an event when time of interest changes", function () {
|
||||
expect(eventListener).not.toHaveBeenCalled();
|
||||
tc.on("timeOfInterest", eventListener);
|
||||
tc.timeOfInterest(toi);
|
||||
expect(eventListener).toHaveBeenCalledWith(toi);
|
||||
});
|
||||
|
||||
it("Emits an event when bounds change", function () {
|
||||
expect(eventListener).not.toHaveBeenCalled();
|
||||
tc.on("bounds", eventListener);
|
||||
tc.bounds(bounds);
|
||||
expect(eventListener).toHaveBeenCalledWith(bounds);
|
||||
});
|
||||
|
||||
it("Emits an event when follow mode changes", function () {
|
||||
expect(eventListener).not.toHaveBeenCalled();
|
||||
tc.on("follow", eventListener);
|
||||
tc.follow(follow);
|
||||
expect(eventListener).toHaveBeenCalledWith(follow);
|
||||
});
|
||||
|
||||
it("If bounds are set and TOI lies inside them, do not change TOI", function () {
|
||||
tc.timeOfInterest(6);
|
||||
tc.bounds({start: 1, end: 10});
|
||||
expect(tc.timeOfInterest()).toEqual(6);
|
||||
});
|
||||
|
||||
it("If bounds are set and TOI lies outside them, reset TOI", function () {
|
||||
tc.timeOfInterest(11);
|
||||
tc.bounds({start: 1, end: 10});
|
||||
expect(tc.timeOfInterest()).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
@ -21,7 +21,7 @@
|
||||
*****************************************************************************/
|
||||
|
||||
define([
|
||||
'./TimeConductor',
|
||||
'./time/TimeAPI',
|
||||
'./objects/ObjectAPI',
|
||||
'./composition/CompositionAPI',
|
||||
'./types/TypeRegistry',
|
||||
@ -29,7 +29,7 @@ define([
|
||||
'./ui/GestureAPI',
|
||||
'./telemetry/TelemetryAPI'
|
||||
], function (
|
||||
TimeConductor,
|
||||
TimeAPI,
|
||||
ObjectAPI,
|
||||
CompositionAPI,
|
||||
TypeRegistry,
|
||||
@ -38,7 +38,7 @@ define([
|
||||
TelemetryAPI
|
||||
) {
|
||||
return {
|
||||
TimeConductor: TimeConductor,
|
||||
TimeAPI: TimeAPI,
|
||||
ObjectAPI: ObjectAPI,
|
||||
CompositionAPI: CompositionAPI,
|
||||
Dialog: Dialog,
|
||||
|
@ -22,6 +22,8 @@
|
||||
|
||||
define(['EventEmitter'], function (EventEmitter) {
|
||||
|
||||
var tick;
|
||||
|
||||
/**
|
||||
* The public API for setting and querying time conductor state. The
|
||||
* time conductor is the means by which the temporal bounds of a view
|
||||
@ -35,7 +37,7 @@ define(['EventEmitter'], function (EventEmitter) {
|
||||
* @interface
|
||||
* @memberof module:openmct
|
||||
*/
|
||||
function TimeConductor() {
|
||||
function TimeAPI() {
|
||||
EventEmitter.call(this);
|
||||
|
||||
//The Time System
|
||||
@ -48,21 +50,68 @@ define(['EventEmitter'], function (EventEmitter) {
|
||||
end: undefined
|
||||
};
|
||||
|
||||
//Default to fixed mode
|
||||
this.followMode = false;
|
||||
this.timeSystems = new Map();
|
||||
this.clocks = new Map();
|
||||
this.activeClock = undefined;
|
||||
this.offsets = undefined;
|
||||
|
||||
/**
|
||||
* Tick is not exposed via public API, even @privately to avoid misuse.
|
||||
*/
|
||||
|
||||
tick = function (timestamp) {
|
||||
var newBounds = {
|
||||
start: timestamp + this.offsets.start,
|
||||
end: timestamp + this.offsets.end
|
||||
};
|
||||
|
||||
this.boundsVal = newBounds;
|
||||
this.emit('bounds', this.boundsVal, true);
|
||||
|
||||
// If a bounds change results in a TOI outside of the current
|
||||
// bounds, unset it
|
||||
if (this.toi < newBounds.start || this.toi > newBounds.end) {
|
||||
this.timeOfInterest(undefined);
|
||||
}
|
||||
}.bind(this);
|
||||
|
||||
}
|
||||
|
||||
TimeConductor.prototype = Object.create(EventEmitter.prototype);
|
||||
TimeAPI.prototype = Object.create(EventEmitter.prototype);
|
||||
|
||||
TimeAPI.prototype.addTimeSystem = function (timeSystem) {
|
||||
this.timeSystems.set(timeSystem.key, timeSystem);
|
||||
};
|
||||
|
||||
TimeAPI.prototype.availableTimeSystems = function (key) {
|
||||
return Array.from(this.timeSystems.values());
|
||||
}
|
||||
|
||||
TimeAPI.prototype.getTimeSystem = function (key) {
|
||||
return this.timeSystems.get(key);
|
||||
};
|
||||
|
||||
TimeAPI.prototype.addClock = function (clock) {
|
||||
this.clocks.set(clock.key, clock);
|
||||
};
|
||||
|
||||
TimeAPI.prototype.getClock = function (key) {
|
||||
return this.clocks.get(key);
|
||||
};
|
||||
|
||||
TimeAPI.prototype.availableClocks = function (key) {
|
||||
return Array.from(this.availableClocks.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the given bounds. This can be used for pre-validation of
|
||||
* bounds, for example by views validating user inputs.
|
||||
* @param bounds The start and end time of the conductor.
|
||||
* @returns {string | true} A validation error, or true if valid
|
||||
* @memberof module:openmct.TimeConductor#
|
||||
* @memberof module:openmct.TimeAPI#
|
||||
* @method validateBounds
|
||||
*/
|
||||
TimeConductor.prototype.validateBounds = function (bounds) {
|
||||
TimeAPI.prototype.validateBounds = function (bounds) {
|
||||
if ((bounds.start === undefined) ||
|
||||
(bounds.end === undefined) ||
|
||||
isNaN(bounds.start) ||
|
||||
@ -75,51 +124,25 @@ define(['EventEmitter'], function (EventEmitter) {
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get or set the follow mode of the time conductor. In follow mode the
|
||||
* time conductor ticks, regularly updating the bounds from a timing
|
||||
* source appropriate to the selected time system and mode of the time
|
||||
* conductor.
|
||||
* @fires module:openmct.TimeConductor~follow
|
||||
* @param {boolean} followMode
|
||||
* @returns {boolean}
|
||||
* @memberof module:openmct.TimeConductor#
|
||||
* @method follow
|
||||
*/
|
||||
TimeConductor.prototype.follow = function (followMode) {
|
||||
if (arguments.length > 0) {
|
||||
this.followMode = followMode;
|
||||
/**
|
||||
* The TimeConductor has toggled into or out of follow mode.
|
||||
* @event follow
|
||||
* @memberof module:openmct.TimeConductor~
|
||||
* @property {boolean} followMode true if follow mode is
|
||||
* enabled, otherwise false.
|
||||
*/
|
||||
this.emit('follow', this.followMode);
|
||||
}
|
||||
return this.followMode;
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef {Object} TimeConductorBounds
|
||||
* @property {number} start The start time displayed by the time conductor in ms since epoch. Epoch determined by current time system
|
||||
* @property {number} end The end time displayed by the time conductor in ms since epoch.
|
||||
* @memberof module:openmct.TimeConductor~
|
||||
* @memberof module:openmct.TimeAPI~
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get or set the start and end time of the time conductor. Basic validation
|
||||
* of bounds is performed.
|
||||
*
|
||||
* @param {module:openmct.TimeConductorBounds~TimeConductorBounds} newBounds
|
||||
* @param {module:openmct.TimeAPI~TimeConductorBounds} newBounds
|
||||
* @throws {Error} Validation error
|
||||
* @fires module:openmct.TimeConductor~bounds
|
||||
* @returns {module:openmct.TimeConductorBounds~TimeConductorBounds}
|
||||
* @memberof module:openmct.TimeConductor#
|
||||
* @fires module:openmct.TimeAPI~bounds
|
||||
* @returns {module:openmct.TimeAPI~TimeConductorBounds}
|
||||
* @memberof module:openmct.TimeAPI#
|
||||
* @method bounds
|
||||
*/
|
||||
TimeConductor.prototype.bounds = function (newBounds) {
|
||||
TimeAPI.prototype.bounds = function (newBounds) {
|
||||
if (arguments.length > 0) {
|
||||
var validationResult = this.validateBounds(newBounds);
|
||||
if (validationResult !== true) {
|
||||
@ -130,10 +153,12 @@ define(['EventEmitter'], function (EventEmitter) {
|
||||
/**
|
||||
* The start time, end time, or both have been updated.
|
||||
* @event bounds
|
||||
* @memberof module:openmct.TimeConductor~
|
||||
* @property {TimeConductorBounds} bounds
|
||||
* @memberof module:openmct.TimeAPI~
|
||||
* @property {TimeConductorBounds} bounds The newly updated bounds
|
||||
* @property {boolean} [tick] `true` if the bounds update was due to
|
||||
* a "tick" event (ie. was an automatic update), false otherwise.
|
||||
*/
|
||||
this.emit('bounds', this.boundsVal);
|
||||
this.emit('bounds', this.boundsVal, false);
|
||||
|
||||
// If a bounds change results in a TOI outside of the current
|
||||
// bounds, unset it
|
||||
@ -146,34 +171,40 @@ define(['EventEmitter'], function (EventEmitter) {
|
||||
};
|
||||
|
||||
/**
|
||||
* Get or set the time system of the TimeConductor. Time systems determine
|
||||
* Get or set the time system of the TimeAPI. Time systems determine
|
||||
* units, epoch, and other aspects of time representation. When changing
|
||||
* the time system in use, new valid bounds must also be provided.
|
||||
* @param {TimeSystem} newTimeSystem
|
||||
* @param {module:openmct.TimeConductor~TimeConductorBounds} bounds
|
||||
* @fires module:openmct.TimeConductor~timeSystem
|
||||
* @param {module:openmct.TimeAPI~TimeConductorBounds} bounds
|
||||
* @fires module:openmct.TimeAPI~timeSystem
|
||||
* @returns {TimeSystem} The currently applied time system
|
||||
* @memberof module:openmct.TimeConductor#
|
||||
* @memberof module:openmct.TimeAPI#
|
||||
* @method timeSystem
|
||||
*/
|
||||
TimeConductor.prototype.timeSystem = function (newTimeSystem, bounds) {
|
||||
TimeAPI.prototype.timeSystem = function (newTimeSystem, bounds) {
|
||||
if (arguments.length >= 2) {
|
||||
this.system = newTimeSystem;
|
||||
if (newTimeSystem === 'undefined') {
|
||||
throw "Please provide a time system";
|
||||
}
|
||||
if (this.timeSystems.get(newTimeSystem) === undefined){
|
||||
throw "Unknown time system " + newTimeSystem + ". Has it been registered with 'addTimeSystem'?";
|
||||
}
|
||||
this.system = this.timeSystems.get(newTimeSystem);
|
||||
/**
|
||||
* The time system used by the time
|
||||
* conductor has changed. A change in Time System will always be
|
||||
* followed by a bounds event specifying new query bounds.
|
||||
*
|
||||
* @event module:openmct.TimeConductor~timeSystem
|
||||
* @event module:openmct.TimeAPI~timeSystem
|
||||
* @property {TimeSystem} The value of the currently applied
|
||||
* Time System
|
||||
* */
|
||||
this.emit('timeSystem', this.system);
|
||||
this.emit('timeSystem', this.system.key);
|
||||
this.bounds(bounds);
|
||||
} else if (arguments.length === 1) {
|
||||
throw new Error('Must set bounds when changing time system');
|
||||
}
|
||||
return this.system;
|
||||
return this.system && this.system.key;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -181,19 +212,19 @@ define(['EventEmitter'], function (EventEmitter) {
|
||||
* focus of the current view. It can be manipulated by the user from the
|
||||
* time conductor or from other views.The time of interest can
|
||||
* effectively be unset by assigning a value of 'undefined'.
|
||||
* @fires module:openmct.TimeConductor~timeOfInterest
|
||||
* @fires module:openmct.TimeAPI~timeOfInterest
|
||||
* @param newTOI
|
||||
* @returns {number} the current time of interest
|
||||
* @memberof module:openmct.TimeConductor#
|
||||
* @memberof module:openmct.TimeAPI#
|
||||
* @method timeOfInterest
|
||||
*/
|
||||
TimeConductor.prototype.timeOfInterest = function (newTOI) {
|
||||
TimeAPI.prototype.timeOfInterest = function (newTOI) {
|
||||
if (arguments.length > 0) {
|
||||
this.toi = newTOI;
|
||||
/**
|
||||
* The Time of Interest has moved.
|
||||
* @event timeOfInterest
|
||||
* @memberof module:openmct.TimeConductor~
|
||||
* @memberof module:openmct.TimeAPI~
|
||||
* @property {number} Current time of interest
|
||||
*/
|
||||
this.emit('timeOfInterest', this.toi);
|
||||
@ -201,5 +232,63 @@ define(['EventEmitter'], function (EventEmitter) {
|
||||
return this.toi;
|
||||
};
|
||||
|
||||
return TimeConductor;
|
||||
/**
|
||||
* Return the follow state of the Conductor.
|
||||
* @returns {boolean} `true` if conductor is currently following a tick source.
|
||||
* `false` otherwise.
|
||||
*/
|
||||
TimeAPI.prototype.follow = function () {
|
||||
return this.activeClock !== undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the active clock. Tick source will be immediately subscribed to
|
||||
* and ticking will begin. Offsets from 'now' must also be provided.
|
||||
* @param {string} key the key of the tick source to activate
|
||||
* @param {ClockOffsets} offsets on each tick these will be used to calculate
|
||||
* the start and end bounds. This maintains a sliding time window of a fixed
|
||||
* width that automatically updates.
|
||||
*/
|
||||
TimeAPI.prototype.clock = function (key, offsets) {
|
||||
if (arguments.length === 2) {
|
||||
if (!this.clocks.has(key)){
|
||||
throw "Unknown clock '" + key + "'. Has it been registered with 'addClock'?";
|
||||
}
|
||||
if (this.activeClock !== undefined) {
|
||||
this.activeClock.off("tick", tick);
|
||||
} else {
|
||||
this.emit("follow", true);
|
||||
}
|
||||
if (key !== undefined) {
|
||||
this.activeClock = this.clocks.get(key);
|
||||
this.offsets = offsets;
|
||||
this.activeClock.on("tick", tick);
|
||||
} else {
|
||||
this.activeClock = undefined;
|
||||
this.emit("follow", false);
|
||||
}
|
||||
} else if (arguments.length === 1){
|
||||
throw "When setting clock, clock offsets must also be provided"
|
||||
}
|
||||
|
||||
return this.activeClock && this.activeClock.key;
|
||||
};
|
||||
|
||||
TimeAPI.prototype.clockOffsets = function (offsets) {
|
||||
if (arguments.length > 0) {
|
||||
this.offsets = offsets;
|
||||
this.emit("clockOffsets", offsets);
|
||||
}
|
||||
// TODO: Should setting clock offsets trigger a bounds event, or wait until next tick?
|
||||
return this.offsets;
|
||||
};
|
||||
|
||||
TimeAPI.prototype.stopClock = function () {
|
||||
if (this.activeClock) {
|
||||
this.activeClock.off("tick", tick);
|
||||
this.activeClock = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
return TimeAPI;
|
||||
});
|
216
src/api/time/TimeAPISpec.js
Normal file
216
src/api/time/TimeAPISpec.js
Normal file
@ -0,0 +1,216 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2016, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define(['./TimeAPI'], function (TimeAPI) {
|
||||
describe("The Time API", function () {
|
||||
var api,
|
||||
timeSystemKey,
|
||||
timeSystem,
|
||||
bounds,
|
||||
eventListener,
|
||||
toi,
|
||||
follow;
|
||||
|
||||
beforeEach(function () {
|
||||
api = new TimeAPI();
|
||||
timeSystemKey = "timeSystemKey";
|
||||
timeSystem = {key: timeSystemKey};
|
||||
bounds = {start: 0, end: 0};
|
||||
eventListener = jasmine.createSpy("eventListener");
|
||||
toi = 111;
|
||||
follow = true;
|
||||
});
|
||||
|
||||
it("Supports setting and querying of time of interest", function () {
|
||||
expect(api.timeOfInterest()).not.toBe(toi);
|
||||
api.timeOfInterest(toi);
|
||||
expect(api.timeOfInterest()).toBe(toi);
|
||||
});
|
||||
|
||||
it("Allows setting of valid bounds", function () {
|
||||
bounds = {start: 0, end: 1};
|
||||
expect(api.bounds()).not.toBe(bounds);
|
||||
expect(api.bounds.bind(api, bounds)).not.toThrow();
|
||||
expect(api.bounds()).toEqual(bounds);
|
||||
});
|
||||
|
||||
it("Disallows setting of invalid bounds", function () {
|
||||
bounds = {start: 1, end: 0};
|
||||
expect(api.bounds()).not.toEqual(bounds);
|
||||
expect(api.bounds.bind(api, bounds)).toThrow();
|
||||
expect(api.bounds()).not.toEqual(bounds);
|
||||
|
||||
bounds = {start: 1};
|
||||
expect(api.bounds()).not.toEqual(bounds);
|
||||
expect(api.bounds.bind(api, bounds)).toThrow();
|
||||
expect(api.bounds()).not.toEqual(bounds);
|
||||
});
|
||||
|
||||
it("Allows setting of previously registered time system with bounds", function () {
|
||||
api.addTimeSystem(timeSystem);
|
||||
expect(api.timeSystem()).not.toBe(timeSystemKey);
|
||||
expect(function() {
|
||||
api.timeSystem(timeSystemKey, bounds);
|
||||
}).not.toThrow();
|
||||
expect(api.timeSystem()).toBe(timeSystemKey);
|
||||
});
|
||||
|
||||
it("Disallows setting of time system without bounds", function () {
|
||||
api.addTimeSystem(timeSystem);
|
||||
expect(api.timeSystem()).not.toBe(timeSystemKey);
|
||||
expect(function () {
|
||||
api.timeSystem(timeSystemKey)
|
||||
}).toThrow();
|
||||
expect(api.timeSystem()).not.toBe(timeSystemKey);
|
||||
});
|
||||
|
||||
it("Emits an event when time system changes", function () {
|
||||
api.addTimeSystem(timeSystem);
|
||||
expect(eventListener).not.toHaveBeenCalled();
|
||||
api.on("timeSystem", eventListener);
|
||||
api.timeSystem(timeSystemKey, bounds);
|
||||
expect(eventListener).toHaveBeenCalledWith(timeSystemKey);
|
||||
});
|
||||
|
||||
it("Emits an event when time of interest changes", function () {
|
||||
expect(eventListener).not.toHaveBeenCalled();
|
||||
api.on("timeOfInterest", eventListener);
|
||||
api.timeOfInterest(toi);
|
||||
expect(eventListener).toHaveBeenCalledWith(toi);
|
||||
});
|
||||
|
||||
it("Emits an event when bounds change", function () {
|
||||
expect(eventListener).not.toHaveBeenCalled();
|
||||
api.on("bounds", eventListener);
|
||||
api.bounds(bounds);
|
||||
expect(eventListener).toHaveBeenCalledWith(bounds, false);
|
||||
});
|
||||
|
||||
it("If bounds are set and TOI lies inside them, do not change TOI", function () {
|
||||
api.timeOfInterest(6);
|
||||
api.bounds({start: 1, end: 10});
|
||||
expect(api.timeOfInterest()).toEqual(6);
|
||||
});
|
||||
|
||||
it("If bounds are set and TOI lies outside them, reset TOI", function () {
|
||||
api.timeOfInterest(11);
|
||||
api.bounds({start: 1, end: 10});
|
||||
expect(api.timeOfInterest()).toBeUndefined();
|
||||
});
|
||||
|
||||
it("Maintains delta during tick", function () {
|
||||
});
|
||||
|
||||
|
||||
it("Allows registered time system to be activated", function () {
|
||||
});
|
||||
|
||||
it("Allows a registered tick source to be activated", function () {
|
||||
var mockTickSource = jasmine.createSpyObj("mockTickSource", [
|
||||
"on",
|
||||
"off",
|
||||
"currentValue"
|
||||
]);
|
||||
mockTickSource.key = 'mockTickSource'
|
||||
});
|
||||
|
||||
describe(" when enabling a tick source", function () {
|
||||
var mockTickSource;
|
||||
var anotherMockTickSource;
|
||||
var mockOffsets = {
|
||||
start: 0,
|
||||
end: 0
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
mockTickSource = jasmine.createSpyObj("clock", [
|
||||
"on",
|
||||
"off"
|
||||
]);
|
||||
mockTickSource.key = "mts";
|
||||
|
||||
anotherMockTickSource = jasmine.createSpyObj("clock", [
|
||||
"on",
|
||||
"off"
|
||||
]);
|
||||
anotherMockTickSource.key = "amts";
|
||||
|
||||
api.addClock(mockTickSource);
|
||||
api.addClock(anotherMockTickSource);
|
||||
});
|
||||
|
||||
it("a new tick listener is registered", function () {
|
||||
api.clock("mts", mockOffsets);
|
||||
expect(mockTickSource.on).toHaveBeenCalledWith("tick", jasmine.any(Function));
|
||||
});
|
||||
|
||||
it("listener of existing tick source is reregistered", function () {
|
||||
api.clock("mts", mockOffsets);
|
||||
api.clock("amts", mockOffsets);
|
||||
expect(mockTickSource.off).toHaveBeenCalledWith("tick", jasmine.any(Function));
|
||||
});
|
||||
|
||||
it("Follow correctly reflects whether the conductor is following a " +
|
||||
"tick source", function () {
|
||||
expect(api.follow()).toBe(false);
|
||||
api.clock("mts", mockOffsets);
|
||||
expect(api.follow()).toBe(true);
|
||||
api.stopClock();
|
||||
expect(api.follow()).toBe(false);
|
||||
});
|
||||
|
||||
it("emits an event when follow mode changes", function () {
|
||||
var callback = jasmine.createSpy("followCallback");
|
||||
expect(api.follow()).toBe(false);
|
||||
|
||||
api.on("follow", callback);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it("on tick, observes deltas, and indicates tick in bounds callback", function () {
|
||||
var mockTickSource = jasmine.createSpyObj("clock", [
|
||||
"on",
|
||||
"off"
|
||||
]);
|
||||
var tickCallback;
|
||||
var boundsCallback = jasmine.createSpy("boundsCallback");
|
||||
var clockOffsets = {
|
||||
start: -100,
|
||||
end: 100
|
||||
};
|
||||
mockTickSource.key = "mts";
|
||||
|
||||
api.addClock(mockTickSource);
|
||||
api.clock("mts", clockOffsets);
|
||||
|
||||
api.on("bounds", boundsCallback);
|
||||
|
||||
tickCallback = mockTickSource.on.mostRecentCall.args[1]
|
||||
tickCallback(1000);
|
||||
expect(boundsCallback).toHaveBeenCalledWith({
|
||||
start: 900,
|
||||
end: 1100
|
||||
}, true);
|
||||
});
|
||||
});
|
||||
});
|
@ -44,7 +44,7 @@ define(['./Type'], function (Type) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a new type of view.
|
||||
* Register a new object type.
|
||||
*
|
||||
* @param {string} typeKey a string identifier for this type
|
||||
* @param {module:openmct.Type} type the type to add
|
||||
|
@ -68,7 +68,6 @@ define([
|
||||
'../platform/features/fixed/bundle',
|
||||
'../platform/features/conductor/core/bundle',
|
||||
'../platform/features/conductor/compatibility/bundle',
|
||||
'../platform/features/conductor/utcTimeSystem/bundle',
|
||||
'../platform/features/imagery/bundle',
|
||||
'../platform/features/layout/bundle',
|
||||
'../platform/features/my-items/bundle',
|
||||
|
@ -22,12 +22,14 @@
|
||||
|
||||
define([
|
||||
'lodash',
|
||||
'../../platform/features/conductor/utcTimeSystem/src/UTCTimeSystem',
|
||||
'../../example/generator/plugin'
|
||||
'./utcTimeSystem/plugin',
|
||||
'../../example/generator/plugin',
|
||||
'../../platform/features/autoflow/plugin'
|
||||
], function (
|
||||
_,
|
||||
UTCTimeSystem,
|
||||
GeneratorPlugin
|
||||
GeneratorPlugin,
|
||||
AutoflowPlugin
|
||||
) {
|
||||
var bundleMap = {
|
||||
CouchDB: 'platform/persistence/couch',
|
||||
@ -46,65 +48,38 @@ define([
|
||||
};
|
||||
});
|
||||
|
||||
plugins.UTCTimeSystem = function () {
|
||||
return function (openmct) {
|
||||
openmct.legacyExtension("timeSystems", {
|
||||
"implementation": UTCTimeSystem,
|
||||
"depends": ["$timeout"]
|
||||
});
|
||||
};
|
||||
};
|
||||
plugins.UTCTimeSystem = UTCTimeSystem;
|
||||
|
||||
var conductorInstalled = false;
|
||||
/**
|
||||
* A tabular view showing the latest values of multiple telemetry points at
|
||||
* once. Formatted so that labels and values are aligned.
|
||||
*
|
||||
* @param {Object} [options] Optional settings to apply to the autoflow
|
||||
* tabular view. Currently supports one option, 'type'.
|
||||
* @param {string} [options.type] The key of an object type to apply this view
|
||||
* to exclusively.
|
||||
*/
|
||||
plugins.AutoflowView = AutoflowPlugin;
|
||||
|
||||
plugins.Conductor = function (options) {
|
||||
if (!options) {
|
||||
options = {};
|
||||
plugins.Conductor = function (config) {
|
||||
/*
|
||||
{
|
||||
// Default 'fixed' configuration shows last 30 mins of data. May also provide specific bounds.
|
||||
{timeSystems: ['utc'], defaultDeltas: {start: 30 * ONE_MINUTE, end: 0}, zoomOutLimit: ONE_YEAR, zoomInLimit: ONE_MINUTE},
|
||||
// Some tick source driven menu options
|
||||
{clock: 'localClock', timeSystems: ['utc'], defaultDeltas: {start: 15 * ONE_MINUTE, end: 0}, zoomOutLimit: ONE_YEAR, zoomInLimit: ONE_MINUTE},
|
||||
{clock: 'latestAvailable', timeSystems: ['utc'], defaultDeltas: {start: 15 * 60 * 1000, end: 0}}
|
||||
}
|
||||
|
||||
function applyDefaults(openmct, timeConductorViewService) {
|
||||
var defaults = {};
|
||||
var timeSystem = timeConductorViewService.systems.find(function (ts) {
|
||||
return ts.metadata.key === options.defaultTimeSystem;
|
||||
});
|
||||
if (timeSystem !== undefined) {
|
||||
defaults = timeSystem.defaults();
|
||||
|
||||
if (options.defaultTimespan !== undefined) {
|
||||
defaults.deltas.start = options.defaultTimespan;
|
||||
defaults.bounds.start = defaults.bounds.end - options.defaultTimespan;
|
||||
timeSystem.defaults(defaults);
|
||||
}
|
||||
|
||||
openmct.conductor.timeSystem(timeSystem, defaults.bounds);
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
return function (openmct) {
|
||||
openmct.legacyExtension('constants', {
|
||||
key: 'DEFAULT_TIMECONDUCTOR_MODE',
|
||||
value: options.showConductor ? 'fixed' : 'realtime',
|
||||
priority: conductorInstalled ? 'mandatory' : 'fallback'
|
||||
key: 'CONDUCTOR_CONFIG',
|
||||
value: config,
|
||||
priority: 'mandatory'
|
||||
});
|
||||
if (options.showConductor !== undefined) {
|
||||
openmct.legacyExtension('constants', {
|
||||
key: 'SHOW_TIMECONDUCTOR',
|
||||
value: options.showConductor,
|
||||
priority: conductorInstalled ? 'mandatory' : 'fallback'
|
||||
});
|
||||
}
|
||||
if (options.defaultTimeSystem !== undefined || options.defaultTimespan !== undefined) {
|
||||
openmct.legacyExtension('runs', {
|
||||
implementation: applyDefaults,
|
||||
depends: ["openmct", "timeConductorViewService"]
|
||||
});
|
||||
}
|
||||
|
||||
if (!conductorInstalled) {
|
||||
openmct.legacyRegistry.enable('platform/features/conductor/core');
|
||||
openmct.legacyRegistry.enable('platform/features/conductor/compatibility');
|
||||
}
|
||||
conductorInstalled = true;
|
||||
openmct.legacyRegistry.enable('platform/features/conductor/core');
|
||||
openmct.legacyRegistry.enable('platform/features/conductor/compatibility');
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -20,46 +20,53 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define(['./TickSource'], function (TickSource) {
|
||||
define(['EventEmitter'], function (EventEmitter) {
|
||||
/**
|
||||
* @implements TickSource
|
||||
* @constructor
|
||||
*/
|
||||
function LocalClock($timeout, period) {
|
||||
TickSource.call(this);
|
||||
function LocalClock(period) {
|
||||
EventEmitter.call(this);
|
||||
|
||||
this.metadata = {
|
||||
key: 'local',
|
||||
mode: 'realtime',
|
||||
cssClass: 'icon-clock',
|
||||
label: 'Real-time',
|
||||
name: 'Real-time Mode',
|
||||
description: 'Monitor real-time streaming data as it comes in. The Time Conductor and displays will automatically advance themselves based on a UTC clock.'
|
||||
};
|
||||
/*
|
||||
Metadata fields
|
||||
*/
|
||||
this.key = 'local';
|
||||
this.mode = 'realtime';
|
||||
this.cssClass = 'icon-clock';
|
||||
this.label = 'Real-time';
|
||||
this.name = 'Real-time Mode';
|
||||
this.description = 'Monitor real-time streaming data as it comes in. The ' +
|
||||
'Time Conductor and displays will automatically advance themselves ' +
|
||||
'based on a UTC clock.';
|
||||
|
||||
this.period = period;
|
||||
this.$timeout = $timeout;
|
||||
this.timeoutHandle = undefined;
|
||||
}
|
||||
|
||||
LocalClock.prototype = Object.create(TickSource.prototype);
|
||||
LocalClock.prototype = Object.create(EventEmitter.prototype);
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
LocalClock.prototype.start = function () {
|
||||
this.timeoutHandle = this.$timeout(this.tick.bind(this), this.period);
|
||||
this.timeoutHandle = setTimeout(this.tick.bind(this), this.period);
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
LocalClock.prototype.stop = function () {
|
||||
if (this.timeoutHandle) {
|
||||
this.$timeout.cancel(this.timeoutHandle);
|
||||
clearTimeout(this.timeoutHandle);
|
||||
this.timeoutHandle = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
LocalClock.prototype.tick = function () {
|
||||
var now = Date.now();
|
||||
this.listeners.forEach(function (listener) {
|
||||
listener(now);
|
||||
});
|
||||
this.timeoutHandle = this.$timeout(this.tick.bind(this), this.period);
|
||||
this.emit("tick", now);
|
||||
this.timeoutHandle = setTimeout(this.tick.bind(this), this.period);
|
||||
};
|
||||
|
||||
/**
|
||||
@ -69,20 +76,30 @@ define(['./TickSource'], function (TickSource) {
|
||||
* @param listener
|
||||
* @returns {function} a function for deregistering the provided listener
|
||||
*/
|
||||
LocalClock.prototype.listen = function (listener) {
|
||||
var listeners = this.listeners;
|
||||
listeners.push(listener);
|
||||
LocalClock.prototype.on = function (event, listener) {
|
||||
var result = this.on.apply(this, arguments);
|
||||
|
||||
if (listeners.length === 1) {
|
||||
if (this.listeners(event).length === 1) {
|
||||
this.start();
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
return function () {
|
||||
listeners.splice(listeners.indexOf(listener));
|
||||
if (listeners.length === 0) {
|
||||
this.stop();
|
||||
}
|
||||
}.bind(this);
|
||||
/**
|
||||
* Register a listener for the local clock. When it ticks, the local
|
||||
* clock will provide the current local system time
|
||||
*
|
||||
* @param listener
|
||||
* @returns {function} a function for deregistering the provided listener
|
||||
*/
|
||||
LocalClock.prototype.off = function (event, listener) {
|
||||
var result = this.off.apply(this, arguments);
|
||||
|
||||
if (this.listeners(event).length === 0) {
|
||||
this.stop();
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
return LocalClock;
|
@ -20,39 +20,28 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define([
|
||||
'../../core/src/timeSystems/TimeSystem',
|
||||
'../../core/src/timeSystems/LocalClock'
|
||||
], function (TimeSystem, LocalClock) {
|
||||
var FIFTEEN_MINUTES = 15 * 60 * 1000,
|
||||
DEFAULT_PERIOD = 100;
|
||||
define([], function () {
|
||||
var FIFTEEN_MINUTES = 15 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* This time system supports UTC dates and provides a ticking clock source.
|
||||
* @implements TimeSystem
|
||||
* @constructor
|
||||
*/
|
||||
function UTCTimeSystem($timeout) {
|
||||
TimeSystem.call(this);
|
||||
function UTCTimeSystem() {
|
||||
|
||||
/**
|
||||
* Some metadata, which will be used to identify the time system in
|
||||
* the UI
|
||||
* @type {{key: string, name: string, cssClass: string}}
|
||||
*/
|
||||
this.metadata = {
|
||||
'key': 'utc',
|
||||
'name': 'UTC',
|
||||
'cssClass': 'icon-clock'
|
||||
};
|
||||
this.key = 'utc';
|
||||
this.name = 'UTC';
|
||||
this.cssClass = 'icon-clock';
|
||||
|
||||
this.fmts = ['utc'];
|
||||
this.sources = [new LocalClock($timeout, DEFAULT_PERIOD)];
|
||||
this.defaultValues = undefined;
|
||||
}
|
||||
|
||||
UTCTimeSystem.prototype = Object.create(TimeSystem.prototype);
|
||||
|
||||
UTCTimeSystem.prototype.formats = function () {
|
||||
return this.fmts;
|
||||
};
|
||||
@ -61,29 +50,22 @@ define([
|
||||
return 'duration';
|
||||
};
|
||||
|
||||
UTCTimeSystem.prototype.tickSources = function () {
|
||||
return this.sources;
|
||||
UTCTimeSystem.prototype.isUTCBased = function () {
|
||||
return true;
|
||||
};
|
||||
|
||||
UTCTimeSystem.prototype.defaults = function (defaults) {
|
||||
if (arguments.length > 0) {
|
||||
this.defaultValues = defaults;
|
||||
}
|
||||
UTCTimeSystem.prototype.defaults = function () {
|
||||
var now = Math.ceil(Date.now() / 1000) * 1000;
|
||||
var ONE_MINUTE = 60 * 1 * 1000;
|
||||
var FIFTY_YEARS = 50 * 365 * 24 * 60 * 60 * 1000;
|
||||
|
||||
if (this.defaultValues === undefined) {
|
||||
var now = Math.ceil(Date.now() / 1000) * 1000;
|
||||
var ONE_MINUTE = 60 * 1 * 1000;
|
||||
var FIFTY_YEARS = 50 * 365 * 24 * 60 * 60 * 1000;
|
||||
|
||||
this.defaultValues = {
|
||||
key: 'utc-default',
|
||||
name: 'UTC time system defaults',
|
||||
deltas: {start: FIFTEEN_MINUTES, end: 0},
|
||||
bounds: {start: now - FIFTEEN_MINUTES, end: now},
|
||||
zoom: {min: FIFTY_YEARS, max: ONE_MINUTE}
|
||||
};
|
||||
}
|
||||
return this.defaultValues;
|
||||
return {
|
||||
key: 'utc-default',
|
||||
name: 'UTC time system defaults',
|
||||
deltas: {start: FIFTEEN_MINUTES, end: 0},
|
||||
bounds: {start: now - FIFTEEN_MINUTES, end: now},
|
||||
zoom: {min: FIFTY_YEARS, max: ONE_MINUTE}
|
||||
};
|
||||
};
|
||||
|
||||
return UTCTimeSystem;
|
@ -21,20 +21,16 @@
|
||||
*****************************************************************************/
|
||||
|
||||
define([
|
||||
"./src/UTCTimeSystem",
|
||||
"legacyRegistry"
|
||||
"./UTCTimeSystem",
|
||||
"./LocalClock"
|
||||
], function (
|
||||
UTCTimeSystem,
|
||||
legacyRegistry
|
||||
LocalClock
|
||||
) {
|
||||
legacyRegistry.register("platform/features/conductor/utcTimeSystem", {
|
||||
"extensions": {
|
||||
"timeSystems": [
|
||||
{
|
||||
"implementation": UTCTimeSystem,
|
||||
"depends": ["$timeout"]
|
||||
}
|
||||
]
|
||||
return function () {
|
||||
return function (openmct) {
|
||||
openmct.time.addTimeSystem(new UTCTimeSystem());
|
||||
openmct.time.addClock(new LocalClock());
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
Reference in New Issue
Block a user