Compare commits
79 Commits
view-api-1
...
rename-typ
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
047d050cef | ||
|
|
46c7399867 | ||
|
|
9a6745635d | ||
|
|
d3b4ad41c2 | ||
|
|
b28eb049dc | ||
|
|
e712edba4e | ||
|
|
35d8024aaa | ||
|
|
17564aa489 | ||
|
|
3ae0fd7bc9 | ||
|
|
df7d59bc9c | ||
|
|
4f24c46e9b | ||
|
|
d262c4428e | ||
|
|
9f9d28deef | ||
|
|
ea74385ac8 | ||
|
|
06cc95efb1 | ||
|
|
09ebeeb8e4 | ||
|
|
7e8e861468 | ||
|
|
df2ce72e39 | ||
|
|
fe8398017c | ||
|
|
c2253f5010 | ||
|
|
290dd0abf0 | ||
|
|
49560698f6 | ||
|
|
1ce1d29c87 | ||
|
|
d522d105ad | ||
|
|
72b753c67f | ||
|
|
16ec65f38c | ||
|
|
ed67866f45 | ||
|
|
36b5197733 | ||
|
|
fa28393c14 | ||
|
|
077f076c43 | ||
|
|
c7ae520d7e | ||
|
|
39eb7ba862 | ||
|
|
116bb2c25f | ||
|
|
6bf293f96b | ||
|
|
eceaa38ed8 | ||
|
|
b6aa087536 | ||
|
|
d94e8b10d8 | ||
|
|
590a0fe080 | ||
|
|
41d0e953f5 | ||
|
|
3436455201 | ||
|
|
bb63e13770 | ||
|
|
62685cb892 | ||
|
|
6fe0ce70eb | ||
|
|
bee22311a7 | ||
|
|
2cde80237f | ||
|
|
3b93454c53 | ||
|
|
a5a17b9502 | ||
|
|
381f3d9b69 | ||
|
|
94319df69b | ||
|
|
1666c42f78 | ||
|
|
f1870e286d | ||
|
|
94194ff675 | ||
|
|
d37caa7665 | ||
|
|
a768b12985 | ||
|
|
6f257593c8 | ||
|
|
c4d47ddc26 | ||
|
|
60d1b73160 | ||
|
|
96c054415d | ||
|
|
89be1c810a | ||
|
|
6328bd9354 | ||
|
|
fcda211800 | ||
|
|
daa71c4f69 | ||
|
|
f2d61604f7 | ||
|
|
f0b9292458 | ||
|
|
0b79ec1235 | ||
|
|
b9601ff819 | ||
|
|
6f417fc4c7 | ||
|
|
d99b4d35ab | ||
|
|
6936ab6100 | ||
|
|
532f7a76f9 | ||
|
|
09e79d38ff | ||
|
|
1c40fa88ce | ||
|
|
a4ff5ffb43 | ||
|
|
799c418124 | ||
|
|
215e9b26a8 | ||
|
|
43132ea6f8 | ||
|
|
90a7ca8ae5 | ||
|
|
f077f4fc1b | ||
|
|
0cd849fe42 |
@@ -14,7 +14,8 @@
|
||||
"nonew": true,
|
||||
"predef": [
|
||||
"define",
|
||||
"Promise"
|
||||
"Promise",
|
||||
"WeakMap"
|
||||
],
|
||||
"shadow": "outer",
|
||||
"strict": "implied",
|
||||
|
||||
64
API.md
64
API.md
@@ -129,8 +129,10 @@ provider.
|
||||
|
||||
The "composition" of a domain object is the list of objects it contains,
|
||||
as shown (for example) in the tree for browsing. Open MCT provides a
|
||||
default solution for composition, but there may be cases where you want
|
||||
to provide the composition of a certain object (or type of object) dynamically.
|
||||
[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:
|
||||
@@ -146,6 +148,29 @@ openmct.composition.addProvider({
|
||||
});
|
||||
```
|
||||
|
||||
#### 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.:
|
||||
|
||||
```js
|
||||
var domainObject = {
|
||||
name: "My Object",
|
||||
type: 'folder',
|
||||
composition: [
|
||||
{
|
||||
key: '412229c3-922c-444b-8624-736d85516247',
|
||||
namespace: 'foo'
|
||||
},
|
||||
{
|
||||
key: 'd6e0ce02-5b85-4e55-8006-a8a505b64c75',
|
||||
namespace: 'foo'
|
||||
}
|
||||
]
|
||||
};
|
||||
```
|
||||
|
||||
### Adding Telemetry Providers
|
||||
|
||||
When connecting to a new telemetry source, you will want to register a new
|
||||
@@ -265,6 +290,41 @@ openmct.install(myPlugin);
|
||||
|
||||
The plugin will be invoked to configure Open MCT before it is started.
|
||||
|
||||
### Included Plugins
|
||||
|
||||
Open MCT is packaged along with a few general-purpose plugins:
|
||||
|
||||
* `openmct.plugins.CouchDB` is an adapter for using CouchDB for persistence
|
||||
of user-created objects. This is a constructor that takes the URL for the
|
||||
CouchDB database as a parameter, e.g.
|
||||
`openmct.install(new openmct.plugins.CouchDB('http://localhost:5984/openmct'))`
|
||||
* `openmct.plugins.Elasticsearch` is an adapter for using Elasticsearch for
|
||||
persistence of user-created objects. This is a
|
||||
constructor that takes the URL for the Elasticsearch instance as a
|
||||
parameter, e.g.
|
||||
`openmct.install(new openmct.plugins.CouchDB('http://localhost:9200'))`.
|
||||
Domain objects will be indexed at `/mct/domain_object`.
|
||||
* `openmct.plugins.espresso` and `openmct.plugins.snow` are two different
|
||||
themes (dark and light) available for Open MCT. Note that at least one
|
||||
of these themes must be installed for Open MCT to appear correctly.
|
||||
* `openmct.plugins.localStorage` provides persistence of user-created
|
||||
objects in browser-local storage. This is particularly useful in
|
||||
development environments.
|
||||
* `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.
|
||||
|
||||
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):
|
||||
|
||||
```
|
||||
openmct.install(openmct.plugins.localStorage);
|
||||
openmct.install(openmct.plugins.myItems);
|
||||
```
|
||||
|
||||
### Writing Plugins
|
||||
|
||||
Plugins configure Open MCT, and should utilize the
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Open MCT [](http://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
||||
Open MCT is a next-generation mission control framework for visualization of data on desktop and mobile devices. It is developed at NASA's Ames Research Center, and is being used by NASA for data analysis of spacecraft missions, as well as planning and operation of experimental rover systems. As a generalizable and open source framework, Open MCT could be used as the basis for building applications for planning, operation, and analysis of any systems producing telemetry data.
|
||||
Open MCT (Open Mission Control Technologies) is a next-generation mission control framework for visualization of data on desktop and mobile devices. It is developed at NASA's Ames Research Center, and is being used by NASA for data analysis of spacecraft missions, as well as planning and operation of experimental rover systems. As a generalizable and open source framework, Open MCT could be used as the basis for building applications for planning, operation, and analysis of any systems producing telemetry data.
|
||||
|
||||
Please visit our [Official Site](https://nasa.github.io/openmct/) and [Getting Started Guide](https://nasa.github.io/openmct/getting-started/)
|
||||
|
||||
|
||||
@@ -131,7 +131,7 @@ Keeping that in mind, there are a few useful patterns supported by the
|
||||
framework that are useful to keep in mind.
|
||||
|
||||
The specific service infrastructure provided by the platform is described
|
||||
in the [Platform Architecture](Platform.md).
|
||||
in the [Platform Architecture](platform.md).
|
||||
|
||||
## Extension Categories
|
||||
|
||||
|
||||
@@ -1338,55 +1338,6 @@ are supported:
|
||||
|
||||
Open MCT defines several Angular directives that are intended for use both
|
||||
internally within the platform, and by plugins.
|
||||
|
||||
## Before Unload
|
||||
|
||||
The `mct-before-unload` directive is used to listen for (and prompt for user
|
||||
confirmation) of navigation changes in the browser. This includes reloading,
|
||||
following links out of Open MCT, or changing routes. It is used to hook into
|
||||
both `onbeforeunload` event handling as well as route changes from within
|
||||
Angular.
|
||||
|
||||
This directive is useable as an attribute. Its value should be an Angular
|
||||
expression. When an action that would trigger an unload and/or route change
|
||||
occurs, this Angular expression is evaluated. Its result should be a message to
|
||||
display to the user to confirm their navigation change; if this expression
|
||||
evaluates to a falsy value, no message will be displayed.
|
||||
|
||||
## Chart
|
||||
|
||||
The `mct-chart` directive is used to support drawing of simple charts. It is
|
||||
present to support the Plot view, and its functionality is limited to the
|
||||
functionality that is relevant for that view.
|
||||
|
||||
This directive is used at the element level and takes one attribute, `draw`
|
||||
which is an Angular expression which will should evaluate to a drawing object.
|
||||
This drawing object should contain the following properties:
|
||||
|
||||
* `dimensions`: The size, in logical coordinates, of the chart area. A
|
||||
two-element array or numbers.
|
||||
* `origin`: The position, in logical coordinates, of the lower-left corner of
|
||||
the chart area. A two-element array or numbers.
|
||||
* `lines`: An array of lines (e.g. as a plot line) to draw, where each line is
|
||||
expressed as an object containing:
|
||||
* `buffer`: A Float32Array containing points in the line, in logical
|
||||
coordinates, in sequential x,y pairs.
|
||||
* `color`: The color of the line, as a four-element RGBA array, where
|
||||
each element is a number in the range of 0.0-1.0.
|
||||
* `points`: The number of points in the line.
|
||||
* `boxes`: An array of rectangles to draw in the chart area. Each is an object
|
||||
containing:
|
||||
* `start`: The first corner of the rectangle, as a two-element array of
|
||||
numbers, in logical coordinates.
|
||||
* `end`: The opposite corner of the rectangle, as a two-element array of
|
||||
numbers, in logical coordinates. color : The color of the line, as a
|
||||
four-element RGBA array, where each element is a number in the range of
|
||||
0.0-1.0.
|
||||
|
||||
While `mct-chart` is intended to support plots specifically, it does perform
|
||||
some useful management of canvas objects (e.g. choosing between WebGL and Canvas
|
||||
2D APIs for drawing based on browser support) so its usage is recommended when
|
||||
its supported drawing primitives are sufficient for other charting tasks.
|
||||
|
||||
## Container
|
||||
|
||||
|
||||
@@ -129,7 +129,7 @@ We will create this file in the directory tutorials/todo (we can hereafter refer
|
||||
to this plugin as tutorials/todo as well.) We will start with an "empty bundle",
|
||||
one which exposes no extensions - which looks like:
|
||||
|
||||
```diff
|
||||
```js
|
||||
define([
|
||||
'openmct'
|
||||
], function (
|
||||
@@ -154,7 +154,7 @@ The tutorials will be updated with the new bundle registration mechanism once it
|
||||
has been finalized.
|
||||
|
||||
#### Before
|
||||
```diff
|
||||
```html
|
||||
<!--
|
||||
Open MCT, Copyright (c) 2014-2016, United States Government
|
||||
as represented by the Administrator of the National Aeronautics and Space
|
||||
@@ -219,7 +219,7 @@ __index.html__
|
||||
|
||||
#### After
|
||||
|
||||
```diff
|
||||
```html
|
||||
<!--
|
||||
Open MCT, Copyright (c) 2014-2016, United States Government
|
||||
as represented by the Administrator of the National Aeronautics and Space
|
||||
@@ -305,7 +305,7 @@ In the case of our to-do list feature, the to-do list itself is the thing we'll
|
||||
want users to be able to create and edit. So, we will add that as a new type in
|
||||
our bundle definition:
|
||||
|
||||
```diff
|
||||
```js
|
||||
define([
|
||||
'openmct'
|
||||
], function (
|
||||
@@ -340,8 +340,9 @@ Going through the properties we've defined:
|
||||
domain objects of this type.
|
||||
* The `name` of "To-Do List" is the human-readable name for this type, and will
|
||||
be shown to users.
|
||||
* The `glyph` refers to a special character in Open MCT's custom font set;
|
||||
this will be used as an icon.
|
||||
* The `cssclass` maps to an icon that will be shown for each To-Do List. The icons
|
||||
are defined in our [custom open MCT icon set](https://github.com/nasa/openmct/blob/master/platform/commonUI/general/res/sass/_glyphs.scss).
|
||||
A complete list of available icons will be provided in the future.
|
||||
* The `description` is also human-readable, and will be used whenever a longer
|
||||
explanation of what this type is should be shown.
|
||||
* Finally, the `features` property describes some special features of objects of
|
||||
@@ -369,7 +370,7 @@ directory `tutorials/todo/res/templates` (`res` is, by default, the directory
|
||||
where bundle-related resources are kept, and `templates` is where HTML templates
|
||||
are stored by convention.)
|
||||
|
||||
```diff
|
||||
```html
|
||||
<div>
|
||||
<a href="">All</a>
|
||||
<a href="">Incomplete</a>
|
||||
@@ -401,7 +402,7 @@ boolean `completed` flag.
|
||||
To expose this view in Open MCT, we need to declare it in our bundle
|
||||
definition:
|
||||
|
||||
```diff
|
||||
```js
|
||||
define([
|
||||
'openmct'
|
||||
], function (
|
||||
@@ -446,7 +447,7 @@ the domain object type, but could have chosen any unique name.
|
||||
domain objects of that type. This means that we'll see this view for To-do Lists
|
||||
that we create, but not for other domain objects (such as Folders.)
|
||||
|
||||
* The `glyph` and `name` properties describe the icon and human-readable name
|
||||
* The `cssclass` and `name` properties describe the icon and human-readable name
|
||||
for this view to display in the UI where needed (if multiple views are available
|
||||
for To-do Lists, the user will be able to choose one.)
|
||||
|
||||
@@ -458,7 +459,7 @@ the user to create these yet. As a temporary workaround to test the view, we
|
||||
will specify an initial state for To-do List domain object models in the
|
||||
definition of that type.
|
||||
|
||||
```diff
|
||||
```js
|
||||
define([
|
||||
'openmct'
|
||||
], function (
|
||||
@@ -529,7 +530,7 @@ in the directory `tutorials/todo/src/controllers` (`src` is, by default, the
|
||||
directory where bundle-related source code is kept, and controllers is where
|
||||
Angular controllers are stored by convention.)
|
||||
|
||||
```diff
|
||||
```js
|
||||
define(function () {
|
||||
function TodoController($scope) {
|
||||
var showAll = true,
|
||||
@@ -594,7 +595,7 @@ prior to our template being utilized.
|
||||
On its own, this controller merely exposes these functions; the next step is to
|
||||
use them from our template:
|
||||
|
||||
```diff
|
||||
```html
|
||||
+ <div ng-controller="TodoController">
|
||||
<div>
|
||||
+ <a ng-click="setVisibility(true)">All</a>
|
||||
@@ -630,7 +631,7 @@ If we were to try to run at this point, we'd run into problems because the
|
||||
`TodoController` has not been registered with Angular. We need to first declare
|
||||
it in our bundle definition, as an extension of category `controllers`:
|
||||
|
||||
```diff
|
||||
```js
|
||||
define([
|
||||
'openmct',
|
||||
+ './src/controllers/TodoController'
|
||||
@@ -724,7 +725,7 @@ An Editing user interface is typically handled in a tool bar associated with a
|
||||
view. The contents of this tool bar are defined declaratively in a view's
|
||||
extension definition.
|
||||
|
||||
```diff
|
||||
```js
|
||||
define([
|
||||
'openmct',
|
||||
'./src/controllers/TodoController'
|
||||
@@ -813,7 +814,7 @@ all the applicable controls, which means no controls at all.
|
||||
|
||||
To support selection, we will need to make some changes to our controller:
|
||||
|
||||
```diff
|
||||
```js
|
||||
define(function () {
|
||||
+ // Form to display when adding new tasks
|
||||
+ var NEW_TASK_FORM = {
|
||||
@@ -928,7 +929,7 @@ Additionally, we need to make changes to our template to select specific tasks
|
||||
in response to some user gesture. Here, we will select tasks when a user clicks
|
||||
the description.
|
||||
|
||||
```diff
|
||||
```html
|
||||
<div ng-controller="TodoController">
|
||||
<div>
|
||||
<a ng-click="setVisibility(true)">All</a>
|
||||
@@ -954,7 +955,7 @@ __tutorials/todo/res/templates/todo.html__
|
||||
Finally, the `TodoController` uses the `dialogService` now, so we need to
|
||||
declare that dependency in its extension definition:
|
||||
|
||||
```diff
|
||||
```js
|
||||
define([
|
||||
'openmct',
|
||||
'./src/controllers/TodoController'
|
||||
@@ -1058,7 +1059,7 @@ In this section, our goal is to:
|
||||
To support the first two, we'll need to expose some methods for checking these
|
||||
states in the controller:
|
||||
|
||||
```diff
|
||||
```js
|
||||
define(function () {
|
||||
// Form to display when adding new tasks
|
||||
var NEW_TASK_FORM = {
|
||||
@@ -1175,7 +1176,7 @@ states visually, and to generally improve the appearance of our view. We add
|
||||
another file to the res directory of our bundle; this time, it is `css/todo.css`
|
||||
(with the `css` directory again being a convention.)
|
||||
|
||||
```diff
|
||||
```css
|
||||
.example-todo div.example-button-group {
|
||||
margin-top: 12px;
|
||||
margin-bottom: 12px;
|
||||
@@ -1219,7 +1220,7 @@ To include this CSS file in our running instance of Open MCT, we need to
|
||||
declare it in our bundle definition, this time as an extension of category
|
||||
`stylesheets`:
|
||||
|
||||
```diff
|
||||
```js
|
||||
define([
|
||||
'openmct',
|
||||
'./src/controllers/TodoController'
|
||||
@@ -1299,7 +1300,7 @@ To-Do List's type above; now To-Do Lists will start off empty.
|
||||
|
||||
Finally, let's utilize these changes from our view's template:
|
||||
|
||||
```diff
|
||||
```html
|
||||
+ <div ng-controller="TodoController" class="example-todo">
|
||||
+ <div class="example-button-group">
|
||||
+ <a ng-class="{ selected: checkVisibility(true) }"
|
||||
@@ -1359,7 +1360,7 @@ We'll also be defining some custom styles, so we'll include that extension as
|
||||
well. We'll be creating this plugin in `tutorials/bargraph`, so our initial
|
||||
bundle definition looks like:
|
||||
|
||||
```diff
|
||||
```js
|
||||
define([
|
||||
'openmct'
|
||||
], function (
|
||||
@@ -1406,7 +1407,7 @@ For this tutorial, we'll assume that we've sketched out our template and CSS
|
||||
file ahead of time to describe the general look we want for the view. These
|
||||
look like:
|
||||
|
||||
```diff
|
||||
```html
|
||||
<div class="example-bargraph">
|
||||
<div class="example-tick-labels">
|
||||
<div class="example-tick-label" style="bottom: 0%">High</div>
|
||||
@@ -1457,7 +1458,7 @@ bar corresponds to which telemetry point. Inline `style` attributes are used
|
||||
wherever dynamic positioning (handled by a script) is anticipated.
|
||||
The corresponding CSS file which styles and positions these elements:
|
||||
|
||||
```diff
|
||||
```css
|
||||
.example-bargraph {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@@ -1555,7 +1556,7 @@ Notably, we will not try to show telemetry data after this step.
|
||||
|
||||
To support this, we will add a new controller which supports our Bar Graph view:
|
||||
|
||||
```diff
|
||||
```js
|
||||
define(function () {
|
||||
function BarGraphController($scope, telemetryHandler) {
|
||||
var handle;
|
||||
@@ -1607,7 +1608,7 @@ telemetry objects in view, as well as the width for each bar.
|
||||
|
||||
We will also utilize this from our template:
|
||||
|
||||
```diff
|
||||
```html
|
||||
+ <div class="example-bargraph" ng-controller="BarGraphController">
|
||||
<div class="example-tick-labels">
|
||||
+ <div ng-repeat="value in [low, middle, high] track by $index"
|
||||
@@ -1660,7 +1661,7 @@ Finally, we expose our controller from our bundle definition. Note that the
|
||||
depends declaration includes both `$scope` as well as the `telemetryHandler`
|
||||
service we made use of.
|
||||
|
||||
```diff
|
||||
```js
|
||||
define([
|
||||
'openmct',
|
||||
'./src/controllers/BarGraphController'
|
||||
@@ -1715,7 +1716,7 @@ First, let's add expose some more functionality from our controller. To make it
|
||||
simple, we'll expose the top and bottom for a bar graph for a given
|
||||
telemetry-providing domain object, as percentages.
|
||||
|
||||
```diff
|
||||
```js
|
||||
define(function () {
|
||||
function BarGraphController($scope, telemetryHandler) {
|
||||
var handle;
|
||||
@@ -1767,7 +1768,7 @@ decide this.
|
||||
|
||||
Next, we utilize this functionality from the template:
|
||||
|
||||
```diff
|
||||
```html
|
||||
<div class="example-bargraph" ng-controller="BarGraphController">
|
||||
<div class="example-tick-labels">
|
||||
<div ng-repeat="value in [low, middle, high] track by $index"
|
||||
@@ -1826,7 +1827,7 @@ when we return to our view later, those changes will be persisted.
|
||||
|
||||
First, let's add a tool bar for changing these three values in Edit mode:
|
||||
|
||||
```diff
|
||||
```js
|
||||
define([
|
||||
'openmct',
|
||||
'./src/controllers/BarGraphController'
|
||||
@@ -1900,7 +1901,7 @@ a view proxy to work from. We will add this to our controller, and additionally
|
||||
will start reading/writing those properties to the view's `configuration`
|
||||
object.
|
||||
|
||||
```diff
|
||||
```js
|
||||
define(function () {
|
||||
function BarGraphController($scope, telemetryHandler) {
|
||||
var handle;
|
||||
@@ -2023,7 +2024,7 @@ For purposes of this tutorial, a simple node server is provided to stand
|
||||
in place of this existing telemetry system. It generates real-time data
|
||||
and exposes it over a WebSocket connection.
|
||||
|
||||
```diff
|
||||
```js
|
||||
/*global require,process,console*/
|
||||
|
||||
var CONFIG = {
|
||||
@@ -2205,7 +2206,7 @@ used by the server. It uses a custom format and, for purposes of example,
|
||||
contains three "subsystems" containing a mix of numeric and string-based
|
||||
telemetry.
|
||||
|
||||
```diff
|
||||
```json
|
||||
{
|
||||
"name": "Example Spacecraft",
|
||||
"identifier": "sc",
|
||||
@@ -2432,7 +2433,7 @@ server. Our first step will be to add a service that will handle interactions
|
||||
with the server; this will not be used by Open MCT directly, but will be
|
||||
used by subsequent components we add.
|
||||
|
||||
```diff
|
||||
```js
|
||||
/*global define,WebSocket*/
|
||||
|
||||
define(
|
||||
@@ -2487,7 +2488,7 @@ subsystems. This means that we need to convert the data from the dictionary
|
||||
into domain object models, and expose these to Open MCT via a
|
||||
`modelService`.
|
||||
|
||||
```diff
|
||||
```js
|
||||
/*global define*/
|
||||
|
||||
define(
|
||||
@@ -2621,7 +2622,7 @@ This allows our telemetry dictionary to be expressed as domain object models
|
||||
fix this, we will need another script which will add these subsystems to the
|
||||
root-level object we added in Step 1.
|
||||
|
||||
```diff
|
||||
```js
|
||||
/*global define*/
|
||||
|
||||
define(
|
||||
@@ -2686,7 +2687,7 @@ Finally, we wire in these changes by modifying our plugin's `bundle.js` to
|
||||
provide metadata about how these pieces interact (both with each other, and
|
||||
with the platform):
|
||||
|
||||
```diff
|
||||
```js
|
||||
define([
|
||||
'openmct',
|
||||
+ './src/ExampleTelemetryServerAdapter',
|
||||
@@ -2834,7 +2835,7 @@ will do so for the server's historical telemetry.
|
||||
Our first step will be to add a method to our server adapter which allows us to
|
||||
send history requests to the server:
|
||||
|
||||
```diff
|
||||
```js
|
||||
/*global define,WebSocket*/
|
||||
|
||||
define(
|
||||
@@ -2893,7 +2894,7 @@ identifier, the pending promise is resolved.
|
||||
This `history` method will be used by a `telemetryService` provider which we
|
||||
will implement:
|
||||
|
||||
```diff
|
||||
```js
|
||||
/*global define*/
|
||||
|
||||
define(
|
||||
@@ -2979,7 +2980,7 @@ Finally, note that we also have a `subscribe` method, to satisfy the interface o
|
||||
|
||||
This script uses an `ExampleTelemetrySeries` class, which looks like:
|
||||
|
||||
```diff
|
||||
```js
|
||||
/*global define*/
|
||||
|
||||
define(
|
||||
@@ -3011,7 +3012,7 @@ it with the interface expected by the platform (the methods shown.)
|
||||
|
||||
Finally, we expose this `telemetryService` provider declaratively:
|
||||
|
||||
```diff
|
||||
```js
|
||||
define([
|
||||
'openmct',
|
||||
'./src/ExampleTelemetryServerAdapter',
|
||||
@@ -3126,7 +3127,7 @@ Finally, we want to utilize the server's ability to subscribe to telemetry
|
||||
from Open MCT. To do this, first we want to expose some new methods for
|
||||
this from our server adapter:
|
||||
|
||||
```diff
|
||||
```js
|
||||
/*global define,WebSocket*/
|
||||
|
||||
define(
|
||||
@@ -3199,7 +3200,7 @@ with these subscriptions.
|
||||
|
||||
We then need only to utilize these methods from our `telemetryService`:
|
||||
|
||||
```diff
|
||||
```js
|
||||
/*global define*/
|
||||
|
||||
define(
|
||||
@@ -3305,4 +3306,4 @@ server can handle this.)
|
||||
Running Open MCT again, we can still plot our historical telemetry - but
|
||||
now we also see that it updates in real-time as more data comes in from the
|
||||
server.
|
||||
|
||||
|
||||
|
||||
@@ -92,7 +92,10 @@ define([
|
||||
"features": "creation",
|
||||
"model": {
|
||||
"telemetry": {
|
||||
"period": 10
|
||||
"period": 10,
|
||||
"amplitude": 1,
|
||||
"offset": 0,
|
||||
"dataRateInHz": 1
|
||||
}
|
||||
},
|
||||
"telemetry": {
|
||||
@@ -135,6 +138,42 @@ define([
|
||||
"period"
|
||||
],
|
||||
"pattern": "^\\d*(\\.\\d*)?$"
|
||||
},
|
||||
{
|
||||
"name": "Amplitude",
|
||||
"control": "textfield",
|
||||
"cssclass": "l-input-sm l-numeric",
|
||||
"key": "amplitude",
|
||||
"required": true,
|
||||
"property": [
|
||||
"telemetry",
|
||||
"amplitude"
|
||||
],
|
||||
"pattern": "^\\d*(\\.\\d*)?$"
|
||||
},
|
||||
{
|
||||
"name": "Offset",
|
||||
"control": "textfield",
|
||||
"cssclass": "l-input-sm l-numeric",
|
||||
"key": "offset",
|
||||
"required": true,
|
||||
"property": [
|
||||
"telemetry",
|
||||
"offset"
|
||||
],
|
||||
"pattern": "^\\d*(\\.\\d*)?$"
|
||||
},
|
||||
{
|
||||
"name": "Data Rate (hz)",
|
||||
"control": "textfield",
|
||||
"cssclass": "l-input-sm l-numeric",
|
||||
"key": "dataRateInHz",
|
||||
"required": true,
|
||||
"property": [
|
||||
"telemetry",
|
||||
"dataRateInHz"
|
||||
],
|
||||
"pattern": "^\\d*(\\.\\d*)?$"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
83
example/generator/src/GeneratorProvider.js
Normal file
83
example/generator/src/GeneratorProvider.js
Normal file
@@ -0,0 +1,83 @@
|
||||
/*****************************************************************************
|
||||
* 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([
|
||||
'./WorkerInterface'
|
||||
], function (
|
||||
WorkerInterface
|
||||
) {
|
||||
|
||||
var REQUEST_DEFAULTS = {
|
||||
amplitude: 1,
|
||||
period: 10,
|
||||
offset: 0,
|
||||
dataRateInHz: 1
|
||||
};
|
||||
|
||||
function GeneratorProvider() {
|
||||
this.workerInterface = new WorkerInterface();
|
||||
}
|
||||
|
||||
GeneratorProvider.prototype.canProvideTelemetry = function (domainObject) {
|
||||
return domainObject.type === 'generator';
|
||||
};
|
||||
|
||||
GeneratorProvider.prototype.makeWorkerRequest = function (domainObject, request) {
|
||||
var props = [
|
||||
'amplitude',
|
||||
'period',
|
||||
'offset',
|
||||
'dataRateInHz'
|
||||
];
|
||||
|
||||
var workerRequest = {};
|
||||
|
||||
props.forEach(function (prop) {
|
||||
if (domainObject.telemetry && domainObject.telemetry.hasOwnProperty(prop)) {
|
||||
workerRequest[prop] = domainObject.telemetry[prop];
|
||||
}
|
||||
if (request.hasOwnProperty(prop)) {
|
||||
workerRequest[prop] = request[prop];
|
||||
}
|
||||
if (!workerRequest[prop]) {
|
||||
workerRequest[prop] = REQUEST_DEFAULTS[prop];
|
||||
}
|
||||
workerRequest[prop] = Number(workerRequest[prop]);
|
||||
});
|
||||
|
||||
return workerRequest;
|
||||
};
|
||||
|
||||
GeneratorProvider.prototype.request = function (domainObject, request) {
|
||||
var workerRequest = this.makeWorkerRequest(domainObject, request);
|
||||
workerRequest.start = request.start;
|
||||
workerRequest.end = request.end;
|
||||
return this.workerInterface.request(workerRequest);
|
||||
};
|
||||
|
||||
GeneratorProvider.prototype.subscribe = function (domainObject, callback, request) {
|
||||
var workerRequest = this.makeWorkerRequest(domainObject, request);
|
||||
return this.workerInterface.subscribe(workerRequest, callback);
|
||||
};
|
||||
|
||||
return GeneratorProvider;
|
||||
});
|
||||
@@ -24,96 +24,47 @@
|
||||
/**
|
||||
* Module defining SinewaveTelemetryProvider. Created by vwoeltje on 11/12/14.
|
||||
*/
|
||||
define(
|
||||
["./SinewaveTelemetrySeries"],
|
||||
function (SinewaveTelemetrySeries) {
|
||||
"use strict";
|
||||
define([
|
||||
"./SinewaveTelemetrySeries",
|
||||
"./GeneratorProvider"
|
||||
], function (
|
||||
SinewaveTelemetrySeries,
|
||||
GeneratorProvider
|
||||
) {
|
||||
|
||||
/**
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
function SinewaveTelemetryProvider($q, $timeout) {
|
||||
var subscriptions = [],
|
||||
generating = false;
|
||||
function SinewaveTelemetryProvider() {
|
||||
this.provider = new GeneratorProvider();
|
||||
}
|
||||
|
||||
//
|
||||
function matchesSource(request) {
|
||||
return request.source === "generator";
|
||||
}
|
||||
|
||||
// Used internally; this will be repacked by doPackage
|
||||
function generateData(request) {
|
||||
return {
|
||||
key: request.key,
|
||||
telemetry: new SinewaveTelemetrySeries(request)
|
||||
SinewaveTelemetryProvider.prototype.requestTelemetry = function (requests) {
|
||||
if (requests[0].source !== 'generator') {
|
||||
return Promise.resolve({});
|
||||
}
|
||||
return this.provider.request({}, requests[0])
|
||||
.then(function (data) {
|
||||
var res = {
|
||||
generator: {}
|
||||
};
|
||||
}
|
||||
res.generator[requests[0].key] = new SinewaveTelemetrySeries(data);
|
||||
return res;
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
function doPackage(results) {
|
||||
var packaged = {};
|
||||
results.forEach(function (result) {
|
||||
packaged[result.key] = result.telemetry;
|
||||
});
|
||||
// Format as expected (sources -> keys -> telemetry)
|
||||
return { generator: packaged };
|
||||
}
|
||||
|
||||
function requestTelemetry(requests) {
|
||||
return $timeout(function () {
|
||||
return doPackage(requests.filter(matchesSource).map(generateData));
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function handleSubscriptions() {
|
||||
subscriptions.forEach(function (subscription) {
|
||||
var requests = subscription.requests;
|
||||
subscription.callback(doPackage(
|
||||
requests.filter(matchesSource).map(generateData)
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
function startGenerating() {
|
||||
generating = true;
|
||||
$timeout(function () {
|
||||
handleSubscriptions();
|
||||
if (generating && subscriptions.length > 0) {
|
||||
startGenerating();
|
||||
} else {
|
||||
generating = false;
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function subscribe(callback, requests) {
|
||||
var subscription = {
|
||||
callback: callback,
|
||||
requests: requests
|
||||
};
|
||||
|
||||
function unsubscribe() {
|
||||
subscriptions = subscriptions.filter(function (s) {
|
||||
return s !== subscription;
|
||||
});
|
||||
}
|
||||
|
||||
subscriptions.push(subscription);
|
||||
|
||||
if (!generating) {
|
||||
startGenerating();
|
||||
}
|
||||
|
||||
return unsubscribe;
|
||||
}
|
||||
|
||||
return {
|
||||
requestTelemetry: requestTelemetry,
|
||||
subscribe: subscribe
|
||||
};
|
||||
SinewaveTelemetryProvider.prototype.subscribe = function (callback, requests) {
|
||||
if (requests[0].source !== 'generator') {
|
||||
return function unsubscribe() {};
|
||||
}
|
||||
|
||||
return SinewaveTelemetryProvider;
|
||||
}
|
||||
);
|
||||
function wrapper(data) {
|
||||
var res = {
|
||||
generator: {}
|
||||
};
|
||||
res.generator[requests[0].key] = new SinewaveTelemetrySeries(data);
|
||||
callback(res);
|
||||
}
|
||||
|
||||
return this.provider.subscribe({}, wrapper, requests[0]);
|
||||
};
|
||||
|
||||
return SinewaveTelemetryProvider;
|
||||
});
|
||||
|
||||
@@ -24,55 +24,52 @@
|
||||
/**
|
||||
* Module defining SinewaveTelemetry. Created by vwoeltje on 11/12/14.
|
||||
*/
|
||||
define(
|
||||
['./SinewaveConstants'],
|
||||
function (SinewaveConstants) {
|
||||
define([
|
||||
|
||||
], function (
|
||||
|
||||
) {
|
||||
"use strict";
|
||||
|
||||
var ONE_DAY = 60 * 60 * 24,
|
||||
firstObservedTime = Math.floor(SinewaveConstants.START_TIME / 1000);
|
||||
|
||||
/**
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
function SinewaveTelemetrySeries(request) {
|
||||
var timeOffset = (request.domain === 'yesterday') ? ONE_DAY : 0,
|
||||
latestTime = Math.floor(Date.now() / 1000) - timeOffset,
|
||||
firstTime = firstObservedTime - timeOffset,
|
||||
endTime = (request.end !== undefined) ?
|
||||
Math.floor(request.end / 1000) : latestTime,
|
||||
count = Math.min(endTime, latestTime) - firstTime,
|
||||
period = +request.period || 30,
|
||||
generatorData = {},
|
||||
requestStart = (request.start === undefined) ? firstTime :
|
||||
Math.max(Math.floor(request.start / 1000), firstTime),
|
||||
offset = requestStart - firstTime;
|
||||
|
||||
if (request.size !== undefined) {
|
||||
offset = Math.max(offset, count - request.size);
|
||||
function SinewaveTelemetrySeries(data) {
|
||||
if (!Array.isArray(data)) {
|
||||
data = [data];
|
||||
}
|
||||
|
||||
generatorData.getPointCount = function () {
|
||||
return count - offset;
|
||||
};
|
||||
|
||||
generatorData.getDomainValue = function (i, domain) {
|
||||
// delta uses the same numeric values as the default domain,
|
||||
// so it's not checked for here, just formatted for display
|
||||
// differently.
|
||||
return (i + offset) * 1000 + firstTime * 1000 -
|
||||
(domain === 'yesterday' ? (ONE_DAY * 1000) : 0);
|
||||
};
|
||||
|
||||
generatorData.getRangeValue = function (i, range) {
|
||||
range = range || "sin";
|
||||
return Math[range]((i + offset) * Math.PI * 2 / period);
|
||||
};
|
||||
|
||||
return generatorData;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
SinewaveTelemetrySeries.prototype.getPointCount = function () {
|
||||
return this.data.length;
|
||||
};
|
||||
|
||||
|
||||
SinewaveTelemetrySeries.prototype.getDomainValue = function (
|
||||
index,
|
||||
domain
|
||||
) {
|
||||
domain = domain || 'time';
|
||||
|
||||
return this.getDatum(index)[domain];
|
||||
};
|
||||
|
||||
SinewaveTelemetrySeries.prototype.getRangeValue = function (
|
||||
index,
|
||||
range
|
||||
) {
|
||||
range = range || 'sin';
|
||||
return this.getDatum(index)[range];
|
||||
};
|
||||
|
||||
SinewaveTelemetrySeries.prototype.getDatum = function (index) {
|
||||
if (index > this.data.length || index < 0) {
|
||||
throw new Error('IndexOutOfRange: index not available in series.');
|
||||
}
|
||||
return this.data[index];
|
||||
};
|
||||
|
||||
SinewaveTelemetrySeries.prototype.getData = function () {
|
||||
return this.data;
|
||||
};
|
||||
|
||||
return SinewaveTelemetrySeries;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
115
example/generator/src/WorkerInterface.js
Normal file
115
example/generator/src/WorkerInterface.js
Normal file
@@ -0,0 +1,115 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2016, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define([
|
||||
'text!./generatorWorker.js',
|
||||
'uuid'
|
||||
], function (
|
||||
workerText,
|
||||
uuid
|
||||
) {
|
||||
|
||||
var workerBlob = new Blob(
|
||||
[workerText],
|
||||
{type: 'application/javascript'}
|
||||
);
|
||||
var workerUrl = URL.createObjectURL(workerBlob);
|
||||
|
||||
function WorkerInterface() {
|
||||
this.worker = new Worker(workerUrl);
|
||||
this.worker.onmessage = this.onMessage.bind(this);
|
||||
this.callbacks = {};
|
||||
}
|
||||
|
||||
WorkerInterface.prototype.onMessage = function (message) {
|
||||
message = message.data;
|
||||
var callback = this.callbacks[message.id];
|
||||
if (callback) {
|
||||
if (callback(message)) {
|
||||
delete this.callbacks[message.id];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
WorkerInterface.prototype.dispatch = function (request, data, callback) {
|
||||
var message = {
|
||||
request: request,
|
||||
data: data,
|
||||
id: uuid()
|
||||
};
|
||||
|
||||
if (callback) {
|
||||
this.callbacks[message.id] = callback;
|
||||
}
|
||||
|
||||
this.worker.postMessage(message);
|
||||
|
||||
return message.id;
|
||||
};
|
||||
|
||||
WorkerInterface.prototype.request = function (request) {
|
||||
var deferred = {};
|
||||
var promise = new Promise(function (resolve, reject) {
|
||||
deferred.resolve = resolve;
|
||||
deferred.reject = reject;
|
||||
});
|
||||
|
||||
function callback(message) {
|
||||
if (message.error) {
|
||||
deferred.reject(message.error);
|
||||
} else {
|
||||
deferred.resolve(message.data);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
this.dispatch('request', request, callback);
|
||||
|
||||
return promise;
|
||||
};
|
||||
|
||||
WorkerInterface.prototype.subscribe = function (request, cb) {
|
||||
var isCancelled = false;
|
||||
|
||||
var callback = function (message) {
|
||||
if (isCancelled) {
|
||||
return true;
|
||||
}
|
||||
cb(message.data);
|
||||
};
|
||||
|
||||
var messageId = this.dispatch('subscribe', request, callback)
|
||||
|
||||
return function () {
|
||||
isCancelled = true;
|
||||
this.dispatch('unsubscribe', {
|
||||
id: messageId
|
||||
});
|
||||
}.bind(this);
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
return WorkerInterface;
|
||||
});
|
||||
150
example/generator/src/generatorWorker.js
Normal file
150
example/generator/src/generatorWorker.js
Normal file
@@ -0,0 +1,150 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2016, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*global self*/
|
||||
|
||||
(function () {
|
||||
|
||||
|
||||
var handlers = {
|
||||
subscribe: onSubscribe,
|
||||
unsubscribe: onUnsubscribe,
|
||||
request: onRequest
|
||||
};
|
||||
|
||||
var subscriptions = {};
|
||||
|
||||
function workSubscriptions(timestamp) {
|
||||
var now = Date.now();
|
||||
var nextWork = Math.min.apply(Math, Object.values(subscriptions).map(function (subscription) {
|
||||
return subscription(now);
|
||||
}));
|
||||
var wait = nextWork - now;
|
||||
if (wait < 0) {
|
||||
wait = 0;
|
||||
}
|
||||
|
||||
if (Number.isFinite(wait)) {
|
||||
setTimeout(workSubscriptions, wait);
|
||||
}
|
||||
}
|
||||
|
||||
function onSubscribe(message) {
|
||||
var data = message.data;
|
||||
|
||||
var start = Date.now();
|
||||
var step = 1000 / data.dataRateInHz;
|
||||
var nextStep = start - (start % step) + step;
|
||||
|
||||
function work(now) {
|
||||
while (nextStep < now) {
|
||||
self.postMessage({
|
||||
id: message.id,
|
||||
data: {
|
||||
utc: nextStep,
|
||||
yesterday: nextStep - 60*60*24*1000,
|
||||
delta: 60*60*24*1000,
|
||||
sin: sin(nextStep, data.period, data.amplitude, data.offset),
|
||||
cos: cos(nextStep, data.period, data.amplitude, data.offset)
|
||||
}
|
||||
});
|
||||
nextStep += step;
|
||||
}
|
||||
return nextStep;
|
||||
}
|
||||
|
||||
subscriptions[message.id] = work;
|
||||
workSubscriptions();
|
||||
}
|
||||
|
||||
function onUnsubscribe(message) {
|
||||
delete subscriptions[message.data.id];
|
||||
}
|
||||
|
||||
function onRequest(message) {
|
||||
var data = message.data;
|
||||
if (!data.start || !data.end) {
|
||||
throw new Error('missing start and end!');
|
||||
}
|
||||
|
||||
var now = Date.now();
|
||||
var start = data.start;
|
||||
var end = data.end > now ? now : data.end;
|
||||
var amplitude = data.amplitude;
|
||||
var period = data.period;
|
||||
var offset = data.offset;
|
||||
var dataRateInHz = data.dataRateInHz;
|
||||
|
||||
var step = 1000 / dataRateInHz;
|
||||
var nextStep = start - (start % step) + step;
|
||||
|
||||
var data = [];
|
||||
|
||||
for (; nextStep < end; nextStep += step) {
|
||||
data.push({
|
||||
utc: nextStep,
|
||||
yesterday: nextStep - 60*60*24*1000,
|
||||
delta: 60*60*24*1000,
|
||||
sin: sin(nextStep, period, amplitude, offset),
|
||||
cos: cos(nextStep, period, amplitude, offset)
|
||||
});
|
||||
}
|
||||
self.postMessage({
|
||||
id: message.id,
|
||||
data: data
|
||||
});
|
||||
}
|
||||
|
||||
function cos(timestamp, period, amplitude, offset) {
|
||||
return amplitude *
|
||||
Math.cos(timestamp / period / 1000 * Math.PI * 2) + offset;
|
||||
}
|
||||
|
||||
function sin(timestamp, period, amplitude, offset) {
|
||||
return amplitude *
|
||||
Math.sin(timestamp / period / 1000 * Math.PI * 2) + offset;
|
||||
}
|
||||
|
||||
function sendError(error, message) {
|
||||
self.postMessage({
|
||||
error: error.name + ': ' + error.message,
|
||||
message: message,
|
||||
id: message.id
|
||||
});
|
||||
}
|
||||
|
||||
self.onmessage = function handleMessage(event) {
|
||||
var message = event.data;
|
||||
var handler = handlers[message.request];
|
||||
|
||||
if (!handler) {
|
||||
sendError(new Error('unknown message type'), message);
|
||||
} else {
|
||||
try {
|
||||
handler(message);
|
||||
} catch (e) {
|
||||
sendError(e, message);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}());
|
||||
@@ -37,74 +37,79 @@ define([
|
||||
legacyRegistry.register("example/msl", {
|
||||
"name" : "Mars Science Laboratory Data Adapter",
|
||||
"extensions" : {
|
||||
"types": [
|
||||
{
|
||||
"name":"Mars Science Laboratory",
|
||||
"key": "msl.curiosity",
|
||||
"cssclass": "icon-object"
|
||||
},
|
||||
{
|
||||
"name": "Instrument",
|
||||
"key": "msl.instrument",
|
||||
"cssclass": "icon-object",
|
||||
"model": {"composition": []}
|
||||
},
|
||||
{
|
||||
"name": "Measurement",
|
||||
"key": "msl.measurement",
|
||||
"cssclass": "icon-telemetry",
|
||||
"model": {"telemetry": {}},
|
||||
"telemetry": {
|
||||
"source": "rems.source",
|
||||
"domains": [
|
||||
{
|
||||
"name": "Time",
|
||||
"key": "utc",
|
||||
"format": "utc"
|
||||
}
|
||||
]
|
||||
"types": [
|
||||
{
|
||||
"name":"Mars Science Laboratory",
|
||||
"key": "msl.curiosity",
|
||||
"cssclass": "icon-object"
|
||||
},
|
||||
{
|
||||
"name": "Instrument",
|
||||
"key": "msl.instrument",
|
||||
"cssclass": "icon-object",
|
||||
"model": {"composition": []}
|
||||
},
|
||||
{
|
||||
"name": "Measurement",
|
||||
"key": "msl.measurement",
|
||||
"cssclass": "icon-telemetry",
|
||||
"model": {"telemetry": {}},
|
||||
"telemetry": {
|
||||
"source": "rems.source",
|
||||
"domains": [
|
||||
{
|
||||
"name": "Time",
|
||||
"key": "utc",
|
||||
"format": "utc"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"constants": [
|
||||
{
|
||||
"key": "REMS_WS_URL",
|
||||
"value": "/proxyUrl?url=http://cab.inta-csic.es/rems/wp-content/plugins/marsweather-widget/api.php"
|
||||
}
|
||||
],
|
||||
"roots": [
|
||||
{
|
||||
"id": "msl:curiosity",
|
||||
"priority" : "preferred",
|
||||
"model": {
|
||||
"type": "msl.curiosity",
|
||||
"name": "Mars Science Laboratory",
|
||||
"composition": ["msl_tlm:rems"]
|
||||
],
|
||||
"constants": [
|
||||
{
|
||||
"key": "REMS_WS_URL",
|
||||
"value": "/proxyUrl?url=http://cab.inta-csic.es/rems/wp-content/plugins/marsweather-widget/api.php"
|
||||
}
|
||||
}
|
||||
],
|
||||
"services": [
|
||||
{
|
||||
"key":"rems.adapter",
|
||||
"implementation": RemsTelemetryServerAdapter,
|
||||
"depends": ["$q", "$http", "$log", "REMS_WS_URL"]
|
||||
}
|
||||
],
|
||||
"components": [
|
||||
{
|
||||
"provides": "modelService",
|
||||
"type": "provider",
|
||||
"implementation": RemsTelemetryModelProvider,
|
||||
"depends": ["rems.adapter"]
|
||||
},
|
||||
{
|
||||
"provides": "telemetryService",
|
||||
"type": "provider",
|
||||
"implementation": RemsTelemetryProvider,
|
||||
"depends": ["rems.adapter", "$q"]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"roots": [
|
||||
{
|
||||
"id": "msl:curiosity"
|
||||
}
|
||||
],
|
||||
"models": [
|
||||
{
|
||||
"id": "msl:curiosity",
|
||||
"priority": "preferred",
|
||||
"model": {
|
||||
"type": "msl.curiosity",
|
||||
"name": "Mars Science Laboratory",
|
||||
"composition": ["msl_tlm:rems"]
|
||||
}
|
||||
}
|
||||
],
|
||||
"services": [
|
||||
{
|
||||
"key":"rems.adapter",
|
||||
"implementation": RemsTelemetryServerAdapter,
|
||||
"depends": ["$q", "$http", "$log", "REMS_WS_URL"]
|
||||
}
|
||||
],
|
||||
"components": [
|
||||
{
|
||||
"provides": "modelService",
|
||||
"type": "provider",
|
||||
"implementation": RemsTelemetryModelProvider,
|
||||
"depends": ["rems.adapter"]
|
||||
},
|
||||
{
|
||||
"provides": "telemetryService",
|
||||
"type": "provider",
|
||||
"implementation": RemsTelemetryProvider,
|
||||
"depends": ["rems.adapter", "$q"]
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -33,6 +33,11 @@ define([
|
||||
legacyRegistry.register("example/scratchpad", {
|
||||
"extensions": {
|
||||
"roots": [
|
||||
{
|
||||
"id": "scratch:root"
|
||||
}
|
||||
],
|
||||
"models": [
|
||||
{
|
||||
"id": "scratch:root",
|
||||
"model": {
|
||||
|
||||
@@ -35,6 +35,11 @@ define([
|
||||
"description": "Example illustrating the addition of a static top-level hierarchy",
|
||||
"extensions": {
|
||||
"roots": [
|
||||
{
|
||||
"id": "exampleTaxonomy"
|
||||
}
|
||||
],
|
||||
"models": [
|
||||
{
|
||||
"id": "exampleTaxonomy",
|
||||
"model": {
|
||||
|
||||
@@ -32,12 +32,13 @@
|
||||
[
|
||||
'example/imagery',
|
||||
'example/eventGenerator',
|
||||
'example/generator',
|
||||
'platform/features/my-items',
|
||||
'platform/persistence/local'
|
||||
'example/generator'
|
||||
].forEach(
|
||||
openmct.legacyRegistry.enable.bind(openmct.legacyRegistry)
|
||||
);
|
||||
openmct.install(openmct.plugins.myItems);
|
||||
openmct.install(openmct.plugins.localStorage);
|
||||
openmct.install(openmct.plugins.espresso);
|
||||
openmct.start();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -76,7 +76,7 @@ module.exports = function(config) {
|
||||
// Specify browsers to run tests in.
|
||||
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
|
||||
browsers: [
|
||||
'PhantomJS'
|
||||
'Chrome'
|
||||
],
|
||||
|
||||
// Code coverage reporting.
|
||||
|
||||
@@ -25,13 +25,12 @@
|
||||
"jsdoc": "^3.3.2",
|
||||
"jshint": "^2.7.0",
|
||||
"karma": "^0.13.3",
|
||||
"karma-chrome-launcher": "^0.1.8",
|
||||
"karma-chrome-launcher": "^0.1.12",
|
||||
"karma-cli": "0.0.4",
|
||||
"karma-coverage": "^0.5.3",
|
||||
"karma-html-reporter": "^0.2.7",
|
||||
"karma-jasmine": "^0.1.5",
|
||||
"karma-junit-reporter": "^0.3.8",
|
||||
"karma-phantomjs-launcher": "^1.0.0",
|
||||
"karma-requirejs": "^0.2.2",
|
||||
"lodash": "^3.10.1",
|
||||
"markdown-toc": "^0.11.7",
|
||||
@@ -40,7 +39,6 @@
|
||||
"mkdirp": "^0.5.1",
|
||||
"moment": "^2.11.1",
|
||||
"node-bourbon": "^4.2.3",
|
||||
"phantomjs-prebuilt": "2.1.11 || >2.1.12 <3.0.0",
|
||||
"requirejs": "2.1.x",
|
||||
"split": "^1.0.0"
|
||||
},
|
||||
|
||||
@@ -41,7 +41,6 @@ define([
|
||||
"text!./res/templates/items/items.html",
|
||||
"text!./res/templates/browse/object-properties.html",
|
||||
"text!./res/templates/browse/inspector-region.html",
|
||||
"text!./res/templates/view-object.html",
|
||||
'legacyRegistry'
|
||||
], function (
|
||||
BrowseController,
|
||||
@@ -64,7 +63,6 @@ define([
|
||||
itemsTemplate,
|
||||
objectPropertiesTemplate,
|
||||
inspectorRegionTemplate,
|
||||
viewObjectTemplate,
|
||||
legacyRegistry
|
||||
) {
|
||||
|
||||
@@ -141,10 +139,6 @@ define([
|
||||
}
|
||||
],
|
||||
"representations": [
|
||||
{
|
||||
"key": "view-object",
|
||||
"template": viewObjectTemplate
|
||||
},
|
||||
{
|
||||
"key": "browse-object",
|
||||
"template": browseObjectTemplate,
|
||||
@@ -204,7 +198,10 @@ define([
|
||||
"services": [
|
||||
{
|
||||
"key": "navigationService",
|
||||
"implementation": NavigationService
|
||||
"implementation": NavigationService,
|
||||
"depends": [
|
||||
"$window"
|
||||
]
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
@@ -212,10 +209,7 @@ define([
|
||||
"key": "navigate",
|
||||
"implementation": NavigateAction,
|
||||
"depends": [
|
||||
"navigationService",
|
||||
"$q",
|
||||
"policyService",
|
||||
"$window"
|
||||
"navigationService"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
<mct-split-pane class='l-object-and-inspector contents abs' anchor='right'>
|
||||
<div class='split-pane-component t-object pane primary-pane left'>
|
||||
<mct-representation mct-object="navigatedObject"
|
||||
key="'view-object'"
|
||||
key="navigatedObject.getCapability('status').get('editing') ? 'edit-object' : 'browse-object'"
|
||||
class="abs holder holder-object">
|
||||
</mct-representation>
|
||||
<a class="mini-tab-icon anchor-right mobile-hide toggle-pane toggle-inspect flush-right"
|
||||
|
||||
@@ -1,33 +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.
|
||||
-->
|
||||
<!--
|
||||
A representation that allows the 'View' region of an object view to change
|
||||
dynamically (eg. between browse and edit modes). Values correspond to a
|
||||
representation key, and currently defaults to 'browse-object'.
|
||||
|
||||
In the case of edit, the EditRepresenter will change this to editable
|
||||
representation of the object as needed.
|
||||
-->
|
||||
<mct-representation mct-object="domainObject"
|
||||
key="viewObjectTemplate || 'browse-object'"
|
||||
class="abs holder">
|
||||
</mct-representation>
|
||||
@@ -48,11 +48,16 @@ define(
|
||||
defaultPath
|
||||
) {
|
||||
var initialPath = ($route.current.params.ids || defaultPath).split("/");
|
||||
|
||||
var currentIds = $route.current.params.ids;
|
||||
var currentIds;
|
||||
|
||||
$scope.treeModel = {
|
||||
selectedObject: undefined
|
||||
selectedObject: undefined,
|
||||
onSelection: function (object) {
|
||||
navigationService.setNavigation(object, true);
|
||||
},
|
||||
allowSelection: function (object) {
|
||||
return navigationService.shouldNavigate();
|
||||
}
|
||||
};
|
||||
|
||||
function idsForObject(domainObject) {
|
||||
@@ -103,7 +108,6 @@ define(
|
||||
function navigateToObject(desiredObject) {
|
||||
$scope.navigatedObject = desiredObject;
|
||||
$scope.treeModel.selectedObject = desiredObject;
|
||||
navigationService.setNavigation(desiredObject);
|
||||
currentIds = idsForObject(desiredObject);
|
||||
$route.current.pathParams.ids = currentIds;
|
||||
$location.path('/browse/' + currentIds);
|
||||
@@ -114,10 +118,11 @@ define(
|
||||
.then(function (root) {
|
||||
return findViaComposition(root, path);
|
||||
})
|
||||
.then(navigateToObject);
|
||||
.then(function (object) {
|
||||
navigationService.setNavigation(object);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
getObject('ROOT')
|
||||
.then(function (root) {
|
||||
$scope.domainObject = root;
|
||||
@@ -137,15 +142,6 @@ define(
|
||||
// Listen for changes in navigation state.
|
||||
navigationService.addListener(navigateDirectlyToModel);
|
||||
|
||||
// Also listen for changes which come from the tree. Changes in
|
||||
// the tree will trigger a change in browse navigation state.
|
||||
$scope.$watch("treeModel.selectedObject", function (newObject, oldObject) {
|
||||
if (oldObject !== newObject) {
|
||||
navigateDirectlyToModel(newObject);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Listen for route changes which are caused by browser events
|
||||
// (e.g. bookmarks to pages in OpenMCT) and prevent them. Instead,
|
||||
// navigate to the path ourselves, which results in it being
|
||||
|
||||
@@ -33,12 +33,9 @@ define(
|
||||
* @constructor
|
||||
* @implements {Action}
|
||||
*/
|
||||
function NavigateAction(navigationService, $q, policyService, $window, context) {
|
||||
function NavigateAction(navigationService, context) {
|
||||
this.domainObject = context.domainObject;
|
||||
this.$q = $q;
|
||||
this.navigationService = navigationService;
|
||||
this.policyService = policyService;
|
||||
this.$window = $window;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -47,36 +44,12 @@ define(
|
||||
* navigation has been updated
|
||||
*/
|
||||
NavigateAction.prototype.perform = function () {
|
||||
var self = this,
|
||||
navigateTo = this.domainObject,
|
||||
currentObject = self.navigationService.getNavigation();
|
||||
|
||||
function allow() {
|
||||
var navigationAllowed = true;
|
||||
self.policyService.allow("navigation", currentObject, navigateTo, function (message) {
|
||||
navigationAllowed = self.$window.confirm(message + "\r\n\r\n" +
|
||||
" Are you sure you want to continue?");
|
||||
});
|
||||
return navigationAllowed;
|
||||
}
|
||||
|
||||
function cancelIfEditing() {
|
||||
var editing = currentObject.hasCapability('editor') &&
|
||||
currentObject.getCapability('editor').isEditContextRoot();
|
||||
|
||||
return self.$q.when(editing && currentObject.getCapability("editor").finish());
|
||||
}
|
||||
|
||||
function navigate() {
|
||||
return self.navigationService.setNavigation(navigateTo);
|
||||
}
|
||||
|
||||
if (allow()) {
|
||||
return cancelIfEditing().then(navigate);
|
||||
} else {
|
||||
return this.$q.when(false);
|
||||
if (this.navigationService.shouldNavigate()) {
|
||||
this.navigationService.setNavigation(this.domainObject, true);
|
||||
return Promise.resolve({});
|
||||
}
|
||||
|
||||
return Promise.reject('Navigation Prevented by User');
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -30,16 +30,23 @@ define(
|
||||
/**
|
||||
* The navigation service maintains the application's current
|
||||
* navigation state, and allows listening for changes thereto.
|
||||
*
|
||||
* @memberof platform/commonUI/browse
|
||||
* @constructor
|
||||
*/
|
||||
function NavigationService() {
|
||||
function NavigationService($window) {
|
||||
this.navigated = undefined;
|
||||
this.callbacks = [];
|
||||
this.checks = [];
|
||||
this.$window = $window;
|
||||
|
||||
this.oldUnload = $window.onbeforeunload;
|
||||
$window.onbeforeunload = this.onBeforeUnload.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current navigation state.
|
||||
*
|
||||
* @returns {DomainObject} the object that is navigated-to
|
||||
*/
|
||||
NavigationService.prototype.getNavigation = function () {
|
||||
@@ -47,16 +54,33 @@ define(
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the current navigation state. This will invoke listeners.
|
||||
* Navigate to a specified object. If navigation checks exist and
|
||||
* return reasons to prevent navigation, it will prompt the user before
|
||||
* continuing. Trying to navigate to the currently navigated object will
|
||||
* do nothing.
|
||||
*
|
||||
* If a truthy value is passed for `force`, it will skip navigation
|
||||
* and will not prevent navigation to an already selected object.
|
||||
*
|
||||
* @param {DomainObject} domainObject the domain object to navigate to
|
||||
* @param {Boolean} force if true, force navigation to occur.
|
||||
* @returns {Boolean} true if navigation occured, otherwise false.
|
||||
*/
|
||||
NavigationService.prototype.setNavigation = function (value) {
|
||||
if (this.navigated !== value) {
|
||||
this.navigated = value;
|
||||
this.callbacks.forEach(function (callback) {
|
||||
callback(value);
|
||||
});
|
||||
NavigationService.prototype.setNavigation = function (domainObject, force) {
|
||||
if (force) {
|
||||
this.doNavigation(domainObject);
|
||||
return true;
|
||||
}
|
||||
if (this.navigated === domainObject) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var doNotNavigate = this.shouldWarnBeforeNavigate();
|
||||
if (doNotNavigate && !this.$window.confirm(doNotNavigate)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.doNavigation(domainObject);
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -64,6 +88,7 @@ define(
|
||||
* Listen for changes in navigation. The passed callback will
|
||||
* be invoked with the new domain object of navigation when
|
||||
* this changes.
|
||||
*
|
||||
* @param {function} callback the callback to invoke when
|
||||
* navigation state changes
|
||||
*/
|
||||
@@ -73,6 +98,7 @@ define(
|
||||
|
||||
/**
|
||||
* Stop listening for changes in navigation state.
|
||||
*
|
||||
* @param {function} callback the callback which should
|
||||
* no longer be invoked when navigation state
|
||||
* changes
|
||||
@@ -83,6 +109,89 @@ define(
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if navigation should proceed. May prompt a user for input
|
||||
* if any checkFns return messages. Returns true if the user wishes to
|
||||
* navigate, otherwise false. If using this prior to calling
|
||||
* `setNavigation`, you should call `setNavigation` with `force=true`
|
||||
* to prevent duplicate dialogs being displayed to the user.
|
||||
*
|
||||
* @returns {Boolean} true if the user wishes to navigate, otherwise false.
|
||||
*/
|
||||
NavigationService.prototype.shouldNavigate = function () {
|
||||
var doNotNavigate = this.shouldWarnBeforeNavigate();
|
||||
return !doNotNavigate || this.$window.confirm(doNotNavigate);
|
||||
};
|
||||
|
||||
/**
|
||||
* Register a check function to be called before any navigation occurs.
|
||||
* Check functions should return a human readable "message" if
|
||||
* there are any reasons to prevent navigation. Otherwise, they should
|
||||
* return falsy. Returns a function which can be called to remove the
|
||||
* check function.
|
||||
*
|
||||
* @param {Function} checkFn a function to call before navigation occurs.
|
||||
* @returns {Function} removeCheck call to remove check
|
||||
*/
|
||||
NavigationService.prototype.checkBeforeNavigation = function (checkFn) {
|
||||
this.checks.push(checkFn);
|
||||
return function removeCheck() {
|
||||
this.checks = this.checks.filter(function (fn) {
|
||||
return checkFn !== fn;
|
||||
});
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
/**
|
||||
* Private method to actually perform navigation.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
NavigationService.prototype.doNavigation = function (value) {
|
||||
this.navigated = value;
|
||||
this.callbacks.forEach(function (callback) {
|
||||
callback(value);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns either a false value, or a string that should be displayed
|
||||
* to the user before navigation is allowed.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
NavigationService.prototype.shouldWarnBeforeNavigate = function () {
|
||||
var reasons = [];
|
||||
|
||||
this.checks.forEach(function (checkFn) {
|
||||
var reason = checkFn();
|
||||
if (reason) {
|
||||
reasons.push(reason);
|
||||
}
|
||||
});
|
||||
|
||||
if (reasons.length) {
|
||||
return reasons.join('\n');
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Listener for window on before unload event-- will warn before
|
||||
* navigation is allowed.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
NavigationService.prototype.onBeforeUnload = function () {
|
||||
var shouldWarnBeforeNavigate = this.shouldWarnBeforeNavigate();
|
||||
if (shouldWarnBeforeNavigate) {
|
||||
return shouldWarnBeforeNavigate;
|
||||
}
|
||||
if (this.oldUnload) {
|
||||
return this.oldUnload.apply(undefined, [].slice.apply(arguments));
|
||||
}
|
||||
};
|
||||
|
||||
return NavigationService;
|
||||
}
|
||||
);
|
||||
|
||||
@@ -24,8 +24,14 @@
|
||||
* MCTRepresentationSpec. Created by vwoeltje on 11/6/14.
|
||||
*/
|
||||
define(
|
||||
["../src/BrowseController"],
|
||||
function (BrowseController) {
|
||||
[
|
||||
"../src/BrowseController",
|
||||
"../src/navigation/NavigationService"
|
||||
],
|
||||
function (
|
||||
BrowseController,
|
||||
NavigationService
|
||||
) {
|
||||
|
||||
describe("The browse controller", function () {
|
||||
var mockScope,
|
||||
@@ -44,7 +50,7 @@ define(
|
||||
function waitsForNavigation() {
|
||||
var calls = mockNavigationService.setNavigation.calls.length;
|
||||
waitsFor(function () {
|
||||
return mockNavigationService.setNavigation.calls.length > calls ;
|
||||
return mockNavigationService.setNavigation.calls.length > calls;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -92,15 +98,16 @@ define(
|
||||
"objectService",
|
||||
["getObjects"]
|
||||
);
|
||||
mockNavigationService = jasmine.createSpyObj(
|
||||
"navigationService",
|
||||
[
|
||||
"getNavigation",
|
||||
"setNavigation",
|
||||
"addListener",
|
||||
"removeListener"
|
||||
]
|
||||
);
|
||||
mockNavigationService = new NavigationService({});
|
||||
[
|
||||
"getNavigation",
|
||||
"setNavigation",
|
||||
"addListener",
|
||||
"removeListener"
|
||||
].forEach(function (method) {
|
||||
spyOn(mockNavigationService, method)
|
||||
.andCallThrough();
|
||||
});
|
||||
mockRootObject = jasmine.createSpyObj(
|
||||
"rootObjectContainer",
|
||||
["getId", "getCapability", "getModel", "useCapability", "hasCapability"]
|
||||
|
||||
@@ -23,145 +23,74 @@
|
||||
/**
|
||||
* MCTRepresentationSpec. Created by vwoeltje on 11/6/14.
|
||||
*/
|
||||
define(
|
||||
["../../src/navigation/NavigateAction"],
|
||||
function (NavigateAction) {
|
||||
define([
|
||||
"../../src/navigation/NavigateAction"
|
||||
], function (
|
||||
NavigateAction
|
||||
) {
|
||||
|
||||
describe("The navigate action", function () {
|
||||
var mockNavigationService,
|
||||
mockQ,
|
||||
mockDomainObject,
|
||||
mockPolicyService,
|
||||
mockNavigatedObject,
|
||||
mockWindow,
|
||||
capabilities,
|
||||
action;
|
||||
describe("The navigate action", function () {
|
||||
var mockNavigationService,
|
||||
mockDomainObject,
|
||||
action;
|
||||
|
||||
function mockPromise(value) {
|
||||
return {
|
||||
then: function (callback) {
|
||||
return mockPromise(callback(value));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
capabilities = {};
|
||||
|
||||
mockQ = { when: mockPromise };
|
||||
mockNavigatedObject = jasmine.createSpyObj(
|
||||
"domainObject",
|
||||
[
|
||||
"getId",
|
||||
"getModel",
|
||||
"hasCapability",
|
||||
"getCapability"
|
||||
]
|
||||
);
|
||||
|
||||
capabilities.editor = jasmine.createSpyObj("editorCapability", [
|
||||
"isEditContextRoot",
|
||||
"finish"
|
||||
]);
|
||||
|
||||
mockNavigatedObject.getCapability.andCallFake(function (capability) {
|
||||
return capabilities[capability];
|
||||
});
|
||||
mockNavigatedObject.hasCapability.andReturn(false);
|
||||
|
||||
mockNavigationService = jasmine.createSpyObj(
|
||||
"navigationService",
|
||||
[
|
||||
"setNavigation",
|
||||
"getNavigation"
|
||||
]
|
||||
);
|
||||
mockNavigationService.getNavigation.andReturn(mockNavigatedObject);
|
||||
|
||||
mockDomainObject = jasmine.createSpyObj(
|
||||
"domainObject",
|
||||
[
|
||||
"getId",
|
||||
"getModel"
|
||||
]
|
||||
);
|
||||
|
||||
mockPolicyService = jasmine.createSpyObj("policyService",
|
||||
[
|
||||
"allow"
|
||||
]);
|
||||
mockWindow = jasmine.createSpyObj("$window",
|
||||
[
|
||||
"confirm"
|
||||
]);
|
||||
|
||||
action = new NavigateAction(
|
||||
mockNavigationService,
|
||||
mockQ,
|
||||
mockPolicyService,
|
||||
mockWindow,
|
||||
{ domainObject: mockDomainObject }
|
||||
);
|
||||
function waitForCall() {
|
||||
var called = false;
|
||||
waitsFor(function () {
|
||||
return called;
|
||||
});
|
||||
return function () {
|
||||
called = true;
|
||||
};
|
||||
}
|
||||
|
||||
it("invokes the policy service to determine if navigation" +
|
||||
" allowed", function () {
|
||||
action.perform();
|
||||
expect(mockPolicyService.allow)
|
||||
.toHaveBeenCalledWith("navigation", jasmine.any(Object), jasmine.any(Object), jasmine.any(Function));
|
||||
});
|
||||
beforeEach(function () {
|
||||
mockNavigationService = jasmine.createSpyObj(
|
||||
"navigationService",
|
||||
[
|
||||
"shouldNavigate",
|
||||
"setNavigation"
|
||||
]
|
||||
);
|
||||
|
||||
it("prompts user if policy rejection", function () {
|
||||
action.perform();
|
||||
expect(mockPolicyService.allow).toHaveBeenCalled();
|
||||
mockPolicyService.allow.mostRecentCall.args[3]();
|
||||
expect(mockWindow.confirm).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("shows a prompt", function () {
|
||||
beforeEach(function () {
|
||||
// Ensure the allow callback is called synchronously
|
||||
mockPolicyService.allow.andCallFake(function () {
|
||||
return arguments[3]();
|
||||
});
|
||||
});
|
||||
it("does not navigate on prompt rejection", function () {
|
||||
mockWindow.confirm.andReturn(false);
|
||||
action.perform();
|
||||
expect(mockNavigationService.setNavigation)
|
||||
.not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does navigate on prompt acceptance", function () {
|
||||
mockWindow.confirm.andReturn(true);
|
||||
action.perform();
|
||||
expect(mockNavigationService.setNavigation)
|
||||
.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("in edit mode", function () {
|
||||
beforeEach(function () {
|
||||
mockNavigatedObject.hasCapability.andCallFake(function (capability) {
|
||||
return capability === "editor";
|
||||
});
|
||||
capabilities.editor.isEditContextRoot.andReturn(true);
|
||||
});
|
||||
|
||||
it("finishes editing if in edit mode", function () {
|
||||
action.perform();
|
||||
expect(capabilities.editor.finish)
|
||||
.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("is only applicable when a domain object is in context", function () {
|
||||
expect(NavigateAction.appliesTo({})).toBeFalsy();
|
||||
expect(NavigateAction.appliesTo({
|
||||
domainObject: mockDomainObject
|
||||
})).toBeTruthy();
|
||||
});
|
||||
mockDomainObject = {};
|
||||
|
||||
action = new NavigateAction(
|
||||
mockNavigationService,
|
||||
{ domainObject: mockDomainObject }
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
it("sets navigation if it is allowed", function () {
|
||||
mockNavigationService.shouldNavigate.andReturn(true);
|
||||
action.perform()
|
||||
.then(waitForCall());
|
||||
runs(function () {
|
||||
expect(mockNavigationService.setNavigation)
|
||||
.toHaveBeenCalledWith(mockDomainObject, true);
|
||||
});
|
||||
});
|
||||
|
||||
it("does not set navigation if it is not allowed", function () {
|
||||
mockNavigationService.shouldNavigate.andReturn(false);
|
||||
var onSuccess = jasmine.createSpy('onSuccess');
|
||||
action.perform()
|
||||
.then(onSuccess, waitForCall());
|
||||
runs(function () {
|
||||
expect(onSuccess).not.toHaveBeenCalled();
|
||||
expect(mockNavigationService.setNavigation)
|
||||
.not
|
||||
.toHaveBeenCalledWith(mockDomainObject);
|
||||
});
|
||||
});
|
||||
|
||||
it("is only applicable when a domain object is in context", function () {
|
||||
expect(NavigateAction.appliesTo({})).toBeFalsy();
|
||||
expect(NavigateAction.appliesTo({
|
||||
domainObject: mockDomainObject
|
||||
})).toBeTruthy();
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,10 +28,12 @@ define(
|
||||
function (NavigationService) {
|
||||
|
||||
describe("The navigation service", function () {
|
||||
var navigationService;
|
||||
var $window,
|
||||
navigationService;
|
||||
|
||||
beforeEach(function () {
|
||||
navigationService = new NavigationService();
|
||||
$window = jasmine.createSpyObj('$window', ['confirm']);
|
||||
navigationService = new NavigationService($window);
|
||||
});
|
||||
|
||||
it("stores navigation state", function () {
|
||||
|
||||
@@ -53,7 +53,8 @@ define([
|
||||
"depends": [
|
||||
"overlayService",
|
||||
"$q",
|
||||
"$log"
|
||||
"$log",
|
||||
"$document"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -35,11 +35,15 @@ define(
|
||||
* @memberof platform/commonUI/dialog
|
||||
* @constructor
|
||||
*/
|
||||
function DialogService(overlayService, $q, $log) {
|
||||
function DialogService(overlayService, $q, $log, $document) {
|
||||
this.overlayService = overlayService;
|
||||
this.$q = $q;
|
||||
this.$log = $log;
|
||||
this.activeOverlay = undefined;
|
||||
|
||||
this.findBody = function () {
|
||||
return $document.find('body');
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -60,7 +64,8 @@ define(
|
||||
// input is asynchronous.
|
||||
var deferred = this.$q.defer(),
|
||||
self = this,
|
||||
overlay;
|
||||
overlay,
|
||||
handleEscKeydown;
|
||||
|
||||
// Confirm function; this will be passed in to the
|
||||
// overlay-dialog template and associated with a
|
||||
@@ -76,13 +81,22 @@ define(
|
||||
// Cancel or X button click
|
||||
function cancel() {
|
||||
deferred.reject();
|
||||
self.findBody().off('keydown', handleEscKeydown);
|
||||
self.dismissOverlay(overlay);
|
||||
}
|
||||
|
||||
handleEscKeydown = function (event) {
|
||||
if (event.keyCode === 27) {
|
||||
cancel();
|
||||
}
|
||||
};
|
||||
|
||||
// Add confirm/cancel callbacks
|
||||
model.confirm = confirm;
|
||||
model.cancel = cancel;
|
||||
|
||||
this.findBody().on('keydown', handleEscKeydown);
|
||||
|
||||
if (this.canShowDialog(model)) {
|
||||
// Add the overlay using the OverlayService, which
|
||||
// will handle actual insertion into the DOM
|
||||
|
||||
@@ -33,6 +33,8 @@ define(
|
||||
mockLog,
|
||||
mockOverlay,
|
||||
mockDeferred,
|
||||
mockDocument,
|
||||
mockBody,
|
||||
dialogService;
|
||||
|
||||
beforeEach(function () {
|
||||
@@ -56,6 +58,13 @@ define(
|
||||
"deferred",
|
||||
["resolve", "reject"]
|
||||
);
|
||||
mockDocument = jasmine.createSpyObj(
|
||||
"$document",
|
||||
["find"]
|
||||
);
|
||||
mockBody = jasmine.createSpyObj('body', ['on', 'off']);
|
||||
mockDocument.find.andReturn(mockBody);
|
||||
|
||||
mockDeferred.promise = "mock promise";
|
||||
|
||||
mockQ.defer.andReturn(mockDeferred);
|
||||
@@ -64,7 +73,8 @@ define(
|
||||
dialogService = new DialogService(
|
||||
mockOverlayService,
|
||||
mockQ,
|
||||
mockLog
|
||||
mockLog,
|
||||
mockDocument
|
||||
);
|
||||
});
|
||||
|
||||
@@ -130,6 +140,36 @@ define(
|
||||
);
|
||||
});
|
||||
|
||||
it("adds a keydown event listener to the body", function () {
|
||||
dialogService.getUserInput({}, {});
|
||||
expect(mockDocument.find).toHaveBeenCalledWith("body");
|
||||
expect(mockBody.on).toHaveBeenCalledWith("keydown", jasmine.any(Function));
|
||||
});
|
||||
|
||||
it("destroys the event listener when the dialog is cancelled", function () {
|
||||
dialogService.getUserInput({}, {});
|
||||
mockOverlayService.createOverlay.mostRecentCall.args[1].cancel();
|
||||
expect(mockBody.off).toHaveBeenCalledWith("keydown", jasmine.any(Function));
|
||||
});
|
||||
|
||||
it("cancels the dialog when an escape keydown event is triggered", function () {
|
||||
dialogService.getUserInput({}, {});
|
||||
mockBody.on.mostRecentCall.args[1]({
|
||||
keyCode: 27
|
||||
});
|
||||
expect(mockDeferred.reject).toHaveBeenCalled();
|
||||
expect(mockDeferred.resolve).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("ignores non escape keydown events", function () {
|
||||
dialogService.getUserInput({}, {});
|
||||
mockBody.on.mostRecentCall.args[1]({
|
||||
keyCode: 13
|
||||
});
|
||||
expect(mockDeferred.reject).not.toHaveBeenCalled();
|
||||
expect(mockDeferred.resolve).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("the blocking message dialog", function () {
|
||||
var dialogModel = {};
|
||||
var dialogHandle;
|
||||
|
||||
@@ -2,25 +2,6 @@ Contains sources and resources associated with Edit mode.
|
||||
|
||||
# Extensions
|
||||
|
||||
## Directives
|
||||
|
||||
This bundle introduces the `mct-before-unload` directive, primarily for
|
||||
internal use (to prompt the user to confirm navigation away from unsaved
|
||||
changes in Edit mode.)
|
||||
|
||||
The `mct-before-unload` directive is used as an attribute whose value is
|
||||
an Angular expression that is evaluated when navigation changes (either
|
||||
via browser-level changes, such as the refresh button, or changes to
|
||||
the Angular route, which happens when hitting the back button in Edit
|
||||
mode.) The result of this evaluation, when truthy, is shown in a browser
|
||||
dialog to allow the user to confirm navigation. When falsy, no prompt is
|
||||
shown, allowing these dialogs to be shown conditionally. (For instance, in
|
||||
Edit mode, prompts are only shown if user-initiated changes have
|
||||
occurred.)
|
||||
|
||||
This directive may be attached to any element; its behavior will be enforced
|
||||
so long as that element remains within the DOM.
|
||||
|
||||
# Toolbars
|
||||
|
||||
Views may specify the contents of a toolbar through a `toolbar`
|
||||
|
||||
@@ -25,7 +25,6 @@ define([
|
||||
"./src/controllers/EditPanesController",
|
||||
"./src/controllers/ElementsController",
|
||||
"./src/controllers/EditObjectController",
|
||||
"./src/directives/MCTBeforeUnload",
|
||||
"./src/actions/EditAndComposeAction",
|
||||
"./src/actions/EditAction",
|
||||
"./src/actions/PropertiesAction",
|
||||
@@ -37,7 +36,6 @@ define([
|
||||
"./src/policies/EditActionPolicy",
|
||||
"./src/policies/EditableLinkPolicy",
|
||||
"./src/policies/EditableMovePolicy",
|
||||
"./src/policies/EditNavigationPolicy",
|
||||
"./src/policies/EditContextualActionPolicy",
|
||||
"./src/representers/EditRepresenter",
|
||||
"./src/representers/EditToolbarRepresenter",
|
||||
@@ -65,7 +63,6 @@ define([
|
||||
EditPanesController,
|
||||
ElementsController,
|
||||
EditObjectController,
|
||||
MCTBeforeUnload,
|
||||
EditAndComposeAction,
|
||||
EditAction,
|
||||
PropertiesAction,
|
||||
@@ -77,7 +74,6 @@ define([
|
||||
EditActionPolicy,
|
||||
EditableLinkPolicy,
|
||||
EditableMovePolicy,
|
||||
EditNavigationPolicy,
|
||||
EditContextualActionPolicy,
|
||||
EditRepresenter,
|
||||
EditToolbarRepresenter,
|
||||
@@ -132,7 +128,7 @@ define([
|
||||
"depends": [
|
||||
"$scope",
|
||||
"$location",
|
||||
"policyService"
|
||||
"navigationService"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -152,15 +148,6 @@ define([
|
||||
]
|
||||
}
|
||||
],
|
||||
"directives": [
|
||||
{
|
||||
"key": "mctBeforeUnload",
|
||||
"implementation": MCTBeforeUnload,
|
||||
"depends": [
|
||||
"$window"
|
||||
]
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"key": "compose",
|
||||
@@ -211,7 +198,8 @@ define([
|
||||
"cssclass": "icon-save labeled",
|
||||
"description": "Save changes made to these objects.",
|
||||
"depends": [
|
||||
"dialogService"
|
||||
"dialogService",
|
||||
"notificationService"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -222,7 +210,8 @@ define([
|
||||
"cssclass": "icon-save labeled",
|
||||
"description": "Save changes made to these objects.",
|
||||
"depends": [
|
||||
"dialogService"
|
||||
"dialogService",
|
||||
"notificationService"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -236,7 +225,8 @@ define([
|
||||
"$injector",
|
||||
"policyService",
|
||||
"dialogService",
|
||||
"copyService"
|
||||
"copyService",
|
||||
"notificationService"
|
||||
],
|
||||
"priority": "mandatory"
|
||||
},
|
||||
@@ -270,11 +260,6 @@ define([
|
||||
"category": "action",
|
||||
"implementation": EditableLinkPolicy
|
||||
},
|
||||
{
|
||||
"category": "navigation",
|
||||
"message": "Continuing will cause the loss of any unsaved changes.",
|
||||
"implementation": EditNavigationPolicy
|
||||
},
|
||||
{
|
||||
"implementation": CreationPolicy,
|
||||
"category": "creation"
|
||||
@@ -389,7 +374,6 @@ define([
|
||||
{
|
||||
"implementation": EditRepresenter,
|
||||
"depends": [
|
||||
"$q",
|
||||
"$log"
|
||||
]
|
||||
},
|
||||
|
||||
@@ -20,8 +20,7 @@
|
||||
at runtime from the About dialog for additional information.
|
||||
-->
|
||||
<div class="abs l-flex-col" ng-controller="EditObjectController as EditObjectController">
|
||||
<div mct-before-unload="EditObjectController.getUnloadWarning()"
|
||||
class="holder flex-elem l-flex-row object-browse-bar ">
|
||||
<div class="holder flex-elem l-flex-row object-browse-bar ">
|
||||
<div class="items-select left flex-elem l-flex-row grows">
|
||||
<mct-representation key="'back-arrow'"
|
||||
mct-object="domainObject"
|
||||
|
||||
@@ -56,13 +56,13 @@ define(
|
||||
//navigate back to parent because nothing to show.
|
||||
return domainObject.getCapability("location").getOriginal().then(function (original) {
|
||||
parent = original.getCapability("context").getParent();
|
||||
parent.getCapability("action").perform("navigate");
|
||||
return parent.getCapability("action").perform("navigate");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function cancel(allowed) {
|
||||
return allowed && domainObject.getCapability("editor").finish();
|
||||
function cancel() {
|
||||
return domainObject.getCapability("editor").finish();
|
||||
}
|
||||
|
||||
//Do navigation first in order to trigger unsaved changes dialog
|
||||
|
||||
@@ -33,10 +33,12 @@ define(
|
||||
*/
|
||||
function SaveAction(
|
||||
dialogService,
|
||||
notificationService,
|
||||
context
|
||||
) {
|
||||
this.domainObject = (context || {}).domainObject;
|
||||
this.dialogService = dialogService;
|
||||
this.notificationService = notificationService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -47,7 +49,8 @@ define(
|
||||
* @memberof platform/commonUI/edit.SaveAction#
|
||||
*/
|
||||
SaveAction.prototype.perform = function () {
|
||||
var domainObject = this.domainObject,
|
||||
var self = this,
|
||||
domainObject = this.domainObject,
|
||||
dialog = new SaveInProgressDialog(this.dialogService);
|
||||
|
||||
// Invoke any save behavior introduced by the editor capability;
|
||||
@@ -58,15 +61,21 @@ define(
|
||||
return domainObject.getCapability("editor").save();
|
||||
}
|
||||
|
||||
function hideBlockingDialog() {
|
||||
function onSuccess() {
|
||||
dialog.hide();
|
||||
self.notificationService.info("Save Succeeded");
|
||||
}
|
||||
|
||||
function onFailure() {
|
||||
dialog.hide();
|
||||
self.notificationService.error("Save Failed");
|
||||
}
|
||||
|
||||
dialog.show();
|
||||
|
||||
return doSave()
|
||||
.then(hideBlockingDialog)
|
||||
.catch(hideBlockingDialog);
|
||||
.then(onSuccess)
|
||||
.catch(onFailure);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -33,11 +33,13 @@ define(
|
||||
*/
|
||||
function SaveAndStopEditingAction(
|
||||
dialogService,
|
||||
notificationService,
|
||||
context
|
||||
) {
|
||||
this.context = context;
|
||||
this.domainObject = (context || {}).domainObject;
|
||||
this.dialogService = dialogService;
|
||||
this.notificationService = notificationService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,7 +51,7 @@ define(
|
||||
*/
|
||||
SaveAndStopEditingAction.prototype.perform = function () {
|
||||
var domainObject = this.domainObject,
|
||||
saveAction = new SaveAction(this.dialogService, this.context);
|
||||
saveAction = new SaveAction(this.dialogService, this.notificationService, this.context);
|
||||
|
||||
function closeEditor() {
|
||||
return domainObject.getCapability("editor").finish();
|
||||
|
||||
@@ -43,6 +43,7 @@ define([
|
||||
policyService,
|
||||
dialogService,
|
||||
copyService,
|
||||
notificationService,
|
||||
context
|
||||
) {
|
||||
this.domainObject = (context || {}).domainObject;
|
||||
@@ -52,6 +53,7 @@ define([
|
||||
this.policyService = policyService;
|
||||
this.dialogService = dialogService;
|
||||
this.copyService = copyService;
|
||||
this.notificationService = notificationService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -117,8 +119,10 @@ define([
|
||||
|
||||
return self.dialogService
|
||||
.getUserInput(wizard.getFormStructure(true),
|
||||
wizard.getInitialFormValue()
|
||||
).then(wizard.populateObjectFromInput.bind(wizard));
|
||||
wizard.getInitialFormValue())
|
||||
.then(wizard.populateObjectFromInput.bind(wizard), function (failureReason) {
|
||||
return Promise.reject("user canceled");
|
||||
});
|
||||
}
|
||||
|
||||
function showBlockingDialog(object) {
|
||||
@@ -176,8 +180,16 @@ define([
|
||||
});
|
||||
}
|
||||
|
||||
function onFailure() {
|
||||
function onSuccess(object) {
|
||||
self.notificationService.info("Save Succeeded");
|
||||
return object;
|
||||
}
|
||||
|
||||
function onFailure(reason) {
|
||||
hideBlockingDialog();
|
||||
if (reason !== "user canceled") {
|
||||
self.notificationService.error("Save Failed");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -190,6 +202,7 @@ define([
|
||||
.then(saveAfterClone)
|
||||
.then(finishEditing)
|
||||
.then(hideBlockingDialog)
|
||||
.then(onSuccess)
|
||||
.catch(onFailure);
|
||||
};
|
||||
|
||||
|
||||
@@ -28,18 +28,49 @@ define(
|
||||
[],
|
||||
function () {
|
||||
|
||||
function isDirty(domainObject) {
|
||||
var navigatedObject = domainObject,
|
||||
editorCapability = navigatedObject &&
|
||||
navigatedObject.getCapability("editor");
|
||||
|
||||
return editorCapability &&
|
||||
editorCapability.isEditContextRoot() &&
|
||||
editorCapability.dirty();
|
||||
}
|
||||
|
||||
function cancelEditing(domainObject) {
|
||||
var navigatedObject = domainObject,
|
||||
editorCapability = navigatedObject &&
|
||||
navigatedObject.getCapability("editor");
|
||||
|
||||
return editorCapability &&
|
||||
editorCapability.finish();
|
||||
}
|
||||
|
||||
/**
|
||||
* Controller which is responsible for populating the scope for
|
||||
* Edit mode
|
||||
* @memberof platform/commonUI/edit
|
||||
* @constructor
|
||||
*/
|
||||
function EditObjectController($scope, $location, policyService) {
|
||||
function EditObjectController($scope, $location, navigationService) {
|
||||
this.scope = $scope;
|
||||
this.policyService = policyService;
|
||||
var domainObject = $scope.domainObject;
|
||||
|
||||
var navigatedObject;
|
||||
function setViewForDomainObject(domainObject) {
|
||||
var removeCheck = navigationService
|
||||
.checkBeforeNavigation(function () {
|
||||
if (isDirty(domainObject)) {
|
||||
return "Continuing will cause the loss of any unsaved changes.";
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
$scope.$on('$destroy', function () {
|
||||
removeCheck();
|
||||
cancelEditing(domainObject);
|
||||
});
|
||||
|
||||
function setViewForDomainObject() {
|
||||
|
||||
var locationViewKey = $location.search().view;
|
||||
|
||||
@@ -54,34 +85,15 @@ define(
|
||||
((domainObject && domainObject.useCapability('view')) || [])
|
||||
.forEach(selectViewIfMatching);
|
||||
}
|
||||
navigatedObject = domainObject;
|
||||
}
|
||||
|
||||
$scope.$watch('domainObject', setViewForDomainObject);
|
||||
setViewForDomainObject();
|
||||
|
||||
$scope.doAction = function (action) {
|
||||
return $scope[action] && $scope[action]();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the warning to show if the user attempts to navigate
|
||||
* away from Edit mode while unsaved changes are present.
|
||||
* @returns {string} the warning to show, or undefined if
|
||||
* there are no unsaved changes
|
||||
*/
|
||||
EditObjectController.prototype.getUnloadWarning = function () {
|
||||
var navigatedObject = this.scope.domainObject,
|
||||
policyMessage;
|
||||
|
||||
this.policyService.allow("navigation", navigatedObject, undefined, function (message) {
|
||||
policyMessage = message;
|
||||
});
|
||||
|
||||
return policyMessage;
|
||||
|
||||
};
|
||||
|
||||
return EditObjectController;
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,104 +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(
|
||||
[],
|
||||
function () {
|
||||
|
||||
/**
|
||||
* Defines the `mct-before-unload` directive. The expression bound
|
||||
* to this attribute will be evaluated during page navigation events
|
||||
* and, if it returns a truthy value, will be used to populate a
|
||||
* prompt to the user to confirm this navigation.
|
||||
* @memberof platform/commonUI/edit
|
||||
* @constructor
|
||||
* @param $window the window
|
||||
*/
|
||||
function MCTBeforeUnload($window) {
|
||||
var unloads = [],
|
||||
oldBeforeUnload = $window.onbeforeunload;
|
||||
|
||||
// Run all unload functions, returning the first returns truthily.
|
||||
function checkUnloads() {
|
||||
var result;
|
||||
unloads.forEach(function (unload) {
|
||||
result = result || unload();
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
// Link function for an mct-before-unload directive usage
|
||||
function link(scope, element, attrs) {
|
||||
// Invoke the
|
||||
function unload() {
|
||||
return scope.$eval(attrs.mctBeforeUnload);
|
||||
}
|
||||
|
||||
// Stop using this unload expression
|
||||
function removeUnload() {
|
||||
unloads = unloads.filter(function (callback) {
|
||||
return callback !== unload;
|
||||
});
|
||||
if (unloads.length === 0) {
|
||||
$window.onbeforeunload = oldBeforeUnload;
|
||||
}
|
||||
}
|
||||
|
||||
// Show a dialog before allowing a location change
|
||||
function checkLocationChange(event) {
|
||||
// Get an unload message (if any)
|
||||
var warning = unload();
|
||||
// Prompt the user if there's an unload message
|
||||
if (warning && !$window.confirm(warning)) {
|
||||
// ...and prevent the route change if it was confirmed
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
// If this is the first active instance of this directive,
|
||||
// register as the window's beforeunload handler
|
||||
if (unloads.length === 0) {
|
||||
$window.onbeforeunload = checkUnloads;
|
||||
}
|
||||
|
||||
// Include this instance of the directive's unload function
|
||||
unloads.push(unload);
|
||||
|
||||
// Remove it when the scope is destroyed
|
||||
scope.$on("$destroy", removeUnload);
|
||||
|
||||
// Also handle route changes
|
||||
scope.$on("$locationChangeStart", checkLocationChange);
|
||||
}
|
||||
|
||||
return {
|
||||
// Applicable as an attribute
|
||||
restrict: "A",
|
||||
// Link with the provided function
|
||||
link: link
|
||||
};
|
||||
}
|
||||
|
||||
return MCTBeforeUnload;
|
||||
|
||||
}
|
||||
);
|
||||
@@ -1,64 +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(
|
||||
[],
|
||||
function () {
|
||||
|
||||
/**
|
||||
* Policy controlling whether navigation events should proceed
|
||||
* when object is being edited.
|
||||
* @memberof platform/commonUI/edit
|
||||
* @constructor
|
||||
* @implements {Policy.<Action, ActionContext>}
|
||||
*/
|
||||
function EditNavigationPolicy(policyService) {
|
||||
this.policyService = policyService;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
EditNavigationPolicy.prototype.isDirty = function (domainObject) {
|
||||
var navigatedObject = domainObject,
|
||||
editorCapability = navigatedObject &&
|
||||
navigatedObject.getCapability("editor");
|
||||
|
||||
return editorCapability &&
|
||||
editorCapability.isEditContextRoot() &&
|
||||
editorCapability.dirty();
|
||||
};
|
||||
|
||||
/**
|
||||
* Allow navigation if an object is not dirty, or if the user elects
|
||||
* to proceed anyway.
|
||||
* @param currentNavigation
|
||||
* @returns {boolean|*} true if the object model is clean; or if
|
||||
* it's dirty and the user wishes to proceed anyway.
|
||||
*/
|
||||
EditNavigationPolicy.prototype.allow = function (currentNavigation) {
|
||||
return !this.isDirty(currentNavigation);
|
||||
};
|
||||
|
||||
return EditNavigationPolicy;
|
||||
}
|
||||
);
|
||||
@@ -43,98 +43,55 @@ define(
|
||||
* @implements {Representer}
|
||||
* @constructor
|
||||
*/
|
||||
function EditRepresenter($q, $log, scope) {
|
||||
var self = this;
|
||||
|
||||
this.scope = scope;
|
||||
this.listenHandle = undefined;
|
||||
|
||||
// Mutate and persist a new version of a domain object's model.
|
||||
function doMutate(model) {
|
||||
var domainObject = self.domainObject;
|
||||
|
||||
// First, mutate; then, persist.
|
||||
return $q.when(domainObject.useCapability("mutation", function () {
|
||||
return model;
|
||||
}));
|
||||
}
|
||||
|
||||
// Handle changes to model and/or view configuration
|
||||
function commit(message) {
|
||||
// Look up from scope; these will have been populated by
|
||||
// mct-representation.
|
||||
var model = scope.model,
|
||||
configuration = scope.configuration,
|
||||
domainObject = self.domainObject;
|
||||
|
||||
// Log the commit message
|
||||
$log.debug([
|
||||
"Committing ",
|
||||
domainObject && domainObject.getModel().name,
|
||||
"(" + (domainObject && domainObject.getId()) + "):",
|
||||
message
|
||||
].join(" "));
|
||||
|
||||
// Update the configuration stored in the model, and persist.
|
||||
if (domainObject) {
|
||||
// Configurations for specific views are stored by
|
||||
// key in the "configuration" field of the model.
|
||||
if (self.key && configuration) {
|
||||
model.configuration = model.configuration || {};
|
||||
model.configuration[self.key] = configuration;
|
||||
}
|
||||
doMutate(model);
|
||||
}
|
||||
}
|
||||
|
||||
// Place the "commit" method in the scope
|
||||
scope.commit = commit;
|
||||
|
||||
// Clean up when the scope is destroyed
|
||||
scope.$on("$destroy", function () {
|
||||
self.destroy();
|
||||
});
|
||||
function EditRepresenter($log, $scope) {
|
||||
this.$log = $log;
|
||||
this.$scope = $scope;
|
||||
|
||||
this.$scope.commit = this.commit.bind(this);
|
||||
}
|
||||
|
||||
// Handle a specific representation of a specific domain object
|
||||
EditRepresenter.prototype.represent = function represent(representation, representedObject) {
|
||||
var scope = this.scope;
|
||||
/**
|
||||
* Commit any changes made to the in-scope model to the domain object.
|
||||
* Also commits any changes made to $scope.configuration to the proper
|
||||
* configuration value for the current representation.
|
||||
*
|
||||
* @param {String} message a message to log with the commit message.
|
||||
*/
|
||||
EditRepresenter.prototype.commit = function (message) {
|
||||
var model = this.$scope.model,
|
||||
configuration = this.$scope.configuration,
|
||||
domainObject = this.domainObject;
|
||||
|
||||
// Track the key, to know which view configuration to save to.
|
||||
this.key = (representation || {}).key;
|
||||
// Track the represented object
|
||||
this.domainObject = representedObject;
|
||||
this.$log.debug([
|
||||
"Committing ",
|
||||
domainObject && domainObject.getModel().name,
|
||||
"(" + (domainObject && domainObject.getId()) + "):",
|
||||
message
|
||||
].join(" "));
|
||||
|
||||
// Ensure existing watches are released
|
||||
this.destroy();
|
||||
|
||||
function setEditing() {
|
||||
scope.viewObjectTemplate = 'edit-object';
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen for changes in object state. If the object becomes
|
||||
* editable then change the view and inspector regions
|
||||
* object representation accordingly
|
||||
*/
|
||||
this.listenHandle = this.domainObject.getCapability('status').listen(function (statuses) {
|
||||
if (statuses.indexOf('editing') !== -1) {
|
||||
setEditing();
|
||||
} else {
|
||||
delete scope.viewObjectTemplate;
|
||||
if (this.domainObject) {
|
||||
if (this.key && configuration) {
|
||||
model.configuration = model.configuration || {};
|
||||
model.configuration[this.key] = configuration;
|
||||
}
|
||||
});
|
||||
domainObject.useCapability('mutation', function () {
|
||||
return model;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (representedObject.hasCapability('editor') && representedObject.getCapability('editor').isEditContextRoot()) {
|
||||
setEditing();
|
||||
// Handle a specific representation of a specific domain object
|
||||
EditRepresenter.prototype.represent = function (representation, representedObject) {
|
||||
this.domainObject = representedObject;
|
||||
if (representation) {
|
||||
this.key = representation.key;
|
||||
} else {
|
||||
delete this.key;
|
||||
}
|
||||
};
|
||||
|
||||
// Respond to the destruction of the current representation.
|
||||
EditRepresenter.prototype.destroy = function destroy() {
|
||||
return this.listenHandle && this.listenHandle();
|
||||
};
|
||||
EditRepresenter.prototype.destroy = function () {};
|
||||
|
||||
return EditRepresenter;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
/*global describe,it,expect,beforeEach,jasmine,waitsFor,runs*/
|
||||
|
||||
define(
|
||||
["../../src/actions/SaveAction"],
|
||||
@@ -28,7 +29,8 @@ define(
|
||||
var mockDomainObject,
|
||||
mockEditorCapability,
|
||||
actionContext,
|
||||
dialogService,
|
||||
mockDialogService,
|
||||
mockNotificationService,
|
||||
mockActionCapability,
|
||||
capabilities = {},
|
||||
action;
|
||||
@@ -68,11 +70,17 @@ define(
|
||||
actionContext = {
|
||||
domainObject: mockDomainObject
|
||||
};
|
||||
dialogService = jasmine.createSpyObj(
|
||||
|
||||
mockDialogService = jasmine.createSpyObj(
|
||||
"dialogService",
|
||||
["showBlockingMessage"]
|
||||
);
|
||||
|
||||
mockNotificationService = jasmine.createSpyObj(
|
||||
"notificationService",
|
||||
["info", "error"]
|
||||
);
|
||||
|
||||
mockDomainObject.hasCapability.andReturn(true);
|
||||
mockDomainObject.getCapability.andCallFake(function (capability) {
|
||||
return capabilities[capability];
|
||||
@@ -81,7 +89,7 @@ define(
|
||||
mockEditorCapability.save.andReturn(mockPromise(true));
|
||||
mockEditorCapability.isEditContextRoot.andReturn(true);
|
||||
|
||||
action = new SaveAction(dialogService, actionContext);
|
||||
action = new SaveAction(mockDialogService, mockNotificationService, actionContext);
|
||||
});
|
||||
|
||||
it("only applies to domain object with an editor capability", function () {
|
||||
@@ -105,30 +113,54 @@ define(
|
||||
expect(mockEditorCapability.save).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("a blocking dialog", function () {
|
||||
describe("in order to keep the user in the loop", function () {
|
||||
var mockDialogHandle;
|
||||
|
||||
beforeEach(function () {
|
||||
mockDialogHandle = jasmine.createSpyObj("dialogHandle", ["dismiss"]);
|
||||
dialogService.showBlockingMessage.andReturn(mockDialogHandle);
|
||||
mockDialogService.showBlockingMessage.andReturn(mockDialogHandle);
|
||||
});
|
||||
|
||||
|
||||
it("shows a dialog while saving", function () {
|
||||
mockEditorCapability.save.andReturn(new Promise(function () {
|
||||
}));
|
||||
action.perform();
|
||||
expect(dialogService.showBlockingMessage).toHaveBeenCalled();
|
||||
expect(mockDialogService.showBlockingMessage).toHaveBeenCalled();
|
||||
expect(mockDialogHandle.dismiss).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("hides a dialog when saving is complete", function () {
|
||||
it("hides the dialog when saving is complete", function () {
|
||||
action.perform();
|
||||
expect(dialogService.showBlockingMessage).toHaveBeenCalled();
|
||||
expect(mockDialogService.showBlockingMessage).toHaveBeenCalled();
|
||||
expect(mockDialogHandle.dismiss).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("notifies if saving succeeded", function () {
|
||||
var mockCallback = jasmine.createSpy("callback");
|
||||
mockEditorCapability.save.andReturn(Promise.resolve("success"));
|
||||
action.perform().then(mockCallback);
|
||||
waitsFor(function () {
|
||||
return mockCallback.calls.length > 0;
|
||||
});
|
||||
runs(function () {
|
||||
expect(mockNotificationService.info).toHaveBeenCalled();
|
||||
expect(mockNotificationService.error).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("notifies if saving failed", function () {
|
||||
var mockCallback = jasmine.createSpy("callback");
|
||||
mockEditorCapability.save.andReturn(Promise.reject("some failure reason"));
|
||||
action.perform().then(mockCallback);
|
||||
waitsFor(function () {
|
||||
return mockCallback.calls.length > 0;
|
||||
});
|
||||
runs(function () {
|
||||
expect(mockNotificationService.error).toHaveBeenCalled();
|
||||
expect(mockNotificationService.info).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
/*global describe,it,expect,beforeEach,jasmine*/
|
||||
|
||||
define(
|
||||
["../../src/actions/SaveAndStopEditingAction"],
|
||||
@@ -35,6 +36,7 @@ define(
|
||||
mockEditorCapability,
|
||||
actionContext,
|
||||
dialogService,
|
||||
notificationService,
|
||||
mockActionCapability,
|
||||
capabilities = {},
|
||||
action;
|
||||
@@ -79,6 +81,11 @@ define(
|
||||
["showBlockingMessage"]
|
||||
);
|
||||
|
||||
notificationService = jasmine.createSpyObj(
|
||||
"notificationService",
|
||||
["info", "error"]
|
||||
);
|
||||
|
||||
mockDomainObject.hasCapability.andReturn(true);
|
||||
mockDomainObject.getCapability.andCallFake(function (capability) {
|
||||
return capabilities[capability];
|
||||
@@ -87,7 +94,7 @@ define(
|
||||
mockEditorCapability.save.andReturn(mockPromise(true));
|
||||
mockEditorCapability.isEditContextRoot.andReturn(true);
|
||||
|
||||
action = new SaveAndStopEditingAction(dialogService, actionContext);
|
||||
action = new SaveAndStopEditingAction(dialogService, notificationService, actionContext);
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -27,11 +27,13 @@ define(
|
||||
|
||||
describe("The Save As action", function () {
|
||||
var mockDomainObject,
|
||||
mockClonedObject,
|
||||
mockEditorCapability,
|
||||
mockActionCapability,
|
||||
mockObjectService,
|
||||
mockDialogService,
|
||||
mockCopyService,
|
||||
mockNotificationService,
|
||||
mockParent,
|
||||
actionContext,
|
||||
capabilities = {},
|
||||
@@ -57,7 +59,8 @@ define(
|
||||
[
|
||||
"getCapability",
|
||||
"hasCapability",
|
||||
"getModel"
|
||||
"getModel",
|
||||
"getId"
|
||||
]
|
||||
);
|
||||
mockDomainObject.hasCapability.andReturn(true);
|
||||
@@ -65,6 +68,15 @@ define(
|
||||
return capabilities[capability];
|
||||
});
|
||||
mockDomainObject.getModel.andReturn({location: 'a', persisted: undefined});
|
||||
mockDomainObject.getId.andReturn(0);
|
||||
|
||||
mockClonedObject = jasmine.createSpyObj(
|
||||
"clonedObject",
|
||||
[
|
||||
"getId"
|
||||
]
|
||||
);
|
||||
mockClonedObject.getId.andReturn(1);
|
||||
|
||||
mockParent = jasmine.createSpyObj(
|
||||
"parentObject",
|
||||
@@ -111,12 +123,27 @@ define(
|
||||
"perform"
|
||||
]
|
||||
);
|
||||
mockCopyService.perform.andReturn(mockPromise(mockClonedObject));
|
||||
|
||||
mockNotificationService = jasmine.createSpyObj(
|
||||
"notificationService",
|
||||
[
|
||||
"info",
|
||||
"error"
|
||||
]
|
||||
);
|
||||
|
||||
actionContext = {
|
||||
domainObject: mockDomainObject
|
||||
};
|
||||
|
||||
action = new SaveAsAction(undefined, undefined, mockDialogService, mockCopyService, actionContext);
|
||||
action = new SaveAsAction(
|
||||
undefined,
|
||||
undefined,
|
||||
mockDialogService,
|
||||
mockCopyService,
|
||||
mockNotificationService,
|
||||
actionContext);
|
||||
|
||||
spyOn(action, "getObjectService");
|
||||
action.getObjectService.andReturn(mockObjectService);
|
||||
@@ -186,7 +213,7 @@ define(
|
||||
expect(mockDialogService.getUserInput).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("a blocking dialog", function () {
|
||||
describe("in order to keep the user in the loop", function () {
|
||||
var mockDialogHandle;
|
||||
|
||||
beforeEach(function () {
|
||||
@@ -194,14 +221,14 @@ define(
|
||||
mockDialogService.showBlockingMessage.andReturn(mockDialogHandle);
|
||||
});
|
||||
|
||||
it("indicates that a save is taking place", function () {
|
||||
it("shows a blocking dialog indicating that saving is in progress", function () {
|
||||
mockEditorCapability.save.andReturn(new Promise(function () {}));
|
||||
action.perform();
|
||||
expect(mockDialogService.showBlockingMessage).toHaveBeenCalled();
|
||||
expect(mockDialogHandle.dismiss).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("is hidden after saving", function () {
|
||||
it("hides the blocking dialog after saving finishes", function () {
|
||||
var mockCallback = jasmine.createSpy();
|
||||
action.perform().then(mockCallback);
|
||||
expect(mockDialogService.showBlockingMessage).toHaveBeenCalled();
|
||||
@@ -212,6 +239,31 @@ define(
|
||||
expect(mockDialogHandle.dismiss).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("notifies if saving succeeded", function () {
|
||||
var mockCallback = jasmine.createSpy();
|
||||
action.perform().then(mockCallback);
|
||||
waitsFor(function () {
|
||||
return mockCallback.calls.length > 0;
|
||||
});
|
||||
runs(function () {
|
||||
expect(mockNotificationService.info).toHaveBeenCalled();
|
||||
expect(mockNotificationService.error).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("notifies if saving failed", function () {
|
||||
mockCopyService.perform.andReturn(Promise.reject("some failure reason"));
|
||||
var mockCallback = jasmine.createSpy();
|
||||
action.perform().then(mockCallback);
|
||||
waitsFor(function () {
|
||||
return mockCallback.calls.length > 0;
|
||||
});
|
||||
runs(function () {
|
||||
expect(mockNotificationService.error).toHaveBeenCalled();
|
||||
expect(mockNotificationService.info).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -24,32 +24,19 @@ define(
|
||||
["../../src/controllers/EditObjectController"],
|
||||
function (EditObjectController) {
|
||||
|
||||
describe("The Edit mode controller", function () {
|
||||
describe("The Edit Object controller", function () {
|
||||
var mockScope,
|
||||
mockObject,
|
||||
mockType,
|
||||
testViews,
|
||||
mockEditorCapability,
|
||||
mockLocation,
|
||||
mockNavigationService,
|
||||
removeCheck,
|
||||
mockStatusCapability,
|
||||
mockCapabilities,
|
||||
mockPolicyService,
|
||||
controller;
|
||||
|
||||
// Utility function; look for a $watch on scope and fire it
|
||||
function fireWatch(expr, value) {
|
||||
mockScope.$watch.calls.forEach(function (call) {
|
||||
if (call.args[0] === expr) {
|
||||
call.args[1](value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
mockPolicyService = jasmine.createSpyObj(
|
||||
"policyService",
|
||||
[
|
||||
"allow"
|
||||
]
|
||||
);
|
||||
mockScope = jasmine.createSpyObj(
|
||||
"$scope",
|
||||
["$on", "$watch"]
|
||||
@@ -58,16 +45,16 @@ define(
|
||||
"domainObject",
|
||||
["getId", "getModel", "getCapability", "hasCapability", "useCapability"]
|
||||
);
|
||||
mockType = jasmine.createSpyObj(
|
||||
"type",
|
||||
["hasFeature"]
|
||||
mockEditorCapability = jasmine.createSpyObj(
|
||||
"mockEditorCapability",
|
||||
["isEditContextRoot", "dirty", "finish"]
|
||||
);
|
||||
mockStatusCapability = jasmine.createSpyObj('statusCapability',
|
||||
["get"]
|
||||
);
|
||||
|
||||
mockCapabilities = {
|
||||
"type" : mockType,
|
||||
"editor" : mockEditorCapability,
|
||||
"status": mockStatusCapability
|
||||
};
|
||||
|
||||
@@ -75,52 +62,70 @@ define(
|
||||
["search"]
|
||||
);
|
||||
mockLocation.search.andReturn({"view": "fixed"});
|
||||
mockNavigationService = jasmine.createSpyObj('navigationService',
|
||||
["checkBeforeNavigation"]
|
||||
);
|
||||
|
||||
removeCheck = jasmine.createSpy('removeCheck');
|
||||
mockNavigationService.checkBeforeNavigation.andReturn(removeCheck);
|
||||
|
||||
mockObject.getId.andReturn("test");
|
||||
mockObject.getModel.andReturn({ name: "Test object" });
|
||||
mockObject.getCapability.andCallFake(function (key) {
|
||||
return mockCapabilities[key];
|
||||
});
|
||||
mockType.hasFeature.andReturn(true);
|
||||
|
||||
mockScope.domainObject = mockObject;
|
||||
|
||||
controller = new EditObjectController(
|
||||
mockScope,
|
||||
mockLocation,
|
||||
mockPolicyService
|
||||
);
|
||||
});
|
||||
|
||||
it("exposes a warning message for unload", function () {
|
||||
var errorMessage = "Unsaved changes";
|
||||
|
||||
// Normally, should be undefined
|
||||
expect(controller.getUnloadWarning()).toBeUndefined();
|
||||
|
||||
// Override the policy service to prevent navigation
|
||||
mockPolicyService.allow.andCallFake(function (category, object, context, callback) {
|
||||
callback(errorMessage);
|
||||
});
|
||||
|
||||
// Should have some warning message here now
|
||||
expect(controller.getUnloadWarning()).toEqual(errorMessage);
|
||||
});
|
||||
|
||||
|
||||
it("sets the active view from query parameters", function () {
|
||||
var testViews = [
|
||||
{ key: 'abc' },
|
||||
{ key: 'def', someKey: 'some value' },
|
||||
{ key: 'xyz' }
|
||||
];
|
||||
testViews = [
|
||||
{ key: 'abc' },
|
||||
{ key: 'def', someKey: 'some value' },
|
||||
{ key: 'xyz' }
|
||||
];
|
||||
|
||||
mockObject.useCapability.andCallFake(function (c) {
|
||||
return (c === 'view') && testViews;
|
||||
});
|
||||
mockLocation.search.andReturn({ view: 'def' });
|
||||
|
||||
fireWatch('domainObject', mockObject);
|
||||
mockScope.domainObject = mockObject;
|
||||
|
||||
controller = new EditObjectController(
|
||||
mockScope,
|
||||
mockLocation,
|
||||
mockNavigationService
|
||||
);
|
||||
});
|
||||
|
||||
it("adds a check before navigation", function () {
|
||||
expect(mockNavigationService.checkBeforeNavigation)
|
||||
.toHaveBeenCalledWith(jasmine.any(Function));
|
||||
|
||||
var checkFn = mockNavigationService.checkBeforeNavigation.mostRecentCall.args[0];
|
||||
|
||||
mockEditorCapability.isEditContextRoot.andReturn(false);
|
||||
mockEditorCapability.dirty.andReturn(false);
|
||||
|
||||
expect(checkFn()).toBe(false);
|
||||
|
||||
mockEditorCapability.isEditContextRoot.andReturn(true);
|
||||
expect(checkFn()).toBe(false);
|
||||
|
||||
mockEditorCapability.dirty.andReturn(true);
|
||||
expect(checkFn())
|
||||
.toBe("Continuing will cause the loss of any unsaved changes.");
|
||||
|
||||
});
|
||||
|
||||
it("cleans up on destroy", function () {
|
||||
expect(mockScope.$on)
|
||||
.toHaveBeenCalledWith("$destroy", jasmine.any(Function));
|
||||
|
||||
mockScope.$on.mostRecentCall.args[1]();
|
||||
|
||||
expect(mockEditorCapability.finish).toHaveBeenCalled();
|
||||
expect(removeCheck).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("sets the active view from query parameters", function () {
|
||||
expect(mockScope.representation.selected)
|
||||
.toEqual(testViews[1]);
|
||||
});
|
||||
@@ -1,114 +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/directives/MCTBeforeUnload"],
|
||||
function (MCTBeforeUnload) {
|
||||
|
||||
describe("The mct-before-unload directive", function () {
|
||||
var mockWindow,
|
||||
mockScope,
|
||||
testAttrs,
|
||||
mockEvent,
|
||||
directive;
|
||||
|
||||
function fireListener(eventType, value) {
|
||||
mockScope.$on.calls.forEach(function (call) {
|
||||
if (call.args[0] === eventType) {
|
||||
call.args[1](value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
mockWindow = jasmine.createSpyObj("$window", ['confirm']);
|
||||
mockScope = jasmine.createSpyObj("$scope", ['$eval', '$on']);
|
||||
testAttrs = { mctBeforeUnload: "someExpression" };
|
||||
mockEvent = jasmine.createSpyObj("event", ["preventDefault"]);
|
||||
directive = new MCTBeforeUnload(mockWindow);
|
||||
directive.link(mockScope, {}, testAttrs);
|
||||
});
|
||||
|
||||
it("can be used only as an attribute", function () {
|
||||
expect(directive.restrict).toEqual('A');
|
||||
});
|
||||
|
||||
it("listens for beforeunload", function () {
|
||||
expect(mockWindow.onbeforeunload).toEqual(jasmine.any(Function));
|
||||
});
|
||||
|
||||
it("listens for route changes", function () {
|
||||
expect(mockScope.$on).toHaveBeenCalledWith(
|
||||
"$locationChangeStart",
|
||||
jasmine.any(Function)
|
||||
);
|
||||
});
|
||||
|
||||
it("listens for its scope's destroy event", function () {
|
||||
expect(mockScope.$on).toHaveBeenCalledWith(
|
||||
"$destroy",
|
||||
jasmine.any(Function)
|
||||
);
|
||||
});
|
||||
|
||||
it("uses result of evaluated expression as a warning", function () {
|
||||
mockScope.$eval.andReturn(undefined);
|
||||
expect(mockWindow.onbeforeunload(mockEvent)).toBeUndefined();
|
||||
mockScope.$eval.andReturn("some message");
|
||||
expect(mockWindow.onbeforeunload(mockEvent)).toEqual("some message");
|
||||
// Verify that the right expression was evaluated
|
||||
expect(mockScope.$eval).toHaveBeenCalledWith(testAttrs.mctBeforeUnload);
|
||||
});
|
||||
|
||||
it("confirms route changes", function () {
|
||||
// First, try with no unsaved changes;
|
||||
// should not confirm or preventDefault
|
||||
mockScope.$eval.andReturn(undefined);
|
||||
fireListener("$locationChangeStart", mockEvent);
|
||||
expect(mockWindow.confirm).not.toHaveBeenCalled();
|
||||
expect(mockEvent.preventDefault).not.toHaveBeenCalled();
|
||||
|
||||
// Next, try with unsaved changes that the user confirms;
|
||||
// should prompt, but not preventDefault
|
||||
mockScope.$eval.andReturn("some message");
|
||||
mockWindow.confirm.andReturn(true);
|
||||
fireListener("$locationChangeStart", mockEvent);
|
||||
expect(mockWindow.confirm).toHaveBeenCalledWith("some message");
|
||||
expect(mockEvent.preventDefault).not.toHaveBeenCalled();
|
||||
|
||||
// Finally, act as if the user said no to this dialog;
|
||||
// this should preventDefault on the location change.
|
||||
mockWindow.confirm.andReturn(false);
|
||||
fireListener("$locationChangeStart", mockEvent);
|
||||
expect(mockWindow.confirm).toHaveBeenCalledWith("some message");
|
||||
expect(mockEvent.preventDefault).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("cleans up listeners when destroyed", function () {
|
||||
fireListener("$destroy", mockEvent);
|
||||
expect(mockWindow.onbeforeunload).toBeUndefined();
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -20,103 +20,70 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define(
|
||||
["../../src/representers/EditRepresenter"],
|
||||
function (EditRepresenter) {
|
||||
define([
|
||||
'../../src/representers/EditRepresenter'
|
||||
], function (
|
||||
EditRepresenter
|
||||
) {
|
||||
describe('EditRepresenter', function () {
|
||||
var $log,
|
||||
$scope,
|
||||
representer;
|
||||
|
||||
describe("The Edit mode representer", function () {
|
||||
var mockQ,
|
||||
mockLog,
|
||||
mockScope,
|
||||
testRepresentation,
|
||||
mockDomainObject,
|
||||
mockStatusCapability,
|
||||
mockEditorCapability,
|
||||
mockCapabilities,
|
||||
representer;
|
||||
|
||||
function mockPromise(value) {
|
||||
return {
|
||||
then: function (callback) {
|
||||
return mockPromise(callback(value));
|
||||
}
|
||||
};
|
||||
}
|
||||
beforeEach(function () {
|
||||
$log = jasmine.createSpyObj('$log', ['debug']);
|
||||
$scope = {};
|
||||
representer = new EditRepresenter($log, $scope);
|
||||
});
|
||||
|
||||
it('injects a commit function in scope', function () {
|
||||
expect($scope.commit).toEqual(jasmine.any(Function));
|
||||
});
|
||||
|
||||
describe('representation', function () {
|
||||
var domainObject,
|
||||
representation;
|
||||
|
||||
beforeEach(function () {
|
||||
mockQ = { when: mockPromise };
|
||||
mockLog = jasmine.createSpyObj("$log", ["info", "debug"]);
|
||||
mockScope = jasmine.createSpyObj("$scope", ["$watch", "$on"]);
|
||||
testRepresentation = { key: "test" };
|
||||
mockDomainObject = jasmine.createSpyObj("domainObject", [
|
||||
"getId",
|
||||
"getModel",
|
||||
"getCapability",
|
||||
"useCapability",
|
||||
"hasCapability"
|
||||
domainObject = jasmine.createSpyObj('domainObject', [
|
||||
'getId',
|
||||
'getModel',
|
||||
'useCapability'
|
||||
]);
|
||||
mockStatusCapability =
|
||||
jasmine.createSpyObj("statusCapability", ["listen"]);
|
||||
mockEditorCapability =
|
||||
jasmine.createSpyObj("editorCapability", ["isEditContextRoot"]);
|
||||
|
||||
mockCapabilities = {
|
||||
'status': mockStatusCapability,
|
||||
'editor': mockEditorCapability
|
||||
domainObject.getId.andReturn('anId');
|
||||
domainObject.getModel.andReturn({name: 'anObject'});
|
||||
|
||||
representation = {
|
||||
key: 'someRepresentation'
|
||||
};
|
||||
|
||||
mockDomainObject.getModel.andReturn({});
|
||||
mockDomainObject.hasCapability.andReturn(true);
|
||||
mockDomainObject.useCapability.andReturn(true);
|
||||
mockDomainObject.getCapability.andCallFake(function (capability) {
|
||||
return mockCapabilities[capability];
|
||||
});
|
||||
|
||||
representer = new EditRepresenter(mockQ, mockLog, mockScope);
|
||||
representer.represent(testRepresentation, mockDomainObject);
|
||||
$scope.model = {name: 'anotherName'};
|
||||
$scope.configuration = {some: 'config'};
|
||||
representer.represent(representation, domainObject);
|
||||
});
|
||||
|
||||
it("provides a commit method in scope", function () {
|
||||
expect(mockScope.commit).toEqual(jasmine.any(Function));
|
||||
it('logs a message when commiting', function () {
|
||||
$scope.commit('Test Message');
|
||||
expect($log.debug)
|
||||
.toHaveBeenCalledWith('Committing anObject (anId): Test Message');
|
||||
});
|
||||
|
||||
it("Sets edit view template on edit mode", function () {
|
||||
mockStatusCapability.listen.mostRecentCall.args[0](['editing']);
|
||||
mockEditorCapability.isEditContextRoot.andReturn(true);
|
||||
expect(mockScope.viewObjectTemplate).toEqual('edit-object');
|
||||
it('mutates the object when committing', function () {
|
||||
$scope.commit('Test Message');
|
||||
|
||||
expect(domainObject.useCapability)
|
||||
.toHaveBeenCalledWith('mutation', jasmine.any(Function));
|
||||
|
||||
var mutateValue = domainObject.useCapability.calls[0].args[1]();
|
||||
|
||||
expect(mutateValue.configuration.someRepresentation)
|
||||
.toEqual({some: 'config'});
|
||||
expect(mutateValue.name).toEqual('anotherName');
|
||||
});
|
||||
|
||||
it("Cleans up listeners on scope destroy", function () {
|
||||
representer.listenHandle = jasmine.createSpy('listen');
|
||||
mockScope.$on.mostRecentCall.args[1]();
|
||||
expect(representer.listenHandle).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("mutates upon observed changes", function () {
|
||||
mockScope.model = { someKey: "some value" };
|
||||
mockScope.configuration = { someConfiguration: "something" };
|
||||
|
||||
mockScope.commit("Some message");
|
||||
|
||||
// Should have mutated the object...
|
||||
expect(mockDomainObject.useCapability).toHaveBeenCalledWith(
|
||||
"mutation",
|
||||
jasmine.any(Function)
|
||||
);
|
||||
|
||||
// Finally, check that the provided mutation function
|
||||
// includes both model and configuration
|
||||
expect(
|
||||
mockDomainObject.useCapability.mostRecentCall.args[1]()
|
||||
).toEqual({
|
||||
someKey: "some value",
|
||||
configuration: {
|
||||
test: { someConfiguration: "something" }
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -55,9 +55,7 @@
|
||||
height: 70%;
|
||||
width: 50%;
|
||||
min-height: 300px;
|
||||
max-height: 800px;
|
||||
min-width: 600px;
|
||||
max-width: 1000px;
|
||||
z-index: 101;
|
||||
> .contents {
|
||||
$m: $overlayMargin;
|
||||
@@ -141,4 +139,4 @@
|
||||
$h: 225px;
|
||||
min-height: $h;
|
||||
height: $h;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
this source code distribution or the Licensing information page available
|
||||
at runtime from the About dialog for additional information.
|
||||
-->
|
||||
<mct-tree mct-object="domainObject" mct-model="ngModel.selectedObject">
|
||||
<mct-tree root-object="domainObject"
|
||||
selected-object="ngModel.selectedObject"
|
||||
on-selection="ngModel.onSelection"
|
||||
allow-selection="ngModel.allowSelection">
|
||||
</mct-tree>
|
||||
|
||||
|
||||
@@ -26,25 +26,54 @@ define([
|
||||
], function (angular, TreeView) {
|
||||
function MCTTree(gestureService) {
|
||||
function link(scope, element) {
|
||||
var treeView = new TreeView(gestureService),
|
||||
unobserve = treeView.observe(function (domainObject) {
|
||||
if (scope.mctModel !== domainObject) {
|
||||
scope.mctModel = domainObject;
|
||||
scope.$apply();
|
||||
}
|
||||
});
|
||||
if (!scope.allowSelection) {
|
||||
scope.allowSelection = function () {
|
||||
return true;
|
||||
};
|
||||
}
|
||||
if (!scope.onSelection) {
|
||||
scope.onSelection = function () {};
|
||||
}
|
||||
var currentSelection = scope.selectedObject;
|
||||
var treeView = new TreeView(gestureService);
|
||||
|
||||
function setSelection(domainObject, event) {
|
||||
if (currentSelection === domainObject) {
|
||||
return;
|
||||
}
|
||||
if (!scope.allowSelection(domainObject)) {
|
||||
treeView.value(currentSelection);
|
||||
return;
|
||||
}
|
||||
currentSelection = domainObject;
|
||||
scope.onSelection(domainObject);
|
||||
scope.selectedObject = domainObject;
|
||||
if (event && event instanceof MouseEvent) {
|
||||
scope.$apply();
|
||||
}
|
||||
}
|
||||
|
||||
var unobserve = treeView.observe(setSelection);
|
||||
|
||||
element.append(angular.element(treeView.elements()));
|
||||
|
||||
scope.$watch('mctModel', treeView.value.bind(treeView));
|
||||
scope.$watch('mctObject', treeView.model.bind(treeView));
|
||||
scope.$watch('selectedObject', function (object) {
|
||||
currentSelection = object;
|
||||
treeView.value(object);
|
||||
});
|
||||
scope.$watch('rootObject', treeView.model.bind(treeView));
|
||||
scope.$on('$destroy', unobserve);
|
||||
}
|
||||
|
||||
return {
|
||||
restrict: "E",
|
||||
link: link,
|
||||
scope: { mctObject: "=", mctModel: "=" }
|
||||
scope: {
|
||||
rootObject: "=",
|
||||
selectedObject: "=",
|
||||
onSelection: "=?",
|
||||
allowSelection: "=?"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -49,8 +49,8 @@ define([
|
||||
|
||||
this.labelView = new TreeLabelView(gestureService);
|
||||
|
||||
$(this.labelView.elements()).on('click', function () {
|
||||
selectFn(this.activeObject);
|
||||
$(this.labelView.elements()).on('click', function (event) {
|
||||
selectFn(this.activeObject, event);
|
||||
}.bind(this));
|
||||
|
||||
this.li.append($(nodeTemplate));
|
||||
|
||||
@@ -109,11 +109,11 @@ define([
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
TreeView.prototype.value = function (domainObject) {
|
||||
TreeView.prototype.value = function (domainObject, event) {
|
||||
this.selectedObject = domainObject;
|
||||
this.updateNodeViewSelection();
|
||||
this.callbacks.forEach(function (callback) {
|
||||
callback(domainObject);
|
||||
callback(domainObject, event);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -19,10 +19,12 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
/* global console*/
|
||||
|
||||
define([
|
||||
'../../src/directives/MCTTree'
|
||||
], function (MCTTree) {
|
||||
'../../src/directives/MCTTree',
|
||||
'../../src/ui/TreeView'
|
||||
], function (MCTTree, TreeView) {
|
||||
describe("The mct-tree directive", function () {
|
||||
var mockParse,
|
||||
mockGestureService,
|
||||
@@ -50,6 +52,7 @@ define([
|
||||
mockExpr = jasmine.createSpy('expr');
|
||||
mockExpr.assign = jasmine.createSpy('assign');
|
||||
mockParse.andReturn(mockExpr);
|
||||
spyOn(TreeView.prototype, 'observe').andCallThrough();
|
||||
|
||||
mctTree = new MCTTree(mockParse, mockGestureService);
|
||||
});
|
||||
@@ -58,8 +61,13 @@ define([
|
||||
expect(mctTree.restrict).toEqual("E");
|
||||
});
|
||||
|
||||
it("two-way binds to mctObject and mctModel", function () {
|
||||
expect(mctTree.scope).toEqual({ mctObject: "=", mctModel: "=" });
|
||||
it("two-way binds", function () {
|
||||
expect(mctTree.scope).toEqual({
|
||||
rootObject: "=",
|
||||
selectedObject: "=",
|
||||
allowSelection: "=?",
|
||||
onSelection: "=?"
|
||||
});
|
||||
});
|
||||
|
||||
describe("link", function () {
|
||||
@@ -81,16 +89,16 @@ define([
|
||||
expect(mockElement.append).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("watches for mct-model's expression in the parent", function () {
|
||||
it("watches for selected-object expression in the parent", function () {
|
||||
expect(mockScope.$watch).toHaveBeenCalledWith(
|
||||
"mctModel",
|
||||
"selectedObject",
|
||||
jasmine.any(Function)
|
||||
);
|
||||
});
|
||||
|
||||
it("watches for changes to mct-object", function () {
|
||||
it("watches for changes to root-object", function () {
|
||||
expect(mockScope.$watch).toHaveBeenCalledWith(
|
||||
"mctObject",
|
||||
"rootObject",
|
||||
jasmine.any(Function)
|
||||
);
|
||||
});
|
||||
@@ -102,6 +110,10 @@ define([
|
||||
);
|
||||
});
|
||||
|
||||
it("watches for changes in tree view", function () {
|
||||
|
||||
});
|
||||
|
||||
// https://github.com/nasa/openmct/issues/1114
|
||||
it("does not trigger $apply during $watches", function () {
|
||||
mockScope.mctObject = makeMockDomainObject('root');
|
||||
@@ -111,14 +123,18 @@ define([
|
||||
});
|
||||
expect(mockScope.$apply).not.toHaveBeenCalled();
|
||||
});
|
||||
it("does trigger $apply from other value changes", function () {
|
||||
it("does trigger $apply from tree manipulation", function () {
|
||||
if (/PhantomJS/g.test(window.navigator.userAgent)) {
|
||||
console.log('Unable to run test in PhantomJS due to lack of support for event constructors');
|
||||
return;
|
||||
}
|
||||
// White-boxy; we know this is the setter for the tree's value
|
||||
var treeValueFn = mockScope.$watch.calls[0].args[1];
|
||||
var treeValueFn = TreeView.prototype.observe.calls[0].args[0];
|
||||
|
||||
mockScope.mctObject = makeMockDomainObject('root');
|
||||
mockScope.mctMode = makeMockDomainObject('selection');
|
||||
|
||||
treeValueFn(makeMockDomainObject('other'));
|
||||
treeValueFn(makeMockDomainObject('other'), new MouseEvent("click"));
|
||||
|
||||
expect(mockScope.$apply).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -289,8 +289,9 @@ define([
|
||||
});
|
||||
|
||||
it("notifies listeners when value is changed", function () {
|
||||
treeView.value(mockDomainObject);
|
||||
expect(mockCallback).toHaveBeenCalledWith(mockDomainObject);
|
||||
treeView.value(mockDomainObject, {some: event});
|
||||
expect(mockCallback)
|
||||
.toHaveBeenCalledWith(mockDomainObject, {some: event});
|
||||
});
|
||||
|
||||
it("does not notify listeners when deactivated", function () {
|
||||
|
||||
@@ -64,6 +64,7 @@ define(
|
||||
}).join(' ') :
|
||||
"";
|
||||
element.css('filter', styleValue);
|
||||
element.css('webkitFilter', styleValue);
|
||||
}
|
||||
|
||||
function nextImage(url) {
|
||||
|
||||
37
platform/features/plot/README.md
Normal file
37
platform/features/plot/README.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Plot README
|
||||
|
||||
## Chart
|
||||
|
||||
The `mct-chart` directive is used to support drawing of simple charts. It is
|
||||
present to support the Plot view, and its functionality is limited to the
|
||||
functionality that is relevant for that view.
|
||||
|
||||
This directive is used at the element level and takes one attribute, `draw`
|
||||
which is an Angular expression which will should evaluate to a drawing object.
|
||||
This drawing object should contain the following properties:
|
||||
|
||||
* `dimensions`: The size, in logical coordinates, of the chart area. A
|
||||
two-element array or numbers.
|
||||
* `origin`: The position, in logical coordinates, of the lower-left corner of
|
||||
the chart area. A two-element array or numbers.
|
||||
* `lines`: An array of lines (e.g. as a plot line) to draw, where each line is
|
||||
expressed as an object containing:
|
||||
* `buffer`: A Float32Array containing points in the line, in logical
|
||||
coordinates, in sequential x,y pairs.
|
||||
* `color`: The color of the line, as a four-element RGBA array, where
|
||||
each element is a number in the range of 0.0-1.0.
|
||||
* `points`: The number of points in the line.
|
||||
* `boxes`: An array of rectangles to draw in the chart area. Each is an object
|
||||
containing:
|
||||
* `start`: The first corner of the rectangle, as a two-element array of
|
||||
numbers, in logical coordinates.
|
||||
* `end`: The opposite corner of the rectangle, as a two-element array of
|
||||
numbers, in logical coordinates. color : The color of the line, as a
|
||||
four-element RGBA array, where each element is a number in the range of
|
||||
0.0-1.0.
|
||||
|
||||
While `mct-chart` is intended to support plots specifically, it does perform
|
||||
some useful management of canvas objects (e.g. choosing between WebGL and Canvas
|
||||
2D APIs for drawing based on browser support) so its usage is recommended when
|
||||
its supported drawing primitives are sufficient for other charting tasks.
|
||||
|
||||
@@ -38,6 +38,7 @@ define([
|
||||
"./src/directives/MCTSwimlaneDrop",
|
||||
"./src/directives/MCTSwimlaneDrag",
|
||||
"./src/services/ObjectLoader",
|
||||
"./src/chart/MCTTimelineChart",
|
||||
"text!./res/templates/values.html",
|
||||
"text!./res/templates/timeline.html",
|
||||
"text!./res/templates/activity-gantt.html",
|
||||
@@ -67,6 +68,7 @@ define([
|
||||
MCTSwimlaneDrop,
|
||||
MCTSwimlaneDrag,
|
||||
ObjectLoader,
|
||||
MCTTimelineChart,
|
||||
valuesTemplate,
|
||||
timelineTemplate,
|
||||
activityGanttTemplate,
|
||||
@@ -556,6 +558,14 @@ define([
|
||||
"depends": [
|
||||
"dndService"
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "mctTimelineChart",
|
||||
"implementation": MCTTimelineChart,
|
||||
"depends": [
|
||||
"$interval",
|
||||
"$log"
|
||||
]
|
||||
}
|
||||
],
|
||||
"services": [
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<span ng-controller="TimelineGraphController as graphController">
|
||||
<div class="t-graph l-graph" ng-repeat="graph in parameters.graphs">
|
||||
<div class="l-graph-area l-canvas-holder">
|
||||
<mct-chart draw="graph.drawingObject"></mct-chart>
|
||||
<mct-timeline-chart draw="graph.drawingObject"></mct-timeline-chart>
|
||||
</div>
|
||||
<div class="t-graph-labels l-graph-labels">
|
||||
<mct-include key="'timeline-resource-graph-labels'"
|
||||
@@ -31,4 +31,4 @@
|
||||
</mct-include>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
117
platform/features/timeline/src/chart/Canvas2DChart.js
Normal file
117
platform/features/timeline/src/chart/Canvas2DChart.js
Normal file
@@ -0,0 +1,117 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2016, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define(
|
||||
[],
|
||||
function () {
|
||||
|
||||
/**
|
||||
* Create a new chart which uses Canvas's 2D API for rendering.
|
||||
*
|
||||
* @memberof platform/features/plot
|
||||
* @constructor
|
||||
* @implements {platform/features/plot.Chart}
|
||||
* @param {CanvasElement} canvas the canvas object to render upon
|
||||
* @throws {Error} an error is thrown if Canvas's 2D API is unavailable.
|
||||
*/
|
||||
function Canvas2DChart(canvas) {
|
||||
this.canvas = canvas;
|
||||
this.c2d = canvas.getContext('2d');
|
||||
this.width = canvas.width;
|
||||
this.height = canvas.height;
|
||||
this.dimensions = [this.width, this.height];
|
||||
this.origin = [0, 0];
|
||||
|
||||
if (!this.c2d) {
|
||||
throw new Error("Canvas 2d API unavailable.");
|
||||
}
|
||||
}
|
||||
|
||||
// Convert from logical to physical x coordinates
|
||||
Canvas2DChart.prototype.x = function (v) {
|
||||
return ((v - this.origin[0]) / this.dimensions[0]) * this.width;
|
||||
};
|
||||
|
||||
// Convert from logical to physical y coordinates
|
||||
Canvas2DChart.prototype.y = function (v) {
|
||||
return this.height -
|
||||
((v - this.origin[1]) / this.dimensions[1]) * this.height;
|
||||
};
|
||||
|
||||
// Set the color to be used for drawing operations
|
||||
Canvas2DChart.prototype.setColor = function (color) {
|
||||
var mappedColor = color.map(function (c, i) {
|
||||
return i < 3 ? Math.floor(c * 255) : (c);
|
||||
}).join(',');
|
||||
this.c2d.strokeStyle = "rgba(" + mappedColor + ")";
|
||||
this.c2d.fillStyle = "rgba(" + mappedColor + ")";
|
||||
};
|
||||
|
||||
|
||||
Canvas2DChart.prototype.clear = function () {
|
||||
var canvas = this.canvas;
|
||||
this.width = canvas.width;
|
||||
this.height = canvas.height;
|
||||
this.c2d.clearRect(0, 0, this.width, this.height);
|
||||
};
|
||||
|
||||
Canvas2DChart.prototype.setDimensions = function (newDimensions, newOrigin) {
|
||||
this.dimensions = newDimensions;
|
||||
this.origin = newOrigin;
|
||||
};
|
||||
|
||||
Canvas2DChart.prototype.drawLine = function (buf, color, points) {
|
||||
var i;
|
||||
|
||||
this.setColor(color);
|
||||
|
||||
// Configure context to draw two-pixel-thick lines
|
||||
this.c2d.lineWidth = 2;
|
||||
|
||||
// Start a new path...
|
||||
if (buf.length > 1) {
|
||||
this.c2d.beginPath();
|
||||
this.c2d.moveTo(this.x(buf[0]), this.y(buf[1]));
|
||||
}
|
||||
|
||||
// ...and add points to it...
|
||||
for (i = 2; i < points * 2; i = i + 2) {
|
||||
this.c2d.lineTo(this.x(buf[i]), this.y(buf[i + 1]));
|
||||
}
|
||||
|
||||
// ...before finally drawing it.
|
||||
this.c2d.stroke();
|
||||
};
|
||||
|
||||
Canvas2DChart.prototype.drawSquare = function (min, max, color) {
|
||||
var x1 = this.x(min[0]),
|
||||
y1 = this.y(min[1]),
|
||||
w = this.x(max[0]) - x1,
|
||||
h = this.y(max[1]) - y1;
|
||||
|
||||
this.setColor(color);
|
||||
this.c2d.fillRect(x1, y1, w, h);
|
||||
};
|
||||
|
||||
return Canvas2DChart;
|
||||
}
|
||||
);
|
||||
160
platform/features/timeline/src/chart/GLChart.js
Normal file
160
platform/features/timeline/src/chart/GLChart.js
Normal file
@@ -0,0 +1,160 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* Module defining GLPlot. Created by vwoeltje on 11/12/14.
|
||||
*/
|
||||
define(
|
||||
[],
|
||||
function () {
|
||||
|
||||
// WebGL shader sources (for drawing plain colors)
|
||||
var FRAGMENT_SHADER = [
|
||||
"precision mediump float;",
|
||||
"uniform vec4 uColor;",
|
||||
"void main(void) {",
|
||||
"gl_FragColor = uColor;",
|
||||
"}"
|
||||
].join('\n'),
|
||||
VERTEX_SHADER = [
|
||||
"attribute vec2 aVertexPosition;",
|
||||
"uniform vec2 uDimensions;",
|
||||
"uniform vec2 uOrigin;",
|
||||
"void main(void) {",
|
||||
"gl_Position = vec4(2.0 * ((aVertexPosition - uOrigin) / uDimensions) - vec2(1,1), 0, 1);",
|
||||
"}"
|
||||
].join('\n');
|
||||
|
||||
/**
|
||||
* Create a new chart which uses WebGL for rendering.
|
||||
*
|
||||
* @memberof platform/features/plot
|
||||
* @constructor
|
||||
* @implements {platform/features/plot.Chart}
|
||||
* @param {CanvasElement} canvas the canvas object to render upon
|
||||
* @throws {Error} an error is thrown if WebGL is unavailable.
|
||||
*/
|
||||
function GLChart(canvas) {
|
||||
var gl = canvas.getContext("webgl", { preserveDrawingBuffer: true }) ||
|
||||
canvas.getContext("experimental-webgl", { preserveDrawingBuffer: true }),
|
||||
vertexShader,
|
||||
fragmentShader,
|
||||
program,
|
||||
aVertexPosition,
|
||||
uColor,
|
||||
uDimensions,
|
||||
uOrigin;
|
||||
|
||||
// Ensure a context was actually available before proceeding
|
||||
if (!gl) {
|
||||
throw new Error("WebGL unavailable.");
|
||||
}
|
||||
|
||||
// Initialize shaders
|
||||
vertexShader = gl.createShader(gl.VERTEX_SHADER);
|
||||
gl.shaderSource(vertexShader, VERTEX_SHADER);
|
||||
gl.compileShader(vertexShader);
|
||||
fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
|
||||
gl.shaderSource(fragmentShader, FRAGMENT_SHADER);
|
||||
gl.compileShader(fragmentShader);
|
||||
|
||||
// Assemble vertex/fragment shaders into programs
|
||||
program = gl.createProgram();
|
||||
gl.attachShader(program, vertexShader);
|
||||
gl.attachShader(program, fragmentShader);
|
||||
gl.linkProgram(program);
|
||||
gl.useProgram(program);
|
||||
|
||||
// Get locations for attribs/uniforms from the
|
||||
// shader programs (to pass values into shaders at draw-time)
|
||||
aVertexPosition = gl.getAttribLocation(program, "aVertexPosition");
|
||||
uColor = gl.getUniformLocation(program, "uColor");
|
||||
uDimensions = gl.getUniformLocation(program, "uDimensions");
|
||||
uOrigin = gl.getUniformLocation(program, "uOrigin");
|
||||
gl.enableVertexAttribArray(aVertexPosition);
|
||||
|
||||
// Create a buffer to holds points which will be drawn
|
||||
this.buffer = gl.createBuffer();
|
||||
|
||||
// Use a line width of 2.0 for legibility
|
||||
gl.lineWidth(2.0);
|
||||
|
||||
// Enable blending, for smoothness
|
||||
gl.enable(gl.BLEND);
|
||||
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
this.gl = gl;
|
||||
this.aVertexPosition = aVertexPosition;
|
||||
this.uColor = uColor;
|
||||
this.uDimensions = uDimensions;
|
||||
this.uOrigin = uOrigin;
|
||||
}
|
||||
|
||||
// Utility function to handle drawing of a buffer;
|
||||
// drawType will determine whether this is a box, line, etc.
|
||||
GLChart.prototype.doDraw = function (drawType, buf, color, points) {
|
||||
var gl = this.gl;
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, buf, gl.DYNAMIC_DRAW);
|
||||
gl.vertexAttribPointer(this.aVertexPosition, 2, gl.FLOAT, false, 0, 0);
|
||||
gl.uniform4fv(this.uColor, color);
|
||||
gl.drawArrays(drawType, 0, points);
|
||||
};
|
||||
|
||||
GLChart.prototype.clear = function () {
|
||||
var gl = this.gl;
|
||||
|
||||
// Set the viewport size; note that we use the width/height
|
||||
// that our WebGL context reports, which may be lower
|
||||
// resolution than the canvas we requested.
|
||||
gl.viewport(
|
||||
0,
|
||||
0,
|
||||
gl.drawingBufferWidth,
|
||||
gl.drawingBufferHeight
|
||||
);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT + gl.DEPTH_BUFFER_BIT);
|
||||
};
|
||||
|
||||
|
||||
GLChart.prototype.setDimensions = function (dimensions, origin) {
|
||||
var gl = this.gl;
|
||||
if (dimensions && dimensions.length > 0 &&
|
||||
origin && origin.length > 0) {
|
||||
gl.uniform2fv(this.uDimensions, dimensions);
|
||||
gl.uniform2fv(this.uOrigin, origin);
|
||||
}
|
||||
};
|
||||
|
||||
GLChart.prototype.drawLine = function (buf, color, points) {
|
||||
this.doDraw(this.gl.LINE_STRIP, buf, color, points);
|
||||
};
|
||||
|
||||
GLChart.prototype.drawSquare = function (min, max, color) {
|
||||
this.doDraw(this.gl.TRIANGLE_FAN, new Float32Array(
|
||||
min.concat([min[0], max[1]]).concat(max).concat([max[0], min[1]])
|
||||
), color, 4);
|
||||
};
|
||||
|
||||
return GLChart;
|
||||
}
|
||||
);
|
||||
250
platform/features/timeline/src/chart/MCTTimelineChart.js
Normal file
250
platform/features/timeline/src/chart/MCTTimelineChart.js
Normal file
@@ -0,0 +1,250 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* Module defining MCTTimelineChart. Created by vwoeltje on 11/12/14.
|
||||
*/
|
||||
define(
|
||||
["./GLChart", "./Canvas2DChart"],
|
||||
function (GLChart, Canvas2DChart) {
|
||||
|
||||
var TEMPLATE = "<canvas style='position: absolute; background: none; width: 100%; height: 100%;'></canvas>";
|
||||
|
||||
/**
|
||||
* The mct-timeline-chart directive provides a canvas element which can be
|
||||
* drawn upon, to support Plot view and similar visualizations.
|
||||
*
|
||||
* This directive takes one attribute, "draw", which is an Angular
|
||||
* expression which will be two-way bound to a drawing object. This
|
||||
* drawing object should contain:
|
||||
*
|
||||
* * `dimensions`: An object describing the logical bounds of the
|
||||
* drawable area, containing two fields:
|
||||
* * `origin`: The position, in logical coordinates, of the
|
||||
* lower-left corner of the chart area. A two-element array.
|
||||
* * `dimensions`: A two-element array containing the width
|
||||
* and height of the chart area, in logical coordinates.
|
||||
* * `lines`: An array of lines to be drawn, where each line is
|
||||
* expressed as an object containing:
|
||||
* * `buffer`: A Float32Array containing points in the line,
|
||||
* in logical coordinate, in sequential x/y pairs.
|
||||
* * `color`: The color of the line, as a four-element RGBA
|
||||
* array, where each element is in the range of 0.0-1.0
|
||||
* * `points`: The number of points in the line.
|
||||
* * `boxes`: An array of rectangles to draw in the chart area
|
||||
* (used for marquee zoom). Each is an object containing:
|
||||
* * `start`: The first corner of the rectangle (as a two-element
|
||||
* array, logical coordinates)
|
||||
* * `end`: The opposite corner of the rectangle (again, as a
|
||||
* two-element array)
|
||||
* * `color`: The color of the box, as a four-element RGBA
|
||||
* array, where each element is in the range of 0.0-1.0
|
||||
*
|
||||
* @memberof platform/features/plot
|
||||
* @constructor
|
||||
*/
|
||||
function MCTTimelineChart($interval, $log) {
|
||||
// Get an underlying chart implementation
|
||||
function getChart(Charts, canvas) {
|
||||
// Try the first available option...
|
||||
var Chart = Charts[0];
|
||||
|
||||
// This function recursively try-catches all options;
|
||||
// if these all fail, issue a warning.
|
||||
if (!Chart) {
|
||||
$log.warn("Cannot initialize mct-timeline-chart.");
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Try first option; if it fails, try remaining options
|
||||
try {
|
||||
return new Chart(canvas);
|
||||
} catch (e) {
|
||||
$log.warn([
|
||||
"Could not instantiate chart",
|
||||
Chart.name,
|
||||
";",
|
||||
e.message
|
||||
].join(" "));
|
||||
|
||||
return getChart(Charts.slice(1), canvas);
|
||||
}
|
||||
}
|
||||
|
||||
function linkChart(scope, element) {
|
||||
var canvas = element.find("canvas")[0],
|
||||
activeInterval,
|
||||
chart;
|
||||
|
||||
// Handle drawing, based on contents of the "draw" object
|
||||
// in scope
|
||||
function doDraw(draw) {
|
||||
// Ensure canvas context has same resolution
|
||||
// as canvas element
|
||||
canvas.width = canvas.offsetWidth;
|
||||
canvas.height = canvas.offsetHeight;
|
||||
|
||||
// Clear previous contents
|
||||
chart.clear();
|
||||
|
||||
// Nothing to draw if no draw object defined
|
||||
if (!draw) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set logical boundaries for the chart
|
||||
chart.setDimensions(
|
||||
draw.dimensions || [1, 1],
|
||||
draw.origin || [0, 0]
|
||||
);
|
||||
|
||||
// Draw line segments
|
||||
(draw.lines || []).forEach(function (line) {
|
||||
chart.drawLine(
|
||||
line.buffer,
|
||||
line.color,
|
||||
line.points
|
||||
);
|
||||
});
|
||||
|
||||
// Draw boxes (e.g. marquee zoom rect)
|
||||
(draw.boxes || []).forEach(function (box) {
|
||||
chart.drawSquare(
|
||||
box.start,
|
||||
box.end,
|
||||
box.color
|
||||
);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
// Issue a drawing call, if-and-only-if canvas size
|
||||
// has changed. This will be called on a timer, since
|
||||
// there is no event to depend on.
|
||||
function drawIfResized() {
|
||||
if (canvas.width !== canvas.offsetWidth ||
|
||||
canvas.height !== canvas.offsetHeight) {
|
||||
doDraw(scope.draw);
|
||||
scope.$apply();
|
||||
}
|
||||
}
|
||||
|
||||
// Stop watching for changes to size (scope destroyed)
|
||||
function releaseInterval() {
|
||||
if (activeInterval) {
|
||||
$interval.cancel(activeInterval);
|
||||
}
|
||||
}
|
||||
|
||||
// Switch from WebGL to plain 2D if context is lost
|
||||
function fallbackFromWebGL() {
|
||||
element.html(TEMPLATE);
|
||||
canvas = element.find("canvas")[0];
|
||||
chart = getChart([Canvas2DChart], canvas);
|
||||
if (chart) {
|
||||
doDraw(scope.draw);
|
||||
}
|
||||
}
|
||||
|
||||
// Try to initialize a chart.
|
||||
chart = getChart([GLChart, Canvas2DChart], canvas);
|
||||
|
||||
// If that failed, there's nothing more we can do here.
|
||||
// (A warning will already have been issued)
|
||||
if (!chart) {
|
||||
return;
|
||||
}
|
||||
|
||||
// WebGL is a bit of a special case; it may work, then fail
|
||||
// later for various reasons, so we need to listen for this
|
||||
// and fall back to plain canvas drawing when it occurs.
|
||||
canvas.addEventListener("webglcontextlost", fallbackFromWebGL);
|
||||
|
||||
// Check for resize, on a timer
|
||||
activeInterval = $interval(drawIfResized, 1000, 0, false);
|
||||
|
||||
// Watch "draw" for external changes to the set of
|
||||
// things to be drawn.
|
||||
scope.$watchCollection("draw", doDraw);
|
||||
|
||||
// Stop checking for resize when scope is destroyed
|
||||
scope.$on("$destroy", releaseInterval);
|
||||
}
|
||||
|
||||
return {
|
||||
// Apply directive only to elements
|
||||
restrict: "E",
|
||||
|
||||
// Template to use (a canvas element)
|
||||
template: TEMPLATE,
|
||||
|
||||
// Link function; set up scope
|
||||
link: linkChart,
|
||||
|
||||
// Initial, isolate scope for the directive
|
||||
scope: { draw: "=" }
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @interface platform/features/plot.Chart
|
||||
* @private
|
||||
*/
|
||||
|
||||
/**
|
||||
* Clear the chart.
|
||||
* @method platform/features/plot.Chart#clear
|
||||
*/
|
||||
/**
|
||||
* Set the logical boundaries of the chart.
|
||||
* @param {number[]} dimensions the horizontal and
|
||||
* vertical dimensions of the chart
|
||||
* @param {number[]} origin the horizontal/vertical
|
||||
* origin of the chart
|
||||
* @memberof platform/features/plot.Chart#setDimensions
|
||||
*/
|
||||
/**
|
||||
* Draw the supplied buffer as a line strip (a sequence
|
||||
* of line segments), in the chosen color.
|
||||
* @param {Float32Array} buf the line strip to draw,
|
||||
* in alternating x/y positions
|
||||
* @param {number[]} color the color to use when drawing
|
||||
* the line, as an RGBA color where each element
|
||||
* is in the range of 0.0-1.0
|
||||
* @param {number} points the number of points to draw
|
||||
* @memberof platform/features/plot.Chart#drawLine
|
||||
*/
|
||||
/**
|
||||
* Draw a rectangle extending from one corner to another,
|
||||
* in the chosen color.
|
||||
* @param {number[]} min the first corner of the rectangle
|
||||
* @param {number[]} max the opposite corner
|
||||
* @param {number[]} color the color to use when drawing
|
||||
* the rectangle, as an RGBA color where each element
|
||||
* is in the range of 0.0-1.0
|
||||
* @memberof platform/features/plot.Chart#drawSquare
|
||||
*/
|
||||
|
||||
return MCTTimelineChart;
|
||||
}
|
||||
);
|
||||
|
||||
@@ -167,7 +167,7 @@ define(
|
||||
*/
|
||||
setBounds: function (offset, duration) {
|
||||
// We don't update in-place, because we need the change
|
||||
// to trigger a watch in mct-chart.
|
||||
// to trigger a watch in mct-timeline-chart.
|
||||
drawingObject.origin = [offset, drawingObject.origin[1]];
|
||||
drawingObject.dimensions = [duration, drawingObject.dimensions[1]];
|
||||
},
|
||||
|
||||
@@ -26,7 +26,7 @@ define(
|
||||
|
||||
/**
|
||||
* Responsible for preparing data for display by
|
||||
* `mct-chart` in a timeline's resource graph.
|
||||
* `mct-timeline-chart` in a timeline's resource graph.
|
||||
* @constructor
|
||||
*/
|
||||
function TimelineGraphRenderer() {
|
||||
@@ -54,7 +54,7 @@ define(
|
||||
* Convert an HTML color (in #-prefixed 6-digit hexadecimal)
|
||||
* to an array of floating point values in a range of 0.0-1.0.
|
||||
* An alpha element is included to facilitate display in an
|
||||
* `mct-chart` (which uses WebGL.)
|
||||
* `mct-timeline-chart` (which uses WebGL.)
|
||||
* @param {string} the color
|
||||
* @returns {number[]} the same color, in floating-point format
|
||||
*/
|
||||
|
||||
95
platform/features/timeline/test/chart/Canvas2DChartSpec.js
Normal file
95
platform/features/timeline/test/chart/Canvas2DChartSpec.js
Normal file
@@ -0,0 +1,95 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* MergeModelsSpec. Created by vwoeltje on 11/6/14.
|
||||
*/
|
||||
define(
|
||||
["../../src/chart/Canvas2DChart"],
|
||||
function (Canvas2DChart) {
|
||||
|
||||
describe("A canvas 2d chart", function () {
|
||||
var mockCanvas,
|
||||
mock2d,
|
||||
chart;
|
||||
|
||||
beforeEach(function () {
|
||||
mockCanvas = jasmine.createSpyObj("canvas", ["getContext"]);
|
||||
mock2d = jasmine.createSpyObj(
|
||||
"2d",
|
||||
[
|
||||
"clearRect",
|
||||
"beginPath",
|
||||
"moveTo",
|
||||
"lineTo",
|
||||
"stroke",
|
||||
"fillRect"
|
||||
]
|
||||
);
|
||||
mockCanvas.getContext.andReturn(mock2d);
|
||||
|
||||
chart = new Canvas2DChart(mockCanvas);
|
||||
});
|
||||
|
||||
// Note that tests below are less specific than they
|
||||
// could be, esp. w.r.t. arguments to drawing calls;
|
||||
// this is a fallback option so is a lower test priority.
|
||||
|
||||
it("allows the canvas to be cleared", function () {
|
||||
chart.clear();
|
||||
expect(mock2d.clearRect).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not construct if 2D is unavailable", function () {
|
||||
mockCanvas.getContext.andReturn(undefined);
|
||||
expect(function () {
|
||||
return new Canvas2DChart(mockCanvas);
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it("allows dimensions to be set", function () {
|
||||
// No return value, just verify API is present
|
||||
chart.setDimensions([120, 120], [0, 10]);
|
||||
});
|
||||
|
||||
it("allows lines to be drawn", function () {
|
||||
var testBuffer = [0, 1, 3, 8],
|
||||
testColor = [0.25, 0.33, 0.66, 1.0],
|
||||
testPoints = 2;
|
||||
chart.drawLine(testBuffer, testColor, testPoints);
|
||||
expect(mock2d.beginPath).toHaveBeenCalled();
|
||||
expect(mock2d.lineTo.calls.length).toEqual(1);
|
||||
expect(mock2d.stroke).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("allows squares to be drawn", function () {
|
||||
var testMin = [0, 1],
|
||||
testMax = [10, 10],
|
||||
testColor = [0.25, 0.33, 0.66, 1.0];
|
||||
|
||||
chart.drawSquare(testMin, testMax, testColor);
|
||||
expect(mock2d.fillRect).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
||||
143
platform/features/timeline/test/chart/GLChartSpec.js
Normal file
143
platform/features/timeline/test/chart/GLChartSpec.js
Normal file
@@ -0,0 +1,143 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* MergeModelsSpec. Created by vwoeltje on 11/6/14.
|
||||
*/
|
||||
define(
|
||||
["../../src/chart/GLChart"],
|
||||
function (GLChart) {
|
||||
|
||||
describe("A WebGL chart", function () {
|
||||
var mockCanvas,
|
||||
mockGL,
|
||||
glChart;
|
||||
|
||||
beforeEach(function () {
|
||||
mockCanvas = jasmine.createSpyObj("canvas", ["getContext"]);
|
||||
mockGL = jasmine.createSpyObj(
|
||||
"gl",
|
||||
[
|
||||
"createShader",
|
||||
"compileShader",
|
||||
"shaderSource",
|
||||
"attachShader",
|
||||
"createProgram",
|
||||
"linkProgram",
|
||||
"useProgram",
|
||||
"enableVertexAttribArray",
|
||||
"getAttribLocation",
|
||||
"getUniformLocation",
|
||||
"createBuffer",
|
||||
"lineWidth",
|
||||
"enable",
|
||||
"blendFunc",
|
||||
"viewport",
|
||||
"clear",
|
||||
"uniform2fv",
|
||||
"uniform4fv",
|
||||
"bufferData",
|
||||
"bindBuffer",
|
||||
"vertexAttribPointer",
|
||||
"drawArrays"
|
||||
]
|
||||
);
|
||||
mockGL.ARRAY_BUFFER = "ARRAY_BUFFER";
|
||||
mockGL.DYNAMIC_DRAW = "DYNAMIC_DRAW";
|
||||
mockGL.TRIANGLE_FAN = "TRIANGLE_FAN";
|
||||
mockGL.LINE_STRIP = "LINE_STRIP";
|
||||
|
||||
// Echo back names for uniform locations, so we can
|
||||
// test which of these are set for certain operations.
|
||||
mockGL.getUniformLocation.andCallFake(function (a, name) {
|
||||
return name;
|
||||
});
|
||||
|
||||
mockCanvas.getContext.andReturn(mockGL);
|
||||
|
||||
glChart = new GLChart(mockCanvas);
|
||||
});
|
||||
|
||||
it("allows the canvas to be cleared", function () {
|
||||
glChart.clear();
|
||||
expect(mockGL.clear).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not construct if WebGL is unavailable", function () {
|
||||
mockCanvas.getContext.andReturn(undefined);
|
||||
expect(function () {
|
||||
return new GLChart(mockCanvas);
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it("allows dimensions to be set", function () {
|
||||
glChart.setDimensions([120, 120], [0, 10]);
|
||||
expect(mockGL.uniform2fv)
|
||||
.toHaveBeenCalledWith("uDimensions", [120, 120]);
|
||||
expect(mockGL.uniform2fv)
|
||||
.toHaveBeenCalledWith("uOrigin", [0, 10]);
|
||||
});
|
||||
|
||||
it("allows lines to be drawn", function () {
|
||||
var testBuffer = [0, 1, 3, 8],
|
||||
testColor = [0.25, 0.33, 0.66, 1.0],
|
||||
testPoints = 2;
|
||||
glChart.drawLine(testBuffer, testColor, testPoints);
|
||||
expect(mockGL.bufferData).toHaveBeenCalledWith(
|
||||
mockGL.ARRAY_BUFFER,
|
||||
testBuffer,
|
||||
mockGL.DYNAMIC_DRAW
|
||||
);
|
||||
expect(mockGL.uniform4fv)
|
||||
.toHaveBeenCalledWith("uColor", testColor);
|
||||
expect(mockGL.drawArrays)
|
||||
.toHaveBeenCalledWith("LINE_STRIP", 0, testPoints);
|
||||
});
|
||||
|
||||
it("allows squares to be drawn", function () {
|
||||
var testMin = [0, 1],
|
||||
testMax = [10, 10],
|
||||
testColor = [0.25, 0.33, 0.66, 1.0];
|
||||
|
||||
glChart.drawSquare(testMin, testMax, testColor);
|
||||
|
||||
expect(mockGL.uniform4fv)
|
||||
.toHaveBeenCalledWith("uColor", testColor);
|
||||
expect(mockGL.drawArrays)
|
||||
.toHaveBeenCalledWith("TRIANGLE_FAN", 0, 4);
|
||||
});
|
||||
|
||||
it("uses buffer sizes reported by WebGL", function () {
|
||||
// Make sure that GLChart uses the GL buffer size, which may
|
||||
// differ from what canvas requested. WTD-852
|
||||
mockCanvas.width = 300;
|
||||
mockCanvas.height = 150;
|
||||
mockGL.drawingBufferWidth = 200;
|
||||
mockGL.drawingBufferHeight = 175;
|
||||
|
||||
glChart.clear();
|
||||
|
||||
expect(mockGL.viewport).toHaveBeenCalledWith(0, 0, 200, 175);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
216
platform/features/timeline/test/chart/MCTTimelineChartSpec.js
Normal file
216
platform/features/timeline/test/chart/MCTTimelineChartSpec.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.
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* MCTTimelineChart. Created by vwoeltje on 11/6/14.
|
||||
*/
|
||||
define(
|
||||
["../../src/chart/MCTTimelineChart"],
|
||||
function (MCTTimelineChart) {
|
||||
|
||||
describe("The mct-timeline-chart directive", function () {
|
||||
var mockInterval,
|
||||
mockLog,
|
||||
mockScope,
|
||||
mockElement,
|
||||
mockCanvas,
|
||||
mockGL,
|
||||
mockC2d,
|
||||
mockPromise,
|
||||
mctChart;
|
||||
|
||||
beforeEach(function () {
|
||||
mockInterval =
|
||||
jasmine.createSpy("$interval");
|
||||
mockLog =
|
||||
jasmine.createSpyObj("$log", ["warn", "info", "debug"]);
|
||||
mockScope = jasmine.createSpyObj(
|
||||
"$scope",
|
||||
["$watchCollection", "$on", "$apply"]
|
||||
);
|
||||
mockElement =
|
||||
jasmine.createSpyObj("element", ["find", "html"]);
|
||||
mockInterval.cancel = jasmine.createSpy("cancelInterval");
|
||||
mockPromise = jasmine.createSpyObj("promise", ["then"]);
|
||||
|
||||
|
||||
// mct-timeline-chart uses GLChart, so it needs WebGL API
|
||||
mockCanvas =
|
||||
jasmine.createSpyObj("canvas", ["getContext", "addEventListener"]);
|
||||
mockGL = jasmine.createSpyObj(
|
||||
"gl",
|
||||
[
|
||||
"createShader",
|
||||
"compileShader",
|
||||
"shaderSource",
|
||||
"attachShader",
|
||||
"createProgram",
|
||||
"linkProgram",
|
||||
"useProgram",
|
||||
"enableVertexAttribArray",
|
||||
"getAttribLocation",
|
||||
"getUniformLocation",
|
||||
"createBuffer",
|
||||
"lineWidth",
|
||||
"enable",
|
||||
"blendFunc",
|
||||
"viewport",
|
||||
"clear",
|
||||
"uniform2fv",
|
||||
"uniform4fv",
|
||||
"bufferData",
|
||||
"bindBuffer",
|
||||
"vertexAttribPointer",
|
||||
"drawArrays"
|
||||
]
|
||||
);
|
||||
mockC2d = jasmine.createSpyObj('c2d', ['clearRect']);
|
||||
mockGL.ARRAY_BUFFER = "ARRAY_BUFFER";
|
||||
mockGL.DYNAMIC_DRAW = "DYNAMIC_DRAW";
|
||||
mockGL.TRIANGLE_FAN = "TRIANGLE_FAN";
|
||||
mockGL.LINE_STRIP = "LINE_STRIP";
|
||||
|
||||
// Echo back names for uniform locations, so we can
|
||||
// test which of these are set for certain operations.
|
||||
mockGL.getUniformLocation.andCallFake(function (a, name) {
|
||||
return name;
|
||||
});
|
||||
|
||||
mockElement.find.andReturn([mockCanvas]);
|
||||
mockCanvas.getContext.andCallFake(function (type) {
|
||||
return { webgl: mockGL, '2d': mockC2d }[type];
|
||||
});
|
||||
mockInterval.andReturn(mockPromise);
|
||||
|
||||
mctChart = new MCTTimelineChart(mockInterval, mockLog);
|
||||
});
|
||||
|
||||
it("is applicable at the element level", function () {
|
||||
expect(mctChart.restrict).toEqual("E");
|
||||
});
|
||||
|
||||
it("places a 'draw' attribute in-scope", function () {
|
||||
// Should ask Angular for the draw attribute
|
||||
expect(mctChart.scope.draw).toEqual("=");
|
||||
});
|
||||
|
||||
it("watches for changes in the drawn object", function () {
|
||||
mctChart.link(mockScope, mockElement);
|
||||
expect(mockScope.$watchCollection)
|
||||
.toHaveBeenCalledWith("draw", jasmine.any(Function));
|
||||
});
|
||||
|
||||
it("issues one draw call per line", function () {
|
||||
mctChart.link(mockScope, mockElement);
|
||||
mockScope.$watchCollection.mostRecentCall.args[1]({
|
||||
lines: [{}, {}, {}]
|
||||
});
|
||||
expect(mockGL.drawArrays.calls.length).toEqual(3);
|
||||
});
|
||||
|
||||
it("issues one draw call per box", function () {
|
||||
mctChart.link(mockScope, mockElement);
|
||||
mockScope.$watchCollection.mostRecentCall.args[1]({
|
||||
boxes: [
|
||||
{ start: [0, 0], end: [1, 1] },
|
||||
{ start: [0, 0], end: [1, 1] },
|
||||
{ start: [0, 0], end: [1, 1] },
|
||||
{ start: [0, 0], end: [1, 1] }
|
||||
]
|
||||
});
|
||||
expect(mockGL.drawArrays.calls.length).toEqual(4);
|
||||
});
|
||||
|
||||
it("does not fail if no draw object is in scope", function () {
|
||||
mctChart.link(mockScope, mockElement);
|
||||
expect(mockScope.$watchCollection.mostRecentCall.args[1])
|
||||
.not.toThrow();
|
||||
});
|
||||
|
||||
it("draws on canvas resize", function () {
|
||||
mctChart.link(mockScope, mockElement);
|
||||
|
||||
// Should track canvas size in an interval
|
||||
expect(mockInterval).toHaveBeenCalledWith(
|
||||
jasmine.any(Function),
|
||||
jasmine.any(Number),
|
||||
0,
|
||||
false
|
||||
);
|
||||
|
||||
// Verify pre-condition
|
||||
expect(mockGL.clear).not.toHaveBeenCalled();
|
||||
|
||||
mockCanvas.width = 100;
|
||||
mockCanvas.offsetWidth = 150;
|
||||
mockCanvas.height = 200;
|
||||
mockCanvas.offsetHeight = 200;
|
||||
mockInterval.mostRecentCall.args[0]();
|
||||
|
||||
// Use clear as an indication that drawing has occurred
|
||||
expect(mockGL.clear).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("warns if no WebGL context is available", function () {
|
||||
mockCanvas.getContext.andReturn(undefined);
|
||||
mctChart.link(mockScope, mockElement);
|
||||
expect(mockLog.warn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("falls back to Canvas 2d API if WebGL context is lost", function () {
|
||||
mctChart.link(mockScope, mockElement);
|
||||
expect(mockCanvas.addEventListener)
|
||||
.toHaveBeenCalledWith("webglcontextlost", jasmine.any(Function));
|
||||
expect(mockCanvas.getContext).not.toHaveBeenCalledWith('2d');
|
||||
mockCanvas.addEventListener.mostRecentCall.args[1]();
|
||||
expect(mockCanvas.getContext).toHaveBeenCalledWith('2d');
|
||||
});
|
||||
|
||||
it("logs nothing in nominal situations (WebGL available)", function () {
|
||||
// Complement the previous test
|
||||
mctChart.link(mockScope, mockElement);
|
||||
expect(mockLog.warn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Avoid resource leaks
|
||||
it("stops polling for size changes on destroy", function () {
|
||||
mctChart.link(mockScope, mockElement);
|
||||
|
||||
// Should be listening for a destroy event
|
||||
expect(mockScope.$on).toHaveBeenCalledWith(
|
||||
"$destroy",
|
||||
jasmine.any(Function)
|
||||
);
|
||||
|
||||
// Precondition - interval still active
|
||||
expect(mockInterval.cancel).not.toHaveBeenCalled();
|
||||
|
||||
// Broadcast a $destroy
|
||||
mockScope.$on.mostRecentCall.args[1]();
|
||||
|
||||
// Should have stopped the interval
|
||||
expect(mockInterval.cancel).toHaveBeenCalledWith(mockPromise);
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -122,9 +122,12 @@ define(
|
||||
*/
|
||||
self.getDatum = function (telemetryObject, index) {
|
||||
function makeNewDatum(series) {
|
||||
return series ?
|
||||
subscription.makeDatum(telemetryObject, series, index) :
|
||||
undefined;
|
||||
if (series) {
|
||||
if (series.getDatum) {
|
||||
return series.getDatum(index);
|
||||
}
|
||||
return subscription.makeDatum(telemetryObject, series, index);
|
||||
}
|
||||
}
|
||||
|
||||
return typeof index !== 'number' ?
|
||||
|
||||
@@ -25,18 +25,18 @@ define([
|
||||
'legacyRegistry',
|
||||
'uuid',
|
||||
'./api/api',
|
||||
'text!./adapter/templates/edit-object-replacement.html',
|
||||
'./selection/Selection',
|
||||
'./api/objects/object-utils',
|
||||
'./plugins/plugins',
|
||||
'./ui/ViewRegistry'
|
||||
], function (
|
||||
EventEmitter,
|
||||
legacyRegistry,
|
||||
uuid,
|
||||
api,
|
||||
editObjectTemplate,
|
||||
Selection,
|
||||
objectUtils,
|
||||
plugins,
|
||||
ViewRegistry
|
||||
) {
|
||||
/**
|
||||
@@ -192,7 +192,7 @@ define([
|
||||
* @memberof module:openmct.MCT#
|
||||
* @name telemetry
|
||||
*/
|
||||
this.telemetry = new api.TelemetryAPI();
|
||||
this.telemetry = new api.TelemetryAPI(this);
|
||||
|
||||
this.TimeConductor = this.conductor; // compatibility for prototype
|
||||
this.on('navigation', this.selection.clear.bind(this.selection));
|
||||
@@ -280,5 +280,7 @@ define([
|
||||
plugin(this);
|
||||
};
|
||||
|
||||
MCT.prototype.plugins = plugins;
|
||||
|
||||
return MCT;
|
||||
});
|
||||
|
||||
@@ -29,9 +29,8 @@ define([], function () {
|
||||
}
|
||||
|
||||
var domainObject = legacyObject.useCapability('adapter');
|
||||
var context = { item: domainObject };
|
||||
var providers = openmct.mainViews.get(context);
|
||||
$scope.view = providers[0] && providers[0].view(context);
|
||||
var providers = openmct.mainViews.get(domainObject);
|
||||
$scope.view = providers[0] && providers[0].view(domainObject);
|
||||
}
|
||||
|
||||
$scope.$watch('domainObject', refresh);
|
||||
|
||||
@@ -31,8 +31,7 @@ define([], function () {
|
||||
) {
|
||||
if (view.key === 'adapted-view') {
|
||||
var domainObject = legacyObject.useCapability('adapter');
|
||||
var context = { item: domainObject };
|
||||
return this.openmct.mainViews.get(context).length > 0;
|
||||
return this.openmct.mainViews.get(domainObject).length > 0;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
<div class="abs l-flex-col" ng-controller="EditObjectController as EditObjectController">
|
||||
<div mct-before-unload="EditObjectController.getUnloadWarning()"
|
||||
class="holder flex-elem l-flex-row object-browse-bar ">
|
||||
<div class="items-select left flex-elem l-flex-row grows">
|
||||
<mct-representation key="'back-arrow'"
|
||||
mct-object="domainObject"
|
||||
class="flex-elem l-back"></mct-representation>
|
||||
<mct-representation key="'object-header'"
|
||||
mct-object="domainObject"
|
||||
class="l-flex-row flex-elem grows object-header">
|
||||
</mct-representation>
|
||||
</div>
|
||||
<div class="btn-bar right l-flex-row flex-elem flex-justify-end flex-fixed">
|
||||
<mct-representation key="'switcher'"
|
||||
mct-object="domainObject"
|
||||
ng-model="representation">
|
||||
</mct-representation>
|
||||
<!-- Temporarily, on mobile, the action buttons are hidden-->
|
||||
<mct-representation key="'action-group'"
|
||||
mct-object="domainObject"
|
||||
parameters="{ category: 'view-control' }"
|
||||
class="mobile-hide">
|
||||
</mct-representation>
|
||||
</div>
|
||||
</div>
|
||||
<div class="holder l-flex-col flex-elem grows l-object-wrapper">
|
||||
<div class="holder l-flex-col flex-elem grows l-object-wrapper-inner">
|
||||
<!-- Toolbar and Save/Cancel buttons -->
|
||||
<div class="l-edit-controls flex-elem l-flex-row flex-align-end">
|
||||
<mct-representation key="'adapted-view-TOOLBAR'"
|
||||
mct-object="domainObject"
|
||||
class="flex-elem grows">
|
||||
</mct-representation>
|
||||
<mct-representation key="'edit-action-buttons'"
|
||||
mct-object="domainObject"
|
||||
class='flex-elem conclude-editing'>
|
||||
</mct-representation>
|
||||
</div>
|
||||
<mct-representation key="representation.selected.key"
|
||||
mct-object="representation.selected.key && domainObject"
|
||||
class="abs flex-elem grows object-holder-main scroll"
|
||||
toolbar="toolbar">
|
||||
</mct-representation>
|
||||
</div><!--/ l-object-wrapper-inner -->
|
||||
</div>
|
||||
</div>
|
||||
@@ -21,13 +21,12 @@
|
||||
*****************************************************************************/
|
||||
|
||||
define([
|
||||
'./object-utils',
|
||||
'./objectEventEmitter'
|
||||
'./object-utils'
|
||||
], function (
|
||||
utils,
|
||||
objectEventEmitter
|
||||
utils
|
||||
) {
|
||||
function ObjectServiceProvider(objectService, instantiate, topic) {
|
||||
function ObjectServiceProvider(eventEmitter, objectService, instantiate, topic) {
|
||||
this.eventEmitter = eventEmitter;
|
||||
this.objectService = objectService;
|
||||
this.instantiate = instantiate;
|
||||
|
||||
@@ -61,12 +60,12 @@ define([
|
||||
var newStyleObject = utils.toNewFormat(legacyObject.getModel(), legacyObject.getId());
|
||||
|
||||
//Don't trigger self
|
||||
objectEventEmitter.off('mutation', handleMutation);
|
||||
objectEventEmitter.emit(newStyleObject.identifier.key + ":*", newStyleObject);
|
||||
objectEventEmitter.on('mutation', handleMutation);
|
||||
this.eventEmitter.off('mutation', handleMutation);
|
||||
this.eventEmitter.emit(newStyleObject.identifier.key + ":*", newStyleObject);
|
||||
this.eventEmitter.on('mutation', handleMutation);
|
||||
}.bind(this);
|
||||
|
||||
objectEventEmitter.on('mutation', handleMutation);
|
||||
this.eventEmitter.on('mutation', handleMutation);
|
||||
removeGeneralTopicListener = this.generalTopic.listen(handleLegacyMutation);
|
||||
};
|
||||
|
||||
@@ -96,6 +95,8 @@ define([
|
||||
// Injects new object API as a decorator so that it hijacks all requests.
|
||||
// Object providers implemented on new API should just work, old API should just work, many things may break.
|
||||
function LegacyObjectAPIInterceptor(openmct, ROOTS, instantiate, topic, objectService) {
|
||||
var eventEmitter = openmct.objects.eventEmitter;
|
||||
|
||||
this.getObjects = function (keys) {
|
||||
var results = {},
|
||||
promises = keys.map(function (keyString) {
|
||||
@@ -114,7 +115,12 @@ define([
|
||||
};
|
||||
|
||||
openmct.objects.supersecretSetFallbackProvider(
|
||||
new ObjectServiceProvider(objectService, instantiate, topic)
|
||||
new ObjectServiceProvider(
|
||||
eventEmitter,
|
||||
objectService,
|
||||
instantiate,
|
||||
topic
|
||||
)
|
||||
);
|
||||
|
||||
ROOTS.forEach(function (r) {
|
||||
|
||||
@@ -21,11 +21,9 @@
|
||||
*****************************************************************************/
|
||||
|
||||
define([
|
||||
'lodash',
|
||||
'./objectEventEmitter'
|
||||
'lodash'
|
||||
], function (
|
||||
_,
|
||||
objectEventEmitter
|
||||
_
|
||||
) {
|
||||
var ANY_OBJECT_EVENT = "mutation";
|
||||
|
||||
@@ -36,7 +34,8 @@ define([
|
||||
* @param object
|
||||
* @interface MutableObject
|
||||
*/
|
||||
function MutableObject(object) {
|
||||
function MutableObject(eventEmitter, object) {
|
||||
this.eventEmitter = eventEmitter;
|
||||
this.object = object;
|
||||
this.unlisteners = [];
|
||||
}
|
||||
@@ -61,8 +60,11 @@ define([
|
||||
*/
|
||||
MutableObject.prototype.on = function (path, callback) {
|
||||
var fullPath = qualifiedEventName(this.object, path);
|
||||
objectEventEmitter.on(fullPath, callback);
|
||||
this.unlisteners.push(objectEventEmitter.off.bind(objectEventEmitter, fullPath, callback));
|
||||
var eventOff =
|
||||
this.eventEmitter.off.bind(this.eventEmitter, fullPath, callback);
|
||||
|
||||
this.eventEmitter.on(fullPath, callback);
|
||||
this.unlisteners.push(eventOff);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -78,12 +80,12 @@ define([
|
||||
_.set(this.object, 'modified', Date.now());
|
||||
|
||||
//Emit event specific to property
|
||||
objectEventEmitter.emit(qualifiedEventName(this.object, path), value);
|
||||
this.eventEmitter.emit(qualifiedEventName(this.object, path), value);
|
||||
//Emit wildcare event
|
||||
objectEventEmitter.emit(qualifiedEventName(this.object, '*'), this.object);
|
||||
this.eventEmitter.emit(qualifiedEventName(this.object, '*'), this.object);
|
||||
|
||||
//Emit a general "any object" event
|
||||
objectEventEmitter.emit(ANY_OBJECT_EVENT, this.object);
|
||||
this.eventEmitter.emit(ANY_OBJECT_EVENT, this.object);
|
||||
};
|
||||
|
||||
return MutableObject;
|
||||
|
||||
@@ -25,13 +25,15 @@ define([
|
||||
'./object-utils',
|
||||
'./MutableObject',
|
||||
'./RootRegistry',
|
||||
'./RootObjectProvider'
|
||||
'./RootObjectProvider',
|
||||
'EventEmitter'
|
||||
], function (
|
||||
_,
|
||||
utils,
|
||||
MutableObject,
|
||||
RootRegistry,
|
||||
RootObjectProvider
|
||||
RootObjectProvider,
|
||||
EventEmitter
|
||||
) {
|
||||
|
||||
|
||||
@@ -42,6 +44,7 @@ define([
|
||||
*/
|
||||
|
||||
function ObjectAPI() {
|
||||
this.eventEmitter = new EventEmitter();
|
||||
this.providers = {};
|
||||
this.rootRegistry = new RootRegistry();
|
||||
this.rootProvider = new RootObjectProvider(this.rootRegistry);
|
||||
@@ -175,7 +178,9 @@ define([
|
||||
* @memberof module:openmct.ObjectAPI#
|
||||
*/
|
||||
ObjectAPI.prototype.mutate = function (domainObject, path, value) {
|
||||
return new MutableObject(domainObject).set(path, value);
|
||||
var mutableObject =
|
||||
new MutableObject(this.eventEmitter, domainObject);
|
||||
return mutableObject.set(path, value);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -188,7 +193,8 @@ define([
|
||||
* @memberof module:openmct.ObjectAPI#
|
||||
*/
|
||||
ObjectAPI.prototype.observe = function (domainObject, path, callback) {
|
||||
var mutableObject = new MutableObject(domainObject);
|
||||
var mutableObject =
|
||||
new MutableObject(this.eventEmitter, domainObject);
|
||||
mutableObject.on(path, callback);
|
||||
return mutableObject.stopListening.bind(mutableObject);
|
||||
};
|
||||
|
||||
151
src/api/telemetry/LegacyTelemetryProvider.js
Normal file
151
src/api/telemetry/LegacyTelemetryProvider.js
Normal file
@@ -0,0 +1,151 @@
|
||||
/*****************************************************************************
|
||||
* 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([
|
||||
'../objects/object-utils'
|
||||
], function (
|
||||
utils
|
||||
) {
|
||||
/**
|
||||
* @implements module:openmct.TelemetryAPI~TelemetryProvider
|
||||
* @constructor
|
||||
*/
|
||||
function LegacyTelemetryProvider(instantiate) {
|
||||
this.instantiate = instantiate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Can provide telemetry for all objects that have the "telemetry"
|
||||
* capability
|
||||
*
|
||||
* @see module:openmct.TelemetryAPI~TelemetryProvider#canProvideTelemetry
|
||||
*/
|
||||
LegacyTelemetryProvider.prototype.canProvideTelemetry = function (domainObject) {
|
||||
return this.instantiate(utils.toOldFormat(domainObject),
|
||||
utils.makeKeyString(domainObject.identifier)).hasCapability("telemetry");
|
||||
};
|
||||
|
||||
function createDatum(domainObject, metadata, legacySeries, i) {
|
||||
if (legacySeries.getDatum) {
|
||||
return legacySeries.getDatum(i);
|
||||
}
|
||||
var datum = {};
|
||||
|
||||
metadata.domains.reduce(function (d, domain) {
|
||||
d[domain.key] = legacySeries.getDomainValue(i, domain.key);
|
||||
return d;
|
||||
}, datum);
|
||||
|
||||
metadata.ranges.reduce(function (d, range) {
|
||||
d[range.key] = legacySeries.getRangeValue(i, range.key);
|
||||
return d;
|
||||
}, datum);
|
||||
|
||||
datum.name = domainObject.name;
|
||||
|
||||
return datum;
|
||||
}
|
||||
|
||||
function adaptSeries(domainObject, metadata, legacySeries) {
|
||||
var series = [];
|
||||
|
||||
for (var i = 0; i < legacySeries.getPointCount(); i++) {
|
||||
series.push(createDatum(domainObject, metadata, legacySeries, i));
|
||||
}
|
||||
|
||||
return series;
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {object} ConvertedTelemetryObject
|
||||
* Telemetry data objects are converted from TelemetrySeries. Metadata is used
|
||||
* to populate the returned object with attributes corresponding to the keys
|
||||
* of domains and ranges. The attribute values are those returned by calls to
|
||||
* [TelemetrySeries.getDomainValue()]{@link TelemetrySeries#getDomainValue}
|
||||
* and [TelemetrySeries.getRangeValue()]{@link TelemetrySeries#getRangeValue}.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @see module:openmct.TelemetryAPI~TelemetryProvider#request
|
||||
* @param {module:openmct.DomainObject}
|
||||
* @param {module:openmct.TelemetryAPI~TelemetryRequest} options
|
||||
* options for this request. Passed straight through to legacy provider
|
||||
* @returns {Promise.<ConvertedTelemetryObject[]>} a promise for an array of
|
||||
* telemetry data.
|
||||
*/
|
||||
LegacyTelemetryProvider.prototype.request = function (domainObject, request) {
|
||||
var oldObject = this.instantiate(utils.toOldFormat(domainObject), utils.makeKeyString(domainObject.identifier));
|
||||
var capability = oldObject.getCapability("telemetry");
|
||||
|
||||
return capability.requestData(request).then(function (telemetrySeries) {
|
||||
return Promise.resolve(adaptSeries(domainObject, capability.getMetadata(), telemetrySeries));
|
||||
}).catch(function (error) {
|
||||
return Promise.reject(error);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @callback LegacyTelemetryProvider~SubscribeCallback
|
||||
* @param {ConvertedTelemetryObject}
|
||||
*/
|
||||
|
||||
/**
|
||||
* @see module:openmct.TelemetryAPI~TelemetryProvider#request
|
||||
* @param {module:openmct.DomainObject}
|
||||
* @param {LegacyTelemetryProvider~SubscribeCallback} callback will be called with a single datum when
|
||||
* new data is available.
|
||||
* @param {module:openmct.TelemetryAPI~TelemetryRequest} options
|
||||
* options for this request. Passed straight through to legacy provider
|
||||
* @returns {platform|telemetry.TelemetrySubscription|*}
|
||||
*/
|
||||
LegacyTelemetryProvider.prototype.subscribe = function (domainObject, callback, request) {
|
||||
var oldObject = this.instantiate(utils.toOldFormat(domainObject), utils.makeKeyString(domainObject.identifier));
|
||||
var capability = oldObject.getCapability("telemetry");
|
||||
|
||||
function callbackWrapper(series) {
|
||||
callback(createDatum(domainObject, capability.getMetadata(), series, series.getPointCount() - 1));
|
||||
}
|
||||
|
||||
return capability.subscribe(callbackWrapper, request);
|
||||
};
|
||||
|
||||
LegacyTelemetryProvider.prototype.limitEvaluator = function (domainObject) {
|
||||
var oldObject = this.instantiate(
|
||||
utils.toOldFormat(domainObject),
|
||||
utils.makeKeyString(domainObject.identifier));
|
||||
var limitEvaluator = oldObject.getCapability("limit");
|
||||
|
||||
return {
|
||||
evaluate: function (datum, property) {
|
||||
return limitEvaluator.evaluate(datum, property.key);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
return function (openmct, instantiate) {
|
||||
// Push onto the start of the default providers array so that it's
|
||||
// always the last resort
|
||||
openmct.telemetry.defaultProviders.unshift(
|
||||
new LegacyTelemetryProvider(instantiate));
|
||||
};
|
||||
|
||||
});
|
||||
@@ -21,9 +21,13 @@
|
||||
*****************************************************************************/
|
||||
|
||||
define([
|
||||
'./TelemetryMetadataManager',
|
||||
'./TelemetryValueFormatter',
|
||||
'lodash',
|
||||
'EventEmitter'
|
||||
], function (
|
||||
TelemetryMetadataManager,
|
||||
TelemetryValueFormatter,
|
||||
_,
|
||||
EventEmitter
|
||||
) {
|
||||
@@ -155,9 +159,13 @@ define([
|
||||
* @augments module:openmct.TelemetryAPI~TelemetryProvider
|
||||
* @memberof module:openmct
|
||||
*/
|
||||
function TelemetryAPI() {
|
||||
function TelemetryAPI(MCT) {
|
||||
this.MCT = MCT;
|
||||
this.providersByStrategy = {};
|
||||
this.defaultProviders = [];
|
||||
this.metadataCache = new WeakMap();
|
||||
this.formatMapCache = new WeakMap();
|
||||
this.valueFormatterCache = new WeakMap();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -240,6 +248,85 @@ define([
|
||||
Promise.reject([]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get telemetry metadata for a given domain object. Returns a telemetry
|
||||
* metadata manager which provides methods for interrogating telemetry
|
||||
* metadata.
|
||||
*
|
||||
* @returns {TelemetryMetadataManager}
|
||||
*/
|
||||
TelemetryAPI.prototype.getMetadata = function (domainObject) {
|
||||
if (!this.metadataCache.has(domainObject)) {
|
||||
if (!this.typeService) {
|
||||
this.typeService = this.MCT.$injector.get('typeService');
|
||||
}
|
||||
this.metadataCache.set(
|
||||
domainObject,
|
||||
new TelemetryMetadataManager(domainObject, this.typeService)
|
||||
);
|
||||
}
|
||||
return this.metadataCache.get(domainObject);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return an array of valueMetadatas that are common to all supplied
|
||||
* telemetry objects and match the requested hints.
|
||||
*
|
||||
*/
|
||||
TelemetryAPI.prototype.commonValuesForHints = function (metadatas, hints) {
|
||||
var options = metadatas.map(function (metadata) {
|
||||
var values = metadata.valuesForHints(hints);
|
||||
return _.indexBy(values, 'key');
|
||||
}).reduce(function (a, b) {
|
||||
var results = {};
|
||||
Object.keys(a).forEach(function (key) {
|
||||
if (b.hasOwnProperty(key)) {
|
||||
results[key] = a[key];
|
||||
}
|
||||
});
|
||||
return results;
|
||||
});
|
||||
var sortKeys = hints.map(function (h) {
|
||||
return 'hints.' + h;
|
||||
});
|
||||
return _.sortByAll(options, sortKeys);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a value formatter for a given valueMetadata.
|
||||
*
|
||||
* @returns {TelemetryValueFormatter}
|
||||
*/
|
||||
TelemetryAPI.prototype.getValueFormatter = function (valueMetadata) {
|
||||
if (!this.valueFormatterCache.has(valueMetadata)) {
|
||||
if (!this.formatService) {
|
||||
this.formatService = this.MCT.$injector.get('formatService');
|
||||
}
|
||||
this.valueFormatterCache.set(
|
||||
valueMetadata,
|
||||
new TelemetryValueFormatter(valueMetadata, this.formatService)
|
||||
);
|
||||
}
|
||||
return this.valueFormatterCache.get(valueMetadata);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a format map of all value formatters for a given piece of telemetry
|
||||
* metadata.
|
||||
*
|
||||
* @returns {Object<String, {TelemetryValueFormatter}>}
|
||||
*/
|
||||
TelemetryAPI.prototype.getFormatMap = function (metadata) {
|
||||
if (!this.formatMapCache.has(metadata)) {
|
||||
var formatMap = metadata.values().reduce(function (map, valueMetadata) {
|
||||
map[valueMetadata.key] = this.getValueFormatter(valueMetadata);
|
||||
return map;
|
||||
}.bind(this), {});
|
||||
this.formatMapCache.set(metadata, formatMap);
|
||||
}
|
||||
return this.formatMapCache.get(metadata);
|
||||
};
|
||||
|
||||
/**
|
||||
* Subscribe to realtime telemetry for a specific domain object.
|
||||
* The callback will be called whenever data is received from a
|
||||
|
||||
152
src/api/telemetry/TelemetryMetadataManager.js
Normal file
152
src/api/telemetry/TelemetryMetadataManager.js
Normal file
@@ -0,0 +1,152 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2016, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
|
||||
define([
|
||||
'lodash'
|
||||
], function (
|
||||
_
|
||||
) {
|
||||
|
||||
function valueMetadatasFromOldFormat(metadata) {
|
||||
var valueMetadatas = [];
|
||||
|
||||
valueMetadatas.push({
|
||||
key: 'name',
|
||||
name: 'Name'
|
||||
});
|
||||
|
||||
metadata.domains.forEach(function (domain, index) {
|
||||
var valueMetadata = _.clone(domain);
|
||||
valueMetadata.hints = {
|
||||
x: index + 1,
|
||||
domain: index + 1
|
||||
};
|
||||
valueMetadatas.push(valueMetadata);
|
||||
});
|
||||
|
||||
metadata.ranges.forEach(function (range, index) {
|
||||
var valueMetadata = _.clone(range);
|
||||
valueMetadata.hints = {
|
||||
y: index,
|
||||
range: index,
|
||||
priority: index + metadata.domains.length + 1
|
||||
};
|
||||
|
||||
if (valueMetadata.type === 'enum') {
|
||||
valueMetadata.key = 'enum';
|
||||
valueMetadata.hints.y -= 10;
|
||||
valueMetadata.hints.range -= 10;
|
||||
valueMetadata.enumerations =
|
||||
_.sortBy(valueMetadata.enumerations.map(function (e) {
|
||||
return {
|
||||
string: e.string,
|
||||
value: +e.value
|
||||
};
|
||||
}), 'e.value');
|
||||
valueMetadata.values = _.pluck(valueMetadata.enumerations, 'value');
|
||||
valueMetadata.max = _.max(valueMetadata.values);
|
||||
valueMetadata.min = _.min(valueMetadata.values);
|
||||
}
|
||||
|
||||
valueMetadatas.push(valueMetadata);
|
||||
});
|
||||
|
||||
return valueMetadatas;
|
||||
}
|
||||
|
||||
function applyReasonableDefaults(valueMetadata, index) {
|
||||
valueMetadata.source = valueMetadata.source || valueMetadata.key;
|
||||
valueMetadata.hints = valueMetadata.hints || {};
|
||||
|
||||
if (!valueMetadata.hints.hasOwnProperty('priority')) {
|
||||
valueMetadata.hints.priority = index;
|
||||
}
|
||||
return valueMetadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility class for handling telemetry metadata for a domain object.
|
||||
* Wraps old format metadata to new format metadata.
|
||||
* Provides methods for interrogating telemetry metadata.
|
||||
*/
|
||||
function TelemetryMetadataManager(domainObject, typeService) {
|
||||
this.metadata = domainObject.telemetry || {};
|
||||
|
||||
if (this.metadata.values) {
|
||||
this.valueMetadatas = this.metadata.values;
|
||||
} else {
|
||||
var typeMetadata = typeService
|
||||
.getType(domainObject.type).typeDef.telemetry;
|
||||
|
||||
_.extend(this.metadata, typeMetadata);
|
||||
this.valueMetadatas = valueMetadatasFromOldFormat(this.metadata);
|
||||
}
|
||||
|
||||
this.valueMetadatas = this.valueMetadatas.map(applyReasonableDefaults);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get value metadata for a single key.
|
||||
*/
|
||||
TelemetryMetadataManager.prototype.value = function (key) {
|
||||
return this.valueMetadatas.filter(function (metadata) {
|
||||
return metadata.key === key;
|
||||
})[0];
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns all value metadatas, sorted by priority.
|
||||
*/
|
||||
TelemetryMetadataManager.prototype.values = function () {
|
||||
return this.valuesForHints(['priority']);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get an array of valueMetadatas that posess all hints requested.
|
||||
* Array is sorted based on hint priority.
|
||||
*
|
||||
*/
|
||||
TelemetryMetadataManager.prototype.valuesForHints = function (
|
||||
hints
|
||||
) {
|
||||
function hasHint(hint) {
|
||||
/*jshint validthis: true */
|
||||
return this.hints.hasOwnProperty(hint);
|
||||
}
|
||||
function hasHints(metadata) {
|
||||
return hints.every(hasHint, metadata);
|
||||
}
|
||||
var matchingMetadata = this.valueMetadatas.filter(hasHints);
|
||||
var sortedMetadata = _.sortBy(matchingMetadata, function (metadata) {
|
||||
return hints.map(function (hint) {
|
||||
return metadata.hints[hint];
|
||||
});
|
||||
});
|
||||
return sortedMetadata;
|
||||
};
|
||||
|
||||
|
||||
return TelemetryMetadataManager;
|
||||
|
||||
});
|
||||
98
src/api/telemetry/TelemetryValueFormatter.js
Normal file
98
src/api/telemetry/TelemetryValueFormatter.js
Normal file
@@ -0,0 +1,98 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2016, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define([
|
||||
'lodash'
|
||||
], function (
|
||||
_
|
||||
) {
|
||||
|
||||
// TODO: needs reference to formatService;
|
||||
function TelemetryValueFormatter(valueMetadata, formatService) {
|
||||
this.valueMetadata = valueMetadata;
|
||||
this.parseCache = new WeakMap();
|
||||
this.formatCache = new WeakMap();
|
||||
try {
|
||||
this.formatter = formatService
|
||||
.getFormat(valueMetadata.format, valueMetadata);
|
||||
} catch (e) {
|
||||
// TODO: Better formatting
|
||||
this.formatter = {
|
||||
parse: function (x) {
|
||||
return Number(x);
|
||||
},
|
||||
format: function (x) {
|
||||
return x;
|
||||
},
|
||||
validate: function (x) {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (valueMetadata.type === 'enum') {
|
||||
this.formatter = {};
|
||||
this.enumerations = valueMetadata.enumerations.reduce(function (vm, e) {
|
||||
vm.byValue[e.value] = e.string;
|
||||
vm.byString[e.string] = e.value;
|
||||
return vm;
|
||||
}, {byValue: {}, byString: {}});
|
||||
this.formatter.format = function (value) {
|
||||
return this.enumerations.byValue[value];
|
||||
}.bind(this);
|
||||
this.formatter.parse = function (string) {
|
||||
if (typeof string === "string" && this.enumerations.hasOwnProperty(string)) {
|
||||
return this.enumerations.byString[string];
|
||||
}
|
||||
return Number(string);
|
||||
}.bind(this);
|
||||
}
|
||||
}
|
||||
|
||||
TelemetryValueFormatter.prototype.parse = function (datum) {
|
||||
if (_.isObject(datum)) {
|
||||
if (!this.parseCache.has(datum)) {
|
||||
this.parseCache.set(
|
||||
datum,
|
||||
this.formatter.parse(datum[this.valueMetadata.source])
|
||||
);
|
||||
}
|
||||
return this.parseCache.get(datum);
|
||||
}
|
||||
return this.formatter.parse(datum);
|
||||
};
|
||||
|
||||
TelemetryValueFormatter.prototype.format = function (datum) {
|
||||
if (_.isObject(datum)) {
|
||||
if (!this.formatCache.has(datum)) {
|
||||
this.formatCache.set(
|
||||
datum,
|
||||
this.formatter.format(datum[this.valueMetadata.source])
|
||||
);
|
||||
}
|
||||
return this.formatCache.get(datum);
|
||||
}
|
||||
return this.formatter.format(datum);
|
||||
};
|
||||
|
||||
return TelemetryValueFormatter;
|
||||
});
|
||||
@@ -22,12 +22,14 @@
|
||||
|
||||
define([
|
||||
'./TelemetryAPI',
|
||||
'./LegacyTelemetryProvider',
|
||||
'legacyRegistry'
|
||||
], function (
|
||||
TelemetryAPI,
|
||||
LegacyTelemetryProvider,
|
||||
legacyRegistry
|
||||
) {
|
||||
legacyRegistry.register('api/telemetry-api', {
|
||||
legacyRegistry.register('src/api/telemetry', {
|
||||
name: 'Telemetry API',
|
||||
description: 'The public Telemetry API',
|
||||
extensions: {
|
||||
@@ -38,6 +40,14 @@ define([
|
||||
depends: [
|
||||
'formatService'
|
||||
]
|
||||
},
|
||||
{
|
||||
key: "LegacyTelemetryAdapter",
|
||||
implementation: LegacyTelemetryProvider,
|
||||
depends: [
|
||||
"openmct",
|
||||
"instantiate"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ define(['./Type'], function (Type) {
|
||||
* @method addType
|
||||
* @memberof module:openmct.TypeRegistry#
|
||||
*/
|
||||
TypeRegistry.prototype.addType = function (typeKey, typeDef) {
|
||||
TypeRegistry.prototype.register = function (typeKey, typeDef) {
|
||||
this.types[typeKey] = new Type(typeDef);
|
||||
};
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ define([
|
||||
|
||||
'../src/adapter/bundle',
|
||||
'../src/api/objects/bundle',
|
||||
'../src/api/telemetry/bundle',
|
||||
|
||||
'../example/builtins/bundle',
|
||||
'../example/composite/bundle',
|
||||
@@ -96,6 +97,7 @@ define([
|
||||
var DEFAULTS = [
|
||||
'src/adapter',
|
||||
'src/api/objects',
|
||||
'src/api/telemetry',
|
||||
'platform/framework',
|
||||
'platform/core',
|
||||
'platform/representation',
|
||||
@@ -107,7 +109,6 @@ define([
|
||||
'platform/commonUI/general',
|
||||
'platform/commonUI/inspect',
|
||||
'platform/commonUI/mobile',
|
||||
'platform/commonUI/themes/espresso',
|
||||
'platform/commonUI/notification',
|
||||
'platform/containment',
|
||||
'platform/execution',
|
||||
|
||||
87
src/plugins/plugins.js
Normal file
87
src/plugins/plugins.js
Normal file
@@ -0,0 +1,87 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2016, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define([
|
||||
'lodash'
|
||||
], function (_) {
|
||||
var bundleMap = {
|
||||
couchDB: 'platform/persistence/couch',
|
||||
elasticsearch: 'platform/persistence/elastic',
|
||||
espresso: 'platform/commonUI/themes/espresso',
|
||||
localStorage: 'platform/persistence/local',
|
||||
myItems: 'platform/features/my-items',
|
||||
snow: 'platform/commonUI/themes/snow',
|
||||
utcTimeSystem: 'platform/features/conductor/utcTimeSystem'
|
||||
};
|
||||
|
||||
var plugins = _.mapValues(bundleMap, function (bundleName, pluginName) {
|
||||
return function (openmct) {
|
||||
openmct.legacyRegistry.enable(bundleName);
|
||||
};
|
||||
});
|
||||
|
||||
plugins.CouchDB = function (url) {
|
||||
return function (openmct) {
|
||||
if (url) {
|
||||
var bundleName = "config/couch";
|
||||
openmct.legacyRegistry.register(bundleName, {
|
||||
"extensions": {
|
||||
"constants": [
|
||||
{
|
||||
"key": "COUCHDB_PATH",
|
||||
"value": url,
|
||||
"priority": "mandatory"
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
openmct.legacyRegistry.enable(bundleName);
|
||||
}
|
||||
|
||||
openmct.legacyRegistry.enable(bundleMap.couchDB);
|
||||
};
|
||||
};
|
||||
|
||||
plugins.Elasticsearch = function (url) {
|
||||
return function (openmct) {
|
||||
if (url) {
|
||||
var bundleName = "config/elastic";
|
||||
openmct.legacyRegistry.register(bundleName, {
|
||||
"extensions": {
|
||||
"constants": [
|
||||
{
|
||||
"key": "ELASTIC_ROOT",
|
||||
"value": url,
|
||||
"priority": "mandatory"
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
openmct.legacyRegistry.enable(bundleName);
|
||||
}
|
||||
|
||||
openmct.legacyRegistry.enable(bundleMap.elasticsearch);
|
||||
};
|
||||
};
|
||||
|
||||
return plugins;
|
||||
});
|
||||
@@ -34,14 +34,13 @@ define([], function () {
|
||||
|
||||
/**
|
||||
* @private for platform-internal use
|
||||
* @param {module:openmct.Context} context the view's context,
|
||||
* which includes the item being viewed
|
||||
* @param {*} item the object to be viewed
|
||||
* @returns {module:openmct.ViewProvider[]} any providers
|
||||
* which can provide views of this object
|
||||
*/
|
||||
ViewRegistry.prototype.get = function (context) {
|
||||
ViewRegistry.prototype.get = function (item) {
|
||||
return this.providers.filter(function (provider) {
|
||||
return provider.canView(context);
|
||||
return provider.canView(item);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -106,8 +105,8 @@ define([], function () {
|
||||
*
|
||||
* @method canView
|
||||
* @memberof module:openmct.ViewProvider#
|
||||
* @param {module:openmct.Context} context the view's context,
|
||||
* which includes the item being viewed
|
||||
* @param {module:openmct.DomainObject} domainObject the domain object
|
||||
* to be viewed
|
||||
* @returns {boolean} true if this domain object can be viewed using
|
||||
* this provider
|
||||
*/
|
||||
@@ -123,8 +122,7 @@ define([], function () {
|
||||
*
|
||||
* @method view
|
||||
* @memberof module:openmct.ViewProvider#
|
||||
* @param {module:openmct.Context} context the view's context,
|
||||
* which includes the item being viewed
|
||||
* @param {*} object the object to be viewed
|
||||
* @returns {module:openmct.View} a view of this domain object
|
||||
*/
|
||||
|
||||
|
||||
Reference in New Issue
Block a user