Compare commits

...

48 Commits

Author SHA1 Message Date
62ff404bb3 Testing with non-reactive API 2021-09-27 16:45:54 -07:00
8243cf5d7b Fix missing object handling in several vues (#4259)
* If there is a pending create request for an id, queue a duplicate request.
* Fix downsteam errors when objects are missing
* Changed error logging from console.log to console.warn
2021-09-27 14:25:33 -07:00
c4c1fea17f snapshot clicked while in edit mode should open in preview mode #4115 (#4257) 2021-09-27 10:14:03 -07:00
5e920e90ce Fix bargraph color selection (#4253)
* Fix typo for attribute key
* Adds section heading for Bar Graph Series
2021-09-24 10:07:53 -07:00
886db23eb6 Hide independent time conductor mode if only 1 mode option is available. (#4250)
* If there is a pending create request for an id, queue a duplicate request.
* Hide independent time conductor mode if there is only 1 mode available
2021-09-23 10:47:35 -07:00
0ccb546a2e starting loading as false, since that makes sense (#4247) 2021-09-23 09:42:39 -07:00
271f8ed38f Fix file selection on pressing enter key (#4246)
* Invoke angular digest cycle after file input selection returns
2021-09-22 15:14:30 -07:00
650f84e95c [Telemetry Tables] Handling Request Loading (#4245)
* added two new events for telemetry collections to denote historical requests starting and ending (can be used for loading indicators)

* updating refresh data to use correct outstanding requests variable, binding request count update methods

* removing loading spinner (replaced with progress bar)

* if making a request, cancel any existing ones

* reverting edge case code updates
2021-09-22 15:01:28 -07:00
b70af5a1bb If there is a pending create request for an id, queue a duplicate request. (#4243) 2021-09-22 09:44:22 -07:00
0af21632db Use timeFormatter.parse to get the timestamp of imagery since the source could be something other than key (#4238) 2021-09-21 11:10:18 -07:00
e2f1ff5442 Notebook conflict auto retry 1.7.7 (#4230) 2021-09-20 14:33:38 -07:00
c4b9be18f1 Support for Bar Graphs (#4221)
* Adds new types for Bar Graphs (#4168)
* Adds new spectral test data

Co-authored-by: Scott Bell <scott@traclabs.com>
Co-authored-by: Charles Hacskaylo <charlesh88@gmail.com>
Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2021-09-18 13:00:16 -07:00
eabdf6cd04 Independent time conductor (#3988)
* Independent time API implementation
* Independent time conductor in plan view

Co-authored-by: charlesh88 <charles.f.hacskaylo@nasa.gov>
Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2021-09-18 09:53:35 -07:00
e56c673005 [Telemetry Collections] Add process method to historical request options (for yield requests) (#4201)
* added processor generator to request options in the telemetry API. Allows progressive yielding of request results.

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2021-09-17 19:50:53 -05:00
dad9f12a5c Imagery view for time strip (#4114)
* Imagery view for time strip

Co-authored-by: charlesh88 <charles.f.hacskaylo@nasa.gov>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
2021-09-17 20:05:40 -04:00
aa5edb0b83 Flexible inspector properties (#4109)
* inspector properties can be passed in through context
* defaults to the currently hard-coded details (title, type, modified or created)
2021-09-17 16:20:22 -07:00
b315803180 Add linear progress bar to tables (#4204)
* Add linear progress bar to tables
- Stubbed in markup;
- Better CSS anim for clarity;

* Indeterminate progress bar for telemetry tables
- Significant refinements to use ProgressBar.vue component;
- Stubbed in temp computed property for `progressLoad`;
- Better animation approach for indeterminate progress;
- Refined markup and CSS for `c-progress-bar`;

* Indeterminate progress bar for telemetry tables
- Refinements for determinate progress as shown in notification banners;
- Refined look of progress bar for clarity;
- Vue component now uses progressPerc === undefined instead of 'unknown';
- Animation tweaked;

* Changed progress-bar v-if test
- Per PR change suggestion, replace `v-if=progressLoad.progressPerc !== 100`
to `v-if=loading`;
2021-09-17 15:53:00 -07:00
b27317631b Add new glyphs for Bar Chart and Map views (#4219)
- New glyph and background classes for `icon-bar-chart` and
`bg-icon-bar-chart`;
- New glyph and background classes for `icon-map` and
`bg-icon-map`;
2021-09-17 13:11:09 -07:00
953a9daafb Fixes resizing handler error - adds null check (#4218) 2021-09-16 16:10:48 -07:00
63f9cd449f Refactor Angular Clock and Clock Indicator as plugin (#4106)
* clock and clock indicator installed as a plugin
* { enableIndicator: true } to install indicator
* Vue for views
2021-09-15 11:01:40 -07:00
54220f547b WIP (#4213)
Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2021-09-14 20:18:33 -04:00
93d967c2b3 Snapshot notice link not navigating as expected #4194 (#4199)
Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
2021-09-14 15:17:31 -07:00
1226459c6f View Large overlay doesn't allow taking Snapshots (#4091)
* Added NotebookMenuSwitcher in preview header
* Use preview component inside viewLargeAction
* Added autoHide flag to overlay API that allows developers to specify whether an overlay should remain visible if other overlays are subsequently triggered (eg. the snapshot overlay)
* When in edit mode, disable navigation link to notebook entry.

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2021-09-13 19:36:14 -05:00
d7c9c9cb98 Snapshot images should use the namespace of the notebook they are being saved to or LocalStorage (#4020)
* Snapshot images should use the namespace of the notebook they are being saved to, or LocalStorage #4007

Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
2021-09-13 13:44:38 -07:00
2131ef2397 Clear data for objects clears data for all objects
Clear data for objects clears data for all objects #4065
2021-09-10 11:30:54 -07:00
48c22369a1 Fixed memory leak in import-export plugin (#4061) 2021-09-02 15:45:11 -07:00
6506077f4d refactor panes (#4125)
* remove router listeners
* remove some hard-coding
2021-09-02 14:11:05 -07:00
b1b4266ff3 Add automated security scanning to our repo (#4166) 2021-08-30 15:44:25 -07:00
42b0148f93 Add new icon for aggregate telemetry (#4163) 2021-08-30 11:19:47 -07:00
9461ad8edd Add new Ellipse drawing object in Display Layouts (#4153)
* Add new ellipse drawing object to Display Layouts
- Fix test and linting issues;

Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
2021-08-30 13:29:05 -04:00
40055ba955 Prepare for sprint 1.7.8 (#4156) 2021-08-27 04:55:30 -07:00
9cb85ad176 1.7.7 merge into master (#4155)
* Merge 1.7.7 sprint branch into master

Co-authored-by: Nikhil <nikhil.k.mandlik@nasa.gov>
Co-authored-by: Khalid Adil <khalidadil29@gmail.com>
2021-08-27 04:44:23 -07:00
f2b2953a5d [Display Layout] Right click on item acts as sticky move (#4126)
* Added a left click filter to the mousedown events
* Moved ternary logic out of the template
* Move defauts into function params
2021-08-23 16:15:57 -07:00
62de310686 fix: typo spelling grammar (#4095)
* fix: typo spelling grammar
2021-08-23 15:43:58 -07:00
4b9ff67e49 [Notebook] Create Snapshot directly from any frame in a layout #3300 (#4099) 2021-08-23 14:03:49 -07:00
d5e32ec494 [Telemetry Collection] Fixes: abort requests, naming, bindings, errors, time system handling (#4128)
* naming signal correctly for aborint
* updating api call for requesting telemetry collections

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2021-08-19 17:21:35 -07:00
38880ba3d1 Update job names to be more explicit (#4137)
It's still not clear which jobs pass/fail on tests. This change will update the name of the job which is run to be more explicit that it only pass/fails on the build step
2021-08-19 13:25:07 -07:00
a99ce7733c changed from window resize event, to element ResizeObserver for more accurate tracking of tree size (#4132) 2021-08-18 16:38:48 -07:00
9f48764210 Add Nightly CI Job to Circle to identify flake on master (#4032)
* Nightly job
* Unique job names
2021-08-18 13:16:15 -07:00
a1aaa0dd41 Refractor (#3836)
* Refractor
* Update AgentService.js
* Update ExportAsJSONActionSpec.js
2021-08-18 11:57:47 -07:00
bee15e98c8 Added .npmrc to silence log messages < WARN (#3649)
* Added .npmrc to silence log messages < WARN
2021-08-18 09:31:53 -07:00
092bbe547d Prepare for sprint 1.7.7 (#4127) 2021-08-18 11:56:12 -04:00
6cbe05317c 3788 - Re-implement DeviceClassifier in vanilla JS (#3837)
* Changed AgentService to Agent and exposed in utils
* removed MCTDevice
2021-08-17 10:21:50 -07:00
3b92fcdf6c Prevent extra loading of plots data (#4120) 2021-08-17 09:51:53 -07:00
6dde54bd25 Fix plots performance (#4092)
* Fix no mutating props violation for Browsebar and StyleEditor
* Separate plot series data from the configuration (like it should be!)
2021-08-16 14:21:09 -07:00
359e7377ac Notebook Snapshot menu is only updating section and page names for the editor's view #3982 (#4002)
* if default section/page is missing then clear default notebook and hide option from dropdown.

* handle edge case when section is null/undefined.

* refactored notebook localstorage data + fixed some edge cases when default section/page gets deleted.

Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
2021-08-12 13:29:01 -07:00
9f4190f781 [Linting] Fix linting errors (#4082) 2021-08-11 15:11:17 -07:00
f3fc991a74 [Telemetry Collections] Add Telemetry Collection Functionality to Telemetry API (#3689)
Adds telemetry collections to the telemetry API

Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2021-08-10 10:36:33 -07:00
238 changed files with 10774 additions and 4761 deletions

View File

@ -56,14 +56,38 @@ workflows:
browser: ChromeHeadless
always-pass: false
- test:
name: node12-firefoxESR
name: node12-firefoxESR-build-only
node-version: lts/erbium
browser: FirefoxESR
always-pass: true
- test:
name: node14-chrome
name: node14-chrome-build-only
node-version: lts/fermium
browser: ChromeHeadless
always-pass: true
nightly:
jobs:
- test:
name: node10-chrome-nightly
node-version: lts/dubnium
browser: ChromeHeadless
always-pass: false
- test:
name: node12-firefoxESR-nightly
node-version: lts/erbium
browser: FirefoxESR
always-pass: false
- test:
name: node14-chrome-nightly
node-version: lts/fermium
browser: ChromeHeadless
always-pass: false
triggers:
- schedule:
cron: "0 0 * * *"
filters:
branches:
only:
- master

33
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@ -0,0 +1,33 @@
name: "CodeQL"
on:
push:
branches: [ master ]
schedule:
- cron: '28 21 * * 3'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: javascript
- name: Autobuild
uses: github/codeql-action/autobuild@v1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

1
.npmrc Normal file
View File

@ -0,0 +1 @@
loglevel=warn

2
API.md
View File

@ -996,7 +996,7 @@ reveal additional information when the mouse cursor is hovered over it.
A common use case for indicators is to convey the state of some external system such as a
persistence backend or HTTP server. So long as this system is accessible via HTTP request,
Open MCT provides a general purpose indicator to show whether the server is available and
returing a 2xx status code. The URL Status Indicator is made available as a default plugin. See
returning a 2xx status code. The URL Status Indicator is made available as a default plugin. See
the [documentation](./src/plugins/URLIndicatorPlugin) for details on how to install and configure the
URL Status Indicator.

View File

@ -423,7 +423,7 @@ which can help with this, however.
instead of separate approaches for static and substitutable
dependencies.
* Removes need to understand Angular's DI mechanism.
* Improves useability of documentation (`typeService` is an
* Improves usability of documentation (`typeService` is an
instance of `CompositeService` and implements `TypeService`
so you can easily traverse links in the JSDoc.)
* Can be used more easily from Web Workers, allowing services

View File

@ -25,7 +25,7 @@
## Legacy Documentation
As we transition to a new API, the following documentation for the old API
(which is supported during the transtion) may be useful as well:
(which is supported during the transition) may be useful as well:
* The [Architecture Overview](architecture/) describes the concepts used
throughout Open MCT, and gives a high level overview of the platform's design.

View File

@ -28,6 +28,15 @@ define([
domain: 2
}
},
{
key: "cos",
name: "Cosine",
unit: "deg",
formatString: '%0.2f',
hints: {
domain: 3
}
},
// Need to enable "LocalTimeSystem" plugin to make use of this
// {
// key: "local",
@ -109,6 +118,100 @@ define([
}
}
]
},
'example.spectral-generator': {
values: [
{
key: "name",
name: "Name",
format: "string"
},
{
key: "utc",
name: "Time",
format: "utc",
hints: {
domain: 1
}
},
{
key: "wavelength",
name: "Wavelength",
unit: "Hz",
formatString: '%0.2f',
hints: {
domain: 2,
spectralAttribute: true
}
},
{
key: "cos",
name: "Cosine",
unit: "deg",
formatString: '%0.2f',
hints: {
range: 2,
spectralAttribute: true
}
}
]
},
'example.spectral-aggregate-generator': {
values: [
{
key: "name",
name: "Name",
format: "string"
},
{
key: "utc",
name: "Time",
format: "utc",
hints: {
domain: 1
}
},
{
key: "ch1",
name: "Channel 1",
format: "string",
hints: {
range: 1
}
},
{
key: "ch2",
name: "Channel 2",
format: "string",
hints: {
range: 2
}
},
{
key: "ch3",
name: "Channel 3",
format: "string",
hints: {
range: 3
}
},
{
key: "ch4",
name: "Channel 4",
format: "string",
hints: {
range: 4
}
},
{
key: "ch5",
name: "Channel 5",
format: "string",
hints: {
range: 5
}
}
]
}
};

View File

@ -0,0 +1,86 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
], function (
) {
function SpectralAggregateGeneratorProvider() {
}
function pointForTimestamp(timestamp, count, name) {
return {
name: name,
utc: String(Math.floor(timestamp / count) * count),
ch1: String(Math.floor(timestamp / count) % 1),
ch2: String(Math.floor(timestamp / count) % 2),
ch3: String(Math.floor(timestamp / count) % 3),
ch4: String(Math.floor(timestamp / count) % 4),
ch5: String(Math.floor(timestamp / count) % 5)
};
}
SpectralAggregateGeneratorProvider.prototype.supportsSubscribe = function (domainObject) {
return domainObject.type === 'example.spectral-aggregate-generator';
};
SpectralAggregateGeneratorProvider.prototype.subscribe = function (domainObject, callback) {
var count = 5000;
var interval = setInterval(function () {
var now = Date.now();
var datum = pointForTimestamp(now, count, domainObject.name);
callback(datum);
}, count);
return function () {
clearInterval(interval);
};
};
SpectralAggregateGeneratorProvider.prototype.supportsRequest = function (domainObject, options) {
return domainObject.type === 'example.spectral-aggregate-generator';
};
SpectralAggregateGeneratorProvider.prototype.request = function (domainObject, options) {
var start = options.start;
var end = Math.min(Date.now(), options.end); // no future values
var count = 5000;
if (options.strategy === 'latest' || options.size === 1) {
start = end;
}
var data = [];
while (start <= end && data.length < 5000) {
data.push(pointForTimestamp(start, count, domainObject.name));
start += count;
}
return Promise.resolve(data);
};
return SpectralAggregateGeneratorProvider;
});

View File

@ -0,0 +1,102 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, 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,
wavelength: 1,
period: 10,
offset: 0,
dataRateInHz: 1,
randomness: 0,
phase: 0
};
function SpectralGeneratorProvider() {
this.workerInterface = new WorkerInterface();
}
SpectralGeneratorProvider.prototype.canProvideTelemetry = function (domainObject) {
return domainObject.type === 'example.spectral-generator';
};
SpectralGeneratorProvider.prototype.supportsRequest =
SpectralGeneratorProvider.prototype.supportsSubscribe =
SpectralGeneratorProvider.prototype.canProvideTelemetry;
SpectralGeneratorProvider.prototype.makeWorkerRequest = function (domainObject, request = {}) {
var props = [
'amplitude',
'wavelength',
'period',
'offset',
'dataRateInHz',
'phase',
'randomness'
];
var workerRequest = {};
props.forEach(function (prop) {
if (domainObject.telemetry && Object.prototype.hasOwnProperty.call(domainObject.telemetry, prop)) {
workerRequest[prop] = domainObject.telemetry[prop];
}
if (request && Object.prototype.hasOwnProperty.call(request, prop)) {
workerRequest[prop] = request[prop];
}
if (!Object.prototype.hasOwnProperty.call(workerRequest, prop)) {
workerRequest[prop] = REQUEST_DEFAULTS[prop];
}
workerRequest[prop] = Number(workerRequest[prop]);
});
workerRequest.name = domainObject.name;
return workerRequest;
};
SpectralGeneratorProvider.prototype.request = function (domainObject, request) {
var workerRequest = this.makeWorkerRequest(domainObject, request);
workerRequest.start = request.start;
workerRequest.end = request.end;
workerRequest.spectra = true;
return this.workerInterface.request(workerRequest);
};
SpectralGeneratorProvider.prototype.subscribe = function (domainObject, callback) {
var workerRequest = this.makeWorkerRequest(domainObject, {});
workerRequest.spectra = true;
return this.workerInterface.subscribe(workerRequest, callback);
};
return SpectralGeneratorProvider;
});

View File

@ -63,7 +63,7 @@ define([
StateGeneratorProvider.prototype.request = function (domainObject, options) {
var start = options.start;
var end = options.end;
var end = Math.min(Date.now(), options.end); // no future values
var duration = domainObject.telemetry.duration * 1000;
if (options.strategy === 'latest' || options.size === 1) {
start = end;

View File

@ -54,23 +54,38 @@
var start = Date.now();
var step = 1000 / data.dataRateInHz;
var nextStep = start - (start % step) + step;
let work;
if (data.spectra) {
work = function (now) {
while (nextStep < now) {
const messageCopy = Object.create(message);
message.data.start = nextStep - (60 * 1000);
message.data.end = nextStep;
onRequest(messageCopy);
nextStep += step;
}
function work(now) {
while (nextStep < now) {
self.postMessage({
id: message.id,
data: {
name: data.name,
utc: nextStep,
yesterday: nextStep - 60 * 60 * 24 * 1000,
sin: sin(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness),
cos: cos(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness)
}
});
nextStep += step;
}
return nextStep;
};
} else {
work = function (now) {
while (nextStep < now) {
self.postMessage({
id: message.id,
data: {
name: data.name,
utc: nextStep,
yesterday: nextStep - 60 * 60 * 24 * 1000,
sin: sin(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness),
wavelength: wavelength(start, nextStep),
cos: cos(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness)
}
});
nextStep += step;
}
return nextStep;
return nextStep;
};
}
subscriptions[message.id] = work;
@ -111,13 +126,21 @@
utc: nextStep,
yesterday: nextStep - 60 * 60 * 24 * 1000,
sin: sin(nextStep, period, amplitude, offset, phase, randomness),
wavelength: wavelength(start, nextStep),
cos: cos(nextStep, period, amplitude, offset, phase, randomness)
});
}
self.postMessage({
id: message.id,
data: data
data: request.spectra ? {
wavelength: data.map((item) => {
return item.wavelength;
}),
cos: data.map((item) => {
return item.cos;
})
} : data
});
}
@ -131,6 +154,10 @@
* Math.sin(phase + (timestamp / period / 1000 * Math.PI * 2)) + (amplitude * Math.random() * randomness) + offset;
}
function wavelength(start, nextStep) {
return (nextStep - start) / 10;
}
function sendError(error, message) {
self.postMessage({
error: error.name + ': ' + error.message,

View File

@ -24,11 +24,15 @@ define([
"./GeneratorProvider",
"./SinewaveLimitProvider",
"./StateGeneratorProvider",
"./SpectralGeneratorProvider",
"./SpectralAggregateGeneratorProvider",
"./GeneratorMetadataProvider"
], function (
GeneratorProvider,
SinewaveLimitProvider,
StateGeneratorProvider,
SpectralGeneratorProvider,
SpectralAggregateGeneratorProvider,
GeneratorMetadataProvider
) {
@ -61,6 +65,37 @@ define([
openmct.telemetry.addProvider(new StateGeneratorProvider());
openmct.types.addType("example.spectral-generator", {
name: "Spectral Generator",
description: "For development use. Generates example streaming telemetry data using a simple sine wave algorithm.",
cssClass: "icon-generator-telemetry",
creatable: true,
initialize: function (object) {
object.telemetry = {
period: 10,
amplitude: 1,
wavelength: 1,
frequency: 1,
offset: 0,
dataRateInHz: 1,
phase: 0,
randomness: 0
};
}
});
openmct.telemetry.addProvider(new SpectralGeneratorProvider());
openmct.types.addType("example.spectral-aggregate-generator", {
name: "Spectral Aggregate Generator",
description: "For development use. Generates example streaming telemetry data using a simple state algorithm.",
cssClass: "icon-generator-telemetry",
creatable: true,
initialize: function (object) {
object.telemetry = {};
}
});
openmct.telemetry.addProvider(new SpectralAggregateGeneratorProvider());
openmct.types.addType("generator", {
name: "Sine Wave Generator",
description: "For development use. Generates example streaming telemetry data using a simple sine wave algorithm.",

View File

@ -152,7 +152,7 @@
<h2>How to Use Glyphs</h2>
<div class="cols cols1-1">
<div class="col">
<p>The easiest way to use a glyph is to include its CSS class in an element. The CSS adds a psuedo <code>:before</code> HTML element to whatever element it's attached to that makes proper use of the symbols font.</p>
<p>The easiest way to use a glyph is to include its CSS class in an element. The CSS adds a pseudo <code>:before</code> HTML element to whatever element it's attached to that makes proper use of the symbols font.</p>
<p>Alternately, you can use the <code>.ui-symbol</code> class in an object that contains encoded HTML entities. This method is only recommended if you cannot use the aforementioned CSS class approach.</p>
</div>
<mct-example><a class="s-button icon-gear" title="Settings"></a>

View File

@ -195,6 +195,7 @@
['table', 'telemetry.plot.overlay', 'telemetry.plot.stacked'],
{indicator: true}
));
openmct.install(openmct.plugins.Clock({ enableClockIndicator: true }));
openmct.start();
</script>
</html>

View File

@ -1,8 +1,7 @@
{
"name": "openmct",
"version": "1.7.6-SNAPSHOT",
"version": "1.7.8-SNAPSHOT",
"description": "The Open MCT core platform",
"dependencies": {},
"devDependencies": {
"angular": ">=1.8.0",
"angular-route": "1.4.14",
@ -12,16 +11,9 @@
"copy-webpack-plugin": "^4.5.2",
"cross-env": "^6.0.3",
"css-loader": "^1.0.0",
"d3-array": "1.2.x",
"d3-axis": "1.0.x",
"d3-collection": "1.0.x",
"d3-color": "1.0.x",
"d3-format": "1.2.x",
"d3-interpolate": "1.1.x",
"d3-scale": "1.0.x",
"d3-selection": "1.3.x",
"d3-time": "1.0.x",
"d3-time-format": "2.1.x",
"eslint": "7.0.0",
"eslint-plugin-vue": "^7.5.0",
"eslint-plugin-you-dont-need-lodash-underscore": "^6.10.0",
@ -41,13 +33,13 @@
"jsdoc": "^3.3.2",
"karma": "6.3.4",
"karma-chrome-launcher": "3.1.0",
"karma-firefox-launcher": "2.1.1",
"karma-cli": "2.0.0",
"karma-coverage": "2.0.3",
"karma-coverage-istanbul-reporter": "3.0.3",
"karma-junit-reporter": "2.0.1",
"karma-firefox-launcher": "2.1.1",
"karma-html-reporter": "0.2.7",
"karma-jasmine": "4.0.1",
"karma-junit-reporter": "2.0.1",
"karma-sourcemap-loader": "0.3.8",
"karma-webpack": "4.0.2",
"location-bar": "^3.0.1",
@ -62,6 +54,8 @@
"node-bourbon": "^4.2.3",
"node-sass": "^4.14.1",
"painterro": "^1.2.56",
"plotly.js-basic-dist": "^2.5.0",
"plotly.js-gl2d-dist": "^2.5.0",
"printj": "^1.2.1",
"raw-loader": "^0.5.1",
"request": "^2.69.0",

View File

@ -64,7 +64,7 @@ define(
*
* @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.
* @returns {Boolean} true if navigation occurred, otherwise false.
*/
NavigationService.prototype.setNavigation = function (domainObject, force) {
if (force) {

View File

@ -21,28 +21,14 @@
*****************************************************************************/
define([
"./src/MCTDevice",
"./src/AgentService",
"./src/DeviceClassifier"
"./src/AgentService"
], function (
MCTDevice,
AgentService,
DeviceClassifier
AgentService
) {
return {
name: "platform/commonUI/mobile",
definition: {
"extensions": {
"directives": [
{
"key": "mctDevice",
"implementation": MCTDevice,
"depends": [
"agentService"
]
}
],
"services": [
{
"key": "agentService",
@ -51,15 +37,6 @@ define([
"$window"
]
}
],
"runs": [
{
"implementation": DeviceClassifier,
"depends": [
"agentService",
"$document"
]
}
]
}
}

View File

@ -20,122 +20,12 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
/**
* Provides features which support variant behavior on mobile devices.
*
* @namespace platform/commonUI/mobile
*/
define(
[],
function () {
define(["../../../../src/utils/agent/Agent.js"], function (Agent) {
function AngularAgentServiceWrapper(window) {
const AS = Agent.default;
/**
* The query service handles calls for browser and userAgent
* info using a comparison between the userAgent and key
* device names
* @constructor
* @param $window Angular-injected instance of the window
* @memberof platform/commonUI/mobile
*/
function AgentService($window) {
var userAgent = $window.navigator.userAgent,
matches = userAgent.match(/iPad|iPhone|Android/i) || [];
this.userAgent = userAgent;
this.mobileName = matches[0];
this.$window = $window;
this.touchEnabled = ($window.ontouchstart !== undefined);
}
/**
* Check if the user is on a mobile device.
* @returns {boolean} true on mobile
*/
AgentService.prototype.isMobile = function () {
return Boolean(this.mobileName);
};
/**
* Check if the user is on a phone-sized mobile device.
* @returns {boolean} true on a phone
*/
AgentService.prototype.isPhone = function () {
if (this.isMobile()) {
if (this.isAndroidTablet()) {
return false;
} else if (this.mobileName === 'iPad') {
return false;
} else {
return true;
}
} else {
return false;
}
};
/**
* Check if the user is on a tablet sized android device
* @returns {boolean} true on an android tablet
*/
AgentService.prototype.isAndroidTablet = function () {
if (this.mobileName === 'Android') {
if (this.isPortrait() && window.innerWidth >= 768) {
return true;
} else if (this.isLandscape() && window.innerHeight >= 768) {
return true;
}
} else {
return false;
}
};
/**
* Check if the user is on a tablet-sized mobile device.
* @returns {boolean} true on a tablet
*/
AgentService.prototype.isTablet = function () {
return (this.isMobile() && !this.isPhone() && this.mobileName !== 'Android') || (this.isMobile() && this.isAndroidTablet());
};
/**
* Check if the user's device is in a portrait-style
* orientation (display width is narrower than display height.)
* @returns {boolean} true in portrait mode
*/
AgentService.prototype.isPortrait = function () {
return this.$window.innerWidth < this.$window.innerHeight;
};
/**
* Check if the user's device is in a landscape-style
* orientation (display width is greater than display height.)
* @returns {boolean} true in landscape mode
*/
AgentService.prototype.isLandscape = function () {
return !this.isPortrait();
};
/**
* Check if the user's device supports a touch interface.
* @returns {boolean} true if touch is supported
*/
AgentService.prototype.isTouch = function () {
return this.touchEnabled;
};
/**
* Check if the user agent matches a certain named device,
* as indicated by checking for a case-insensitive substring
* match.
* @param {string} name the name to check for
* @returns {boolean} true if the user agent includes that name
*/
AgentService.prototype.isBrowser = function (name) {
name = name.toLowerCase();
return this.userAgent.toLowerCase().indexOf(name) !== -1;
};
return AgentService;
return new AS(window);
}
);
return AngularAgentServiceWrapper;
});

View File

@ -0,0 +1,96 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import AgentService from "./AgentService";
const TEST_USER_AGENTS = {
DESKTOP:
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.89 Safari/537.36",
IPAD:
"Mozilla/5.0 (iPad; CPU OS 7_0 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53",
IPHONE:
"Mozilla/5.0 (iPhone; CPU iPhone OS 7_0 like Mac OS X; en-us) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53"
};
describe("The AgentService", function () {
let testWindow;
let agentService;
beforeEach(function () {
testWindow = {
innerWidth: 640,
innerHeight: 480,
navigator: {
userAgent: TEST_USER_AGENTS.DESKTOP
}
};
});
it("recognizes desktop devices as non-mobile", function () {
testWindow.navigator.userAgent = TEST_USER_AGENTS.DESKTOP;
agentService = new AgentService(testWindow);
expect(agentService.isMobile()).toBeFalsy();
expect(agentService.isPhone()).toBeFalsy();
expect(agentService.isTablet()).toBeFalsy();
});
it("detects iPhones", function () {
testWindow.navigator.userAgent = TEST_USER_AGENTS.IPHONE;
agentService = new AgentService(testWindow);
expect(agentService.isMobile()).toBeTruthy();
expect(agentService.isPhone()).toBeTruthy();
expect(agentService.isTablet()).toBeFalsy();
});
it("detects iPads", function () {
testWindow.navigator.userAgent = TEST_USER_AGENTS.IPAD;
agentService = new AgentService(testWindow);
expect(agentService.isMobile()).toBeTruthy();
expect(agentService.isPhone()).toBeFalsy();
expect(agentService.isTablet()).toBeTruthy();
});
it("detects display orientation", function () {
agentService = new AgentService(testWindow);
testWindow.innerWidth = 1024;
testWindow.innerHeight = 400;
expect(agentService.isPortrait()).toBeFalsy();
expect(agentService.isLandscape()).toBeTruthy();
testWindow.innerWidth = 400;
testWindow.innerHeight = 1024;
expect(agentService.isPortrait()).toBeTruthy();
expect(agentService.isLandscape()).toBeFalsy();
});
it("detects touch support", function () {
testWindow.ontouchstart = null;
expect(new AgentService(testWindow).isTouch()).toBe(true);
delete testWindow.ontouchstart;
expect(new AgentService(testWindow).isTouch()).toBe(false);
});
it("allows for checking browser type", function () {
testWindow.navigator.userAgent = "Chromezilla Safarifox";
agentService = new AgentService(testWindow);
expect(agentService.isBrowser("Chrome")).toBe(true);
expect(agentService.isBrowser("Firefox")).toBe(false);
});
});

View File

@ -1,72 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, 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(
['./DeviceMatchers'],
function (DeviceMatchers) {
/**
* Runs at application startup and adds a subset of the following
* CSS classes to the body of the document, depending on device
* attributes:
*
* * `mobile`: Phones or tablets.
* * `phone`: Phones specifically.
* * `tablet`: Tablets specifically.
* * `desktop`: Non-mobile devices.
* * `portrait`: Devices in a portrait-style orientation.
* * `landscape`: Devices in a landscape-style orientation.
* * `touch`: Device supports touch events.
*
* @param {platform/commonUI/mobile.AgentService} agentService
* the service used to examine the user agent
* @param $document Angular's jqLite-wrapped document element
* @constructor
*/
function MobileClassifier(agentService, $document) {
var body = $document.find('body');
Object.keys(DeviceMatchers).forEach(function (key, index, array) {
if (DeviceMatchers[key](agentService)) {
body.addClass(key);
}
});
if (agentService.isMobile()) {
var mediaQuery = window.matchMedia('(orientation: landscape)');
mediaQuery.addListener(function (event) {
if (event.matches) {
body.removeClass('portrait');
body.addClass('landscape');
} else {
body.removeClass('landscape');
body.addClass('portrait');
}
});
}
}
return MobileClassifier;
}
);

View File

@ -1,88 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, 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(
['./DeviceMatchers'],
function (DeviceMatchers) {
/**
* The `mct-device` directive, when applied as an attribute,
* only includes the element when the device being used matches
* a set of characteristics required.
*
* Required characteristics are given as space-separated strings
* as the value to this attribute, e.g.:
*
* <span mct-device="mobile portrait">Hello world!</span>
*
* ...will only show Hello world! when viewed on a mobile device
* in the portrait orientation.
*
* Valid device characteristics to detect are:
*
* * `mobile`: Phones or tablets.
* * `phone`: Phones specifically.
* * `tablet`: Tablets specifically.
* * `desktop`: Non-mobile devices.
* * `portrait`: Devices in a portrait-style orientation.
* * `landscape`: Devices in a landscape-style orientation.
* * `touch`: Device supports touch events.
*
* @param {AgentService} agentService used to detect device type
* based on information about the user agent
*/
function MCTDevice(agentService) {
function deviceMatches(tokens) {
tokens = tokens || "";
return tokens.split(" ").every(function (token) {
var fn = DeviceMatchers[token];
return fn && fn(agentService);
});
}
function link(scope, element, attrs, ctrl, transclude) {
if (deviceMatches(attrs.mctDevice)) {
transclude(function (clone) {
element.replaceWith(clone);
});
}
}
return {
link: link,
// We are transcluding the whole element (like ng-if)
transclude: 'element',
// 1 more than ng-if
priority: 601,
// Also terminal, since element will be transcluded
terminal: true,
// Only apply as an attribute
restrict: "A"
};
}
return MCTDevice;
}
);

View File

@ -1,99 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, 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/AgentService"],
function (AgentService) {
var TEST_USER_AGENTS = {
DESKTOP: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.89 Safari/537.36",
IPAD: "Mozilla/5.0 (iPad; CPU OS 7_0 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53",
IPHONE: "Mozilla/5.0 (iPhone; CPU iPhone OS 7_0 like Mac OS X; en-us) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53"
};
describe("The AgentService", function () {
var testWindow, agentService;
beforeEach(function () {
testWindow = {
innerWidth: 640,
innerHeight: 480,
navigator: {
userAgent: TEST_USER_AGENTS.DESKTOP
}
};
});
it("recognizes desktop devices as non-mobile", function () {
testWindow.navigator.userAgent = TEST_USER_AGENTS.DESKTOP;
agentService = new AgentService(testWindow);
expect(agentService.isMobile()).toBeFalsy();
expect(agentService.isPhone()).toBeFalsy();
expect(agentService.isTablet()).toBeFalsy();
});
it("detects iPhones", function () {
testWindow.navigator.userAgent = TEST_USER_AGENTS.IPHONE;
agentService = new AgentService(testWindow);
expect(agentService.isMobile()).toBeTruthy();
expect(agentService.isPhone()).toBeTruthy();
expect(agentService.isTablet()).toBeFalsy();
});
it("detects iPads", function () {
testWindow.navigator.userAgent = TEST_USER_AGENTS.IPAD;
agentService = new AgentService(testWindow);
expect(agentService.isMobile()).toBeTruthy();
expect(agentService.isPhone()).toBeFalsy();
expect(agentService.isTablet()).toBeTruthy();
});
it("detects display orientation", function () {
agentService = new AgentService(testWindow);
testWindow.innerWidth = 1024;
testWindow.innerHeight = 400;
expect(agentService.isPortrait()).toBeFalsy();
expect(agentService.isLandscape()).toBeTruthy();
testWindow.innerWidth = 400;
testWindow.innerHeight = 1024;
expect(agentService.isPortrait()).toBeTruthy();
expect(agentService.isLandscape()).toBeFalsy();
});
it("detects touch support", function () {
testWindow.ontouchstart = null;
expect(new AgentService(testWindow).isTouch())
.toBe(true);
delete testWindow.ontouchstart;
expect(new AgentService(testWindow).isTouch())
.toBe(false);
});
it("allows for checking browser type", function () {
testWindow.navigator.userAgent = "Chromezilla Safarifox";
agentService = new AgentService(testWindow);
expect(agentService.isBrowser("Chrome")).toBe(true);
expect(agentService.isBrowser("Firefox")).toBe(false);
});
});
}
);

View File

@ -1,109 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, 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/DeviceClassifier", "../src/DeviceMatchers"],
function (DeviceClassifier, DeviceMatchers) {
var AGENT_SERVICE_METHODS = [
'isMobile',
'isPhone',
'isTablet',
'isPortrait',
'isLandscape',
'isTouch'
],
TEST_PERMUTATIONS = [
['isMobile', 'isPhone', 'isTouch', 'isPortrait'],
['isMobile', 'isPhone', 'isTouch', 'isLandscape'],
['isMobile', 'isTablet', 'isTouch', 'isPortrait'],
['isMobile', 'isTablet', 'isTouch', 'isLandscape'],
['isTouch'],
[]
];
describe("DeviceClassifier", function () {
var mockAgentService,
mockDocument,
mockBody;
beforeEach(function () {
mockAgentService = jasmine.createSpyObj(
'agentService',
AGENT_SERVICE_METHODS
);
mockDocument = jasmine.createSpyObj(
'$document',
['find']
);
mockBody = jasmine.createSpyObj(
'body',
['addClass']
);
mockDocument.find.and.callFake(function (sel) {
return sel === 'body' && mockBody;
});
AGENT_SERVICE_METHODS.forEach(function (m) {
mockAgentService[m].and.returnValue(false);
});
});
TEST_PERMUTATIONS.forEach(function (trueMethods) {
var summary = trueMethods.length === 0
? "device has no detected characteristics"
: "device " + (trueMethods.join(", "));
describe("when " + summary, function () {
var classifier; // eslint-disable-line
beforeEach(function () {
trueMethods.forEach(function (m) {
mockAgentService[m].and.returnValue(true);
});
classifier = new DeviceClassifier(
mockAgentService,
mockDocument
);
});
it("adds classes for matching, detected characteristics", function () {
Object.keys(DeviceMatchers).filter(function (m) {
return DeviceMatchers[m](mockAgentService);
}).forEach(function (key) {
expect(mockBody.addClass)
.toHaveBeenCalledWith(key);
});
});
it("does not add classes for non-matching characteristics", function () {
Object.keys(DeviceMatchers).filter(function (m) {
return !DeviceMatchers[m](mockAgentService);
}).forEach(function (key) {
expect(mockBody.addClass)
.not.toHaveBeenCalledWith(key);
});
});
});
});
});
}
);

View File

@ -1,78 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, 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/DeviceMatchers"],
function (DeviceMatchers) {
describe("DeviceMatchers", function () {
var mockAgentService;
beforeEach(function () {
mockAgentService = jasmine.createSpyObj(
'agentService',
[
'isMobile',
'isPhone',
'isTablet',
'isPortrait',
'isLandscape',
'isTouch'
]
);
});
it("detects when a device is a desktop device", function () {
mockAgentService.isMobile.and.returnValue(false);
expect(DeviceMatchers.desktop(mockAgentService))
.toBe(true);
mockAgentService.isMobile.and.returnValue(true);
expect(DeviceMatchers.desktop(mockAgentService))
.toBe(false);
});
function method(deviceType) {
return "is" + deviceType[0].toUpperCase() + deviceType.slice(1);
}
[
"mobile",
"phone",
"tablet",
"landscape",
"portrait",
"landscape",
"touch"
].forEach(function (deviceType) {
it("detects when a device is a " + deviceType + " device", function () {
mockAgentService[method(deviceType)].and.returnValue(true);
expect(DeviceMatchers[deviceType](mockAgentService))
.toBe(true);
mockAgentService[method(deviceType)].and.returnValue(false);
expect(DeviceMatchers[deviceType](mockAgentService))
.toBe(false);
});
});
});
}
);

View File

@ -1,168 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, 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/MCTDevice'],
function (MCTDevice) {
var JQLITE_METHODS = ['replaceWith'];
describe("The mct-device directive", function () {
var mockAgentService,
mockTransclude,
mockElement,
mockClone,
testAttrs,
directive;
function link() {
directive.link(null, mockElement, testAttrs, null, mockTransclude);
}
beforeEach(function () {
mockAgentService = jasmine.createSpyObj(
"agentService",
["isMobile", "isPhone", "isTablet", "isPortrait", "isLandscape"]
);
mockTransclude = jasmine.createSpy("$transclude");
mockElement = jasmine.createSpyObj(name, JQLITE_METHODS);
mockClone = jasmine.createSpyObj(name, JQLITE_METHODS);
mockTransclude.and.callFake(function (fn) {
fn(mockClone);
});
// Look desktop-like by default
mockAgentService.isLandscape.and.returnValue(true);
testAttrs = {};
directive = new MCTDevice(mockAgentService);
});
function expectInclusion() {
expect(mockElement.replaceWith)
.toHaveBeenCalledWith(mockClone);
}
function expectExclusion() {
expect(mockElement.replaceWith).not.toHaveBeenCalled();
}
it("is applicable at the attribute level", function () {
expect(directive.restrict).toEqual("A");
});
it("transcludes at the element level", function () {
expect(directive.transclude).toEqual('element');
});
it("has a greater priority number than ng-if", function () {
expect(directive.priority > 600).toBeTruthy();
});
it("restricts element inclusion for mobile devices", function () {
testAttrs.mctDevice = "mobile";
link();
expectExclusion();
mockAgentService.isMobile.and.returnValue(true);
link();
expectInclusion();
});
it("restricts element inclusion for tablet devices", function () {
testAttrs.mctDevice = "tablet";
mockAgentService.isMobile.and.returnValue(true);
link();
expectExclusion();
mockAgentService.isTablet.and.returnValue(true);
link();
expectInclusion();
});
it("restricts element inclusion for phone devices", function () {
testAttrs.mctDevice = "phone";
mockAgentService.isMobile.and.returnValue(true);
link();
expectExclusion();
mockAgentService.isPhone.and.returnValue(true);
link();
expectInclusion();
});
it("restricts element inclusion for desktop devices", function () {
testAttrs.mctDevice = "desktop";
mockAgentService.isMobile.and.returnValue(true);
link();
expectExclusion();
mockAgentService.isMobile.and.returnValue(false);
link();
expectInclusion();
});
it("restricts element inclusion for portrait orientation", function () {
testAttrs.mctDevice = "portrait";
link();
expectExclusion();
mockAgentService.isPortrait.and.returnValue(true);
link();
expectInclusion();
});
it("restricts element inclusion for landscape orientation", function () {
testAttrs.mctDevice = "landscape";
mockAgentService.isLandscape.and.returnValue(false);
mockAgentService.isPortrait.and.returnValue(true);
link();
expectExclusion();
mockAgentService.isLandscape.and.returnValue(true);
link();
expectInclusion();
});
it("allows multiple device characteristics to be requested", function () {
// Won't try to test every permutation here, just
// make sure the multi-characteristic feature has support.
testAttrs.mctDevice = "portrait mobile";
link();
// Neither portrait nor mobile, not called
expectExclusion();
mockAgentService.isPortrait.and.returnValue(true);
link();
// Was portrait, but not mobile, so no
expectExclusion();
mockAgentService.isMobile.and.returnValue(true);
link();
expectInclusion();
});
});
}
);

View File

@ -379,7 +379,7 @@ define([
{
"name": "Math.uuid.js",
"version": "1.4.7",
"description": "Unique identifer generation (code adapted.)",
"description": "Unique identifier generation (code adapted.)",
"author": "Robert Kieffer",
"website": "https://github.com/broofa/node-uuid",
"copyright": "Copyright (c) 2010-2012 Robert Kieffer",

View File

@ -21,32 +21,24 @@
*****************************************************************************/
define([
"moment-timezone",
"./src/indicators/ClockIndicator",
"./src/services/TickerService",
"./src/services/TimerService",
"./src/controllers/ClockController",
"./src/controllers/TimerController",
"./src/controllers/RefreshingController",
"./src/actions/StartTimerAction",
"./src/actions/RestartTimerAction",
"./src/actions/StopTimerAction",
"./src/actions/PauseTimerAction",
"./res/templates/clock.html",
"./res/templates/timer.html"
], function (
MomentTimezone,
ClockIndicator,
TickerService,
TimerService,
ClockController,
TimerController,
RefreshingController,
StartTimerAction,
RestartTimerAction,
StopTimerAction,
PauseTimerAction,
clockTemplate,
timerTemplate
) {
return {
@ -73,16 +65,6 @@ define([
"value": "YYYY/MM/DD HH:mm:ss"
}
],
"indicators": [
{
"implementation": ClockIndicator,
"depends": [
"tickerService",
"CLOCK_INDICATOR_FORMAT"
],
"priority": "preferred"
}
],
"services": [
{
"key": "tickerService",
@ -99,14 +81,6 @@ define([
}
],
"controllers": [
{
"key": "ClockController",
"implementation": ClockController,
"depends": [
"$scope",
"tickerService"
]
},
{
"key": "TimerController",
"implementation": TimerController,
@ -126,12 +100,6 @@ define([
}
],
"views": [
{
"key": "clock",
"type": "clock",
"editable": false,
"template": clockTemplate
},
{
"key": "timer",
"type": "timer",
@ -186,70 +154,6 @@ define([
}
],
"types": [
{
"key": "clock",
"name": "Clock",
"cssClass": "icon-clock",
"description": "A UTC-based clock that supports a variety of display formats. Clocks can be added to Display Layouts.",
"priority": 101,
"features": [
"creation"
],
"properties": [
{
"key": "clockFormat",
"name": "Display Format",
"control": "composite",
"items": [
{
"control": "select",
"options": [
{
"value": "YYYY/MM/DD hh:mm:ss",
"name": "YYYY/MM/DD hh:mm:ss"
},
{
"value": "YYYY/DDD hh:mm:ss",
"name": "YYYY/DDD hh:mm:ss"
},
{
"value": "hh:mm:ss",
"name": "hh:mm:ss"
}
],
"cssClass": "l-inline"
},
{
"control": "select",
"options": [
{
"value": "clock12",
"name": "12hr"
},
{
"value": "clock24",
"name": "24hr"
}
],
"cssClass": "l-inline"
}
]
},
{
"key": "timezone",
"name": "Timezone",
"control": "autocomplete",
"options": MomentTimezone.tz.names()
}
],
"model": {
"clockFormat": [
"YYYY/MM/DD hh:mm:ss",
"clock12"
],
"timezone": "UTC"
}
},
{
"key": "timer",
"name": "Timer",

View File

@ -1,32 +0,0 @@
<!--
Open MCT, Copyright (c) 2009-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.
-->
<div class="c-clock l-time-display u-style-receiver js-style-receiver" ng-controller="ClockController as clock">
<div class="c-clock__timezone">
{{clock.zone()}}
</div>
<div class="c-clock__value">
{{clock.text()}}
</div>
<div class="c-clock__ampm">
{{clock.ampm()}}
</div>
</div>

View File

@ -1,110 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2009-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([
'moment',
'moment-timezone'
],
function (
moment,
momentTimezone
) {
/**
* Controller for views of a Clock domain object.
*
* @constructor
* @memberof platform/features/clock
* @param {angular.Scope} $scope the Angular scope
* @param {platform/features/clock.TickerService} tickerService
* a service used to align behavior with clock ticks
*/
function ClockController($scope, tickerService) {
var lastTimestamp,
unlisten,
timeFormat,
zoneName,
self = this;
function update() {
var m = zoneName
? moment.utc(lastTimestamp).tz(zoneName) : moment.utc(lastTimestamp);
self.zoneAbbr = m.zoneAbbr();
self.textValue = timeFormat && m.format(timeFormat);
self.ampmValue = m.format("A"); // Just the AM or PM part
}
function tick(timestamp) {
lastTimestamp = timestamp;
update();
}
function updateModel(model) {
var baseFormat;
if (model !== undefined) {
baseFormat = model.clockFormat[0];
self.use24 = model.clockFormat[1] === 'clock24';
timeFormat = self.use24
? baseFormat.replace('hh', "HH") : baseFormat;
// If wrong timezone is provided, the UTC will be used
zoneName = momentTimezone.tz.names().includes(model.timezone)
? model.timezone : "UTC";
update();
}
}
// Pull in the model (clockFormat and timezone) from the domain object model
$scope.$watch('model', updateModel);
// Listen for clock ticks ... and stop listening on destroy
unlisten = tickerService.listen(tick);
$scope.$on('$destroy', unlisten);
}
/**
* Get the clock's time zone, as displayable text.
* @returns {string}
*/
ClockController.prototype.zone = function () {
return this.zoneAbbr;
};
/**
* Get the current time, as displayable text.
* @returns {string}
*/
ClockController.prototype.text = function () {
return this.textValue;
};
/**
* Get the text to display to qualify a time as AM or PM.
* @returns {string}
*/
ClockController.prototype.ampm = function () {
return this.use24 ? '' : this.ampmValue;
};
return ClockController;
}
);

View File

@ -1,65 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2009-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(
['moment'],
function (moment) {
/**
* Indicator that displays the current UTC time in the status area.
* @implements {Indicator}
* @memberof platform/features/clock
* @param {platform/features/clock.TickerService} tickerService
* a service used to align behavior with clock ticks
* @param {string} indicatorFormat format string for timestamps
* shown in this indicator
*/
function ClockIndicator(tickerService, indicatorFormat) {
var self = this;
this.text = "";
tickerService.listen(function (timestamp) {
self.text = moment.utc(timestamp)
.format(indicatorFormat) + " UTC";
});
}
ClockIndicator.prototype.getGlyphClass = function () {
return "";
};
ClockIndicator.prototype.getCssClass = function () {
return "t-indicator-clock icon-clock no-minify c-indicator--not-clickable";
};
ClockIndicator.prototype.getText = function () {
return this.text;
};
ClockIndicator.prototype.getDescription = function () {
return "";
};
return ClockIndicator;
}
);

View File

@ -1,107 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2009-2017, 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/controllers/ClockController"],
function (ClockController) {
// Wed, 03 Jun 2015 17:56:14 GMT
var TEST_TIMESTAMP = 1433354174000;
describe("A clock view's controller", function () {
var mockScope,
mockTicker,
mockUnticker,
controller;
beforeEach(function () {
mockScope = jasmine.createSpyObj('$scope', ['$watch', '$on']);
mockTicker = jasmine.createSpyObj('ticker', ['listen']);
mockUnticker = jasmine.createSpy('unticker');
mockTicker.listen.and.returnValue(mockUnticker);
controller = new ClockController(mockScope, mockTicker);
});
it("watches for model (clockFormat and timezone) from the domain object model", function () {
expect(mockScope.$watch).toHaveBeenCalledWith(
"model",
jasmine.any(Function)
);
});
it("subscribes to clock ticks", function () {
expect(mockTicker.listen)
.toHaveBeenCalledWith(jasmine.any(Function));
});
it("unsubscribes to ticks when destroyed", function () {
// Make sure $destroy is being listened for...
expect(mockScope.$on.calls.mostRecent().args[0]).toEqual('$destroy');
expect(mockUnticker).not.toHaveBeenCalled();
// ...and makes sure that its listener unsubscribes from ticker
mockScope.$on.calls.mostRecent().args[1]();
expect(mockUnticker).toHaveBeenCalled();
});
it("formats using the format string from the model", function () {
mockTicker.listen.calls.mostRecent().args[0](TEST_TIMESTAMP);
mockScope.$watch.calls.mostRecent().args[1]({
"clockFormat": [
"YYYY-DDD hh:mm:ss",
"clock24"
],
"timezone": "Canada/Eastern"
});
expect(controller.zone()).toEqual("EDT");
expect(controller.text()).toEqual("2015-154 13:56:14");
expect(controller.ampm()).toEqual("");
});
it("formats 12-hour time", function () {
mockTicker.listen.calls.mostRecent().args[0](TEST_TIMESTAMP);
mockScope.$watch.calls.mostRecent().args[1]({
"clockFormat": [
"YYYY-DDD hh:mm:ss",
"clock12"
],
"timezone": ""
});
expect(controller.zone()).toEqual("UTC");
expect(controller.text()).toEqual("2015-154 05:56:14");
expect(controller.ampm()).toEqual("PM");
});
it("does not throw exceptions when model is undefined", function () {
mockTicker.listen.calls.mostRecent().args[0](TEST_TIMESTAMP);
expect(function () {
mockScope.$watch.calls.mostRecent().args[1](undefined);
}).not.toThrow();
});
});
}
);

View File

@ -1,58 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2009-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/indicators/ClockIndicator"],
function (ClockIndicator) {
// Wed, 03 Jun 2015 17:56:14 GMT
var TEST_TIMESTAMP = 1433354174000,
TEST_FORMAT = "YYYY-DDD HH:mm:ss";
describe("The clock indicator", function () {
var mockTicker,
mockUnticker,
indicator;
beforeEach(function () {
mockTicker = jasmine.createSpyObj('ticker', ['listen']);
mockUnticker = jasmine.createSpy('unticker');
mockTicker.listen.and.returnValue(mockUnticker);
indicator = new ClockIndicator(mockTicker, TEST_FORMAT);
});
it("displays the current time", function () {
mockTicker.listen.calls.mostRecent().args[0](TEST_TIMESTAMP);
expect(indicator.getText()).toEqual("2015-154 17:56:14 UTC");
});
it("implements the Indicator interface", function () {
expect(indicator.getCssClass()).toEqual(jasmine.any(String));
expect(indicator.getText()).toEqual(jasmine.any(String));
expect(indicator.getDescription()).toEqual(jasmine.any(String));
});
});
}
);

View File

@ -44,9 +44,11 @@ define(
setText(result.name);
scope.ngModel[scope.field] = result;
control.$setValidity("file-input", true);
scope.$digest();
}, function () {
setText('Select File');
control.$setValidity("file-input", false);
scope.$digest();
});
}

View File

@ -30,8 +30,8 @@ define([
return function ImportExportPlugin() {
return function (openmct) {
ExportAsJSONAction.appliesTo = function (context) {
return openmct.$injector.get('policyService')
ExportAsJSONAction.prototype.appliesTo = function (context) {
return this.openmct.$injector.get('policyService')
.allow("creation", context.domainObject.getCapability("type")
);
};

View File

@ -29,7 +29,7 @@ define(
],
function (ExportAsJSONAction, domainObjectFactory, MCT, AdapterCapability) {
xdescribe("The export JSON action", function () {
describe("The export JSON action", function () {
var context,
action,
@ -102,7 +102,7 @@ define(
expect(action).toBeDefined();
});
it("doesn't export non-creatable objects in tree", function () {
xit("doesn't export non-creatable objects in tree", function () {
var nonCreatableType = {
hasFeature:
function (feature) {
@ -149,7 +149,7 @@ define(
});
});
it("can export self-containing objects", function () {
xit("can export self-containing objects", function () {
var parent = domainObjectFactory({
name: 'parent',
model: {
@ -191,7 +191,7 @@ define(
});
});
it("exports links to external objects as new objects", function () {
xit("exports links to external objects as new objects", function () {
var parent = domainObjectFactory({
name: 'parent',
model: {

View File

@ -27,7 +27,7 @@ define(
],
function (ImportAsJSONAction, domainObjectFactory) {
xdescribe("The import JSON action", function () {
describe("The import JSON action", function () {
var context = {};
var action,
@ -146,7 +146,7 @@ define(
});
});
it("can import self-containing objects", function () {
xit("can import self-containing objects", function () {
var compDomainObject = domainObjectFactory({
name: 'compObject',
model: { name: 'compObject'},
@ -198,7 +198,7 @@ define(
});
});
it("assigns new ids to each imported object", function () {
xit("assigns new ids to each imported object", function () {
dialogService.getUserInput.and.returnValue(Promise.resolve(
{
selectFile: {

View File

@ -47,7 +47,7 @@ define(
* @param $interval Angular's $interval service
* @param {string} space the name of the persistence space being served
* @param {string} root the root of the path to ElasticSearch
* @param {stirng} path the path to domain objects within ElasticSearch
* @param {string} path the path to domain objects within ElasticSearch
*/
function ElasticPersistenceProvider($http, $q, space, root, path) {
this.spaces = [space];

View File

@ -136,7 +136,7 @@ define([
* @memberof module:openmct.MCT#
* @name conductor
*/
this.time = new api.TimeAPI();
this.time = new api.TimeAPI(this);
/**
* An interface for interacting with the composition of domain objects.
@ -287,6 +287,9 @@ define([
this.install(this.plugins.ViewLargeAction());
this.install(this.plugins.ObjectInterceptors());
this.install(this.plugins.NonEditableFolder());
this.install(this.plugins.DeviceClassifier());
this._isVue = true;
}
MCT.prototype = Object.create(EventEmitter.prototype);

View File

@ -81,14 +81,8 @@ define([
return models;
}
return this.apiFetch(missingIds)
.then(function (apiResults) {
Object.keys(apiResults).forEach(function (k) {
models[k] = apiResults[k];
});
return models;
});
//Temporary fix for missing models - don't retry using this.apiFetch
return models;
}.bind(this));
};

View File

@ -60,9 +60,7 @@ class ActionsAPI extends EventEmitter {
}
_getCachedActionCollection(objectPath, view) {
let cachedActionCollection = this._actionCollections.get(view);
return cachedActionCollection;
return this._actionCollections.get(view);
}
_newActionCollection(objectPath, view, skipEnvironmentObservers) {
@ -112,7 +110,7 @@ class ActionsAPI extends EventEmitter {
return actionsObject;
}
_groupAndSortActions(actionsArray) {
_groupAndSortActions(actionsArray = []) {
if (!Array.isArray(actionsArray) && typeof actionsArray === 'object') {
actionsArray = Object.keys(actionsArray).map(key => actionsArray[key]);
}

View File

@ -46,7 +46,7 @@ define([
StatusAPI
) {
return {
TimeAPI: TimeAPI,
TimeAPI: TimeAPI.default,
ObjectAPI: ObjectAPI,
CompositionAPI: CompositionAPI,
TypeRegistry: TypeRegistry,

View File

@ -42,7 +42,7 @@ import EventEmitter from 'EventEmitter';
*
* @typedef {object} NotificationModel
* @property {string} message The message to be displayed by the notification
* @property {number | 'unknown'} [progress] The progres of some ongoing task. Should be a number between 0 and 100, or
* @property {number | 'unknown'} [progress] The progress of some ongoing task. Should be a number between 0 and 100, or
* with the string literal 'unknown'.
* @property {string} [progressText] A message conveying progress of some ongoing task.
@ -98,7 +98,7 @@ export default class NotificationAPI extends EventEmitter {
* Present an alert to the user.
* @param {string} message The message to display to the user.
* @param {Object} [options] object with following properties
* autoDismissTimeout: {number} in miliseconds to automatically dismisses notification
* autoDismissTimeout: {number} in milliseconds to automatically dismisses notification
* link: {Object} Add a link to notifications for navigation
* onClick: callback function
* cssClass: css class name to add style on link
@ -119,7 +119,7 @@ export default class NotificationAPI extends EventEmitter {
* Present an error message to the user
* @param {string} message
* @param {Object} [options] object with following properties
* autoDismissTimeout: {number} in miliseconds to automatically dismisses notification
* autoDismissTimeout: {number} in milliseconds to automatically dismisses notification
* link: {Object} Add a link to notifications for navigation
* onClick: callback function
* cssClass: css class name to add style on link

View File

@ -0,0 +1,2 @@
export default class ConflictError extends Error {
}

View File

@ -26,6 +26,7 @@ import RootRegistry from './RootRegistry';
import RootObjectProvider from './RootObjectProvider';
import EventEmitter from 'EventEmitter';
import InterceptorRegistry from './InterceptorRegistry';
import ConflictError from './ConflictError';
/**
* Utilities for loading, saving, and manipulating domain objects.
@ -34,6 +35,7 @@ import InterceptorRegistry from './InterceptorRegistry';
*/
function ObjectAPI(typeRegistry, openmct) {
this.openmct = openmct;
this.typeRegistry = typeRegistry;
this.eventEmitter = new EventEmitter();
this.providers = {};
@ -47,6 +49,10 @@ function ObjectAPI(typeRegistry, openmct) {
this.interceptorRegistry = new InterceptorRegistry();
this.SYNCHRONIZED_OBJECT_TYPES = ['notebook', 'plan'];
this.errors = {
Conflict: ConflictError
};
}
/**
@ -181,8 +187,17 @@ ObjectAPI.prototype.get = function (identifier, abortSignal) {
let objectPromise = provider.get(identifier, abortSignal).then(result => {
delete this.cache[keystring];
result = this.applyGetInterceptors(identifier, result);
return result;
}).catch((result) => {
console.warn(`Failed to retrieve ${keystring}:`, result);
delete this.cache[keystring];
result = this.applyGetInterceptors(identifier);
return result;
});
@ -285,6 +300,7 @@ ObjectAPI.prototype.isPersistable = function (idOrKeyString) {
ObjectAPI.prototype.save = function (domainObject) {
let provider = this.getProvider(domainObject.identifier);
let savedResolve;
let savedReject;
let result;
if (!this.isPersistable(domainObject.identifier)) {
@ -294,14 +310,18 @@ ObjectAPI.prototype.save = function (domainObject) {
} else {
const persistedTime = Date.now();
if (domainObject.persisted === undefined) {
result = new Promise((resolve) => {
result = new Promise((resolve, reject) => {
savedResolve = resolve;
savedReject = reject;
});
domainObject.persisted = persistedTime;
provider.create(domainObject).then((response) => {
this.mutate(domainObject, 'persisted', persistedTime);
savedResolve(response);
});
provider.create(domainObject)
.then((response) => {
this.mutate(domainObject, 'persisted', persistedTime);
savedResolve(response);
}).catch((error) => {
savedReject(error);
});
} else {
domainObject.persisted = persistedTime;
this.mutate(domainObject, 'persisted', persistedTime);
@ -358,6 +378,20 @@ ObjectAPI.prototype.applyGetInterceptors = function (identifier, domainObject) {
return domainObject;
};
/**
* Return relative url path from a given object path
* eg: #/browse/mine/cb56f6bf-c900-43b7-b923-2e3b64b412db/6e89e858-77ce-46e4-a1ad-749240286497/....
* @param {Array} objectPath
* @returns {string} relative url for object
*/
ObjectAPI.prototype.getRelativePath = function (objectPath) {
return objectPath
.map(p => this.makeKeyString(p.identifier))
.reverse()
.join('/')
;
};
/**
* Modify a domain object.
* @param {module:openmct.DomainObject} object the object to mutate

View File

@ -10,28 +10,37 @@ const cssClasses = {
};
class Overlay extends EventEmitter {
constructor(options) {
constructor({
buttons,
autoHide = true,
dismissable = true,
element,
onDestroy,
size
} = {}) {
super();
this.dismissable = options.dismissable !== false;
this.container = document.createElement('div');
this.container.classList.add('l-overlay-wrapper', cssClasses[options.size]);
this.container.classList.add('l-overlay-wrapper', cssClasses[size]);
this.autoHide = autoHide;
this.dismissable = dismissable !== false;
this.component = new Vue({
provide: {
dismiss: this.dismiss.bind(this),
element: options.element,
buttons: options.buttons,
dismissable: this.dismissable
},
components: {
OverlayComponent: OverlayComponent
},
provide: {
dismiss: this.dismiss.bind(this),
element,
buttons,
dismissable: this.dismissable
},
template: '<overlay-component></overlay-component>'
});
if (options.onDestroy) {
this.once('destroy', options.onDestroy);
if (onDestroy) {
this.once('destroy', onDestroy);
}
}

View File

@ -30,7 +30,10 @@ class OverlayAPI {
*/
showOverlay(overlay) {
if (this.activeOverlays.length) {
this.activeOverlays[this.activeOverlays.length - 1].container.classList.add('invisible');
const previousOverlay = this.activeOverlays[this.activeOverlays.length - 1];
if (previousOverlay.autoHide) {
previousOverlay.container.classList.add('invisible');
}
}
this.activeOverlays.push(overlay);
@ -60,7 +63,7 @@ class OverlayAPI {
* A description of option properties that can be passed into the overlay
* @typedef options
* @property {object} element DOMElement that is to be inserted/shown on the overlay
* @property {string} size prefered size of the overlay (large, small, fit)
* @property {string} size preferred size of the overlay (large, small, fit)
* @property {array} buttons optional button objects with label and callback properties
* @property {function} onDestroy callback to be called when overlay is destroyed
* @property {boolean} dismissable allow user to dismiss overlay by using esc, and clicking away

View File

@ -12,7 +12,7 @@
></button>
<div
ref="element"
class="c-overlay__contents"
class="c-overlay__contents js-notebook-snapshot-item-wrapper"
tabindex="0"
></div>
<div

View File

@ -20,6 +20,8 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
const { TelemetryCollection } = require("./TelemetryCollection");
define([
'../../plugins/displayLayout/CustomStringFormatter',
'./TelemetryMetadataManager',
@ -273,6 +275,28 @@ define([
}
};
/**
* Request telemetry collection for a domain object.
* The `options` argument allows you to specify filters
* (start, end, etc.), sort order, and strategies for retrieving
* telemetry (aggregation, latest available, etc.).
*
* @method requestCollection
* @memberof module:openmct.TelemetryAPI~TelemetryProvider#
* @param {module:openmct.DomainObject} domainObject the object
* which has associated telemetry
* @param {module:openmct.TelemetryAPI~TelemetryRequest} options
* options for this telemetry collection request
* @returns {TelemetryCollection} a TelemetryCollection instance
*/
TelemetryAPI.prototype.requestCollection = function (domainObject, options = {}) {
return new TelemetryCollection(
this.openmct,
domainObject,
options
);
};
/**
* Request historical telemetry for a domain object.
* The `options` argument allows you to specify filters
@ -459,6 +483,10 @@ define([
* @returns {Object<String, {TelemetryValueFormatter}>}
*/
TelemetryAPI.prototype.getFormatMap = function (metadata) {
if (!metadata) {
return {};
}
if (!this.formatMapCache.has(metadata)) {
const formatMap = metadata.values().reduce(function (map, valueMetadata) {
map[valueMetadata.key] = this.getValueFormatter(valueMetadata);

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,402 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import _ from 'lodash';
import EventEmitter from 'EventEmitter';
const ERRORS = {
TIMESYSTEM_KEY: 'All telemetry metadata must have a telemetry value with a key that matches the key of the active time system.',
LOADED: 'Telemetry Collection has already been loaded.'
};
/** Class representing a Telemetry Collection. */
export class TelemetryCollection extends EventEmitter {
/**
* Creates a Telemetry Collection
*
* @param {object} openmct - Openm MCT
* @param {object} domainObject - Domain Object to user for telemetry collection
* @param {object} options - Any options passed in for request/subscribe
*/
constructor(openmct, domainObject, options) {
super();
this.loaded = false;
this.openmct = openmct;
this.domainObject = domainObject;
this.boundedTelemetry = [];
this.futureBuffer = [];
this.parseTime = undefined;
this.metadata = this.openmct.telemetry.getMetadata(domainObject);
this.unsubscribe = undefined;
this.historicalProvider = undefined;
this.options = options;
this.pageState = undefined;
this.lastBounds = undefined;
this.requestAbort = undefined;
}
/**
* This will start the requests for historical and realtime data,
* as well as setting up initial values and watchers
*/
load() {
if (this.loaded) {
this._error(ERRORS.LOADED);
}
this._timeSystem(this.openmct.time.timeSystem());
this.lastBounds = this.openmct.time.bounds();
this._watchBounds();
this._watchTimeSystem();
this._initiateHistoricalRequests();
this._initiateSubscriptionTelemetry();
this.loaded = true;
}
/**
* can/should be called by the requester of the telemetry collection
* to remove any listeners
*/
destroy() {
if (this.requestAbort) {
this.requestAbort.abort();
}
this._unwatchBounds();
this._unwatchTimeSystem();
if (this.unsubscribe) {
this.unsubscribe();
}
this.removeAllListeners();
}
/**
* This will start the requests for historical and realtime data,
* as well as setting up initial values and watchers
*/
getAll() {
return this.boundedTelemetry;
}
/**
* Sets up the telemetry collection for historical requests,
* this uses the "standardizeRequestOptions" from Telemetry API
* @private
*/
_initiateHistoricalRequests() {
this.openmct.telemetry.standardizeRequestOptions(this.options);
this.historicalProvider = this.openmct.telemetry.
findRequestProvider(this.domainObject, this.options);
this._requestHistoricalTelemetry();
}
/**
* If a historical provider exists, then historical requests will be made
* @private
*/
async _requestHistoricalTelemetry() {
if (!this.historicalProvider) {
return;
}
let historicalData;
this.options.onPartialResponse = this._processNewTelemetry.bind(this);
try {
if (this.requestAbort) {
this.requestAbort.abort();
}
this.requestAbort = new AbortController();
this.options.signal = this.requestAbort.signal;
this.emit('requestStarted');
historicalData = await this.historicalProvider.request(this.domainObject, this.options);
} catch (error) {
if (error.name !== 'AbortError') {
console.error('Error requesting telemetry data...');
this._error(error);
}
}
this.emit('requestEnded');
this.requestAbort = undefined;
this._processNewTelemetry(historicalData);
}
/**
* This uses the built in subscription function from Telemetry API
* @private
*/
_initiateSubscriptionTelemetry() {
if (this.unsubscribe) {
this.unsubscribe();
}
this.unsubscribe = this.openmct.telemetry
.subscribe(
this.domainObject,
datum => this._processNewTelemetry(datum),
this.options
);
}
/**
* Filter any new telemetry (add/page, historical, subscription) based on
* time bounds and dupes
*
* @param {(Object|Object[])} telemetryData - telemetry data object or
* array of telemetry data objects
* @private
*/
_processNewTelemetry(telemetryData) {
let data = Array.isArray(telemetryData) ? telemetryData : [telemetryData];
let parsedValue;
let beforeStartOfBounds;
let afterEndOfBounds;
let added = [];
for (let datum of data) {
parsedValue = this.parseTime(datum);
beforeStartOfBounds = parsedValue < this.lastBounds.start;
afterEndOfBounds = parsedValue > this.lastBounds.end;
if (!afterEndOfBounds && !beforeStartOfBounds) {
let isDuplicate = false;
let startIndex = this._sortedIndex(datum);
let endIndex = undefined;
// dupe check
if (startIndex !== this.boundedTelemetry.length) {
endIndex = _.sortedLastIndexBy(
this.boundedTelemetry,
datum,
boundedDatum => this.parseTime(boundedDatum)
);
if (endIndex > startIndex) {
let potentialDupes = this.boundedTelemetry.slice(startIndex, endIndex);
isDuplicate = potentialDupes.some(_.isEqual.bind(undefined, datum));
}
}
if (!isDuplicate) {
let index = endIndex || startIndex;
this.boundedTelemetry.splice(index, 0, datum);
added.push(datum);
}
} else if (afterEndOfBounds) {
this.futureBuffer.push(datum);
}
}
if (added.length) {
this.emit('add', added);
}
}
/**
* Finds the correct insertion point for the given telemetry datum.
* Leverages lodash's `sortedIndexBy` function which implements a binary search.
* @private
*/
_sortedIndex(datum) {
if (this.boundedTelemetry.length === 0) {
return 0;
}
let parsedValue = this.parseTime(datum);
let lastValue = this.parseTime(this.boundedTelemetry[this.boundedTelemetry.length - 1]);
if (parsedValue > lastValue || parsedValue === lastValue) {
return this.boundedTelemetry.length;
} else {
return _.sortedIndexBy(
this.boundedTelemetry,
datum,
boundedDatum => this.parseTime(boundedDatum)
);
}
}
/**
* when the start time, end time, or both have been updated.
* data could be added OR removed here we update the current
* bounded telemetry
*
* @param {TimeConductorBounds} bounds The newly updated bounds
* @param {boolean} [tick] `true` if the bounds update was due to
* a "tick" event (ie. was an automatic update), false otherwise.
* @private
*/
_bounds(bounds, isTick) {
let startChanged = this.lastBounds.start !== bounds.start;
let endChanged = this.lastBounds.end !== bounds.end;
this.lastBounds = bounds;
if (isTick) {
// need to check futureBuffer and need to check
// if anything has fallen out of bounds
let startIndex = 0;
let endIndex = 0;
let discarded = [];
let added = [];
let testDatum = {};
if (startChanged) {
testDatum[this.timeKey] = bounds.start;
// Calculate the new index of the first item within the bounds
startIndex = _.sortedIndexBy(
this.boundedTelemetry,
testDatum,
datum => this.parseTime(datum)
);
discarded = this.boundedTelemetry.splice(0, startIndex);
}
if (endChanged) {
testDatum[this.timeKey] = bounds.end;
// Calculate the new index of the last item in bounds
endIndex = _.sortedLastIndexBy(
this.futureBuffer,
testDatum,
datum => this.parseTime(datum)
);
added = this.futureBuffer.splice(0, endIndex);
this.boundedTelemetry = [...this.boundedTelemetry, ...added];
}
if (discarded.length > 0) {
this.emit('remove', discarded);
}
if (added.length > 0) {
this.emit('add', added);
}
} else {
// user bounds change, reset
this._reset();
}
}
/**
* whenever the time system is updated need to update related values in
* the Telemetry Collection and reset the telemetry collection
*
* @param {TimeSystem} timeSystem - the value of the currently applied
* Time System
* @private
*/
_timeSystem(timeSystem) {
let domains = this.metadata.valuesForHints(['domain']);
let domain = domains.find((d) => d.key === timeSystem.key);
if (domain === undefined) {
this._error(ERRORS.TIMESYSTEM_KEY);
}
// timeKey is used to create a dummy datum used for sorting
this.timeKey = domain.source; // this defaults to key if no source is set
let metadataValue = this.metadata.value(timeSystem.key) || { format: timeSystem.key };
let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);
this.parseTime = (datum) => {
return valueFormatter.parse(datum);
};
this._reset();
}
/**
* Reset the telemetry data of the collection, and re-request
* historical telemetry
* @private
*
* @todo handle subscriptions more granually
*/
_reset() {
this.boundedTelemetry = [];
this.futureBuffer = [];
this.emit('clear');
this._requestHistoricalTelemetry();
}
/**
* adds the _bounds callback to the 'bounds' timeAPI listener
* @private
*/
_watchBounds() {
this.openmct.time.on('bounds', this._bounds, this);
}
/**
* removes the _bounds callback from the 'bounds' timeAPI listener
* @private
*/
_unwatchBounds() {
this.openmct.time.off('bounds', this._bounds, this);
}
/**
* adds the _timeSystem callback to the 'timeSystem' timeAPI listener
* @private
*/
_watchTimeSystem() {
this.openmct.time.on('timeSystem', this._timeSystem, this);
}
/**
* removes the _timeSystem callback from the 'timeSystem' timeAPI listener
* @private
*/
_unwatchTimeSystem() {
this.openmct.time.off('timeSystem', this._timeSystem, this);
}
/**
* will throw a new Error, for passed in message
* @param {string} message Message describing the error
* @private
*/
_error(message) {
throw new Error(message);
}
}

View File

@ -0,0 +1,106 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import TimeContext from "./TimeContext";
/**
* The GlobalContext handles getting and setting time of the openmct application in general.
* Views will use this context unless they specify an alternate/independent time context
*/
class GlobalTimeContext extends TimeContext {
constructor() {
super();
//The Time Of Interest
this.toi = undefined;
}
/**
* Get or set the start and end time of the time conductor. Basic validation
* of bounds is performed.
*
* @param {module:openmct.TimeAPI~TimeConductorBounds} newBounds
* @throws {Error} Validation error
* @fires module:openmct.TimeAPI~bounds
* @returns {module:openmct.TimeAPI~TimeConductorBounds}
* @memberof module:openmct.TimeAPI#
* @method bounds
*/
bounds(newBounds) {
if (arguments.length > 0) {
super.bounds.call(this, ...arguments);
// If a bounds change results in a TOI outside of the current
// bounds, unset it
if (this.toi < newBounds.start || this.toi > newBounds.end) {
this.timeOfInterest(undefined);
}
}
//Return a copy to prevent direct mutation of time conductor bounds.
return JSON.parse(JSON.stringify(this.boundsVal));
}
/**
* Update bounds based on provided time and current offsets
* @private
* @param {number} timestamp A time from which bounds will be calculated
* using current offsets.
*/
tick(timestamp) {
super.tick.call(this, ...arguments);
// If a bounds change results in a TOI outside of the current
// bounds, unset it
if (this.toi < this.boundsVal.start || this.toi > this.boundsVal.end) {
this.timeOfInterest(undefined);
}
}
/**
* Get or set the Time of Interest. The Time of Interest is a single point
* in time, and constitutes the temporal focus of application views. It can
* be manipulated by the user from the time conductor or from other views.
* The time of interest can effectively be unset by assigning a value of
* 'undefined'.
* @fires module:openmct.TimeAPI~timeOfInterest
* @param newTOI
* @returns {number} the current time of interest
* @memberof module:openmct.TimeAPI#
* @method timeOfInterest
*/
timeOfInterest(newTOI) {
if (arguments.length > 0) {
this.toi = newTOI;
/**
* The Time of Interest has moved.
* @event timeOfInterest
* @memberof module:openmct.TimeAPI~
* @property {number} Current time of interest
*/
this.emit('timeOfInterest', this.toi);
}
return this.toi;
}
}
export default GlobalTimeContext;

View File

@ -0,0 +1,94 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import TimeContext from "./TimeContext";
/**
* The IndependentTimeContext handles getting and setting time of the openmct application in general.
* Views will use the GlobalTimeContext unless they specify an alternate/independent time context here.
*/
class IndependentTimeContext extends TimeContext {
constructor(globalTimeContext, key) {
super();
this.key = key;
this.globalTimeContext = globalTimeContext;
}
/**
* Set the active clock. Tick source will be immediately subscribed to
* and ticking will begin. Offsets from 'now' must also be provided. A clock
* can be unset by calling {@link stopClock}.
*
* @param {Clock || string} keyOrClock The clock to activate, or its key
* @param {ClockOffsets} offsets on each tick these will be used to calculate
* the start and end bounds. This maintains a sliding time window of a fixed
* width that automatically updates.
* @fires module:openmct.TimeAPI~clock
* @return {Clock} the currently active clock;
*/
clock(keyOrClock, offsets) {
if (arguments.length === 2) {
let clock;
if (typeof keyOrClock === 'string') {
clock = this.globalTimeContext.clocks.get(keyOrClock);
if (clock === undefined) {
throw "Unknown clock '" + keyOrClock + "'. Has it been registered with 'addClock'?";
}
} else if (typeof keyOrClock === 'object') {
clock = keyOrClock;
if (!this.globalTimeContext.clocks.has(clock.key)) {
throw "Unknown clock '" + keyOrClock.key + "'. Has it been registered with 'addClock'?";
}
}
const previousClock = this.activeClock;
if (previousClock !== undefined) {
previousClock.off("tick", this.tick);
}
this.activeClock = clock;
/**
* The active clock has changed. Clock can be unset by calling {@link stopClock}
* @event clock
* @memberof module:openmct.TimeAPI~
* @property {Clock} clock The newly activated clock, or undefined
* if the system is no longer following a clock source
*/
this.emit("clock", this.activeClock);
if (this.activeClock !== undefined) {
this.clockOffsets(offsets);
this.activeClock.on("tick", this.tick);
}
} else if (arguments.length === 1) {
throw "When setting the clock, clock offsets must also be provided";
}
return this.activeClock;
}
}
export default IndependentTimeContext;

View File

@ -20,51 +20,35 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(['EventEmitter'], function (EventEmitter) {
/**
* The public API for setting and querying the temporal state of the
* application. The concept of time is integral to Open MCT, and at least
* one {@link TimeSystem}, as well as some default time bounds must be
* registered and enabled via {@link TimeAPI.addTimeSystem} and
* {@link TimeAPI.timeSystem} respectively for Open MCT to work.
*
* Time-sensitive views will typically respond to changes to bounds or other
* properties of the time conductor and update the data displayed based on
* the temporal state of the application. The current time bounds are also
* used in queries for historical data.
*
* The TimeAPI extends the EventEmitter class. A number of events are
* fired when properties of the time conductor change, which are documented
* below.
*
* @interface
* @memberof module:openmct
*/
function TimeAPI() {
EventEmitter.call(this);
//The Time System
this.system = undefined;
//The Time Of Interest
this.toi = undefined;
this.boundsVal = {
start: undefined,
end: undefined
};
this.timeSystems = new Map();
this.clocks = new Map();
this.activeClock = undefined;
this.offsets = undefined;
this.tick = this.tick.bind(this);
import GlobalTimeContext from "./GlobalTimeContext";
import IndependentTimeContext from "@/api/time/IndependentTimeContext";
/**
* The public API for setting and querying the temporal state of the
* application. The concept of time is integral to Open MCT, and at least
* one {@link TimeSystem}, as well as some default time bounds must be
* registered and enabled via {@link TimeAPI.addTimeSystem} and
* {@link TimeAPI.timeSystem} respectively for Open MCT to work.
*
* Time-sensitive views will typically respond to changes to bounds or other
* properties of the time conductor and update the data displayed based on
* the temporal state of the application. The current time bounds are also
* used in queries for historical data.
*
* The TimeAPI extends the GlobalTimeContext which in turn extends the TimeContext/EventEmitter class. A number of events are
* fired when properties of the time conductor change, which are documented
* below.
*
* @interface
* @memberof module:openmct
*/
class TimeAPI extends GlobalTimeContext {
constructor(openmct) {
super();
this.openmct = openmct;
this.independentContexts = new Map();
}
TimeAPI.prototype = Object.create(EventEmitter.prototype);
/**
* A TimeSystem provides meaning to the values returned by the TimeAPI. Open
* MCT supports multiple different types of time values, although all are
@ -94,16 +78,16 @@ define(['EventEmitter'], function (EventEmitter) {
* @memberof module:openmct.TimeAPI#
* @param {TimeSystem} timeSystem A time system object.
*/
TimeAPI.prototype.addTimeSystem = function (timeSystem) {
addTimeSystem(timeSystem) {
this.timeSystems.set(timeSystem.key, timeSystem);
};
}
/**
* @returns {TimeSystem[]}
*/
TimeAPI.prototype.getAllTimeSystems = function () {
getAllTimeSystems() {
return Array.from(this.timeSystems.values());
};
}
/**
* Clocks provide a timing source that is used to
@ -126,340 +110,81 @@ define(['EventEmitter'], function (EventEmitter) {
* @memberof module:openmct.TimeAPI#
* @param {Clock} clock
*/
TimeAPI.prototype.addClock = function (clock) {
addClock(clock) {
this.clocks.set(clock.key, clock);
};
}
/**
* @memberof module:openmct.TimeAPI#
* @returns {Clock[]}
* @memberof module:openmct.TimeAPI#
*/
TimeAPI.prototype.getAllClocks = function () {
getAllClocks() {
return Array.from(this.clocks.values());
};
}
/**
* Validate the given bounds. This can be used for pre-validation of bounds,
* for example by views validating user inputs.
* @param {TimeBounds} bounds The start and end time of the conductor.
* @returns {string | true} A validation error, or true if valid
* Get or set an independent time context which follows the TimeAPI timeSystem,
* but with different offsets for a given domain object
* @param {key | string} key The identifier key of the domain object these offsets are set for
* @param {ClockOffsets | TimeBounds} value This maintains a sliding time window of a fixed width that automatically updates
* @param {key | string} clockKey the real time clock key currently in use
* @memberof module:openmct.TimeAPI#
* @method validateBounds
* @method addIndependentTimeContext
*/
TimeAPI.prototype.validateBounds = function (bounds) {
if ((bounds.start === undefined)
|| (bounds.end === undefined)
|| isNaN(bounds.start)
|| isNaN(bounds.end)
) {
return "Start and end must be specified as integer values";
} else if (bounds.start > bounds.end) {
return "Specified start date exceeds end bound";
addIndependentContext(key, value, clockKey) {
let timeContext = this.independentContexts.get(key);
if (!timeContext) {
timeContext = new IndependentTimeContext(this, key);
this.independentContexts.set(key, timeContext);
}
return true;
};
/**
* Validate the given offsets. This can be used for pre-validation of
* offsets, for example by views validating user inputs.
* @param {ClockOffsets} offsets The start and end offsets from a 'now' value.
* @returns {string | true} A validation error, or true if valid
* @memberof module:openmct.TimeAPI#
* @method validateBounds
*/
TimeAPI.prototype.validateOffsets = function (offsets) {
if ((offsets.start === undefined)
|| (offsets.end === undefined)
|| isNaN(offsets.start)
|| isNaN(offsets.end)
) {
return "Start and end offsets must be specified as integer values";
} else if (offsets.start >= offsets.end) {
return "Specified start offset must be < end offset";
if (clockKey) {
timeContext.clock(clockKey, value);
} else {
timeContext.stopClock();
timeContext.bounds(value);
}
return true;
};
this.emit('timeContext', key);
/**
* @typedef {Object} TimeBounds
* @property {number} start The start time displayed by the time conductor
* in ms since epoch. Epoch determined by currently active time system
* @property {number} end The end time displayed by the time conductor in ms
* since epoch.
* @memberof module:openmct.TimeAPI~
*/
/**
* Get or set the start and end time of the time conductor. Basic validation
* of bounds is performed.
*
* @param {module:openmct.TimeAPI~TimeConductorBounds} newBounds
* @throws {Error} Validation error
* @fires module:openmct.TimeAPI~bounds
* @returns {module:openmct.TimeAPI~TimeConductorBounds}
* @memberof module:openmct.TimeAPI#
* @method bounds
*/
TimeAPI.prototype.bounds = function (newBounds) {
if (arguments.length > 0) {
const validationResult = this.validateBounds(newBounds);
if (validationResult !== true) {
throw new Error(validationResult);
}
//Create a copy to avoid direct mutation of conductor bounds
this.boundsVal = JSON.parse(JSON.stringify(newBounds));
/**
* The start time, end time, or both have been updated.
* @event bounds
* @memberof module:openmct.TimeAPI~
* @property {TimeConductorBounds} bounds The newly updated bounds
* @property {boolean} [tick] `true` if the bounds update was due to
* a "tick" event (ie. was an automatic update), false otherwise.
*/
this.emit('bounds', this.boundsVal, false);
// If a bounds change results in a TOI outside of the current
// bounds, unset it
if (this.toi < newBounds.start || this.toi > newBounds.end) {
this.timeOfInterest(undefined);
}
}
//Return a copy to prevent direct mutation of time conductor bounds.
return JSON.parse(JSON.stringify(this.boundsVal));
};
/**
* Get or set the time system of the TimeAPI.
* @param {TimeSystem | string} timeSystem
* @param {module:openmct.TimeAPI~TimeConductorBounds} bounds
* @fires module:openmct.TimeAPI~timeSystem
* @returns {TimeSystem} The currently applied time system
* @memberof module:openmct.TimeAPI#
* @method timeSystem
*/
TimeAPI.prototype.timeSystem = function (timeSystemOrKey, bounds) {
if (arguments.length >= 1) {
if (arguments.length === 1 && !this.activeClock) {
throw new Error(
"Must specify bounds when changing time system without "
+ "an active clock."
);
}
let timeSystem;
if (timeSystemOrKey === undefined) {
throw "Please provide a time system";
}
if (typeof timeSystemOrKey === 'string') {
timeSystem = this.timeSystems.get(timeSystemOrKey);
if (timeSystem === undefined) {
throw "Unknown time system " + timeSystemOrKey + ". Has it been registered with 'addTimeSystem'?";
}
} else if (typeof timeSystemOrKey === 'object') {
timeSystem = timeSystemOrKey;
if (!this.timeSystems.has(timeSystem.key)) {
throw "Unknown time system " + timeSystem.key + ". Has it been registered with 'addTimeSystem'?";
}
} else {
throw "Attempt to set invalid time system in Time API. Please provide a previously registered time system object or key";
}
this.system = timeSystem;
/**
* The time system used by the time
* conductor has changed. A change in Time System will always be
* followed by a bounds event specifying new query bounds.
*
* @event module:openmct.TimeAPI~timeSystem
* @property {TimeSystem} The value of the currently applied
* Time System
* */
this.emit('timeSystem', this.system);
if (bounds) {
this.bounds(bounds);
}
}
return this.system;
};
/**
* Get or set the Time of Interest. The Time of Interest is a single point
* in time, and constitutes the temporal focus of application views. It can
* be manipulated by the user from the time conductor or from other views.
* The time of interest can effectively be unset by assigning a value of
* 'undefined'.
* @fires module:openmct.TimeAPI~timeOfInterest
* @param newTOI
* @returns {number} the current time of interest
* @memberof module:openmct.TimeAPI#
* @method timeOfInterest
*/
TimeAPI.prototype.timeOfInterest = function (newTOI) {
if (arguments.length > 0) {
this.toi = newTOI;
/**
* The Time of Interest has moved.
* @event timeOfInterest
* @memberof module:openmct.TimeAPI~
* @property {number} Current time of interest
*/
this.emit('timeOfInterest', this.toi);
}
return this.toi;
};
/**
* Update bounds based on provided time and current offsets
* @private
* @param {number} timestamp A time from which boudns will be calculated
* using current offsets.
*/
TimeAPI.prototype.tick = function (timestamp) {
const newBounds = {
start: timestamp + this.offsets.start,
end: timestamp + this.offsets.end
return () => {
this.independentContexts.delete(key);
timeContext.emit('timeContext', key);
};
this.boundsVal = newBounds;
this.emit('bounds', this.boundsVal, true);
// If a bounds change results in a TOI outside of the current
// bounds, unset it
if (this.toi < newBounds.start || this.toi > newBounds.end) {
this.timeOfInterest(undefined);
}
};
}
/**
* Set the active clock. Tick source will be immediately subscribed to
* and ticking will begin. Offsets from 'now' must also be provided. A clock
* can be unset by calling {@link stopClock}.
*
* @param {Clock || string} The clock to activate, or its key
* @param {ClockOffsets} offsets on each tick these will be used to calculate
* the start and end bounds. This maintains a sliding time window of a fixed
* width that automatically updates.
* @fires module:openmct.TimeAPI~clock
* @return {Clock} the currently active clock;
* Get the independent time context which follows the TimeAPI timeSystem,
* but with different offsets.
* @param {key | string} key The identifier key of the domain object these offsets
* @memberof module:openmct.TimeAPI#
* @method getIndependentTimeContext
*/
TimeAPI.prototype.clock = function (keyOrClock, offsets) {
if (arguments.length === 2) {
let clock;
if (typeof keyOrClock === 'string') {
clock = this.clocks.get(keyOrClock);
if (clock === undefined) {
throw "Unknown clock '" + keyOrClock + "'. Has it been registered with 'addClock'?";
}
} else if (typeof keyOrClock === 'object') {
clock = keyOrClock;
if (!this.clocks.has(clock.key)) {
throw "Unknown clock '" + keyOrClock.key + "'. Has it been registered with 'addClock'?";
}
}
const previousClock = this.activeClock;
if (previousClock !== undefined) {
previousClock.off("tick", this.tick);
}
this.activeClock = clock;
/**
* The active clock has changed. Clock can be unset by calling {@link stopClock}
* @event clock
* @memberof module:openmct.TimeAPI~
* @property {Clock} clock The newly activated clock, or undefined
* if the system is no longer following a clock source
*/
this.emit("clock", this.activeClock);
if (this.activeClock !== undefined) {
this.clockOffsets(offsets);
this.activeClock.on("tick", this.tick);
}
} else if (arguments.length === 1) {
throw "When setting the clock, clock offsets must also be provided";
}
return this.activeClock;
};
getIndependentContext(key) {
return this.independentContexts.get(key);
}
/**
* Clock offsets are used to calculate temporal bounds when the system is
* ticking on a clock source.
*
* @typedef {object} ClockOffsets
* @property {number} start A time span relative to the current value of the
* ticking clock, from which start bounds will be calculated. This value must
* be < 0. When a clock is active, bounds will be calculated automatically
* based on the value provided by the clock, and the defined clock offsets.
* @property {number} end A time span relative to the current value of the
* ticking clock, from which end bounds will be calculated. This value must
* be >= 0.
* Get the a timeContext for a view based on it's objectPath. If there is any object in the objectPath with an independent time context, it will be returned.
* Otherwise, the global time context will be returned.
* @param { Array } objectPath The view's objectPath
* @memberof module:openmct.TimeAPI#
* @method getContextForView
*/
/**
* Get or set the currently applied clock offsets. If no parameter is provided,
* the current value will be returned. If provided, the new value will be
* used as the new clock offsets.
* @param {ClockOffsets} offsets
* @returns {ClockOffsets}
*/
TimeAPI.prototype.clockOffsets = function (offsets) {
if (arguments.length > 0) {
getContextForView(objectPath) {
let timeContext = this;
const validationResult = this.validateOffsets(offsets);
if (validationResult !== true) {
throw new Error(validationResult);
objectPath.forEach(item => {
const key = this.openmct.objects.makeKeyString(item.identifier);
if (this.independentContexts.get(key)) {
timeContext = this.independentContexts.get(key);
}
});
this.offsets = offsets;
return timeContext;
}
const currentValue = this.activeClock.currentValue();
const newBounds = {
start: currentValue + offsets.start,
end: currentValue + offsets.end
};
}
this.bounds(newBounds);
/**
* Event that is triggered when clock offsets change.
* @event clockOffsets
* @memberof module:openmct.TimeAPI~
* @property {ClockOffsets} clockOffsets The newly activated clock
* offsets.
*/
this.emit("clockOffsets", offsets);
}
return this.offsets;
};
/**
* Stop the currently active clock from ticking, and unset it. This will
* revert all views to showing a static time frame defined by the current
* bounds.
*/
TimeAPI.prototype.stopClock = function () {
if (this.activeClock) {
this.clock(undefined, undefined);
}
};
return TimeAPI;
});
export default TimeAPI;

View File

@ -19,241 +19,243 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import TimeAPI from "./TimeAPI";
import {createOpenMct} from "utils/testing";
define(['./TimeAPI'], function (TimeAPI) {
describe("The Time API", function () {
let api;
let timeSystemKey;
let timeSystem;
let clockKey;
let clock;
let bounds;
let eventListener;
let toi;
describe("The Time API", function () {
let api;
let timeSystemKey;
let timeSystem;
let clockKey;
let clock;
let bounds;
let eventListener;
let toi;
let openmct;
beforeEach(function () {
openmct = createOpenMct();
api = new TimeAPI(openmct);
timeSystemKey = "timeSystemKey";
timeSystem = {key: timeSystemKey};
clockKey = "someClockKey";
clock = jasmine.createSpyObj("clock", [
"on",
"off",
"currentValue"
]);
clock.currentValue.and.returnValue(100);
clock.key = clockKey;
bounds = {
start: 0,
end: 1
};
eventListener = jasmine.createSpy("eventListener");
toi = 111;
});
it("Supports setting and querying of time of interest", function () {
expect(api.timeOfInterest()).not.toBe(toi);
api.timeOfInterest(toi);
expect(api.timeOfInterest()).toBe(toi);
});
it("Allows setting of valid bounds", function () {
bounds = {
start: 0,
end: 1
};
expect(api.bounds()).not.toBe(bounds);
expect(api.bounds.bind(api, bounds)).not.toThrow();
expect(api.bounds()).toEqual(bounds);
});
it("Disallows setting of invalid bounds", function () {
bounds = {
start: 1,
end: 0
};
expect(api.bounds()).not.toEqual(bounds);
expect(api.bounds.bind(api, bounds)).toThrow();
expect(api.bounds()).not.toEqual(bounds);
bounds = {start: 1};
expect(api.bounds()).not.toEqual(bounds);
expect(api.bounds.bind(api, bounds)).toThrow();
expect(api.bounds()).not.toEqual(bounds);
});
it("Allows setting of previously registered time system with bounds", function () {
api.addTimeSystem(timeSystem);
expect(api.timeSystem()).not.toBe(timeSystem);
expect(function () {
api.timeSystem(timeSystem, bounds);
}).not.toThrow();
expect(api.timeSystem()).toBe(timeSystem);
});
it("Disallows setting of time system without bounds", function () {
api.addTimeSystem(timeSystem);
expect(api.timeSystem()).not.toBe(timeSystem);
expect(function () {
api.timeSystem(timeSystemKey);
}).toThrow();
expect(api.timeSystem()).not.toBe(timeSystem);
});
it("allows setting of timesystem without bounds with clock", function () {
api.addTimeSystem(timeSystem);
api.addClock(clock);
api.clock(clockKey, {
start: 0,
end: 1
});
expect(api.timeSystem()).not.toBe(timeSystem);
expect(function () {
api.timeSystem(timeSystemKey);
}).not.toThrow();
expect(api.timeSystem()).toBe(timeSystem);
});
it("Emits an event when time system changes", function () {
api.addTimeSystem(timeSystem);
expect(eventListener).not.toHaveBeenCalled();
api.on("timeSystem", eventListener);
api.timeSystem(timeSystemKey, bounds);
expect(eventListener).toHaveBeenCalledWith(timeSystem);
});
it("Emits an event when time of interest changes", function () {
expect(eventListener).not.toHaveBeenCalled();
api.on("timeOfInterest", eventListener);
api.timeOfInterest(toi);
expect(eventListener).toHaveBeenCalledWith(toi);
});
it("Emits an event when bounds change", function () {
expect(eventListener).not.toHaveBeenCalled();
api.on("bounds", eventListener);
api.bounds(bounds);
expect(eventListener).toHaveBeenCalledWith(bounds, false);
});
it("If bounds are set and TOI lies inside them, do not change TOI", function () {
api.timeOfInterest(6);
api.bounds({
start: 1,
end: 10
});
expect(api.timeOfInterest()).toEqual(6);
});
it("If bounds are set and TOI lies outside them, reset TOI", function () {
api.timeOfInterest(11);
api.bounds({
start: 1,
end: 10
});
expect(api.timeOfInterest()).toBeUndefined();
});
it("Maintains delta during tick", function () {
});
it("Allows registered time system to be activated", function () {
});
it("Allows a registered tick source to be activated", function () {
const mockTickSource = jasmine.createSpyObj("mockTickSource", [
"on",
"off",
"currentValue"
]);
mockTickSource.key = 'mockTickSource';
});
describe(" when enabling a tick source", function () {
let mockTickSource;
let anotherMockTickSource;
const mockOffsets = {
start: 0,
end: 1
};
beforeEach(function () {
api = new TimeAPI();
timeSystemKey = "timeSystemKey";
timeSystem = {key: timeSystemKey};
clockKey = "someClockKey";
clock = jasmine.createSpyObj("clock", [
mockTickSource = jasmine.createSpyObj("clock", [
"on",
"off",
"currentValue"
]);
clock.currentValue.and.returnValue(100);
clock.key = clockKey;
bounds = {
start: 0,
end: 1
};
eventListener = jasmine.createSpy("eventListener");
toi = 111;
});
it("Supports setting and querying of time of interest", function () {
expect(api.timeOfInterest()).not.toBe(toi);
api.timeOfInterest(toi);
expect(api.timeOfInterest()).toBe(toi);
});
it("Allows setting of valid bounds", function () {
bounds = {
start: 0,
end: 1
};
expect(api.bounds()).not.toBe(bounds);
expect(api.bounds.bind(api, bounds)).not.toThrow();
expect(api.bounds()).toEqual(bounds);
});
it("Disallows setting of invalid bounds", function () {
bounds = {
start: 1,
end: 0
};
expect(api.bounds()).not.toEqual(bounds);
expect(api.bounds.bind(api, bounds)).toThrow();
expect(api.bounds()).not.toEqual(bounds);
bounds = {start: 1};
expect(api.bounds()).not.toEqual(bounds);
expect(api.bounds.bind(api, bounds)).toThrow();
expect(api.bounds()).not.toEqual(bounds);
});
it("Allows setting of previously registered time system with bounds", function () {
api.addTimeSystem(timeSystem);
expect(api.timeSystem()).not.toBe(timeSystem);
expect(function () {
api.timeSystem(timeSystem, bounds);
}).not.toThrow();
expect(api.timeSystem()).toBe(timeSystem);
});
it("Disallows setting of time system without bounds", function () {
api.addTimeSystem(timeSystem);
expect(api.timeSystem()).not.toBe(timeSystem);
expect(function () {
api.timeSystem(timeSystemKey);
}).toThrow();
expect(api.timeSystem()).not.toBe(timeSystem);
});
it("allows setting of timesystem without bounds with clock", function () {
api.addTimeSystem(timeSystem);
api.addClock(clock);
api.clock(clockKey, {
start: 0,
end: 1
});
expect(api.timeSystem()).not.toBe(timeSystem);
expect(function () {
api.timeSystem(timeSystemKey);
}).not.toThrow();
expect(api.timeSystem()).toBe(timeSystem);
});
it("Emits an event when time system changes", function () {
api.addTimeSystem(timeSystem);
expect(eventListener).not.toHaveBeenCalled();
api.on("timeSystem", eventListener);
api.timeSystem(timeSystemKey, bounds);
expect(eventListener).toHaveBeenCalledWith(timeSystem);
});
it("Emits an event when time of interest changes", function () {
expect(eventListener).not.toHaveBeenCalled();
api.on("timeOfInterest", eventListener);
api.timeOfInterest(toi);
expect(eventListener).toHaveBeenCalledWith(toi);
});
it("Emits an event when bounds change", function () {
expect(eventListener).not.toHaveBeenCalled();
api.on("bounds", eventListener);
api.bounds(bounds);
expect(eventListener).toHaveBeenCalledWith(bounds, false);
});
it("If bounds are set and TOI lies inside them, do not change TOI", function () {
api.timeOfInterest(6);
api.bounds({
start: 1,
end: 10
});
expect(api.timeOfInterest()).toEqual(6);
});
it("If bounds are set and TOI lies outside them, reset TOI", function () {
api.timeOfInterest(11);
api.bounds({
start: 1,
end: 10
});
expect(api.timeOfInterest()).toBeUndefined();
});
it("Maintains delta during tick", function () {
});
it("Allows registered time system to be activated", function () {
});
it("Allows a registered tick source to be activated", function () {
const mockTickSource = jasmine.createSpyObj("mockTickSource", [
"on",
"off",
"currentValue"
]);
mockTickSource.key = 'mockTickSource';
});
describe(" when enabling a tick source", function () {
let mockTickSource;
let anotherMockTickSource;
const mockOffsets = {
start: 0,
end: 1
};
beforeEach(function () {
mockTickSource = jasmine.createSpyObj("clock", [
"on",
"off",
"currentValue"
]);
mockTickSource.currentValue.and.returnValue(10);
mockTickSource.key = "mts";
anotherMockTickSource = jasmine.createSpyObj("clock", [
"on",
"off",
"currentValue"
]);
anotherMockTickSource.key = "amts";
anotherMockTickSource.currentValue.and.returnValue(10);
api.addClock(mockTickSource);
api.addClock(anotherMockTickSource);
});
it("sets bounds based on current value", function () {
api.clock("mts", mockOffsets);
expect(api.bounds()).toEqual({
start: 10,
end: 11
});
});
it("a new tick listener is registered", function () {
api.clock("mts", mockOffsets);
expect(mockTickSource.on).toHaveBeenCalledWith("tick", jasmine.any(Function));
});
it("listener of existing tick source is reregistered", function () {
api.clock("mts", mockOffsets);
api.clock("amts", mockOffsets);
expect(mockTickSource.off).toHaveBeenCalledWith("tick", jasmine.any(Function));
});
it("Allows the active clock to be set and unset", function () {
expect(api.clock()).toBeUndefined();
api.clock("mts", mockOffsets);
expect(api.clock()).toBeDefined();
api.stopClock();
expect(api.clock()).toBeUndefined();
});
});
it("on tick, observes offsets, and indicates tick in bounds callback", function () {
const mockTickSource = jasmine.createSpyObj("clock", [
"on",
"off",
"currentValue"
]);
mockTickSource.currentValue.and.returnValue(100);
let tickCallback;
const boundsCallback = jasmine.createSpy("boundsCallback");
const clockOffsets = {
start: -100,
end: 100
};
mockTickSource.currentValue.and.returnValue(10);
mockTickSource.key = "mts";
anotherMockTickSource = jasmine.createSpyObj("clock", [
"on",
"off",
"currentValue"
]);
anotherMockTickSource.key = "amts";
anotherMockTickSource.currentValue.and.returnValue(10);
api.addClock(mockTickSource);
api.clock("mts", clockOffsets);
api.on("bounds", boundsCallback);
tickCallback = mockTickSource.on.calls.mostRecent().args[1];
tickCallback(1000);
expect(boundsCallback).toHaveBeenCalledWith({
start: 900,
end: 1100
}, true);
api.addClock(anotherMockTickSource);
});
it("sets bounds based on current value", function () {
api.clock("mts", mockOffsets);
expect(api.bounds()).toEqual({
start: 10,
end: 11
});
});
it("a new tick listener is registered", function () {
api.clock("mts", mockOffsets);
expect(mockTickSource.on).toHaveBeenCalledWith("tick", jasmine.any(Function));
});
it("listener of existing tick source is reregistered", function () {
api.clock("mts", mockOffsets);
api.clock("amts", mockOffsets);
expect(mockTickSource.off).toHaveBeenCalledWith("tick", jasmine.any(Function));
});
it("Allows the active clock to be set and unset", function () {
expect(api.clock()).toBeUndefined();
api.clock("mts", mockOffsets);
expect(api.clock()).toBeDefined();
api.stopClock();
expect(api.clock()).toBeUndefined();
});
});
it("on tick, observes offsets, and indicates tick in bounds callback", function () {
const mockTickSource = jasmine.createSpyObj("clock", [
"on",
"off",
"currentValue"
]);
mockTickSource.currentValue.and.returnValue(100);
let tickCallback;
const boundsCallback = jasmine.createSpy("boundsCallback");
const clockOffsets = {
start: -100,
end: 100
};
mockTickSource.key = "mts";
api.addClock(mockTickSource);
api.clock("mts", clockOffsets);
api.on("bounds", boundsCallback);
tickCallback = mockTickSource.on.calls.mostRecent().args[1];
tickCallback(1000);
expect(boundsCallback).toHaveBeenCalledWith({
start: 900,
end: 1100
}, true);
});
});

360
src/api/time/TimeContext.js Normal file
View File

@ -0,0 +1,360 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import EventEmitter from 'EventEmitter';
class TimeContext extends EventEmitter {
constructor() {
super();
//The Time System
this.timeSystems = new Map();
this.system = undefined;
this.clocks = new Map();
this.boundsVal = {
start: undefined,
end: undefined
};
this.activeClock = undefined;
this.offsets = undefined;
this.tick = this.tick.bind(this);
}
/**
* Get or set the time system of the TimeAPI.
* @param {TimeSystem | string} timeSystem
* @param {module:openmct.TimeAPI~TimeConductorBounds} bounds
* @fires module:openmct.TimeAPI~timeSystem
* @returns {TimeSystem} The currently applied time system
* @memberof module:openmct.TimeAPI#
* @method timeSystem
*/
timeSystem(timeSystemOrKey, bounds) {
if (arguments.length >= 1) {
if (arguments.length === 1 && !this.activeClock) {
throw new Error(
"Must specify bounds when changing time system without "
+ "an active clock."
);
}
let timeSystem;
if (timeSystemOrKey === undefined) {
throw "Please provide a time system";
}
if (typeof timeSystemOrKey === 'string') {
timeSystem = this.timeSystems.get(timeSystemOrKey);
if (timeSystem === undefined) {
throw "Unknown time system " + timeSystemOrKey + ". Has it been registered with 'addTimeSystem'?";
}
} else if (typeof timeSystemOrKey === 'object') {
timeSystem = timeSystemOrKey;
if (!this.timeSystems.has(timeSystem.key)) {
throw "Unknown time system " + timeSystem.key + ". Has it been registered with 'addTimeSystem'?";
}
} else {
throw "Attempt to set invalid time system in Time API. Please provide a previously registered time system object or key";
}
this.system = timeSystem;
/**
* The time system used by the time
* conductor has changed. A change in Time System will always be
* followed by a bounds event specifying new query bounds.
*
* @event module:openmct.TimeAPI~timeSystem
* @property {TimeSystem} The value of the currently applied
* Time System
* */
this.emit('timeSystem', this.system);
if (bounds) {
this.bounds(bounds);
}
}
return this.system;
}
/**
* Clock offsets are used to calculate temporal bounds when the system is
* ticking on a clock source.
*
* @typedef {object} ValidationResult
* @property {boolean} valid Result of the validation - true or false.
* @property {string} message An error message if valid is false.
*/
/**
* Validate the given bounds. This can be used for pre-validation of bounds,
* for example by views validating user inputs.
* @param {TimeBounds} bounds The start and end time of the conductor.
* @returns {ValidationResult} A validation error, or true if valid
* @memberof module:openmct.TimeAPI#
* @method validateBounds
*/
validateBounds(bounds) {
if ((bounds.start === undefined)
|| (bounds.end === undefined)
|| isNaN(bounds.start)
|| isNaN(bounds.end)
) {
return {
valid: false,
message: "Start and end must be specified as integer values"
};
} else if (bounds.start > bounds.end) {
return {
valid: false,
message: "Specified start date exceeds end bound"
};
}
return {
valid: true,
message: ''
};
}
/**
* Get or set the start and end time of the time conductor. Basic validation
* of bounds is performed.
*
* @param {module:openmct.TimeAPI~TimeConductorBounds} newBounds
* @throws {Error} Validation error
* @fires module:openmct.TimeAPI~bounds
* @returns {module:openmct.TimeAPI~TimeConductorBounds}
* @memberof module:openmct.TimeAPI#
* @method bounds
*/
bounds(newBounds) {
if (arguments.length > 0) {
const validationResult = this.validateBounds(newBounds);
if (validationResult.valid !== true) {
throw new Error(validationResult.message);
}
//Create a copy to avoid direct mutation of conductor bounds
this.boundsVal = JSON.parse(JSON.stringify(newBounds));
/**
* The start time, end time, or both have been updated.
* @event bounds
* @memberof module:openmct.TimeAPI~
* @property {TimeConductorBounds} bounds The newly updated bounds
* @property {boolean} [tick] `true` if the bounds update was due to
* a "tick" event (ie. was an automatic update), false otherwise.
*/
this.emit('bounds', this.boundsVal, false);
}
//Return a copy to prevent direct mutation of time conductor bounds.
return JSON.parse(JSON.stringify(this.boundsVal));
}
/**
* Validate the given offsets. This can be used for pre-validation of
* offsets, for example by views validating user inputs.
* @param {ClockOffsets} offsets The start and end offsets from a 'now' value.
* @returns { ValidationResult } A validation error, and true/false if valid or not
* @memberof module:openmct.TimeAPI#
* @method validateOffsets
*/
validateOffsets(offsets) {
if ((offsets.start === undefined)
|| (offsets.end === undefined)
|| isNaN(offsets.start)
|| isNaN(offsets.end)
) {
return {
valid: false,
message: "Start and end offsets must be specified as integer values"
};
} else if (offsets.start >= offsets.end) {
return {
valid: false,
message: "Specified start offset must be < end offset"
};
}
return {
valid: true,
message: ''
};
}
/**
* @typedef {Object} TimeBounds
* @property {number} start The start time displayed by the time conductor
* in ms since epoch. Epoch determined by currently active time system
* @property {number} end The end time displayed by the time conductor in ms
* since epoch.
* @memberof module:openmct.TimeAPI~
*/
/**
* Clock offsets are used to calculate temporal bounds when the system is
* ticking on a clock source.
*
* @typedef {object} ClockOffsets
* @property {number} start A time span relative to the current value of the
* ticking clock, from which start bounds will be calculated. This value must
* be < 0. When a clock is active, bounds will be calculated automatically
* based on the value provided by the clock, and the defined clock offsets.
* @property {number} end A time span relative to the current value of the
* ticking clock, from which end bounds will be calculated. This value must
* be >= 0.
*/
/**
* Get or set the currently applied clock offsets. If no parameter is provided,
* the current value will be returned. If provided, the new value will be
* used as the new clock offsets.
* @param {ClockOffsets} offsets
* @returns {ClockOffsets}
*/
clockOffsets(offsets) {
if (arguments.length > 0) {
const validationResult = this.validateOffsets(offsets);
if (validationResult.valid !== true) {
throw new Error(validationResult.message);
}
this.offsets = offsets;
const currentValue = this.activeClock.currentValue();
const newBounds = {
start: currentValue + offsets.start,
end: currentValue + offsets.end
};
this.bounds(newBounds);
/**
* Event that is triggered when clock offsets change.
* @event clockOffsets
* @memberof module:openmct.TimeAPI~
* @property {ClockOffsets} clockOffsets The newly activated clock
* offsets.
*/
this.emit("clockOffsets", offsets);
}
return this.offsets;
}
/**
* Stop the currently active clock from ticking, and unset it. This will
* revert all views to showing a static time frame defined by the current
* bounds.
*/
stopClock() {
if (this.activeClock) {
this.clock(undefined, undefined);
}
}
/**
* Set the active clock. Tick source will be immediately subscribed to
* and ticking will begin. Offsets from 'now' must also be provided. A clock
* can be unset by calling {@link stopClock}.
*
* @param {Clock || string} keyOrClock The clock to activate, or its key
* @param {ClockOffsets} offsets on each tick these will be used to calculate
* the start and end bounds. This maintains a sliding time window of a fixed
* width that automatically updates.
* @fires module:openmct.TimeAPI~clock
* @return {Clock} the currently active clock;
*/
clock(keyOrClock, offsets) {
if (arguments.length === 2) {
let clock;
if (typeof keyOrClock === 'string') {
clock = this.clocks.get(keyOrClock);
if (clock === undefined) {
throw "Unknown clock '" + keyOrClock + "'. Has it been registered with 'addClock'?";
}
} else if (typeof keyOrClock === 'object') {
clock = keyOrClock;
if (!this.clocks.has(clock.key)) {
throw "Unknown clock '" + keyOrClock.key + "'. Has it been registered with 'addClock'?";
}
}
const previousClock = this.activeClock;
if (previousClock !== undefined) {
previousClock.off("tick", this.tick);
}
this.activeClock = clock;
/**
* The active clock has changed. Clock can be unset by calling {@link stopClock}
* @event clock
* @memberof module:openmct.TimeAPI~
* @property {Clock} clock The newly activated clock, or undefined
* if the system is no longer following a clock source
*/
this.emit("clock", this.activeClock);
if (this.activeClock !== undefined) {
this.clockOffsets(offsets);
this.activeClock.on("tick", this.tick);
}
} else if (arguments.length === 1) {
throw "When setting the clock, clock offsets must also be provided";
}
return this.activeClock;
}
/**
* Update bounds based on provided time and current offsets
* @param {number} timestamp A time from which bounds will be calculated
* using current offsets.
*/
tick(timestamp) {
if (!this.activeClock) {
return;
}
const newBounds = {
start: timestamp + this.offsets.start,
end: timestamp + this.offsets.end
};
this.boundsVal = newBounds;
this.emit('bounds', this.boundsVal, true);
}
}
export default TimeContext;

View File

@ -0,0 +1,155 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import TimeAPI from "./TimeAPI";
import {createOpenMct} from "utils/testing";
describe("The Independent Time API", function () {
let api;
let domainObjectKey;
let clockKey;
let clock;
let bounds;
let independentBounds;
let eventListener;
let openmct;
beforeEach(function () {
openmct = createOpenMct();
api = new TimeAPI(openmct);
clockKey = "someClockKey";
clock = jasmine.createSpyObj("clock", [
"on",
"off",
"currentValue"
]);
clock.currentValue.and.returnValue(100);
clock.key = clockKey;
api.addClock(clock);
domainObjectKey = 'test-key';
bounds = {
start: 0,
end: 1
};
api.bounds(bounds);
independentBounds = {
start: 10,
end: 11
};
eventListener = jasmine.createSpy("eventListener");
});
it("Creates an independent time context", () => {
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
let timeContext = api.getIndependentContext(domainObjectKey);
expect(timeContext.bounds()).toEqual(independentBounds);
destroyTimeContext();
});
it("Gets an independent time context given the objectPath", () => {
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
let timeContext = api.getContextForView([{
identifier: {
namespace: '',
key: 'blah'
}
}, { identifier: domainObjectKey }]);
expect(timeContext.bounds()).toEqual(independentBounds);
destroyTimeContext();
});
it("defaults to the global time context given the objectPath", () => {
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
let timeContext = api.getContextForView([{
identifier: {
namespace: '',
key: 'blah'
}
}]);
expect(timeContext.bounds()).toEqual(bounds);
destroyTimeContext();
});
it("Allows setting of valid bounds", function () {
bounds = {
start: 0,
end: 1
};
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
let timeContext = api.getContextForView([{identifier: domainObjectKey}]);
expect(timeContext.bounds()).not.toEqual(bounds);
timeContext.bounds(bounds);
expect(timeContext.bounds()).toEqual(bounds);
destroyTimeContext();
});
it("Disallows setting of invalid bounds", function () {
bounds = {
start: 1,
end: 0
};
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
let timeContext = api.getContextForView([{identifier: domainObjectKey}]);
expect(timeContext.bounds()).not.toBe(bounds);
expect(timeContext.bounds.bind(timeContext, bounds)).toThrow();
expect(timeContext.bounds()).not.toEqual(bounds);
bounds = {start: 1};
expect(timeContext.bounds()).not.toEqual(bounds);
expect(timeContext.bounds.bind(timeContext, bounds)).toThrow();
expect(timeContext.bounds()).not.toEqual(bounds);
destroyTimeContext();
});
it("Emits an event when bounds change", function () {
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
let timeContext = api.getContextForView([{identifier: domainObjectKey}]);
expect(eventListener).not.toHaveBeenCalled();
timeContext.on('bounds', eventListener);
timeContext.bounds(bounds);
expect(eventListener).toHaveBeenCalledWith(bounds, false);
destroyTimeContext();
});
describe(" when using real time clock", function () {
const mockOffsets = {
start: 10,
end: 11
};
it("Emits an event when bounds change based on current value", function () {
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
let timeContext = api.getContextForView([{identifier: domainObjectKey}]);
expect(eventListener).not.toHaveBeenCalled();
timeContext.clock('someClockKey', mockOffsets);
timeContext.on('bounds', eventListener);
timeContext.tick(10);
expect(eventListener).toHaveBeenCalledWith({
start: 20,
end: 21
}, true);
destroyTimeContext();
});
});
});

View File

@ -90,7 +90,7 @@ class ImageExporter {
element.id = oldId;
},
removeContainer: true // Set to false to debug what html2canvas renders
}).then(function (canvas) {
}).then(canvas => {
dialog.dismiss();
return new Promise(function (resolve, reject) {
@ -105,9 +105,10 @@ class ImageExporter {
return canvas.toBlob(blob => resolve({ blob }), mimeType);
});
}, function (error) {
console.log('error capturing image', error);
}).catch(error => {
dialog.dismiss();
console.error('error capturing image', error);
const errorDialog = overlays.dialog({
iconClass: 'error',
message: 'Image was not captured successfully!',

View File

@ -41,7 +41,6 @@ const DEFAULTS = [
'platform/forms',
'platform/identity',
'platform/persistence/aggregator',
'platform/persistence/queue',
'platform/policy',
'platform/entanglement',
'platform/search',

View File

@ -32,7 +32,7 @@ describe('the plugin', function () {
let openmct;
let composition;
beforeEach((done) => {
beforeEach(() => {
openmct = createOpenMct();
@ -47,11 +47,6 @@ describe('the plugin', function () {
}
}));
openmct.on('start', done);
openmct.startHeadless();
composition = openmct.composition.get({identifier});
spyOn(couchPlugin.couchProvider, 'getObjectsByFilter').and.returnValue(Promise.resolve([
{
identifier: {
@ -66,6 +61,19 @@ describe('the plugin', function () {
}
}
]));
spyOn(couchPlugin.couchProvider, "get").and.callFake((id) => {
return Promise.resolve({
identifier: id
});
});
return new Promise((resolve) => {
openmct.once('start', resolve);
openmct.startHeadless();
}).then(() => {
composition = openmct.composition.get({identifier});
});
});
afterEach(() => {

View File

@ -0,0 +1,32 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import Agent from "../../utils/agent/Agent";
import DeviceClassifier from "./src/DeviceClassifier";
export default () => {
return (openmct) => {
openmct.on("start", () => {
const agent = new Agent(window);
DeviceClassifier(agent, window.document);
});
};
};

View File

@ -0,0 +1,72 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, 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.
*****************************************************************************/
/**
* Runs at application startup and adds a subset of the following
* CSS classes to the body of the document, depending on device
* attributes:
*
* * `mobile`: Phones or tablets.
* * `phone`: Phones specifically.
* * `tablet`: Tablets specifically.
* * `desktop`: Non-mobile devices.
* * `portrait`: Devices in a portrait-style orientation.
* * `landscape`: Devices in a landscape-style orientation.
* * `touch`: Device supports touch events.
*
* @param {utils/agent/Agent} agent
* the service used to examine the user agent
* @param document the HTML DOM document object
* @constructor
*/
import DeviceMatchers from "./DeviceMatchers";
export default (agent, document) => {
const body = document.body;
Object.keys(DeviceMatchers).forEach((key, index, array) => {
if (DeviceMatchers[key](agent)) {
body.classList.add(key);
}
});
if (agent.isMobile()) {
const mediaQuery = window.matchMedia("(orientation: landscape)");
function eventHandler(event) {
console.log("changed");
if (event.matches) {
body.classList.remove("portrait");
body.classList.add("landscape");
} else {
body.classList.remove("landscape");
body.classList.add("portrait");
}
}
if (mediaQuery.addEventListener) {
mediaQuery.addEventListener(`change`, eventHandler);
} else {
// Deprecated 'MediaQueryList' API, <Safari 14, IE, <Edge 16
mediaQuery.addListener(eventHandler);
}
}
};

View File

@ -0,0 +1,105 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import DeviceClassifier from "./DeviceClassifier";
import DeviceMatchers from "./DeviceMatchers";
const AGENT_METHODS = [
"isMobile",
"isPhone",
"isTablet",
"isPortrait",
"isLandscape",
"isTouch"
];
const TEST_PERMUTATIONS = [
["isMobile", "isPhone", "isTouch", "isPortrait"],
["isMobile", "isPhone", "isTouch", "isLandscape"],
["isMobile", "isTablet", "isTouch", "isPortrait"],
["isMobile", "isTablet", "isTouch", "isLandscape"],
["isTouch"],
[]
];
describe("DeviceClassifier", function () {
let mockAgent;
let mockDocument;
let mockClassList;
beforeEach(function () {
mockAgent = jasmine.createSpyObj(
"agent",
AGENT_METHODS
);
mockClassList = jasmine.createSpyObj("classList", ["add"]);
mockDocument = jasmine.createSpyObj(
"document",
{},
{ body: { classList: mockClassList } }
);
AGENT_METHODS.forEach(function (m) {
mockAgent[m].and.returnValue(false);
});
});
TEST_PERMUTATIONS.forEach(function (trueMethods) {
const summary =
trueMethods.length === 0
? "device has no detected characteristics"
: "device " + trueMethods.join(", ");
describe("when " + summary, function () {
beforeEach(function () {
trueMethods.forEach(function (m) {
mockAgent[m].and.returnValue(true);
});
// eslint-disable-next-line no-new
DeviceClassifier(mockAgent, mockDocument);
});
it("adds classes for matching, detected characteristics", function () {
Object.keys(DeviceMatchers)
.filter(function (m) {
return DeviceMatchers[m](mockAgent);
})
.forEach(function (key) {
expect(mockDocument.body.classList.add).toHaveBeenCalledWith(key);
});
});
it("does not add classes for non-matching characteristics", function () {
Object.keys(DeviceMatchers)
.filter(function (m) {
return !DeviceMatchers[m](mockAgent);
})
.forEach(function (key) {
expect(mockDocument.body.classList.add).not.toHaveBeenCalledWith(
key
);
});
});
});
});
});

View File

@ -0,0 +1,57 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, 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.
*****************************************************************************/
/**
* An object containing key-value pairs, where keys are symbolic of
* device attributes, and values are functions that take the
* `agent` as inputs and return boolean values indicating
* whether or not the current device has these attributes.
*
* For internal use by the mobile support bundle.
*
* @memberof src/plugins/DeviceClassifier
* @private
*/
export default {
mobile: function (agent) {
return agent.isMobile();
},
phone: function (agent) {
return agent.isPhone();
},
tablet: function (agent) {
return agent.isTablet();
},
desktop: function (agent) {
return !agent.isMobile();
},
portrait: function (agent) {
return agent.isPortrait();
},
landscape: function (agent) {
return agent.isLandscape();
},
touch: function (agent) {
return agent.isTouch();
}
};

View File

@ -0,0 +1,65 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import DeviceMatchers from "./DeviceMatchers";
describe("DeviceMatchers", function () {
let mockAgent;
beforeEach(function () {
mockAgent = jasmine.createSpyObj("agent", [
"isMobile",
"isPhone",
"isTablet",
"isPortrait",
"isLandscape",
"isTouch"
]);
});
it("detects when a device is a desktop device", function () {
mockAgent.isMobile.and.returnValue(false);
expect(DeviceMatchers.desktop(mockAgent)).toBe(true);
mockAgent.isMobile.and.returnValue(true);
expect(DeviceMatchers.desktop(mockAgent)).toBe(false);
});
function method(deviceType) {
return "is" + deviceType[0].toUpperCase() + deviceType.slice(1);
}
[
"mobile",
"phone",
"tablet",
"landscape",
"portrait",
"landscape",
"touch"
].forEach(function (deviceType) {
it("detects when a device is a " + deviceType + " device", function () {
mockAgent[method(deviceType)].and.returnValue(true);
expect(DeviceMatchers[deviceType](mockAgent)).toBe(true);
mockAgent[method(deviceType)].and.returnValue(false);
expect(DeviceMatchers[deviceType](mockAgent)).toBe(false);
});
});
});

View File

@ -96,11 +96,11 @@ export default {
this.timestampKey = this.openmct.time.timeSystem().key;
this.valueMetadata = this
this.valueMetadata = this.metadata ? this
.metadata
.valuesForHints(['range'])[0];
.valuesForHints(['range'])[0] : undefined;
this.valueKey = this.valueMetadata.key;
this.valueKey = this.valueMetadata ? this.valueMetadata.key : undefined;
this.unsubscribe = this.openmct
.telemetry
@ -151,7 +151,10 @@ export default {
size: 1,
strategy: 'latest'
})
.then((array) => this.updateValues(array[array.length - 1]));
.then((array) => this.updateValues(array[array.length - 1]))
.catch((error) => {
console.warn('Error fetching data', error);
});
},
updateBounds(bounds, isTick) {
this.bounds = bounds;

View File

@ -73,8 +73,9 @@ export default {
hasUnits() {
let itemsWithUnits = this.items.filter((item) => {
let metadata = this.openmct.telemetry.getMetadata(item.domainObject);
const valueMetadatas = metadata ? metadata.valueMetadatas : [];
return this.metadataHasUnits(metadata.valueMetadatas);
return this.metadataHasUnits(valueMetadatas);
});

View File

@ -0,0 +1,78 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, 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.
*****************************************************************************/
function inSelectionPath(openmct, domainObject) {
const domainObjectIdentifier = domainObject.identifier;
return openmct.selection.get().some(selectionPath => {
return selectionPath.some(objectInPath => {
const objectInPathIdentifier = objectInPath.context.item.identifier;
return openmct.objects.areIdsEqual(objectInPathIdentifier, domainObjectIdentifier);
});
});
}
export default class ClearDataAction {
constructor(openmct, appliesToObjects) {
this.name = 'Clear Data for Object';
this.key = 'clear-data-action';
this.description = 'Clears current data for object, unsubscribes and resubscribes to data';
this.cssClass = 'icon-clear-data';
this._openmct = openmct;
this._appliesToObjects = appliesToObjects;
}
invoke(objectPath) {
let domainObject = null;
if (objectPath) {
domainObject = objectPath[0];
}
this._openmct.objectViews.emit('clearData', domainObject);
}
appliesTo(objectPath) {
if (!objectPath) {
return false;
}
const contextualDomainObject = objectPath[0];
// first check to see if this action applies to this sort of object at all
const appliesToThisObject = this._appliesToObjects.some(type => {
return contextualDomainObject.type === type;
});
if (!appliesToThisObject) {
// we've selected something not applicable
return false;
}
const objectInSelectionPath = inSelectionPath(this._openmct, contextualDomainObject);
if (objectInSelectionPath) {
return true;
} else {
// if this it doesn't match up, check to see if we're in a composition (i.e., layout)
const routerPath = this._openmct.router.path[0];
return routerPath.type === 'layout';
}
}
}

View File

@ -22,7 +22,7 @@
define([
'./components/globalClearIndicator.vue',
'./clearDataAction',
'./ClearDataAction',
'vue'
], function (
GlobaClearIndicator,

View File

@ -0,0 +1,140 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import ClearDataActionPlugin from '../plugin.js';
import ClearDataAction from '../ClearDataAction.js';
describe('When the Clear Data Plugin is installed,', () => {
const mockObjectViews = jasmine.createSpyObj('objectViews', ['emit']);
const mockIndicatorProvider = jasmine.createSpyObj('indicators', ['add']);
const mockActionsProvider = jasmine.createSpyObj('actions', ['register']);
const goodMockSelectionPath = [[{
context: {
item: {
identifier: {
key: 'apple',
namespace: ''
}
}
}
}]];
const openmct = {
objectViews: mockObjectViews,
indicators: mockIndicatorProvider,
actions: mockActionsProvider,
install: function (plugin) {
plugin(this);
},
selection: {
get: function () {
return goodMockSelectionPath;
}
},
objects: {
areIdsEqual: function (obj1, obj2) {
return true;
}
}
};
const mockObjectPath = [
{
name: 'mockObject1',
type: 'apple'
},
{
name: 'mockObject2',
type: 'banana'
}
];
it('Global Clear Indicator is installed', () => {
openmct.install(ClearDataActionPlugin([]));
expect(mockIndicatorProvider.add).toHaveBeenCalled();
});
it('Clear Data context menu action is installed', () => {
openmct.install(ClearDataActionPlugin([]));
expect(mockActionsProvider.register).toHaveBeenCalled();
});
it('clear data action emits a clearData event when invoked', () => {
const action = new ClearDataAction(openmct);
action.invoke(mockObjectPath);
expect(mockObjectViews.emit).toHaveBeenCalledWith('clearData', mockObjectPath[0]);
});
it('clears data on applicable objects', () => {
let action = new ClearDataAction(openmct, ['apple']);
const actionApplies = action.appliesTo(mockObjectPath);
expect(actionApplies).toBe(true);
});
it('does not clear data on inapplicable objects', () => {
let action = new ClearDataAction(openmct, ['pineapple']);
const actionApplies = action.appliesTo(mockObjectPath);
expect(actionApplies).toBe(false);
});
it('does not clear data if not in the selection path and not a layout', () => {
openmct.objects = {
areIdsEqual: function (obj1, obj2) {
return false;
}
};
openmct.router = {
path: [{type: 'not-a-layout'}]
};
let action = new ClearDataAction(openmct, ['apple']);
const actionApplies = action.appliesTo(mockObjectPath);
expect(actionApplies).toBe(false);
});
it('does clear data if not in the selection path and is a layout', () => {
openmct.objects = {
areIdsEqual: function (obj1, obj2) {
return false;
}
};
openmct.router = {
path: [{type: 'layout'}]
};
let action = new ClearDataAction(openmct, ['apple']);
const actionApplies = action.appliesTo(mockObjectPath);
expect(actionApplies).toBe(true);
});
});

View File

@ -1,64 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import ClearDataActionPlugin from '../plugin.js';
import ClearDataAction from '../clearDataAction.js';
describe('When the Clear Data Plugin is installed,', function () {
const mockObjectViews = jasmine.createSpyObj('objectViews', ['emit']);
const mockIndicatorProvider = jasmine.createSpyObj('indicators', ['add']);
const mockActionsProvider = jasmine.createSpyObj('actions', ['register']);
const openmct = {
objectViews: mockObjectViews,
indicators: mockIndicatorProvider,
actions: mockActionsProvider,
install: function (plugin) {
plugin(this);
}
};
const mockObjectPath = [
{name: 'mockObject1'},
{name: 'mockObject2'}
];
it('Global Clear Indicator is installed', function () {
openmct.install(ClearDataActionPlugin([]));
expect(mockIndicatorProvider.add).toHaveBeenCalled();
});
it('Clear Data context menu action is installed', function () {
openmct.install(ClearDataActionPlugin([]));
expect(mockActionsProvider.register).toHaveBeenCalled();
});
it('clear data action emits a clearData event when invoked', function () {
let action = new ClearDataAction(openmct);
action.invoke(mockObjectPath);
expect(mockObjectViews.emit).toHaveBeenCalledWith('clearData', mockObjectPath[0]);
});
});

View File

@ -19,40 +19,41 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(function () {
/**
* An object containing key-value pairs, where keys are symbolic of
* device attributes, and values are functions that take the
* `agentService` as inputs and return boolean values indicating
* whether or not the current device has these attributes.
*
* For internal use by the mobile support bundle.
*
* @memberof platform/commonUI/mobile
* @private
*/
import Clock from './components/Clock.vue';
import Vue from 'vue';
export default function ClockViewProvider(openmct) {
return {
mobile: function (agentService) {
return agentService.isMobile();
key: 'clock.view',
name: 'Clock',
cssClass: 'icon-clock',
canView(domainObject) {
return domainObject.type === 'clock';
},
phone: function (agentService) {
return agentService.isPhone();
},
tablet: function (agentService) {
return agentService.isTablet();
},
desktop: function (agentService) {
return !agentService.isMobile();
},
portrait: function (agentService) {
return agentService.isPortrait();
},
landscape: function (agentService) {
return agentService.isLandscape();
},
touch: function (agentService) {
return agentService.isTouch();
view: function (domainObject) {
let component;
return {
show: function (element) {
component = new Vue({
el: element,
components: {
Clock
},
provide: {
openmct,
domainObject
},
template: '<clock />'
});
},
destroy: function () {
component.$destroy();
component = undefined;
}
};
}
};
});
}

View File

@ -0,0 +1,99 @@
<!--
Open MCT, Copyright (c) 2014-2021, 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.
-->
<template>
<div class="l-angular-ov-wrapper">
<div class="u-contents">
<div class="c-clock l-time-display u-style-receiver js-style-receiver">
<div class="c-clock__timezone">
{{ timeZoneAbbr }}
</div>
<div class="c-clock__value">
{{ timeTextValue }}
</div>
<div class="c-clock__ampm">
{{ timeAmPm }}
</div>
</div>
</div>
</div>
</template>
<script>
import moment from 'moment';
import momentTimezone from 'moment-timezone';
export default {
inject: ['openmct', 'domainObject'],
data() {
return {
lastTimestamp: null
};
},
computed: {
configuration() {
return this.domainObject.configuration;
},
baseFormat() {
return this.configuration.baseFormat;
},
use24() {
return this.configuration.use24 === 'clock24';
},
timezone() {
return this.configuration.timezone;
},
timeFormat() {
return this.use24 ? this.baseFormat.replace('hh', "HH") : this.baseFormat;
},
zoneName() {
return momentTimezone.tz.names().includes(this.timezone) ? this.timezone : "UTC";
},
momentTime() {
return this.zoneName ? moment.utc(this.lastTimestamp).tz(this.zoneName) : moment.utc(this.lastTimestamp);
},
timeZoneAbbr() {
return this.momentTime.zoneAbbr();
},
timeTextValue() {
return this.timeFormat && this.momentTime.format(this.timeFormat);
},
timeAmPm() {
return this.use24 ? '' : this.momentTime.format("A");
}
},
mounted() {
const TickerService = this.openmct.$injector.get('tickerService');
this.unlisten = TickerService.listen(this.tick);
},
beforeDestroy() {
if (this.unlisten) {
this.unlisten();
}
},
methods: {
tick(timestamp) {
this.lastTimestamp = timestamp;
}
}
};
</script>

View File

@ -0,0 +1,64 @@
<!--
Open MCT, Copyright (c) 2014-2021, 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.
-->
<template>
<div class="c-indicator t-indicator-clock icon-clock no-minify c-indicator--not-clickable">
<span class="label c-indicator__label">
{{ timeTextValue }}
</span>
</div>
</template>
<script>
import moment from 'moment';
export default {
inject: ['openmct'],
props: {
indicatorFormat: {
type: String,
required: true
}
},
data() {
return {
timeTextValue: null
};
},
mounted() {
this.openmct.on('start', () => {
const TickerService = this.openmct.$injector.get('tickerService');
this.unlisten = TickerService.listen(this.tick);
});
},
beforeDestroy() {
if (this.unlisten) {
this.unlisten();
}
},
methods: {
tick(timestamp) {
this.timeTextValue = `${moment.utc(timestamp).format(this.indicatorFormat)} UTC`;
}
}
};
</script>

154
src/plugins/clock/plugin.js Normal file
View File

@ -0,0 +1,154 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import ClockViewProvider from './ClockViewProvider';
import ClockIndicator from './components/ClockIndicator.vue';
import momentTimezone from 'moment-timezone';
import Vue from 'vue';
export default function ClockPlugin(options) {
return function install(openmct) {
const CLOCK_INDICATOR_FORMAT = 'YYYY/MM/DD HH:mm:ss';
openmct.types.addType('clock', {
name: 'Clock',
description: 'A UTC-based clock that supports a variety of display formats. Clocks can be added to Display Layouts.',
creatable: true,
cssClass: 'icon-clock',
initialize: function (domainObject) {
domainObject.configuration = {
baseFormat: 'YYYY/MM/DD hh:mm:ss',
use24: 'clock12',
timezone: 'UTC'
};
},
"form": [
{
"key": "displayFormat",
"name": "Display Format",
control: 'select',
options: [
{
value: 'YYYY/MM/DD hh:mm:ss',
name: 'YYYY/MM/DD hh:mm:ss'
},
{
value: 'YYYY/DDD hh:mm:ss',
name: 'YYYY/DDD hh:mm:ss'
},
{
value: 'hh:mm:ss',
name: 'hh:mm:ss'
}
],
cssClass: 'l-inline',
property: [
'configuration',
'baseFormat'
]
},
{
control: 'select',
options: [
{
value: 'clock12',
name: '12hr'
},
{
value: 'clock24',
name: '24hr'
}
],
cssClass: 'l-inline',
property: [
'configuration',
'use24'
]
},
{
"key": "timezone",
"name": "Timezone",
"control": "autocomplete",
"options": momentTimezone.tz.names(),
property: [
'configuration',
'timezone'
]
}
]
});
openmct.objectViews.addProvider(new ClockViewProvider(openmct));
if (options && options.enableClockIndicator === true) {
const clockIndicator = new Vue ({
components: {
ClockIndicator
},
provide: {
openmct
},
data() {
return {
indicatorFormat: CLOCK_INDICATOR_FORMAT
};
},
template: '<ClockIndicator :indicator-format="indicatorFormat" />'
});
const indicator = {
element: clockIndicator.$mount().$el,
key: 'clock-indicator'
};
openmct.indicators.add(indicator);
}
openmct.objects.addGetInterceptor({
appliesTo: (identifier, domainObject) => {
return domainObject && domainObject.type === 'clock';
},
invoke: (identifier, domainObject) => {
if (domainObject.configuration) {
return domainObject;
}
if (domainObject.clockFormat
&& domainObject.timezone) {
const baseFormat = domainObject.clockFormat[0];
const use24 = domainObject.clockFormat[1];
const timezone = domainObject.timezone;
domainObject.configuration = {
baseFormat,
use24,
timezone
};
openmct.objects.mutate(domainObject, 'configuration', domainObject.configuration);
}
return domainObject;
}
});
};
}

View File

@ -0,0 +1,231 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { createOpenMct, resetApplicationState } from 'utils/testing';
import clockPlugin from './plugin';
import Vue from 'vue';
describe("Clock plugin:", () => {
let openmct;
let clockDefinition;
let element;
let child;
let appHolder;
let clockDomainObject;
function setupClock(enableClockIndicator) {
return new Promise((resolve, reject) => {
clockDomainObject = {
identifier: {
key: 'clock',
namespace: 'test-namespace'
},
type: 'clock'
};
appHolder = document.createElement('div');
appHolder.style.width = '640px';
appHolder.style.height = '480px';
document.body.appendChild(appHolder);
openmct = createOpenMct();
element = document.createElement('div');
child = document.createElement('div');
element.appendChild(child);
openmct.install(clockPlugin({ enableClockIndicator }));
clockDefinition = openmct.types.get('clock').definition;
clockDefinition.initialize(clockDomainObject);
openmct.on('start', resolve);
openmct.start(appHolder);
});
}
describe("Clock view:", () => {
let clockViewProvider;
let clockView;
let clockViewObject;
let mutableClockObject;
beforeEach(async () => {
await setupClock(true);
clockViewObject = {
...clockDomainObject,
id: "test-object",
name: 'Clock',
configuration: {
baseFormat: 'YYYY/MM/DD hh:mm:ss',
use24: 'clock12',
timezone: 'UTC'
}
};
spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve(clockViewObject));
spyOn(openmct.objects, 'save').and.returnValue(Promise.resolve(true));
const applicableViews = openmct.objectViews.get(clockViewObject, [clockViewObject]);
clockViewProvider = applicableViews.find(viewProvider => viewProvider.key === 'clock.view');
mutableClockObject = await openmct.objects.getMutable(clockViewObject.identifier);
clockView = clockViewProvider.view(mutableClockObject);
clockView.show(child);
await Vue.nextTick();
});
afterEach(() => {
clockView.destroy();
openmct.objects.destroyMutable(mutableClockObject);
if (appHolder) {
appHolder.remove();
}
return resetApplicationState(openmct);
});
it("has name as Clock", () => {
expect(clockDefinition.name).toEqual('Clock');
});
it("is creatable", () => {
expect(clockDefinition.creatable).toEqual(true);
});
it("provides clock view", () => {
expect(clockViewProvider).toBeDefined();
});
it("renders clock element", () => {
const clockElement = element.querySelectorAll('.c-clock');
expect(clockElement.length).toBe(1);
});
it("renders major elements", () => {
const clockElement = element.querySelector('.c-clock');
const timezone = clockElement.querySelector('.c-clock__timezone');
const time = clockElement.querySelector('.c-clock__value');
const amPm = clockElement.querySelector('.c-clock__ampm');
const hasMajorElements = Boolean(timezone && time && amPm);
expect(hasMajorElements).toBe(true);
});
it("renders time in UTC", () => {
const clockElement = element.querySelector('.c-clock');
const timezone = clockElement.querySelector('.c-clock__timezone').textContent.trim();
expect(timezone).toBe('UTC');
});
it("updates the 24 hour option in the configuration", (done) => {
expect(clockDomainObject.configuration.use24).toBe('clock12');
const new24Option = 'clock24';
openmct.objects.observe(clockViewObject, 'configuration', (changedDomainObject) => {
expect(changedDomainObject.use24).toBe(new24Option);
done();
});
openmct.objects.mutate(clockViewObject, 'configuration.use24', new24Option);
});
it("updates the timezone option in the configuration", (done) => {
expect(clockDomainObject.configuration.timezone).toBe('UTC');
const newZone = 'CST6CDT';
openmct.objects.observe(clockViewObject, 'configuration', (changedDomainObject) => {
expect(changedDomainObject.timezone).toBe(newZone);
done();
});
openmct.objects.mutate(clockViewObject, 'configuration.timezone', newZone);
});
it("updates the time format option in the configuration", (done) => {
expect(clockDomainObject.configuration.baseFormat).toBe('YYYY/MM/DD hh:mm:ss');
const newFormat = 'hh:mm:ss';
openmct.objects.observe(clockViewObject, 'configuration', (changedDomainObject) => {
expect(changedDomainObject.baseFormat).toBe(newFormat);
done();
});
openmct.objects.mutate(clockViewObject, 'configuration.baseFormat', newFormat);
});
});
describe("Clock Indicator view:", () => {
let clockIndicator;
afterEach(() => {
if (clockIndicator) {
clockIndicator.remove();
}
clockIndicator = undefined;
if (appHolder) {
appHolder.remove();
}
return resetApplicationState(openmct);
});
it("doesn't exist", async () => {
await setupClock(false);
clockIndicator = openmct.indicators.indicatorObjects
.find(indicator => indicator.key === 'clock-indicator');
const clockIndicatorMissing = clockIndicator === null || clockIndicator === undefined;
expect(clockIndicatorMissing).toBe(true);
});
it("exists", async () => {
await setupClock(true);
clockIndicator = openmct.indicators.indicatorObjects
.find(indicator => indicator.key === 'clock-indicator').element;
const hasClockIndicator = clockIndicator !== null && clockIndicator !== undefined;
expect(hasClockIndicator).toBe(true);
});
it("contains text", async () => {
await setupClock(true);
clockIndicator = openmct.indicators.indicatorObjects
.find(indicator => indicator.key === 'clock-indicator').element;
const clockIndicatorText = clockIndicator.textContent.trim();
const textIncludesUTC = clockIndicatorText.includes('UTC');
expect(textIncludesUTC).toBe(true);
});
});
});

View File

@ -273,10 +273,7 @@ export default {
this.openmct.objects.getOriginalPath(this.conditionSetDomainObject.identifier).then(
(objectPath) => {
this.objectPath = objectPath;
this.navigateToPath = '#/browse/' + this.objectPath
.map(o => o && this.openmct.objects.makeKeyString(o.identifier))
.reverse()
.join('/');
this.navigateToPath = '#/browse/' + this.openmct.objects.getRelativePath(this.objectPath);
}
);
},

View File

@ -141,6 +141,7 @@ const NON_STYLEABLE_CONTAINER_TYPES = [
const NON_STYLEABLE_LAYOUT_ITEM_TYPES = [
'line-view',
'box-view',
'ellipse-view',
'image-view'
];
@ -296,10 +297,7 @@ export default {
this.openmct.objects.getOriginalPath(this.conditionSetDomainObject.identifier).then(
(objectPath) => {
this.objectPath = objectPath;
this.navigateToPath = '#/browse/' + this.objectPath
.map(o => o && this.openmct.objects.makeKeyString(o.identifier))
.reverse()
.join('/');
this.navigateToPath = '#/browse/' + this.openmct.objects.getRelativePath(this.objectPath);
}
);
},
@ -321,7 +319,7 @@ export default {
if (item) {
const type = this.openmct.types.get(item.type);
if (type && type.definition) {
creatable = (type.definition.creatable === true);
creatable = (type.definition.creatable !== undefined && (type.definition.creatable === 'true' || type.definition.creatable === true));
}
}

View File

@ -98,6 +98,8 @@ describe('the plugin', function () {
conditionSetDefinition.initialize(mockConditionSetDomainObject);
spyOn(openmct.objects, "save").and.returnValue(Promise.resolve(true));
openmct.on('start', done);
openmct.startHeadless();
});
@ -230,7 +232,7 @@ describe('the plugin', function () {
};
const staticStyle = {
"style": {
"backgroundColor": "#717171",
"backgroundColor": "#666666",
"border": "1px solid #00ffff"
}
};
@ -238,7 +240,7 @@ describe('the plugin', function () {
"conditionId": "39584410-cbf9-499e-96dc-76f27e69885d",
"style": {
"isStyleInvisible": "",
"backgroundColor": "#717171",
"backgroundColor": "#666666",
"border": "1px solid #ffff00"
}
};
@ -250,7 +252,7 @@ describe('the plugin', function () {
"configuration": {
"items": [
{
"fill": "#717171",
"fill": "#666666",
"stroke": "",
"x": 1,
"y": 1,
@ -259,12 +261,22 @@ describe('the plugin', function () {
"type": "box-view",
"id": "89b88746-d325-487b-aec4-11b79afff9e8"
},
{
"fill": "#666666",
"stroke": "",
"x": 1,
"y": 1,
"width": 10,
"height": 5,
"type": "ellipse-view",
"id": "19b88746-d325-487b-aec4-11b79afff9z8"
},
{
"x": 18,
"y": 9,
"x2": 23,
"y2": 4,
"stroke": "#717171",
"stroke": "#666666",
"type": "line-view",
"id": "57d49a28-7863-43bd-9593-6570758916f0"
},
@ -299,12 +311,12 @@ describe('the plugin', function () {
"y": 9,
"x2": 23,
"y2": 4,
"stroke": "#717171",
"stroke": "#666666",
"type": "line-view",
"id": "57d49a28-7863-43bd-9593-6570758916f0"
};
boxLayoutItem = {
"fill": "#717171",
"fill": "#666666",
"stroke": "",
"x": 1,
"y": 1,

View File

@ -29,9 +29,10 @@ const styleProps = {
noneValue: NONE_VALUE,
applicableForType: type => {
return !type ? true : (type === 'text-view'
|| type === 'telemetry-view'
|| type === 'box-view'
|| type === 'subobject-view');
|| type === 'telemetry-view'
|| type === 'box-view'
|| type === 'ellipse-view'
|| type === 'subobject-view');
}
},
border: {
@ -41,6 +42,7 @@ const styleProps = {
return !type ? true : (type === 'text-view'
|| type === 'telemetry-view'
|| type === 'box-view'
|| type === 'ellipse-view'
|| type === 'image-view'
|| type === 'line-view'
|| type === 'subobject-view');

View File

@ -149,6 +149,7 @@ define(['lodash'], function (_) {
return type === 'text-view'
|| type === 'telemetry-view'
|| type === 'box-view'
|| type === 'ellipse-view'
|| type === 'image-view'
|| type === 'line-view'
|| type === 'subobject-view';
@ -180,6 +181,10 @@ define(['lodash'], function (_) {
"name": "Box",
"class": "icon-box-round-corners"
},
{
"name": "Ellipse",
"class": "icon-circle"
},
{
"name": "Line",
"class": "icon-line-horz"
@ -745,7 +750,7 @@ define(['lodash'], function (_) {
if (toolbar.remove.length === 0) {
toolbar.remove = [getRemoveButton(selectedParent, selectionPath, selectedObjects)];
}
} else if (layoutItem.type === 'box-view') {
} else if (layoutItem.type === 'box-view' || layoutItem.type === 'ellipse-view') {
if (toolbar.position.length === 0) {
toolbar.position = [
getStackOrder(selectedParent, selectionPath),

View File

@ -43,7 +43,7 @@ import conditionalStylesMixin from '../mixins/objectStyles-mixin';
export default {
makeDefinition() {
return {
fill: '#717171',
fill: '#666666',
stroke: '',
x: 1,
y: 1,

View File

@ -76,6 +76,7 @@ import uuid from 'uuid';
import SubobjectView from './SubobjectView.vue';
import TelemetryView from './TelemetryView.vue';
import BoxView from './BoxView.vue';
import EllipseView from './EllipseView.vue';
import TextView from './TextView.vue';
import LineView from './LineView.vue';
import ImageView from './ImageView.vue';
@ -112,6 +113,7 @@ const ITEM_TYPE_VIEW_MAP = {
'subobject-view': SubobjectView,
'telemetry-view': TelemetryView,
'box-view': BoxView,
'ellipse-view': EllipseView,
'line-view': LineView,
'text-view': TextView,
'image-view': ImageView

View File

@ -28,19 +28,19 @@
>
<div
class="c-frame-edit__handle c-frame-edit__handle--nw"
@mousedown="startResize([1,1], [-1,-1], $event)"
@mousedown.left="startResize([1,1], [-1,-1], $event)"
></div>
<div
class="c-frame-edit__handle c-frame-edit__handle--ne"
@mousedown="startResize([0,1], [1,-1], $event)"
@mousedown.left="startResize([0,1], [1,-1], $event)"
></div>
<div
class="c-frame-edit__handle c-frame-edit__handle--sw"
@mousedown="startResize([1,0], [-1,1], $event)"
@mousedown.left="startResize([1,0], [-1,1], $event)"
></div>
<div
class="c-frame-edit__handle c-frame-edit__handle--se"
@mousedown="startResize([0,0], [1,1], $event)"
@mousedown.left="startResize([0,0], [1,1], $event)"
></div>
</div>
</template>

View File

@ -0,0 +1,122 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, 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.
*****************************************************************************/
<template>
<layout-frame
:item="item"
:grid-size="gridSize"
:is-editing="isEditing"
@move="(gridDelta) => $emit('move', gridDelta)"
@endMove="() => $emit('endMove')"
>
<div
class="c-ellipse-view u-style-receiver js-style-receiver"
:class="[styleClass]"
:style="style"
></div>
</layout-frame>
</template>
<script>
import LayoutFrame from './LayoutFrame.vue';
import conditionalStylesMixin from '../mixins/objectStyles-mixin';
export default {
makeDefinition() {
return {
fill: '#666666',
stroke: '',
x: 1,
y: 1,
width: 10,
height: 10
};
},
components: {
LayoutFrame
},
mixins: [conditionalStylesMixin],
inject: ['openmct'],
props: {
item: {
type: Object,
required: true
},
gridSize: {
type: Array,
required: true,
validator: (arr) => arr && arr.length === 2
&& arr.every(el => typeof el === 'number')
},
index: {
type: Number,
required: true
},
initSelect: Boolean,
isEditing: {
type: Boolean,
required: true
}
},
computed: {
style() {
if (this.itemStyle) {
return this.itemStyle;
} else {
return {
backgroundColor: this.item.fill,
border: this.item.stroke ? '1px solid ' + this.item.stroke : ''
};
}
}
},
watch: {
index(newIndex) {
if (!this.context) {
return;
}
this.context.index = newIndex;
},
item(newItem) {
if (!this.context) {
return;
}
this.context.layoutItem = newItem;
}
},
mounted() {
this.context = {
layoutItem: this.item,
index: this.index
};
this.removeSelectable = this.openmct.selection.selectable(
this.$el, this.context, this.initSelect);
},
destroyed() {
if (this.removeSelectable) {
this.removeSelectable();
}
}
};
</script>

View File

@ -33,7 +33,7 @@
<slot></slot>
<div
class="c-frame__move-bar"
@mousedown="isEditing ? startMove([1,1], [0,0], $event) : null"
@mousedown.left="startMove($event)"
></div>
</div>
</template>
@ -93,7 +93,11 @@ export default {
return value - this.initialPosition[index];
}.bind(this));
},
startMove(posFactor, dimFactor, event) {
startMove(event, posFactor = [1, 1], dimFactor = [0, 0]) {
if (!this.isEditing) {
return;
}
document.body.addEventListener('mousemove', this.continueMove);
document.body.addEventListener('mouseup', this.endMove);
this.dragPosition = {

View File

@ -45,7 +45,7 @@
<div
class="c-frame__move-bar"
@mousedown="startDrag($event)"
@mousedown.left="startDrag($event)"
></div>
<div
v-if="showFrameEdit"
@ -96,7 +96,7 @@ export default {
y: 10,
x2: 10,
y2: 5,
stroke: '#717171'
stroke: '#666666'
};
},
mixins: [conditionalStylesMixin],

View File

@ -72,7 +72,7 @@
<script>
import LayoutFrame from './LayoutFrame.vue';
import conditionalStylesMixin from "../mixins/objectStyles-mixin";
import { getDefaultNotebook } from '@/plugins/notebook/utils/notebook-storage.js';
import { getDefaultNotebook, getNotebookSectionAndPage } from '@/plugins/notebook/utils/notebook-storage.js';
const DEFAULT_TELEMETRY_DIMENSIONS = [10, 5];
const DEFAULT_POSITION = [1, 1];
@ -336,12 +336,15 @@ export default {
},
async getContextMenuActions() {
const defaultNotebook = getDefaultNotebook();
const domainObject = defaultNotebook && await this.openmct.objects.get(defaultNotebook.notebookMeta.identifier);
let defaultNotebookName;
if (defaultNotebook) {
const defaultPath = domainObject && `${domainObject.name} - ${defaultNotebook.section.name} - ${defaultNotebook.page.name}`;
defaultNotebookName = `Copy to Notebook ${defaultPath}`;
const domainObject = await this.openmct.objects.get(defaultNotebook.identifier);
const { section, page } = getNotebookSectionAndPage(domainObject, defaultNotebook.defaultSectionId, defaultNotebook.defaultPageId);
if (section && page) {
const defaultPath = domainObject && `${domainObject.name} - ${section.name} - ${page.name}`;
defaultNotebookName = `Copy to Notebook ${defaultPath}`;
}
}
return CONTEXT_MENU_ACTIONS

View File

@ -1,4 +1,5 @@
.c-box-view {
.c-box-view,
.c-ellipse-view {
border-width: $drawingObjBorderW !important;
display: flex;
align-items: stretch;
@ -8,6 +9,10 @@
}
}
.c-ellipse-view {
border-radius: 50%;
}
.c-line-view {
&.c-frame {
box-shadow: none !important;

View File

@ -186,7 +186,7 @@ describe('the plugin', function () {
'configuration': {
'items': [
{
'fill': '#717171',
'fill': '#666666',
'stroke': '',
'x': 1,
'y': 1,
@ -195,12 +195,22 @@ describe('the plugin', function () {
'type': 'box-view',
'id': '89b88746-d325-487b-aec4-11b79afff9e8'
},
{
'fill': '#666666',
'stroke': '',
'x': 1,
'y': 1,
'width': 10,
'height': 10,
'type': 'ellipse-view',
'id': '19b88746-d325-487b-aec4-11b79afff9z8'
},
{
'x': 18,
'y': 9,
'x2': 23,
'y2': 4,
'stroke': '#717171',
'stroke': '#666666',
'type': 'line-view',
'id': '57d49a28-7863-43bd-9593-6570758916f0'
},
@ -341,7 +351,7 @@ describe('the plugin', function () {
it('provides controls including separators', () => {
const displayLayoutToolbar = openmct.toolbars.get(selection);
expect(displayLayoutToolbar.length).toBe(9);
expect(displayLayoutToolbar.length).toBe(7);
});
});
});

View File

@ -101,7 +101,7 @@ export default {
addChildren(domainObject) {
let keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
let metadata = this.openmct.telemetry.getMetadata(domainObject);
let metadataWithFilters = metadata.valueMetadatas.filter(value => value.filters);
let metadataWithFilters = metadata ? metadata.valueMetadatas.filter(value => value.filters) : [];
let hasFiltersWithKeyString = this.persistedFilters[keyString] !== undefined;
let mutateFilters = false;
let childObject = {

View File

@ -0,0 +1,73 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import ImageryTimeView from './components/ImageryTimeView.vue';
import Vue from "vue";
export default function ImageryTimestripViewProvider(openmct) {
const type = 'example.imagery.time-strip.view';
function hasImageTelemetry(domainObject) {
const metadata = openmct.telemetry.getMetadata(domainObject);
if (!metadata) {
return false;
}
return metadata.valuesForHints(['image']).length > 0;
}
return {
key: type,
name: 'Imagery Timestrip View',
cssClass: 'icon-image',
canView: function (domainObject, objectPath) {
let isChildOfTimeStrip = objectPath.find(object => object.type === 'time-strip');
return hasImageTelemetry(domainObject) && isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath);
},
view: function (domainObject, objectPath) {
let component;
return {
show: function (element) {
component = new Vue({
el: element,
components: {
ImageryTimeView
},
provide: {
openmct: openmct,
domainObject: domainObject,
objectPath: objectPath
},
template: '<imagery-time-view></imagery-time-view>'
});
},
destroy: function () {
component.$destroy();
component = undefined;
}
};
}
};
}

View File

@ -1,4 +1,4 @@
import ImageryViewLayout from './components/ImageryViewLayout.vue';
import ImageryViewComponent from './components/ImageryView.vue';
import Vue from 'vue';
@ -14,7 +14,7 @@ export default class ImageryView {
this.component = new Vue({
el: element,
components: {
ImageryViewLayout
'imagery-view': ImageryViewComponent
},
provide: {
openmct: this.openmct,
@ -22,7 +22,8 @@ export default class ImageryView {
objectPath: this.objectPath,
currentView: this
},
template: '<imagery-view-layout ref="ImageryLayout"></imagery-view-layout>'
template: '<imagery-view ref="ImageryContainer"></imagery-view>'
});
}

View File

@ -37,8 +37,10 @@ export default function ImageryViewProvider(openmct) {
key: type,
name: 'Imagery Layout',
cssClass: 'icon-image',
canView: function (domainObject) {
return hasImageTelemetry(domainObject);
canView: function (domainObject, objectPath) {
let isChildOfTimeStrip = objectPath.find(object => object.type === 'time-strip');
return hasImageTelemetry(domainObject) && (!isChildOfTimeStrip || openmct.router.isNavigatedObject(objectPath));
},
view: function (domainObject, objectPath) {
return new ImageryView(openmct, domainObject, objectPath);

View File

@ -0,0 +1,475 @@
<!--
Open MCT, Copyright (c) 2014-2021, 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.
-->
<template>
<div ref="imagery"
class="c-imagery-tsv c-timeline-holder"
>
<div ref="imageryHolder"
class="c-imagery-tsv__contents u-contents"
>
</div>
</div>
</template>
<script>
import * as d3Scale from 'd3-scale';
import SwimLane from "@/ui/components/swim-lane/SwimLane.vue";
import Vue from "vue";
import imageryData from "../../imagery/mixins/imageryData";
import PreviewAction from "@/ui/preview/PreviewAction";
import _ from "lodash";
const PADDING = 1;
const ROW_HEIGHT = 100;
const IMAGE_WIDTH_THRESHOLD = 40;
export default {
mixins: [imageryData],
inject: ['openmct', 'domainObject', 'objectPath'],
data() {
let timeSystem = this.openmct.time.timeSystem();
this.metadata = {};
this.requestCount = 0;
return {
viewBounds: undefined,
height: 0,
durationFormatter: undefined,
imageHistory: [],
timeSystem: timeSystem,
keyString: undefined
};
},
computed: {
imageHistorySize() {
return this.imageHistory.length;
}
},
watch: {
imageHistorySize(newSize, oldSize) {
this.updatePlotImagery();
}
},
mounted() {
this.previewAction = new PreviewAction(this.openmct);
this.canvas = this.$refs.imagery.appendChild(document.createElement('canvas'));
this.canvas.height = 0;
this.canvasContext = this.canvas.getContext('2d');
this.setDimensions();
this.updateViewBounds();
this.openmct.time.on("timeSystem", this.setScaleAndPlotImagery);
this.openmct.time.on("bounds", this.updateViewBounds);
this.resize = _.debounce(this.resize, 400);
this.imageryStripResizeObserver = new ResizeObserver(this.resize);
this.imageryStripResizeObserver.observe(this.$refs.imagery);
this.unlisten = this.openmct.objects.observe(this.domainObject, '*', this.observeForChanges);
},
beforeDestroy() {
if (this.unsubscribe) {
this.unsubscribe();
delete this.unsubscribe;
}
if (this.imageryStripResizeObserver) {
this.imageryStripResizeObserver.disconnect();
}
this.openmct.time.off("timeSystem", this.setScaleAndPlotImagery);
this.openmct.time.off("bounds", this.updateViewBounds);
if (this.unlisten) {
this.unlisten();
}
},
methods: {
expand(index) {
const path = this.objectPath[0];
this.previewAction.invoke([path]);
},
observeForChanges(mutatedObject) {
this.updateViewBounds();
},
resize() {
let clientWidth = this.getClientWidth();
if (clientWidth !== this.width) {
this.setDimensions();
this.updateViewBounds();
}
},
getClientWidth() {
let clientWidth = this.$refs.imagery.clientWidth;
if (!clientWidth) {
//this is a hack - need a better way to find the parent of this component
let parent = this.openmct.layout.$refs.browseObject.$el;
if (parent) {
clientWidth = parent.getBoundingClientRect().width;
}
}
return clientWidth;
},
updateViewBounds(bounds, isTick) {
this.viewBounds = this.openmct.time.bounds();
//Add a 50% padding to the end bounds to look ahead
let timespan = (this.viewBounds.end - this.viewBounds.start);
let padding = timespan / 2;
this.viewBounds.end = this.viewBounds.end + padding;
if (this.timeSystem === undefined) {
this.timeSystem = this.openmct.time.timeSystem();
}
this.setScaleAndPlotImagery(this.timeSystem, !isTick);
},
setScaleAndPlotImagery(timeSystem, clearAllImagery) {
if (timeSystem !== undefined) {
this.timeSystem = timeSystem;
this.timeFormatter = this.getFormatter(this.timeSystem.key);
}
this.setScale(this.timeSystem);
this.updatePlotImagery(clearAllImagery);
},
getFormatter(key) {
const metadata = this.openmct.telemetry.getMetadata(this.domainObject);
let metadataValue = metadata.value(key) || { format: key };
let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);
return valueFormatter;
},
updatePlotImagery(clearAllImagery) {
this.clearPreviousImagery(clearAllImagery);
if (this.xScale) {
this.drawImagery();
}
},
clearPreviousImagery(clearAllImagery) {
//TODO: Only clear items that are out of bounds
let noItemsEl = this.$el.querySelectorAll(".c-imagery-tsv__no-items");
noItemsEl.forEach(item => {
item.remove();
});
let imagery = this.$el.querySelectorAll(".c-imagery-tsv__image-wrapper");
imagery.forEach(item => {
if (clearAllImagery) {
item.remove();
} else {
const id = this.getNSAttributesForElement(item, 'id');
if (id) {
const timestamp = id.replace('id-', '');
if (!this.isImageryInBounds({
time: timestamp
})) {
item.remove();
}
}
}
});
},
setDimensions() {
const imageryHolder = this.$refs.imagery;
this.width = this.getClientWidth();
this.height = Math.round(imageryHolder.getBoundingClientRect().height);
},
setScale(timeSystem) {
if (!this.width) {
return;
}
if (timeSystem === undefined) {
timeSystem = this.openmct.time.timeSystem();
}
if (timeSystem.isUTCBased) {
this.xScale = d3Scale.scaleUtc();
this.xScale.domain(
[new Date(this.viewBounds.start), new Date(this.viewBounds.end)]
);
} else {
this.xScale = d3Scale.scaleLinear();
this.xScale.domain(
[this.viewBounds.start, this.viewBounds.end]
);
}
this.xScale.range([PADDING, this.width - PADDING * 2]);
},
isImageryInBounds(imageObj) {
return (imageObj.time < this.viewBounds.end) && (imageObj.time > this.viewBounds.start);
},
getImageryContainer() {
let svgHeight = 100;
let svgWidth = this.imageHistory.length ? this.width : 200;
let groupSVG;
let existingSVG = this.$el.querySelector(".c-imagery-tsv__contents svg");
if (existingSVG) {
groupSVG = existingSVG;
this.setNSAttributesForElement(groupSVG, {
width: svgWidth
});
} else {
let component = new Vue({
components: {
SwimLane
},
provide: {
openmct: this.openmct
},
data() {
return {
isNested: true,
height: svgHeight,
width: svgWidth
};
},
template: `<swim-lane :is-nested="isNested" :hide-label="true"><template slot="object"><svg class="c-imagery-tsv-container" :height="height" :width="width"></svg></template></swim-lane>`
});
this.$refs.imageryHolder.appendChild(component.$mount().$el);
groupSVG = component.$el.querySelector('svg');
groupSVG.addEventListener('mouseout', (event) => {
if (event.target.nodeName === 'svg' || event.target.nodeName === 'use') {
this.removeFromForeground();
}
});
}
return groupSVG;
},
isImageryWidthAcceptable() {
// We're calculating if there is enough space between images to show the thumbnails.
// This algorithm could probably be enhanced to check the x co-ordinate distance between 2 consecutive images, but
// we will go with this for now assuming imagery is not sorted by asc time so it's difficult to calculate.
// TODO: Use telemetry.requestCollection to get sorted telemetry
const currentStart = this.viewBounds.start;
const currentEnd = this.viewBounds.end;
const rectX = this.xScale(currentStart);
const rectY = this.xScale(currentEnd);
const imageContainerWidth = this.imageHistory.length ? (rectY - rectX) / this.imageHistory.length : 0;
return imageContainerWidth < IMAGE_WIDTH_THRESHOLD;
},
drawImagery() {
let groupSVG = this.getImageryContainer();
const showImagePlaceholders = this.isImageryWidthAcceptable();
if (this.imageHistory.length) {
this.imageHistory.forEach((currentImageObject, index) => {
if (this.isImageryInBounds(currentImageObject)) {
this.plotImagery(currentImageObject, showImagePlaceholders, groupSVG, index);
}
});
} else {
this.plotNoItems(groupSVG);
}
},
plotNoItems(svgElement) {
let textElement = document.createElementNS('http://www.w3.org/2000/svg', 'text');
this.setNSAttributesForElement(textElement, {
x: "10",
y: "20",
class: "c-imagery-tsv__no-items"
});
textElement.innerHTML = 'No images within timeframe';
svgElement.appendChild(textElement);
},
setNSAttributesForElement(element, attributes) {
Object.keys(attributes).forEach((key) => {
if (key === 'url') {
element.setAttributeNS('http://www.w3.org/1999/xlink', 'href', attributes[key]);
} else {
element.setAttributeNS(null, key, attributes[key]);
}
});
},
getNSAttributesForElement(element, attribute) {
return element.getAttributeNS(null, attribute);
},
getImageWrapper(item) {
const id = `id-${item.time}`;
return this.$el.querySelector(`.c-imagery-tsv__contents g[id=${id}]`);
},
plotImagery(item, showImagePlaceholders, svgElement, index) {
//TODO: Placeholder image
let existingImageWrapper = this.getImageWrapper(item);
//imageWrapper wraps the vertical tick rect and the image
if (existingImageWrapper) {
this.updateExistingImageWrapper(existingImageWrapper, item, showImagePlaceholders);
} else {
let imageWrapper = this.createImageWrapper(index, item, showImagePlaceholders, svgElement);
svgElement.appendChild(imageWrapper);
}
},
updateExistingImageWrapper(existingImageWrapper, item, showImagePlaceholders) {
//Update the x co-ordinates of the handle and image elements and the url of image
//this is to avoid tearing down all elements completely and re-drawing them
this.setNSAttributesForElement(existingImageWrapper, {
showImagePlaceholders
});
let imageTickElement = existingImageWrapper.querySelector('rect.c-imagery-tsv__image-handle');
this.setNSAttributesForElement(imageTickElement, {
x: this.xScale(item.time)
});
let imageRect = existingImageWrapper.querySelector('rect.c-imagery-tsv__image-placeholder');
this.setNSAttributesForElement(imageRect, {
x: this.xScale(item.time) + 2
});
let imageElement = existingImageWrapper.querySelector('image');
const selector = `href*=${existingImageWrapper.id}`;
let hoverEl = this.$el.querySelector(`.c-imagery-tsv__contents use[${selector}]`);
const hideImageUrl = (showImagePlaceholders && !hoverEl);
this.setNSAttributesForElement(imageElement, {
x: this.xScale(item.time) + 2,
url: hideImageUrl ? '' : item.url
});
},
createImageWrapper(index, item, showImagePlaceholders, svgElement) {
const id = `id-${item.time}`;
const imgSize = String(ROW_HEIGHT - 15);
let imageWrapper = document.createElementNS('http://www.w3.org/2000/svg', 'g');
this.setNSAttributesForElement(imageWrapper, {
id,
class: 'c-imagery-tsv__image-wrapper',
showImagePlaceholders
});
//create image tick indicator
let imageTickElement = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
this.setNSAttributesForElement(imageTickElement, {
class: 'c-imagery-tsv__image-handle',
x: this.xScale(item.time),
y: 5,
rx: 0,
width: 2,
height: String(ROW_HEIGHT - 10)
});
imageWrapper.appendChild(imageTickElement);
let imageRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
this.setNSAttributesForElement(imageRect, {
class: 'c-imagery-tsv__image-placeholder',
x: this.xScale(item.time) + 2,
y: 10,
rx: 0,
width: imgSize,
height: imgSize,
mask: `#image-${item.time}`
});
imageWrapper.appendChild(imageRect);
let imageElement = document.createElementNS('http://www.w3.org/2000/svg', 'image');
this.setNSAttributesForElement(imageElement, {
id: `image-${item.time}`,
x: this.xScale(item.time) + 2,
y: 10,
rx: 0,
width: imgSize,
height: imgSize,
url: showImagePlaceholders ? '' : item.url
});
imageWrapper.appendChild(imageElement);
//TODO: Don't add the hover listener if the width is too small
imageWrapper.addEventListener('mouseover', this.bringToForeground.bind(this, svgElement, imageWrapper, index, item.url));
return imageWrapper;
},
bringToForeground(svgElement, imageWrapper, index, url, event) {
const selector = `href*=${imageWrapper.id}`;
let hoverEls = this.$el.querySelectorAll(`.c-imagery-tsv__contents use:not([${selector}])`);
if (hoverEls.length > 0) {
this.removeFromForeground(hoverEls);
}
hoverEls = this.$el.querySelectorAll(`.c-imagery-tsv__contents use[${selector}]`);
if (hoverEls.length) {
return;
}
let imageElement = imageWrapper.querySelector('image');
this.setNSAttributesForElement(imageElement, {
url: url,
fill: 'none'
});
let hoverElement = document.createElementNS('http://www.w3.org/2000/svg', 'use');
this.setNSAttributesForElement(hoverElement, {
class: 'image-highlight',
x: 0,
href: `#${imageWrapper.id}`
});
this.setNSAttributesForElement(imageWrapper, {
class: 'c-imagery-tsv__image-wrapper is-hovered'
});
// We're using mousedown here and not 'click' because 'click' doesn't seem to be triggered reliably
hoverElement.addEventListener('mousedown', (e) => {
if (e.button === 0) {
this.expand(index);
}
});
svgElement.appendChild(hoverElement);
},
removeFromForeground(items) {
let hoverEls;
if (items) {
hoverEls = items;
} else {
hoverEls = this.$el.querySelectorAll(".c-imagery-tsv__contents use");
}
hoverEls.forEach(item => {
let selector = `id*=${this.getNSAttributesForElement(item, 'href').replace('#', '')}`;
let imageWrapper = this.$el.querySelector(`.c-imagery-tsv__contents g[${selector}]`);
this.setNSAttributesForElement(imageWrapper, {
class: 'c-imagery-tsv__image-wrapper'
});
let showImagePlaceholders = this.getNSAttributesForElement(imageWrapper, 'showImagePlaceholders');
if (showImagePlaceholders === 'true') {
let imageElement = imageWrapper.querySelector('image');
this.setNSAttributesForElement(imageElement, {
url: ''
});
}
item.remove();
});
}
}
};
</script>

View File

@ -129,12 +129,11 @@
</div>
</div>
</div>
<div
class="c-imagery__thumbs-wrapper"
:class="[
{ 'is-paused': isPaused },
{ 'is-autoscroll-off': !resizingWindow && !autoScroll && !isPaused }
]"
<div class="c-imagery__thumbs-wrapper"
:class="[
{ 'is-paused': isPaused },
{ 'is-autoscroll-off': !resizingWindow && !autoScroll && !isPaused }
]"
>
<div
ref="thumbsWrapper"
@ -175,7 +174,8 @@ import moment from 'moment';
import RelatedTelemetry from './RelatedTelemetry/RelatedTelemetry';
import Compass from './Compass/Compass.vue';
const DEFAULT_DURATION_FORMATTER = 'duration';
import imageryData from "../../imagery/mixins/imageryData";
const REFRESH_CSS_MS = 500;
const DURATION_TRACK_MS = 1000;
const ARROW_DOWN_DELAY_CHECK_MS = 400;
@ -197,30 +197,29 @@ export default {
components: {
Compass
},
mixins: [imageryData],
inject: ['openmct', 'domainObject', 'objectPath', 'currentView'],
data() {
let timeSystem = this.openmct.time.timeSystem();
this.metadata = {};
this.requestCount = 0;
return {
autoScroll: true,
durationFormatter: undefined,
imageHistory: [],
timeSystem: timeSystem,
keyString: undefined,
autoScroll: true,
filters: {
brightness: 100,
contrast: 100
},
imageHistory: [],
thumbnailClick: THUMBNAIL_CLICKED,
isPaused: false,
metadata: {},
requestCount: 0,
timeSystem: timeSystem,
timeFormatter: undefined,
refreshCSS: false,
keyString: undefined,
focusedImageIndex: undefined,
focusedImageRelatedTelemetry: {},
numericDuration: undefined,
metadataEndpoints: {},
relatedTelemetry: {},
latestRelatedTelemetry: {},
focusedImageNaturalAspectRatio: undefined,
@ -231,6 +230,9 @@ export default {
};
},
computed: {
imageHistorySize() {
return this.imageHistory.length;
},
compassRoseSizingClasses() {
let compassRoseSizingClasses = '';
if (this.sizedImageDimensions.width < 300) {
@ -258,9 +260,6 @@ export default {
canTrackDuration() {
return this.openmct.time.clock() && this.timeSystem.isUTCBased;
},
focusedImageDownloadName() {
return this.getImageDownloadName(this.focusedImage);
},
isNextDisabled() {
let disabled = false;
@ -383,6 +382,10 @@ export default {
}
},
watch: {
imageHistorySize(newSize, oldSize) {
this.setFocusedImage(newSize - 1, false);
this.scrollToRight();
},
focusedImageIndex() {
this.trackDuration();
this.resetAgeCSS();
@ -391,18 +394,9 @@ export default {
}
},
async mounted() {
// listen
this.openmct.time.on('bounds', this.boundsChange);
this.openmct.time.on('timeSystem', this.timeSystemChange);
this.openmct.time.on('clock', this.clockChange);
// set
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
this.imageHints = { ...this.metadata.valuesForHints(['image'])[0] };
this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
this.imageFormatter = this.openmct.telemetry.getValueFormatter(this.imageHints);
this.imageDownloadNameHints = { ...this.metadata.valuesForHints(['imageDownloadName'])[0]};
//listen
this.openmct.time.on('timeSystem', this.trackDuration);
this.openmct.time.on('clock', this.trackDuration);
// related telemetry keys
this.spacecraftPositionKeys = ['positionX', 'positionY', 'positionZ'];
@ -410,56 +404,49 @@ export default {
this.cameraKeys = ['cameraPan', 'cameraTilt'];
this.sunKeys = ['sunOrientation'];
// initialize
this.timeKey = this.timeSystem.key;
this.timeFormatter = this.getFormatter(this.timeKey);
// kickoff
this.subscribe();
this.requestHistory();
// related telemetry
await this.initializeRelatedTelemetry();
this.updateRelatedTelemetryForFocusedImage();
await this.updateRelatedTelemetryForFocusedImage();
this.trackLatestRelatedTelemetry();
// for scrolling through images quickly and resizing the object view
_.debounce(this.updateRelatedTelemetryForFocusedImage, 400);
_.debounce(this.resizeImageContainer, 400);
this.updateRelatedTelemetryForFocusedImage = _.debounce(this.updateRelatedTelemetryForFocusedImage, 400);
this.imageContainerResizeObserver = new ResizeObserver(this.resizeImageContainer);
this.imageContainerResizeObserver.observe(this.$refs.imageBG);
// for resizing the object view
this.resizeImageContainer = _.debounce(this.resizeImageContainer, 400);
if (this.$refs.imageBG) {
this.imageContainerResizeObserver = new ResizeObserver(this.resizeImageContainer);
this.imageContainerResizeObserver.observe(this.$refs.imageBG);
}
// For adjusting scroll bar size and position when resizing thumbs wrapper
this.handleScroll = _.debounce(this.handleScroll, SCROLL_LATENCY);
this.handleThumbWindowResizeEnded = _.debounce(this.handleThumbWindowResizeEnded, SCROLL_LATENCY);
this.handleThumbWindowResizeStart = _.debounce(this.handleThumbWindowResizeStart, SCROLL_LATENCY);
if (this.$refs.thumbsWrapper) {
this.thumbWrapperResizeObserver = new ResizeObserver(this.handleThumbWindowResizeStart);
this.thumbWrapperResizeObserver.observe(this.$refs.thumbsWrapper);
}
this.thumbWrapperResizeObserver = new ResizeObserver(this.handleThumbWindowResizeStart);
this.thumbWrapperResizeObserver.observe(this.$refs.thumbsWrapper);
},
beforeDestroy() {
if (this.unsubscribe) {
this.unsubscribe();
delete this.unsubscribe;
this.openmct.time.off('timeSystem', this.trackDuration);
this.openmct.time.off('clock', this.trackDuration);
if (this.thumbWrapperResizeObserver) {
this.thumbWrapperResizeObserver.disconnect();
}
if (this.imageContainerResizeObserver) {
this.imageContainerResizeObserver.disconnect();
}
if (this.thumbWrapperResizeObserver) {
this.thumbWrapperResizeObserver.disconnect();
}
if (this.relatedTelemetry.hasRelatedTelemetry) {
this.relatedTelemetry.destroy();
}
this.stopDurationTracking();
this.openmct.time.off('bounds', this.boundsChange);
this.openmct.time.off('timeSystem', this.timeSystemChange);
this.openmct.time.off('clock', this.clockChange);
// unsubscribe from related telemetry
if (this.relatedTelemetry.hasRelatedTelemetry) {
for (let key of this.relatedTelemetry.keys) {
@ -576,56 +563,6 @@ export default {
focusElement() {
this.$el.focus();
},
datumIsNotValid(datum) {
if (this.imageHistory.length === 0) {
return false;
}
const datumURL = this.formatImageUrl(datum);
const lastHistoryURL = this.formatImageUrl(this.imageHistory.slice(-1)[0]);
// datum is not valid if it matches the last datum in history,
// or it is before the last datum in the history
const datumTimeCheck = this.parseTime(datum);
const historyTimeCheck = this.parseTime(this.imageHistory.slice(-1)[0]);
const matchesLast = (datumTimeCheck === historyTimeCheck) && (datumURL === lastHistoryURL);
const isStale = datumTimeCheck < historyTimeCheck;
return matchesLast || isStale;
},
formatImageUrl(datum) {
if (!datum) {
return;
}
return this.imageFormatter.format(datum);
},
formatTime(datum) {
if (!datum) {
return;
}
let dateTimeStr = this.timeFormatter.format(datum);
// Replace ISO "T" with a space to allow wrapping
return dateTimeStr.replace("T", " ");
},
getImageDownloadName(datum) {
let imageDownloadName = '';
if (datum) {
const key = this.imageDownloadNameHints.key;
imageDownloadName = datum[key];
}
return imageDownloadName;
},
parseTime(datum) {
if (!datum) {
return;
}
return this.timeFormatter.parse(datum);
},
handleScroll() {
const thumbsWrapper = this.$refs.thumbsWrapper;
if (!thumbsWrapper || this.resizingWindow) {
@ -683,6 +620,10 @@ export default {
setFocusedImage(index, thumbnailClick = false) {
if (this.isPaused && !thumbnailClick) {
this.nextImageIndex = index;
//this could happen if bounds changes
if (this.focusedImageIndex > this.imageHistory.length - 1) {
this.focusedImageIndex = index;
}
return;
}
@ -693,70 +634,6 @@ export default {
this.paused(true);
}
},
boundsChange(bounds, isTick) {
if (!isTick) {
this.requestHistory();
}
},
async requestHistory() {
let bounds = this.openmct.time.bounds();
this.requestCount++;
const requestId = this.requestCount;
this.imageHistory = [];
let data = await this.openmct.telemetry
.request(this.domainObject, bounds) || [];
if (this.requestCount === requestId) {
data.forEach((datum, index) => {
this.updateHistory(datum, index === data.length - 1);
});
}
},
timeSystemChange(system) {
this.timeSystem = this.openmct.time.timeSystem();
this.timeKey = this.timeSystem.key;
this.timeFormatter = this.getFormatter(this.timeKey);
this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
this.trackDuration();
},
clockChange(clock) {
this.trackDuration();
},
subscribe() {
this.unsubscribe = this.openmct.telemetry
.subscribe(this.domainObject, (datum) => {
let parsedTimestamp = this.parseTime(datum);
let bounds = this.openmct.time.bounds();
if (parsedTimestamp >= bounds.start && parsedTimestamp <= bounds.end) {
this.updateHistory(datum);
}
});
},
updateHistory(datum, setFocused = true) {
if (this.datumIsNotValid(datum)) {
return;
}
let image = { ...datum };
image.formattedTime = this.formatTime(datum);
image.url = this.formatImageUrl(datum);
image.time = datum[this.timeKey];
image.imageDownloadName = this.getImageDownloadName(datum);
this.imageHistory.push(image);
if (setFocused) {
this.setFocusedImage(this.imageHistory.length - 1);
this.scrollToRight();
}
},
getFormatter(key) {
let metadataValue = this.metadata.value(key) || { format: key };
let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);
return valueFormatter;
},
trackDuration() {
if (this.canTrackDuration) {
this.stopDurationTracking();
@ -876,6 +753,10 @@ export default {
}, { once: true });
},
resizeImageContainer() {
if (!this.$refs.imageBG) {
return;
}
if (this.$refs.imageBG.clientWidth !== this.imageContainerWidth) {
this.imageContainerWidth = this.$refs.imageBG.clientWidth;
}

View File

@ -312,3 +312,34 @@
@include cArrowButtonSizing($dimOuter: 32px);
}
}
/*************************************** IMAGERY IN TIMESTRIP VIEWS */
.c-imagery-tsv {
g.c-imagery-tsv__image-wrapper {
cursor: pointer;
&.is-hovered {
filter: brightness(1) contrast(1) !important;
[class*='__image-handle'] {
fill: $colorBodyFg;
}
}
}
&__no-items {
fill: $colorBodyFg !important;
}
&__image-handle {
fill: rgba($colorBodyFg, 0.5);
}
&__image-placeholder {
fill: pushBack($colorBodyBg, 0.3);
}
&:hover g.c-imagery-tsv__image-wrapper {
// TODO CH: convert to theme constants
filter: brightness(0.5) contrast(0.7);
}
}

View File

@ -0,0 +1,174 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, 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.
*****************************************************************************/
const DEFAULT_DURATION_FORMATTER = 'duration';
export default {
inject: ['openmct', 'domainObject', 'objectPath'],
mounted() {
// listen
this.openmct.time.on('bounds', this.boundsChange);
this.openmct.time.on('timeSystem', this.timeSystemChange);
// set
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
this.imageHints = { ...this.metadata.valuesForHints(['image'])[0] };
this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
this.imageFormatter = this.openmct.telemetry.getValueFormatter(this.imageHints);
this.imageDownloadNameHints = { ...this.metadata.valuesForHints(['imageDownloadName'])[0]};
// initialize
this.timeKey = this.timeSystem.key;
this.timeFormatter = this.getFormatter(this.timeKey);
// kickoff
this.subscribe();
this.requestHistory();
},
beforeDestroy() {
if (this.unsubscribe) {
this.unsubscribe();
delete this.unsubscribe;
}
this.openmct.time.off('bounds', this.boundsChange);
this.openmct.time.off('timeSystem', this.timeSystemChange);
},
methods: {
datumIsNotValid(datum) {
if (this.imageHistory.length === 0) {
return false;
}
const datumURL = this.formatImageUrl(datum);
const lastHistoryURL = this.formatImageUrl(this.imageHistory.slice(-1)[0]);
// datum is not valid if it matches the last datum in history,
// or it is before the last datum in the history
const datumTimeCheck = this.parseTime(datum);
const historyTimeCheck = this.parseTime(this.imageHistory.slice(-1)[0]);
const matchesLast = (datumTimeCheck === historyTimeCheck) && (datumURL === lastHistoryURL);
const isStale = datumTimeCheck < historyTimeCheck;
return matchesLast || isStale;
},
formatImageUrl(datum) {
if (!datum) {
return;
}
return this.imageFormatter.format(datum);
},
formatTime(datum) {
if (!datum) {
return;
}
let dateTimeStr = this.timeFormatter.format(datum);
// Replace ISO "T" with a space to allow wrapping
return dateTimeStr.replace("T", " ");
},
getImageDownloadName(datum) {
let imageDownloadName = '';
if (datum) {
const key = this.imageDownloadNameHints.key;
imageDownloadName = datum[key];
}
return imageDownloadName;
},
parseTime(datum) {
if (!datum) {
return;
}
return this.timeFormatter.parse(datum);
},
boundsChange(bounds, isTick) {
if (!isTick) {
this.requestHistory();
}
},
async requestHistory() {
let bounds = this.openmct.time.bounds();
this.requestCount++;
const requestId = this.requestCount;
this.imageHistory = [];
let data = await this.openmct.telemetry
.request(this.domainObject, bounds) || [];
if (this.requestCount === requestId) {
let imagery = [];
data.forEach((datum) => {
let image = this.normalizeDatum(datum);
if (image) {
imagery.push(image);
}
});
//this is to optimize anything that reacts to imageHistory length
this.imageHistory = imagery;
}
},
timeSystemChange() {
this.timeSystem = this.openmct.time.timeSystem();
this.timeKey = this.timeSystem.key;
this.timeFormatter = this.getFormatter(this.timeKey);
this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
},
subscribe() {
this.unsubscribe = this.openmct.telemetry
.subscribe(this.domainObject, (datum) => {
let parsedTimestamp = this.parseTime(datum);
let bounds = this.openmct.time.bounds();
if (parsedTimestamp >= bounds.start && parsedTimestamp <= bounds.end) {
let image = this.normalizeDatum(datum);
if (image) {
this.imageHistory.push(image);
}
}
});
},
normalizeDatum(datum) {
if (this.datumIsNotValid(datum)) {
return;
}
let image = { ...datum };
image.formattedTime = this.formatTime(datum);
image.url = this.formatImageUrl(datum);
image.time = this.parseTime(image.formattedTime);
image.imageDownloadName = this.getImageDownloadName(datum);
return image;
},
getFormatter(key) {
let metadataValue = this.metadata.value(key) || { format: key };
let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);
return valueFormatter;
}
}
};

View File

@ -21,10 +21,12 @@
*****************************************************************************/
import ImageryViewProvider from './ImageryViewProvider';
import ImageryTimestripViewProvider from './ImageryTimestripViewProvider';
export default function () {
return function install(openmct) {
openmct.objectViews.addProvider(new ImageryViewProvider(openmct));
openmct.objectViews.addProvider(new ImageryTimestripViewProvider(openmct));
};
}

View File

@ -32,19 +32,19 @@ const TEN_MINUTES = ONE_MINUTE * 10;
const MAIN_IMAGE_CLASS = '.js-imageryView-image';
const NEW_IMAGE_CLASS = '.c-imagery__age.c-imagery--new';
const REFRESH_CSS_MS = 500;
const TOLERANCE = 0.50;
// const TOLERANCE = 0.50;
function comparisonFunction(valueOne, valueTwo) {
let larger = valueOne;
let smaller = valueTwo;
if (larger < smaller) {
larger = valueTwo;
smaller = valueOne;
}
return (larger - smaller) < TOLERANCE;
}
// function comparisonFunction(valueOne, valueTwo) {
// let larger = valueOne;
// let smaller = valueTwo;
//
// if (larger < smaller) {
// larger = valueTwo;
// smaller = valueOne;
// }
//
// return (larger - smaller) < TOLERANCE;
// }
function getImageInfo(doc) {
let imageElement = doc.querySelectorAll(MAIN_IMAGE_CLASS)[0];
@ -84,12 +84,14 @@ function generateTelemetry(start, count) {
return telemetry;
}
describe("The Imagery View Layout", () => {
describe("The Imagery View Layouts", () => {
const imageryKey = 'example.imagery';
const imageryForTimeStripKey = 'example.imagery.time-strip.view';
const START = Date.now();
const COUNT = 10;
let resolveFunction;
let originalRouterPath;
let openmct;
let appHolder;
@ -116,51 +118,51 @@ describe("The Imagery View Layout", () => {
"image": 1,
"priority": 3
},
"source": "url",
"relatedTelemetry": {
"heading": {
"comparisonFunction": comparisonFunction,
"historical": {
"telemetryObjectId": "heading",
"valueKey": "value"
}
},
"roll": {
"comparisonFunction": comparisonFunction,
"historical": {
"telemetryObjectId": "roll",
"valueKey": "value"
}
},
"pitch": {
"comparisonFunction": comparisonFunction,
"historical": {
"telemetryObjectId": "pitch",
"valueKey": "value"
}
},
"cameraPan": {
"comparisonFunction": comparisonFunction,
"historical": {
"telemetryObjectId": "cameraPan",
"valueKey": "value"
}
},
"cameraTilt": {
"comparisonFunction": comparisonFunction,
"historical": {
"telemetryObjectId": "cameraTilt",
"valueKey": "value"
}
},
"sunOrientation": {
"comparisonFunction": comparisonFunction,
"historical": {
"telemetryObjectId": "sunOrientation",
"valueKey": "value"
}
}
}
"source": "url"
// "relatedTelemetry": {
// "heading": {
// "comparisonFunction": comparisonFunction,
// "historical": {
// "telemetryObjectId": "heading",
// "valueKey": "value"
// }
// },
// "roll": {
// "comparisonFunction": comparisonFunction,
// "historical": {
// "telemetryObjectId": "roll",
// "valueKey": "value"
// }
// },
// "pitch": {
// "comparisonFunction": comparisonFunction,
// "historical": {
// "telemetryObjectId": "pitch",
// "valueKey": "value"
// }
// },
// "cameraPan": {
// "comparisonFunction": comparisonFunction,
// "historical": {
// "telemetryObjectId": "cameraPan",
// "valueKey": "value"
// }
// },
// "cameraTilt": {
// "comparisonFunction": comparisonFunction,
// "historical": {
// "telemetryObjectId": "cameraTilt",
// "valueKey": "value"
// }
// },
// "sunOrientation": {
// "comparisonFunction": comparisonFunction,
// "historical": {
// "telemetryObjectId": "sunOrientation",
// "valueKey": "value"
// }
// }
// }
},
{
"name": "Name",
@ -220,6 +222,8 @@ describe("The Imagery View Layout", () => {
spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([]));
spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({}));
originalRouterPath = openmct.router.path;
openmct.on('start', done);
openmct.start(appHolder);
});
@ -229,10 +233,34 @@ describe("The Imagery View Layout", () => {
start: 0,
end: 1
});
openmct.router.path = originalRouterPath;
return resetApplicationState(openmct);
});
it("should provide an imagery time strip view when in a time strip", () => {
openmct.router.path = [{
identifier: {
key: 'test-timestrip',
namespace: ''
},
type: 'time-strip'
}];
let applicableViews = openmct.objectViews.get(imageryObject, [imageryObject, {
identifier: {
key: 'test-timestrip',
namespace: ''
},
type: 'time-strip'
}]);
let imageryView = applicableViews.find(
viewProvider => viewProvider.key === imageryForTimeStripKey
);
expect(imageryView).toBeDefined();
});
it("should provide an imagery view only for imagery producing objects", () => {
let applicableViews = openmct.objectViews.get(imageryObject, []);
let imageryView = applicableViews.find(
@ -242,6 +270,46 @@ describe("The Imagery View Layout", () => {
expect(imageryView).toBeDefined();
});
it("should not provide an imagery view when in a time strip", () => {
openmct.router.path = [{
identifier: {
key: 'test-timestrip',
namespace: ''
},
type: 'time-strip'
}];
let applicableViews = openmct.objectViews.get(imageryObject, [imageryObject, {
identifier: {
key: 'test-timestrip',
namespace: ''
},
type: 'time-strip'
}]);
let imageryView = applicableViews.find(
viewProvider => viewProvider.key === imageryKey
);
expect(imageryView).toBeUndefined();
});
it("should provide an imagery view when navigated to in the composition of a time strip", () => {
openmct.router.path = [imageryObject];
let applicableViews = openmct.objectViews.get(imageryObject, [imageryObject, {
identifier: {
key: 'test-timestrip',
namespace: ''
},
type: 'time-strip'
}]);
let imageryView = applicableViews.find(
viewProvider => viewProvider.key === imageryKey
);
expect(imageryView).toBeDefined();
});
describe("imagery view", () => {
let applicableViews;
let imageryViewProvider;
@ -367,18 +435,18 @@ describe("The Imagery View Layout", () => {
});
it ('shows an auto scroll button when scroll to left', async () => {
// to mock what a scroll would do
imageryView._getInstance().$refs.ImageryLayout.autoScroll = false;
imageryView._getInstance().$refs.ImageryContainer.autoScroll = false;
await Vue.nextTick();
let autoScrollButton = parent.querySelector('.c-imagery__auto-scroll-resume-button');
expect(autoScrollButton).toBeTruthy();
});
it ('scrollToRight is called when clicking on auto scroll button', async () => {
// use spyon to spy the scroll function
spyOn(imageryView._getInstance().$refs.ImageryLayout, 'scrollToRight');
imageryView._getInstance().$refs.ImageryLayout.autoScroll = false;
spyOn(imageryView._getInstance().$refs.ImageryContainer, 'scrollToRight');
imageryView._getInstance().$refs.ImageryContainer.autoScroll = false;
await Vue.nextTick();
parent.querySelector('.c-imagery__auto-scroll-resume-button').click();
expect(imageryView._getInstance().$refs.ImageryLayout.scrollToRight).toHaveBeenCalledWith('reset');
expect(imageryView._getInstance().$refs.ImageryContainer.scrollToRight).toHaveBeenCalledWith('reset');
});
});

View File

@ -1,4 +1,4 @@
import { getDefaultNotebook } from '../utils/notebook-storage';
import { getDefaultNotebook, getNotebookSectionAndPage } from '../utils/notebook-storage';
import { addNotebookEntry } from '../utils/notebook-entries';
export default class CopyToNotebookAction {
@ -15,11 +15,16 @@ export default class CopyToNotebookAction {
copyToNotebook(entryText) {
const notebookStorage = getDefaultNotebook();
this.openmct.objects.get(notebookStorage.notebookMeta.identifier)
this.openmct.objects.get(notebookStorage.identifier)
.then(domainObject => {
addNotebookEntry(this.openmct, domainObject, notebookStorage, null, entryText);
const defaultPath = `${domainObject.name} - ${notebookStorage.section.name} - ${notebookStorage.page.name}`;
const { section, page } = getNotebookSectionAndPage(domainObject, notebookStorage.defaultSectionId, notebookStorage.defaultPageId);
if (!section || !page) {
return;
}
const defaultPath = `${domainObject.name} - ${section.name} - ${page.name}`;
const msg = `Saved to Notebook ${defaultPath}`;
this.openmct.notifications.info(msg);
});

Some files were not shown because too many files have changed in this diff Show More