mirror of
https://github.com/nasa/openmct.git
synced 2025-06-29 20:23:04 +00:00
Compare commits
69 Commits
html2canva
...
bar-graph-
Author | SHA1 | Date | |
---|---|---|---|
b3a774ffa6 | |||
225d3883d1 | |||
bef5fc2e47 | |||
77663ac2b3 | |||
90b98a5c97 | |||
e056b79033 | |||
5362344529 | |||
c4b9be18f1 | |||
eabdf6cd04 | |||
e56c673005 | |||
dad9f12a5c | |||
aa5edb0b83 | |||
b315803180 | |||
b27317631b | |||
953a9daafb | |||
63f9cd449f | |||
54220f547b | |||
93d967c2b3 | |||
1226459c6f | |||
d7c9c9cb98 | |||
2131ef2397 | |||
48c22369a1 | |||
6506077f4d | |||
b1b4266ff3 | |||
42b0148f93 | |||
9461ad8edd | |||
40055ba955 | |||
9cb85ad176 | |||
f2b2953a5d | |||
62de310686 | |||
4b9ff67e49 | |||
d5e32ec494 | |||
38880ba3d1 | |||
a99ce7733c | |||
9f48764210 | |||
a1aaa0dd41 | |||
bee15e98c8 | |||
092bbe547d | |||
6cbe05317c | |||
3b92fcdf6c | |||
6dde54bd25 | |||
359e7377ac | |||
9f4190f781 | |||
f3fc991a74 | |||
2564e75fc9 | |||
f42fe78acf | |||
fe928a1386 | |||
b329ed6ed5 | |||
9b7a0d7e4c | |||
5c15e53abb | |||
f58b3881f2 | |||
071a13b219 | |||
ca66898e51 | |||
94c7b2343a | |||
c397c336ab | |||
eea23f2caf | |||
6665641c02 | |||
c3ebf52dd2 | |||
f8f2e7da9b | |||
240f58b2d0 | |||
7d3baee7b5 | |||
1f5cb7ca42 | |||
4a7ebe326c | |||
10da314a4a | |||
b3ceccd7fb | |||
1bde4c9a0c | |||
4b85360446 | |||
41b860a547 | |||
254b3db966 |
@ -1,36 +1,93 @@
|
|||||||
version: 2
|
version: 2.1
|
||||||
jobs:
|
executors:
|
||||||
build:
|
linux:
|
||||||
docker:
|
docker:
|
||||||
- image: circleci/node:13-browsers
|
- image: cimg/base:stable
|
||||||
environment:
|
orbs:
|
||||||
CHROME_BIN: "/usr/bin/google-chrome"
|
node: circleci/node@4.5.1
|
||||||
steps:
|
browser-tools: circleci/browser-tools@1.1.3
|
||||||
- checkout
|
jobs:
|
||||||
- run:
|
|
||||||
name: Update npm
|
|
||||||
command: 'sudo npm install -g npm@latest'
|
|
||||||
- restore_cache:
|
|
||||||
key: dependency-cache-{{ checksum "package.json" }}
|
|
||||||
- run:
|
|
||||||
name: Installing dependencies (npm install)
|
|
||||||
command: npm install
|
|
||||||
- save_cache:
|
|
||||||
key: dependency-cache-{{ checksum "package.json" }}
|
|
||||||
paths:
|
|
||||||
- node_modules
|
|
||||||
- run:
|
|
||||||
name: npm run test:coverage
|
|
||||||
command: npm run test:coverage
|
|
||||||
- run:
|
|
||||||
name: npm run lint
|
|
||||||
command: npm run lint
|
|
||||||
- store_artifacts:
|
|
||||||
path: dist
|
|
||||||
prefix: dist
|
|
||||||
|
|
||||||
workflows:
|
|
||||||
version: 2
|
|
||||||
test:
|
test:
|
||||||
|
parameters:
|
||||||
|
node-version:
|
||||||
|
type: string
|
||||||
|
browser:
|
||||||
|
type: string
|
||||||
|
always-pass:
|
||||||
|
type: boolean
|
||||||
|
executor: linux
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- restore_cache:
|
||||||
|
key: deps-{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }}
|
||||||
|
- node/install:
|
||||||
|
node-version: << parameters.node-version >>
|
||||||
|
- node/install-packages:
|
||||||
|
override-ci-command: npm install
|
||||||
|
- when: # Just to save time until caching saves the browser bin
|
||||||
|
condition:
|
||||||
|
equal: [ "FirefoxESR", <<parameters.browser>> ]
|
||||||
|
steps:
|
||||||
|
- browser-tools/install-firefox:
|
||||||
|
version: "78.11.0esr" #https://archive.mozilla.org/pub/firefox/releases/
|
||||||
|
- when: # Just to save time until caching saves the browser bin
|
||||||
|
condition:
|
||||||
|
equal: [ "ChromeHeadless", <<parameters.browser>> ]
|
||||||
|
steps:
|
||||||
|
- browser-tools/install-chrome:
|
||||||
|
replace-existing: false
|
||||||
|
- save_cache:
|
||||||
|
key: deps-{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }}
|
||||||
|
paths:
|
||||||
|
- ~/.npm
|
||||||
|
- ~/.cache
|
||||||
|
- node_modules
|
||||||
|
- run: npm run test:coverage -- --browsers=<<parameters.browser>> || <<parameters.always-pass>>
|
||||||
|
- store_test_results:
|
||||||
|
path: dist/reports/tests/
|
||||||
|
- store_artifacts:
|
||||||
|
path: dist/reports/
|
||||||
|
workflows:
|
||||||
|
matrix-tests:
|
||||||
jobs:
|
jobs:
|
||||||
- build
|
- test:
|
||||||
|
name: node10-chrome
|
||||||
|
node-version: lts/dubnium
|
||||||
|
browser: ChromeHeadless
|
||||||
|
always-pass: false
|
||||||
|
- test:
|
||||||
|
name: node12-firefoxESR-build-only
|
||||||
|
node-version: lts/erbium
|
||||||
|
browser: FirefoxESR
|
||||||
|
always-pass: true
|
||||||
|
- test:
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
11
.github/ISSUE_TEMPLATE/bug_report.md
vendored
11
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -1,11 +1,12 @@
|
|||||||
<!--- This is for filing bugs. If you have a general question, please -->
|
|
||||||
<!--- visit https://github.com/nasa/openmct/discussions -->
|
|
||||||
|
|
||||||
---
|
---
|
||||||
name: Bug Report
|
name: Bug report
|
||||||
about: File a Bug !
|
about: File a Bug !
|
||||||
|
title: ''
|
||||||
|
labels: type:bug
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<!--- Focus on user impact in the title. Use the Summary Field to -->
|
<!--- Focus on user impact in the title. Use the Summary Field to -->
|
||||||
<!--- describe the problem technically. -->
|
<!--- describe the problem technically. -->
|
||||||
|
|
||||||
@ -35,7 +36,7 @@ about: File a Bug !
|
|||||||
|
|
||||||
#### Environment
|
#### Environment
|
||||||
* Open MCT Version: <!--- date of build, version, or SHA -->
|
* Open MCT Version: <!--- date of build, version, or SHA -->
|
||||||
* Deployment Type: <!--- npm dev? VIPER Dev? openmct-yams? -->
|
* Deployment Type: <!--- npm dev? VIPER Dev? openmct-yamcs? -->
|
||||||
* OS:
|
* OS:
|
||||||
* Browser:
|
* Browser:
|
||||||
|
|
||||||
|
6
.github/ISSUE_TEMPLATE/config.yml
vendored
6
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -1 +1,5 @@
|
|||||||
blank_issues_enabled: false
|
blank_issues_enabled: true
|
||||||
|
contact_links:
|
||||||
|
- name: Discussions
|
||||||
|
url: https://github.com/nasa/openmct/discussions
|
||||||
|
about: Got a question?
|
||||||
|
20
.github/ISSUE_TEMPLATE/enhancement-request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/enhancement-request.md
vendored
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
name: Enhancement request
|
||||||
|
about: Suggest an enhancement or new improvement for this project
|
||||||
|
title: ''
|
||||||
|
labels: type:enhancement
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
33
.github/workflows/codeql-analysis.yml
vendored
Normal file
33
.github/workflows/codeql-analysis.yml
vendored
Normal 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
|
4
.github/workflows/lighthouse.yml
vendored
4
.github/workflows/lighthouse.yml
vendored
@ -13,6 +13,8 @@ jobs:
|
|||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.inputs.version }}
|
ref: ${{ github.event.inputs.version }}
|
||||||
- uses: actions/setup-node@v1
|
- uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: '14'
|
||||||
- run: npm install && npm install -g @lhci/cli #Don't want to include this in our deps
|
- run: npm install && npm install -g @lhci/cli #Don't want to include this in our deps
|
||||||
- run: lhci autorun
|
- run: lhci autorun
|
2
API.md
2
API.md
@ -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
|
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,
|
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
|
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
|
the [documentation](./src/plugins/URLIndicatorPlugin) for details on how to install and configure the
|
||||||
URL Status Indicator.
|
URL Status Indicator.
|
||||||
|
|
||||||
|
@ -423,7 +423,7 @@ which can help with this, however.
|
|||||||
instead of separate approaches for static and substitutable
|
instead of separate approaches for static and substitutable
|
||||||
dependencies.
|
dependencies.
|
||||||
* Removes need to understand Angular's DI mechanism.
|
* 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`
|
instance of `CompositeService` and implements `TypeService`
|
||||||
so you can easily traverse links in the JSDoc.)
|
so you can easily traverse links in the JSDoc.)
|
||||||
* Can be used more easily from Web Workers, allowing services
|
* Can be used more easily from Web Workers, allowing services
|
||||||
|
@ -25,7 +25,7 @@
|
|||||||
## Legacy Documentation
|
## Legacy Documentation
|
||||||
|
|
||||||
As we transition to a new API, the following documentation for the old API
|
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
|
* The [Architecture Overview](architecture/) describes the concepts used
|
||||||
throughout Open MCT, and gives a high level overview of the platform's design.
|
throughout Open MCT, and gives a high level overview of the platform's design.
|
||||||
|
@ -63,7 +63,7 @@ define([
|
|||||||
|
|
||||||
StateGeneratorProvider.prototype.request = function (domainObject, options) {
|
StateGeneratorProvider.prototype.request = function (domainObject, options) {
|
||||||
var start = options.start;
|
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;
|
var duration = domainObject.telemetry.duration * 1000;
|
||||||
if (options.strategy === 'latest' || options.size === 1) {
|
if (options.strategy === 'latest' || options.size === 1) {
|
||||||
start = end;
|
start = end;
|
||||||
|
@ -49,6 +49,10 @@ define([
|
|||||||
];
|
];
|
||||||
const IMAGE_DELAY = 20000;
|
const IMAGE_DELAY = 20000;
|
||||||
|
|
||||||
|
function getCompassValues(min, max) {
|
||||||
|
return min + Math.random() * (max - min);
|
||||||
|
}
|
||||||
|
|
||||||
function pointForTimestamp(timestamp, name) {
|
function pointForTimestamp(timestamp, name) {
|
||||||
const url = IMAGE_SAMPLES[Math.floor(timestamp / IMAGE_DELAY) % IMAGE_SAMPLES.length];
|
const url = IMAGE_SAMPLES[Math.floor(timestamp / IMAGE_DELAY) % IMAGE_SAMPLES.length];
|
||||||
const urlItems = url.split('/');
|
const urlItems = url.split('/');
|
||||||
@ -59,6 +63,9 @@ define([
|
|||||||
utc: Math.floor(timestamp / IMAGE_DELAY) * IMAGE_DELAY,
|
utc: Math.floor(timestamp / IMAGE_DELAY) * IMAGE_DELAY,
|
||||||
local: Math.floor(timestamp / IMAGE_DELAY) * IMAGE_DELAY,
|
local: Math.floor(timestamp / IMAGE_DELAY) * IMAGE_DELAY,
|
||||||
url,
|
url,
|
||||||
|
sunOrientation: getCompassValues(0, 360),
|
||||||
|
cameraPan: getCompassValues(0, 360),
|
||||||
|
heading: getCompassValues(0, 360),
|
||||||
imageDownloadName
|
imageDownloadName
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import Vue from 'Vue';
|
import Vue from 'vue';
|
||||||
import HelloWorld from './HelloWorld.vue';
|
import HelloWorld from './HelloWorld.vue';
|
||||||
|
|
||||||
function SimpleVuePlugin() {
|
function SimpleVuePlugin() {
|
||||||
|
@ -152,7 +152,7 @@
|
|||||||
<h2>How to Use Glyphs</h2>
|
<h2>How to Use Glyphs</h2>
|
||||||
<div class="cols cols1-1">
|
<div class="cols cols1-1">
|
||||||
<div class="col">
|
<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>
|
<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>
|
</div>
|
||||||
<mct-example><a class="s-button icon-gear" title="Settings"></a>
|
<mct-example><a class="s-button icon-gear" title="Settings"></a>
|
||||||
|
@ -88,6 +88,7 @@
|
|||||||
openmct.install(openmct.plugins.ExampleImagery());
|
openmct.install(openmct.plugins.ExampleImagery());
|
||||||
openmct.install(openmct.plugins.PlanLayout());
|
openmct.install(openmct.plugins.PlanLayout());
|
||||||
openmct.install(openmct.plugins.Timeline());
|
openmct.install(openmct.plugins.Timeline());
|
||||||
|
openmct.install(openmct.plugins.Hyperlink());
|
||||||
openmct.install(openmct.plugins.UTCTimeSystem());
|
openmct.install(openmct.plugins.UTCTimeSystem());
|
||||||
openmct.install(openmct.plugins.AutoflowView({
|
openmct.install(openmct.plugins.AutoflowView({
|
||||||
type: "telemetry.panel"
|
type: "telemetry.panel"
|
||||||
@ -194,6 +195,7 @@
|
|||||||
['table', 'telemetry.plot.overlay', 'telemetry.plot.stacked'],
|
['table', 'telemetry.plot.overlay', 'telemetry.plot.stacked'],
|
||||||
{indicator: true}
|
{indicator: true}
|
||||||
));
|
));
|
||||||
|
openmct.install(openmct.plugins.Clock({ enableClockIndicator: true }));
|
||||||
openmct.start();
|
openmct.start();
|
||||||
</script>
|
</script>
|
||||||
</html>
|
</html>
|
||||||
|
@ -23,9 +23,9 @@
|
|||||||
/*global module,process*/
|
/*global module,process*/
|
||||||
|
|
||||||
const devMode = process.env.NODE_ENV !== 'production';
|
const devMode = process.env.NODE_ENV !== 'production';
|
||||||
const browsers = [process.env.NODE_ENV === 'debug' ? 'ChromeDebugging' : 'FirefoxHeadless'];
|
const browsers = [process.env.NODE_ENV === 'debug' ? 'ChromeDebugging' : 'ChromeHeadless'];
|
||||||
const coverageEnabled = process.env.COVERAGE === 'true';
|
const coverageEnabled = process.env.COVERAGE === 'true';
|
||||||
const reporters = ['progress', 'html'];
|
const reporters = ['progress', 'html', 'junit'];
|
||||||
|
|
||||||
if (coverageEnabled) {
|
if (coverageEnabled) {
|
||||||
reporters.push('coverage-istanbul');
|
reporters.push('coverage-istanbul');
|
||||||
@ -59,7 +59,8 @@ module.exports = (config) => {
|
|||||||
browsers: browsers,
|
browsers: browsers,
|
||||||
client: {
|
client: {
|
||||||
jasmine: {
|
jasmine: {
|
||||||
random: false
|
random: false,
|
||||||
|
timeoutInterval: 30000
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
customLaunchers: {
|
customLaunchers: {
|
||||||
@ -67,6 +68,10 @@ module.exports = (config) => {
|
|||||||
base: 'Chrome',
|
base: 'Chrome',
|
||||||
flags: ['--remote-debugging-port=9222'],
|
flags: ['--remote-debugging-port=9222'],
|
||||||
debug: true
|
debug: true
|
||||||
|
},
|
||||||
|
FirefoxESR: {
|
||||||
|
base: 'FirefoxHeadless',
|
||||||
|
name: 'FirefoxESR'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
colors: true,
|
colors: true,
|
||||||
@ -78,12 +83,21 @@ module.exports = (config) => {
|
|||||||
preserveDescribeNesting: true,
|
preserveDescribeNesting: true,
|
||||||
foldAll: false
|
foldAll: false
|
||||||
},
|
},
|
||||||
browserConsoleLogOptions: { level: "error", format: "%b %T: %m", terminal: true },
|
junitReporter: {
|
||||||
|
outputDir: "dist/reports/tests",
|
||||||
|
outputFile: "test-results.xml",
|
||||||
|
useBrowserName: false
|
||||||
|
},
|
||||||
|
browserConsoleLogOptions: {
|
||||||
|
level: "error",
|
||||||
|
format: "%b %T: %m",
|
||||||
|
terminal: true
|
||||||
|
},
|
||||||
coverageIstanbulReporter: {
|
coverageIstanbulReporter: {
|
||||||
fixWebpackSourcePaths: true,
|
fixWebpackSourcePaths: true,
|
||||||
dir: process.env.CIRCLE_ARTIFACTS ?
|
dir: process.env.CIRCLE_ARTIFACTS
|
||||||
process.env.CIRCLE_ARTIFACTS + '/coverage' :
|
? process.env.CIRCLE_ARTIFACTS + '/coverage'
|
||||||
"dist/reports/coverage",
|
: "dist/reports/coverage",
|
||||||
reports: ['html', 'lcovonly', 'text-summary'],
|
reports: ['html', 'lcovonly', 'text-summary'],
|
||||||
thresholds: {
|
thresholds: {
|
||||||
global: {
|
global: {
|
||||||
|
29
package.json
29
package.json
@ -1,8 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "openmct",
|
"name": "openmct",
|
||||||
"version": "1.7.4",
|
"version": "1.7.8-SNAPSHOT",
|
||||||
"description": "The Open MCT core platform",
|
"description": "The Open MCT core platform",
|
||||||
"dependencies": {},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"angular": ">=1.8.0",
|
"angular": ">=1.8.0",
|
||||||
"angular-route": "1.4.14",
|
"angular-route": "1.4.14",
|
||||||
@ -12,16 +11,9 @@
|
|||||||
"copy-webpack-plugin": "^4.5.2",
|
"copy-webpack-plugin": "^4.5.2",
|
||||||
"cross-env": "^6.0.3",
|
"cross-env": "^6.0.3",
|
||||||
"css-loader": "^1.0.0",
|
"css-loader": "^1.0.0",
|
||||||
"d3-array": "1.2.x",
|
|
||||||
"d3-axis": "1.0.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-scale": "1.0.x",
|
||||||
"d3-selection": "1.3.x",
|
"d3-selection": "1.3.x",
|
||||||
"d3-time": "1.0.x",
|
|
||||||
"d3-time-format": "2.1.x",
|
|
||||||
"eslint": "7.0.0",
|
"eslint": "7.0.0",
|
||||||
"eslint-plugin-vue": "^7.5.0",
|
"eslint-plugin-vue": "^7.5.0",
|
||||||
"eslint-plugin-you-dont-need-lodash-underscore": "^6.10.0",
|
"eslint-plugin-you-dont-need-lodash-underscore": "^6.10.0",
|
||||||
@ -34,20 +26,21 @@
|
|||||||
"git-rev-sync": "^1.4.0",
|
"git-rev-sync": "^1.4.0",
|
||||||
"glob": ">= 3.0.0",
|
"glob": ">= 3.0.0",
|
||||||
"html-loader": "^0.5.5",
|
"html-loader": "^0.5.5",
|
||||||
"html2canvas": "^1.0.0-alpha.12",
|
"html2canvas": "^1.0.0-rc.7",
|
||||||
"imports-loader": "^0.8.0",
|
"imports-loader": "^0.8.0",
|
||||||
"istanbul-instrumenter-loader": "^3.0.1",
|
"istanbul-instrumenter-loader": "^3.0.1",
|
||||||
"jasmine-core": "^3.1.0",
|
"jasmine-core": "^3.7.1",
|
||||||
"jsdoc": "^3.3.2",
|
"jsdoc": "^3.3.2",
|
||||||
"karma": "5.1.1",
|
"karma": "6.3.4",
|
||||||
"karma-chrome-launcher": "3.1.0",
|
"karma-chrome-launcher": "3.1.0",
|
||||||
"karma-cli": "2.0.0",
|
"karma-cli": "2.0.0",
|
||||||
"karma-coverage": "2.0.3",
|
"karma-coverage": "2.0.3",
|
||||||
"karma-coverage-istanbul-reporter": "3.0.3",
|
"karma-coverage-istanbul-reporter": "3.0.3",
|
||||||
"karma-firefox-launcher": "1.3.0",
|
"karma-firefox-launcher": "2.1.1",
|
||||||
"karma-html-reporter": "0.2.7",
|
"karma-html-reporter": "0.2.7",
|
||||||
"karma-jasmine": "3.3.1",
|
"karma-jasmine": "4.0.1",
|
||||||
"karma-sourcemap-loader": "0.3.7",
|
"karma-junit-reporter": "2.0.1",
|
||||||
|
"karma-sourcemap-loader": "0.3.8",
|
||||||
"karma-webpack": "4.0.2",
|
"karma-webpack": "4.0.2",
|
||||||
"location-bar": "^3.0.1",
|
"location-bar": "^3.0.1",
|
||||||
"lodash": "^4.17.12",
|
"lodash": "^4.17.12",
|
||||||
@ -61,6 +54,8 @@
|
|||||||
"node-bourbon": "^4.2.3",
|
"node-bourbon": "^4.2.3",
|
||||||
"node-sass": "^4.14.1",
|
"node-sass": "^4.14.1",
|
||||||
"painterro": "^1.2.56",
|
"painterro": "^1.2.56",
|
||||||
|
"plotly.js-basic-dist": "^2.5.0",
|
||||||
|
"plotly.js-gl2d-dist": "^2.5.0",
|
||||||
"printj": "^1.2.1",
|
"printj": "^1.2.1",
|
||||||
"raw-loader": "^0.5.1",
|
"raw-loader": "^0.5.1",
|
||||||
"request": "^2.69.0",
|
"request": "^2.69.0",
|
||||||
@ -89,6 +84,7 @@
|
|||||||
"test": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run",
|
"test": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run",
|
||||||
"test:debug": "cross-env NODE_ENV=debug karma start --no-single-run",
|
"test:debug": "cross-env NODE_ENV=debug karma start --no-single-run",
|
||||||
"test:coverage": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" COVERAGE=true karma start --single-run",
|
"test:coverage": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" COVERAGE=true karma start --single-run",
|
||||||
|
"test:coverage:firefox": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run --browsers=FirefoxHeadless",
|
||||||
"test:watch": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --no-single-run",
|
"test:watch": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --no-single-run",
|
||||||
"verify": "concurrently 'npm:test' 'npm:lint'",
|
"verify": "concurrently 'npm:test' 'npm:lint'",
|
||||||
"jsdoc": "jsdoc -c jsdoc.json -R API.md -r -d dist/docs/api",
|
"jsdoc": "jsdoc -c jsdoc.json -R API.md -r -d dist/docs/api",
|
||||||
@ -100,6 +96,9 @@
|
|||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/nasa/openmct.git"
|
"url": "https://github.com/nasa/openmct.git"
|
||||||
},
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.10.2 <16.0.0"
|
||||||
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"private": true
|
"private": true
|
||||||
|
@ -64,7 +64,7 @@ define(
|
|||||||
*
|
*
|
||||||
* @param {DomainObject} domainObject the domain object to navigate to
|
* @param {DomainObject} domainObject the domain object to navigate to
|
||||||
* @param {Boolean} force if true, force navigation to occur.
|
* @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) {
|
NavigationService.prototype.setNavigation = function (domainObject, force) {
|
||||||
if (force) {
|
if (force) {
|
||||||
|
@ -21,28 +21,14 @@
|
|||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
define([
|
define([
|
||||||
"./src/MCTDevice",
|
"./src/AgentService"
|
||||||
"./src/AgentService",
|
|
||||||
"./src/DeviceClassifier"
|
|
||||||
], function (
|
], function (
|
||||||
MCTDevice,
|
AgentService
|
||||||
AgentService,
|
|
||||||
DeviceClassifier
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: "platform/commonUI/mobile",
|
name: "platform/commonUI/mobile",
|
||||||
definition: {
|
definition: {
|
||||||
"extensions": {
|
"extensions": {
|
||||||
"directives": [
|
|
||||||
{
|
|
||||||
"key": "mctDevice",
|
|
||||||
"implementation": MCTDevice,
|
|
||||||
"depends": [
|
|
||||||
"agentService"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"services": [
|
"services": [
|
||||||
{
|
{
|
||||||
"key": "agentService",
|
"key": "agentService",
|
||||||
@ -51,15 +37,6 @@ define([
|
|||||||
"$window"
|
"$window"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
|
||||||
"runs": [
|
|
||||||
{
|
|
||||||
"implementation": DeviceClassifier,
|
|
||||||
"depends": [
|
|
||||||
"agentService",
|
|
||||||
"$document"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,122 +20,12 @@
|
|||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
/**
|
define(["../../../../src/utils/agent/Agent.js"], function (Agent) {
|
||||||
* Provides features which support variant behavior on mobile devices.
|
function AngularAgentServiceWrapper(window) {
|
||||||
*
|
const AS = Agent.default;
|
||||||
* @namespace platform/commonUI/mobile
|
|
||||||
*/
|
|
||||||
define(
|
|
||||||
[],
|
|
||||||
function () {
|
|
||||||
|
|
||||||
/**
|
return new AS(window);
|
||||||
* 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 AngularAgentServiceWrapper;
|
||||||
|
});
|
||||||
|
96
platform/commonUI/mobile/src/AgentServiceSpec.js
Normal file
96
platform/commonUI/mobile/src/AgentServiceSpec.js
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
@ -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;
|
|
||||||
|
|
||||||
}
|
|
||||||
);
|
|
@ -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;
|
|
||||||
}
|
|
||||||
);
|
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
@ -379,7 +379,7 @@ define([
|
|||||||
{
|
{
|
||||||
"name": "Math.uuid.js",
|
"name": "Math.uuid.js",
|
||||||
"version": "1.4.7",
|
"version": "1.4.7",
|
||||||
"description": "Unique identifer generation (code adapted.)",
|
"description": "Unique identifier generation (code adapted.)",
|
||||||
"author": "Robert Kieffer",
|
"author": "Robert Kieffer",
|
||||||
"website": "https://github.com/broofa/node-uuid",
|
"website": "https://github.com/broofa/node-uuid",
|
||||||
"copyright": "Copyright (c) 2010-2012 Robert Kieffer",
|
"copyright": "Copyright (c) 2010-2012 Robert Kieffer",
|
||||||
|
@ -21,32 +21,24 @@
|
|||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
define([
|
define([
|
||||||
"moment-timezone",
|
|
||||||
"./src/indicators/ClockIndicator",
|
|
||||||
"./src/services/TickerService",
|
"./src/services/TickerService",
|
||||||
"./src/services/TimerService",
|
"./src/services/TimerService",
|
||||||
"./src/controllers/ClockController",
|
|
||||||
"./src/controllers/TimerController",
|
"./src/controllers/TimerController",
|
||||||
"./src/controllers/RefreshingController",
|
"./src/controllers/RefreshingController",
|
||||||
"./src/actions/StartTimerAction",
|
"./src/actions/StartTimerAction",
|
||||||
"./src/actions/RestartTimerAction",
|
"./src/actions/RestartTimerAction",
|
||||||
"./src/actions/StopTimerAction",
|
"./src/actions/StopTimerAction",
|
||||||
"./src/actions/PauseTimerAction",
|
"./src/actions/PauseTimerAction",
|
||||||
"./res/templates/clock.html",
|
|
||||||
"./res/templates/timer.html"
|
"./res/templates/timer.html"
|
||||||
], function (
|
], function (
|
||||||
MomentTimezone,
|
|
||||||
ClockIndicator,
|
|
||||||
TickerService,
|
TickerService,
|
||||||
TimerService,
|
TimerService,
|
||||||
ClockController,
|
|
||||||
TimerController,
|
TimerController,
|
||||||
RefreshingController,
|
RefreshingController,
|
||||||
StartTimerAction,
|
StartTimerAction,
|
||||||
RestartTimerAction,
|
RestartTimerAction,
|
||||||
StopTimerAction,
|
StopTimerAction,
|
||||||
PauseTimerAction,
|
PauseTimerAction,
|
||||||
clockTemplate,
|
|
||||||
timerTemplate
|
timerTemplate
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
@ -73,16 +65,6 @@ define([
|
|||||||
"value": "YYYY/MM/DD HH:mm:ss"
|
"value": "YYYY/MM/DD HH:mm:ss"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"indicators": [
|
|
||||||
{
|
|
||||||
"implementation": ClockIndicator,
|
|
||||||
"depends": [
|
|
||||||
"tickerService",
|
|
||||||
"CLOCK_INDICATOR_FORMAT"
|
|
||||||
],
|
|
||||||
"priority": "preferred"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"services": [
|
"services": [
|
||||||
{
|
{
|
||||||
"key": "tickerService",
|
"key": "tickerService",
|
||||||
@ -99,14 +81,6 @@ define([
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"controllers": [
|
"controllers": [
|
||||||
{
|
|
||||||
"key": "ClockController",
|
|
||||||
"implementation": ClockController,
|
|
||||||
"depends": [
|
|
||||||
"$scope",
|
|
||||||
"tickerService"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"key": "TimerController",
|
"key": "TimerController",
|
||||||
"implementation": TimerController,
|
"implementation": TimerController,
|
||||||
@ -126,12 +100,6 @@ define([
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"views": [
|
"views": [
|
||||||
{
|
|
||||||
"key": "clock",
|
|
||||||
"type": "clock",
|
|
||||||
"editable": false,
|
|
||||||
"template": clockTemplate
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"key": "timer",
|
"key": "timer",
|
||||||
"type": "timer",
|
"type": "timer",
|
||||||
@ -181,75 +149,11 @@ define([
|
|||||||
],
|
],
|
||||||
"category": "contextual",
|
"category": "contextual",
|
||||||
"name": "Stop",
|
"name": "Stop",
|
||||||
"cssClass": "icon-box",
|
"cssClass": "icon-box-round-corners",
|
||||||
"priority": "preferred"
|
"priority": "preferred"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"types": [
|
"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",
|
"key": "timer",
|
||||||
"name": "Timer",
|
"name": "Timer",
|
||||||
|
@ -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>
|
|
@ -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;
|
|
||||||
}
|
|
||||||
);
|
|
@ -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;
|
|
||||||
}
|
|
||||||
);
|
|
@ -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();
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
@ -101,7 +101,7 @@ define(
|
|||||||
name: "Pause"
|
name: "Pause"
|
||||||
});
|
});
|
||||||
mockStop.getMetadata.and.returnValue({
|
mockStop.getMetadata.and.returnValue({
|
||||||
cssClass: "icon-box",
|
cssClass: "icon-box-round-corners",
|
||||||
name: "Stop"
|
name: "Stop"
|
||||||
});
|
});
|
||||||
mockScope.domainObject = mockDomainObject;
|
mockScope.domainObject = mockDomainObject;
|
||||||
|
@ -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));
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
@ -1,120 +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/HyperlinkController',
|
|
||||||
'./res/templates/hyperlink.html'
|
|
||||||
], function (
|
|
||||||
HyperlinkController,
|
|
||||||
hyperlinkTemplate
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
name: "platform/features/hyperlink",
|
|
||||||
definition: {
|
|
||||||
"name": "Hyperlink",
|
|
||||||
"description": "Insert a hyperlink to reference a link",
|
|
||||||
"extensions": {
|
|
||||||
"types": [
|
|
||||||
{
|
|
||||||
"key": "hyperlink",
|
|
||||||
"name": "Hyperlink",
|
|
||||||
"cssClass": "icon-chain-links",
|
|
||||||
"description": "A hyperlink to redirect to a different link",
|
|
||||||
"features": ["creation"],
|
|
||||||
"properties": [
|
|
||||||
{
|
|
||||||
"key": "url",
|
|
||||||
"name": "URL",
|
|
||||||
"control": "textfield",
|
|
||||||
"required": true,
|
|
||||||
"cssClass": "l-input-lg"
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"key": "displayText",
|
|
||||||
"name": "Text to Display",
|
|
||||||
"control": "textfield",
|
|
||||||
"required": true,
|
|
||||||
"cssClass": "l-input-lg"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "displayFormat",
|
|
||||||
"name": "Display Format",
|
|
||||||
"control": "select",
|
|
||||||
"options": [
|
|
||||||
{
|
|
||||||
"name": "Link",
|
|
||||||
"value": "link"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"value": "button",
|
|
||||||
"name": "Button"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"cssClass": "l-inline"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "openNewTab",
|
|
||||||
"name": "Tab to Open Hyperlink",
|
|
||||||
"control": "select",
|
|
||||||
"options": [
|
|
||||||
{
|
|
||||||
"name": "Open in this tab",
|
|
||||||
"value": "thisTab"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"value": "newTab",
|
|
||||||
"name": "Open in a new tab"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"cssClass": "l-inline"
|
|
||||||
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"model": {
|
|
||||||
"displayFormat": "link",
|
|
||||||
"openNewTab": "thisTab",
|
|
||||||
"removeTitle": true
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"views": [
|
|
||||||
{
|
|
||||||
"key": "hyperlink",
|
|
||||||
"type": "hyperlink",
|
|
||||||
"name": "Hyperlink Display",
|
|
||||||
"template": hyperlinkTemplate,
|
|
||||||
"editable": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"controllers": [
|
|
||||||
{
|
|
||||||
"key": "HyperlinkController",
|
|
||||||
"implementation": HyperlinkController,
|
|
||||||
"depends": ["$scope"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
@ -1,61 +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.
|
|
||||||
*****************************************************************************/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This bundle adds the Hyperlink object type, which can be used to add hyperlinks as a domain Object type
|
|
||||||
and into display Layouts as either a button or link that can be chosen to open in either the same tab or
|
|
||||||
create a new tab to open the link in
|
|
||||||
* @namespace platform/features/hyperlink
|
|
||||||
*/
|
|
||||||
define(
|
|
||||||
[],
|
|
||||||
function () {
|
|
||||||
function HyperlinkController($scope) {
|
|
||||||
this.$scope = $scope;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**Function to analyze the location in which to open the hyperlink
|
|
||||||
@returns true if the hyperlink is chosen to open in a different tab, false if the same tab
|
|
||||||
**/
|
|
||||||
HyperlinkController.prototype.openNewTab = function () {
|
|
||||||
if (this.$scope.domainObject.getModel().openNewTab === "thisTab") {
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**Function to specify the format in which the hyperlink should be created
|
|
||||||
@returns true if the hyperlink is chosen to be created as a button, false if a link
|
|
||||||
**/
|
|
||||||
HyperlinkController.prototype.isButton = function () {
|
|
||||||
if (this.$scope.domainObject.getModel().displayFormat === "link") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
return HyperlinkController;
|
|
||||||
}
|
|
||||||
|
|
||||||
);
|
|
@ -1,89 +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/HyperlinkController"],
|
|
||||||
function (HyperlinkController) {
|
|
||||||
|
|
||||||
describe("The controller for hyperlinks", function () {
|
|
||||||
var domainObject,
|
|
||||||
controller,
|
|
||||||
scope;
|
|
||||||
beforeEach(function () {
|
|
||||||
scope = jasmine.createSpyObj(
|
|
||||||
"$scope",
|
|
||||||
["domainObject"]
|
|
||||||
);
|
|
||||||
domainObject = jasmine.createSpyObj(
|
|
||||||
"domainObject",
|
|
||||||
["getModel"]
|
|
||||||
);
|
|
||||||
scope.domainObject = domainObject;
|
|
||||||
controller = new HyperlinkController(scope);
|
|
||||||
});
|
|
||||||
it("knows when it should open a new tab", function () {
|
|
||||||
scope.domainObject.getModel.and.returnValue({
|
|
||||||
"displayFormat": "link",
|
|
||||||
"openNewTab": "newTab",
|
|
||||||
"showTitle": false
|
|
||||||
}
|
|
||||||
);
|
|
||||||
controller = new HyperlinkController(scope);
|
|
||||||
expect(controller.openNewTab())
|
|
||||||
.toBe(true);
|
|
||||||
});
|
|
||||||
it("knows when it is a button", function () {
|
|
||||||
scope.domainObject.getModel.and.returnValue({
|
|
||||||
"displayFormat": "button",
|
|
||||||
"openNewTab": "thisTab",
|
|
||||||
"showTitle": false
|
|
||||||
}
|
|
||||||
);
|
|
||||||
controller = new HyperlinkController(scope);
|
|
||||||
expect(controller.isButton())
|
|
||||||
.toEqual(true);
|
|
||||||
});
|
|
||||||
it("knows when it should open in the same tab", function () {
|
|
||||||
scope.domainObject.getModel.and.returnValue({
|
|
||||||
"displayFormat": "link",
|
|
||||||
"openNewTab": "thisTab",
|
|
||||||
"showTitle": false
|
|
||||||
}
|
|
||||||
);
|
|
||||||
controller = new HyperlinkController(scope);
|
|
||||||
expect(controller.openNewTab())
|
|
||||||
.toBe(false);
|
|
||||||
});
|
|
||||||
it("knows when it is a link", function () {
|
|
||||||
scope.domainObject.getModel.and.returnValue({
|
|
||||||
"displayFormat": "link",
|
|
||||||
"openNewTab": "thisTab",
|
|
||||||
"showTitle": false
|
|
||||||
}
|
|
||||||
);
|
|
||||||
controller = new HyperlinkController(scope);
|
|
||||||
expect(controller.openNewTab())
|
|
||||||
.toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
@ -1,70 +0,0 @@
|
|||||||
This bundle provides the Timeline domain object type, as well
|
|
||||||
as other associated domain object types and relevant views.
|
|
||||||
|
|
||||||
# Implementation notes
|
|
||||||
|
|
||||||
## Model Properties
|
|
||||||
|
|
||||||
The properties below record properties relevant to using and
|
|
||||||
understanding timelines based on their JSON representation.
|
|
||||||
Additional common properties, such as `modified`
|
|
||||||
or `persisted` timestamps, may also be present.
|
|
||||||
|
|
||||||
### Timeline Model
|
|
||||||
|
|
||||||
A timeline's model looks like:
|
|
||||||
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"type": "timeline",
|
|
||||||
"start": {
|
|
||||||
"timestamp": <number> (milliseconds since epoch),
|
|
||||||
"epoch": <string> (currently, always "SET")
|
|
||||||
},
|
|
||||||
"capacity": <number> (optional; battery capacity in watt-hours)
|
|
||||||
"composition": <string[]> (array of identifiers for contained objects)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The identifiers in a timeline's `composition` field should refer to
|
|
||||||
other Timeline objects, or to Activity objects.
|
|
||||||
|
|
||||||
### Activity Model
|
|
||||||
|
|
||||||
An activity's model looks like:
|
|
||||||
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"type": "activity",
|
|
||||||
"start": {
|
|
||||||
"timestamp": <number> (milliseconds since epoch),
|
|
||||||
"epoch": <string> (currently, always "SET")
|
|
||||||
},
|
|
||||||
"duration": {
|
|
||||||
"timestamp": <number> (duration of this activity, in milliseconds)
|
|
||||||
"epoch": "SET" (this is ignored)
|
|
||||||
},
|
|
||||||
"relationships": {
|
|
||||||
"modes": <string[]> (array of applicable Activity Mode ids)
|
|
||||||
},
|
|
||||||
"link": <string> (optional; URL linking to associated external resource)
|
|
||||||
"composition": <string[]> (array of identifiers for contained objects)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The identifiers in a timeline's `composition` field should only refer to
|
|
||||||
other Activity objects.
|
|
||||||
|
|
||||||
### Activity Mode Model
|
|
||||||
|
|
||||||
An activity mode's model looks like:
|
|
||||||
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"type": "mode",
|
|
||||||
"resources": {
|
|
||||||
"comms": <number> (communications utilization, in Kbps)
|
|
||||||
"power": <number> (power utilization, in watts)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
@ -1,10 +0,0 @@
|
|||||||
<div>
|
|
||||||
Timeline, Activity and Activity Mode objects have been deprecated and will no longer be supported.
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
Please open an issue in the
|
|
||||||
<a href="https://github.com/nasa/openmct/issues" target="_blank">
|
|
||||||
Open MCT Issue tracker
|
|
||||||
</a>
|
|
||||||
if you have any questions about the timeline plugin.
|
|
||||||
</div>
|
|
@ -30,8 +30,8 @@ define([
|
|||||||
|
|
||||||
return function ImportExportPlugin() {
|
return function ImportExportPlugin() {
|
||||||
return function (openmct) {
|
return function (openmct) {
|
||||||
ExportAsJSONAction.appliesTo = function (context) {
|
ExportAsJSONAction.prototype.appliesTo = function (context) {
|
||||||
return openmct.$injector.get('policyService')
|
return this.openmct.$injector.get('policyService')
|
||||||
.allow("creation", context.domainObject.getCapability("type")
|
.allow("creation", context.domainObject.getCapability("type")
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -29,7 +29,7 @@ define(
|
|||||||
],
|
],
|
||||||
function (ExportAsJSONAction, domainObjectFactory, MCT, AdapterCapability) {
|
function (ExportAsJSONAction, domainObjectFactory, MCT, AdapterCapability) {
|
||||||
|
|
||||||
xdescribe("The export JSON action", function () {
|
describe("The export JSON action", function () {
|
||||||
|
|
||||||
var context,
|
var context,
|
||||||
action,
|
action,
|
||||||
@ -102,7 +102,7 @@ define(
|
|||||||
expect(action).toBeDefined();
|
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 = {
|
var nonCreatableType = {
|
||||||
hasFeature:
|
hasFeature:
|
||||||
function (feature) {
|
function (feature) {
|
||||||
@ -149,7 +149,7 @@ define(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("can export self-containing objects", function () {
|
xit("can export self-containing objects", function () {
|
||||||
var parent = domainObjectFactory({
|
var parent = domainObjectFactory({
|
||||||
name: 'parent',
|
name: 'parent',
|
||||||
model: {
|
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({
|
var parent = domainObjectFactory({
|
||||||
name: 'parent',
|
name: 'parent',
|
||||||
model: {
|
model: {
|
||||||
|
@ -27,7 +27,7 @@ define(
|
|||||||
],
|
],
|
||||||
function (ImportAsJSONAction, domainObjectFactory) {
|
function (ImportAsJSONAction, domainObjectFactory) {
|
||||||
|
|
||||||
xdescribe("The import JSON action", function () {
|
describe("The import JSON action", function () {
|
||||||
|
|
||||||
var context = {};
|
var context = {};
|
||||||
var action,
|
var action,
|
||||||
@ -146,7 +146,7 @@ define(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("can import self-containing objects", function () {
|
xit("can import self-containing objects", function () {
|
||||||
var compDomainObject = domainObjectFactory({
|
var compDomainObject = domainObjectFactory({
|
||||||
name: 'compObject',
|
name: 'compObject',
|
||||||
model: { 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(
|
dialogService.getUserInput.and.returnValue(Promise.resolve(
|
||||||
{
|
{
|
||||||
selectFile: {
|
selectFile: {
|
||||||
|
@ -47,7 +47,7 @@ define(
|
|||||||
* @param $interval Angular's $interval service
|
* @param $interval Angular's $interval service
|
||||||
* @param {string} space the name of the persistence space being served
|
* @param {string} space the name of the persistence space being served
|
||||||
* @param {string} root the root of the path to ElasticSearch
|
* @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) {
|
function ElasticPersistenceProvider($http, $q, space, root, path) {
|
||||||
this.spaces = [space];
|
this.spaces = [space];
|
||||||
|
11
src/MCT.js
11
src/MCT.js
@ -122,6 +122,7 @@ define([
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.destroy = this.destroy.bind(this);
|
||||||
/**
|
/**
|
||||||
* Tracks current selection state of the application.
|
* Tracks current selection state of the application.
|
||||||
* @private
|
* @private
|
||||||
@ -135,7 +136,7 @@ define([
|
|||||||
* @memberof module:openmct.MCT#
|
* @memberof module:openmct.MCT#
|
||||||
* @name conductor
|
* @name conductor
|
||||||
*/
|
*/
|
||||||
this.time = new api.TimeAPI();
|
this.time = new api.TimeAPI(this);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An interface for interacting with the composition of domain objects.
|
* An interface for interacting with the composition of domain objects.
|
||||||
@ -262,7 +263,8 @@ define([
|
|||||||
// Plugins that are installed by default
|
// Plugins that are installed by default
|
||||||
|
|
||||||
this.install(this.plugins.Plot());
|
this.install(this.plugins.Plot());
|
||||||
this.install(this.plugins.TelemetryTable());
|
this.install(this.plugins.Chart());
|
||||||
|
this.install(this.plugins.TelemetryTable.default());
|
||||||
this.install(PreviewPlugin.default());
|
this.install(PreviewPlugin.default());
|
||||||
this.install(LegacyIndicatorsPlugin());
|
this.install(LegacyIndicatorsPlugin());
|
||||||
this.install(LicensesPlugin.default());
|
this.install(LicensesPlugin.default());
|
||||||
@ -283,8 +285,10 @@ define([
|
|||||||
this.install(this.plugins.NotificationIndicator());
|
this.install(this.plugins.NotificationIndicator());
|
||||||
this.install(this.plugins.NewFolderAction());
|
this.install(this.plugins.NewFolderAction());
|
||||||
this.install(this.plugins.ViewDatumAction());
|
this.install(this.plugins.ViewDatumAction());
|
||||||
|
this.install(this.plugins.ViewLargeAction());
|
||||||
this.install(this.plugins.ObjectInterceptors());
|
this.install(this.plugins.ObjectInterceptors());
|
||||||
this.install(this.plugins.NonEditableFolder());
|
this.install(this.plugins.NonEditableFolder());
|
||||||
|
this.install(this.plugins.DeviceClassifier());
|
||||||
}
|
}
|
||||||
|
|
||||||
MCT.prototype = Object.create(EventEmitter.prototype);
|
MCT.prototype = Object.create(EventEmitter.prototype);
|
||||||
@ -434,6 +438,8 @@ define([
|
|||||||
Browse(this);
|
Browse(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.addEventListener('beforeunload', this.destroy);
|
||||||
|
|
||||||
this.router.start();
|
this.router.start();
|
||||||
this.emit('start');
|
this.emit('start');
|
||||||
}.bind(this));
|
}.bind(this));
|
||||||
@ -457,6 +463,7 @@ define([
|
|||||||
};
|
};
|
||||||
|
|
||||||
MCT.prototype.destroy = function () {
|
MCT.prototype.destroy = function () {
|
||||||
|
window.removeEventListener('beforeunload', this.destroy);
|
||||||
this.emit('destroy');
|
this.emit('destroy');
|
||||||
this.router.destroy();
|
this.router.destroy();
|
||||||
};
|
};
|
||||||
|
@ -36,8 +36,7 @@ define([
|
|||||||
'./views/installLegacyViews',
|
'./views/installLegacyViews',
|
||||||
'./policies/LegacyCompositionPolicyAdapter',
|
'./policies/LegacyCompositionPolicyAdapter',
|
||||||
'./actions/LegacyActionAdapter',
|
'./actions/LegacyActionAdapter',
|
||||||
'./services/LegacyPersistenceAdapter',
|
'./services/LegacyPersistenceAdapter'
|
||||||
'./services/ExportImageService'
|
|
||||||
], function (
|
], function (
|
||||||
ActionDialogDecorator,
|
ActionDialogDecorator,
|
||||||
AdapterCapability,
|
AdapterCapability,
|
||||||
@ -54,8 +53,7 @@ define([
|
|||||||
installLegacyViews,
|
installLegacyViews,
|
||||||
legacyCompositionPolicyAdapter,
|
legacyCompositionPolicyAdapter,
|
||||||
LegacyActionAdapter,
|
LegacyActionAdapter,
|
||||||
LegacyPersistenceAdapter,
|
LegacyPersistenceAdapter
|
||||||
ExportImageService
|
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
name: 'src/adapter',
|
name: 'src/adapter',
|
||||||
@ -84,13 +82,6 @@ define([
|
|||||||
"identifierService",
|
"identifierService",
|
||||||
"cacheService"
|
"cacheService"
|
||||||
]
|
]
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "exportImageService",
|
|
||||||
"implementation": ExportImageService,
|
|
||||||
"depends": [
|
|
||||||
"dialogService"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
components: [
|
components: [
|
||||||
|
@ -173,10 +173,11 @@ define([
|
|||||||
const limitEvaluator = oldObject.getCapability("limit");
|
const limitEvaluator = oldObject.getCapability("limit");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
limits: function () {
|
limits: () => {
|
||||||
return limitEvaluator.limits();
|
return limitEvaluator.limits.then !== undefined
|
||||||
|
? limitEvaluator.limits()
|
||||||
|
: Promise.resolve(limitEvaluator.limits());
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,218 +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.
|
|
||||||
*****************************************************************************/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Module defining ExportImageService. Created by hudsonfoo on 09/02/16
|
|
||||||
*/
|
|
||||||
define(
|
|
||||||
[
|
|
||||||
"html2canvas",
|
|
||||||
"saveAs"
|
|
||||||
],
|
|
||||||
function (
|
|
||||||
html2canvas,
|
|
||||||
{ saveAs }
|
|
||||||
) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The export image service will export any HTML node to
|
|
||||||
* JPG, or PNG.
|
|
||||||
* @param {object} dialogService
|
|
||||||
* @constructor
|
|
||||||
*/
|
|
||||||
function ExportImageService(dialogService) {
|
|
||||||
this.dialogService = dialogService;
|
|
||||||
this.exportCount = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts an HTML element into a PNG or JPG Blob.
|
|
||||||
* @private
|
|
||||||
* @param {node} element that will be converted to an image
|
|
||||||
* @param {object} options Image options.
|
|
||||||
* @returns {promise}
|
|
||||||
*/
|
|
||||||
ExportImageService.prototype.renderElement = function (element, {imageType, className, thumbnailSize}) {
|
|
||||||
const self = this;
|
|
||||||
const dialogService = this.dialogService;
|
|
||||||
const dialog = dialogService.showBlockingMessage({
|
|
||||||
title: "Capturing...",
|
|
||||||
hint: "Capturing an image",
|
|
||||||
unknownProgress: true,
|
|
||||||
severity: "info",
|
|
||||||
delay: true
|
|
||||||
});
|
|
||||||
|
|
||||||
let mimeType = "image/png";
|
|
||||||
if (imageType === "jpg") {
|
|
||||||
mimeType = "image/jpeg";
|
|
||||||
}
|
|
||||||
|
|
||||||
let exportId = undefined;
|
|
||||||
let oldId = undefined;
|
|
||||||
if (className) {
|
|
||||||
exportId = 'export-element-' + this.exportCount;
|
|
||||||
this.exportCount++;
|
|
||||||
oldId = element.id;
|
|
||||||
element.id = exportId;
|
|
||||||
}
|
|
||||||
|
|
||||||
return html2canvas(element, {
|
|
||||||
onclone: function (document) {
|
|
||||||
if (className) {
|
|
||||||
const clonedElement = document.getElementById(exportId);
|
|
||||||
clonedElement.classList.add(className);
|
|
||||||
}
|
|
||||||
|
|
||||||
element.id = oldId;
|
|
||||||
},
|
|
||||||
removeContainer: true // Set to false to debug what html2canvas renders
|
|
||||||
}).then(function (canvas) {
|
|
||||||
dialog.dismiss();
|
|
||||||
|
|
||||||
return new Promise(function (resolve, reject) {
|
|
||||||
if (thumbnailSize) {
|
|
||||||
const thumbnail = self.getThumbnail(canvas, mimeType, thumbnailSize);
|
|
||||||
|
|
||||||
return canvas.toBlob(blob => resolve({
|
|
||||||
blob,
|
|
||||||
thumbnail
|
|
||||||
}), mimeType);
|
|
||||||
}
|
|
||||||
|
|
||||||
return canvas.toBlob(blob => resolve({ blob }), mimeType);
|
|
||||||
});
|
|
||||||
}, function (error) {
|
|
||||||
console.log('error capturing image', error);
|
|
||||||
dialog.dismiss();
|
|
||||||
const errorDialog = dialogService.showBlockingMessage({
|
|
||||||
title: "Error capturing image",
|
|
||||||
severity: "error",
|
|
||||||
hint: "Image was not captured successfully!",
|
|
||||||
options: [{
|
|
||||||
label: "OK",
|
|
||||||
callback: function () {
|
|
||||||
errorDialog.dismiss();
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
ExportImageService.prototype.getThumbnail = function (canvas, mimeType, size) {
|
|
||||||
const thumbnailCanvas = document.createElement('canvas');
|
|
||||||
thumbnailCanvas.setAttribute('width', size.width);
|
|
||||||
thumbnailCanvas.setAttribute('height', size.height);
|
|
||||||
const ctx = thumbnailCanvas.getContext('2d');
|
|
||||||
ctx.globalCompositeOperation = "copy";
|
|
||||||
ctx.drawImage(canvas, 0, 0, canvas.width, canvas.height, 0, 0, size.width, size.height);
|
|
||||||
|
|
||||||
return thumbnailCanvas.toDataURL(mimeType);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Takes a screenshot of a DOM node and exports to JPG.
|
|
||||||
* @param {node} element to be exported
|
|
||||||
* @param {string} filename the exported image
|
|
||||||
* @param {string} className to be added to element before capturing (optional)
|
|
||||||
* @returns {promise}
|
|
||||||
*/
|
|
||||||
ExportImageService.prototype.exportJPG = function (element, filename, className) {
|
|
||||||
const processedFilename = replaceDotsWithUnderscores(filename);
|
|
||||||
|
|
||||||
return this.renderElement(element, {
|
|
||||||
imageType: 'jpg',
|
|
||||||
className
|
|
||||||
})
|
|
||||||
.then(function (img) {
|
|
||||||
saveAs(img.blob, processedFilename);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Takes a screenshot of a DOM node and exports to PNG.
|
|
||||||
* @param {node} element to be exported
|
|
||||||
* @param {string} filename the exported image
|
|
||||||
* @param {string} className to be added to element before capturing (optional)
|
|
||||||
* @returns {promise}
|
|
||||||
*/
|
|
||||||
ExportImageService.prototype.exportPNG = function (element, filename, className) {
|
|
||||||
const processedFilename = replaceDotsWithUnderscores(filename);
|
|
||||||
|
|
||||||
return this.renderElement(element, {
|
|
||||||
imageType: 'png',
|
|
||||||
className
|
|
||||||
})
|
|
||||||
.then(function (img) {
|
|
||||||
saveAs(img.blob, processedFilename);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Takes a screenshot of a DOM node in PNG format.
|
|
||||||
* @param {node} element to be exported
|
|
||||||
* @param {string} filename the exported image
|
|
||||||
* @returns {promise}
|
|
||||||
*/
|
|
||||||
|
|
||||||
ExportImageService.prototype.exportPNGtoSRC = function (element, options) {
|
|
||||||
|
|
||||||
return this.renderElement(element, {
|
|
||||||
imageType: 'png',
|
|
||||||
...options
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
function replaceDotsWithUnderscores(filename) {
|
|
||||||
const regex = /\./gi;
|
|
||||||
|
|
||||||
return filename.replace(regex, '_');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* canvas.toBlob() not supported in IE < 10, Opera, and Safari. This polyfill
|
|
||||||
* implements the method in browsers that would not otherwise support it.
|
|
||||||
* https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob
|
|
||||||
*/
|
|
||||||
function polyfillToBlob() {
|
|
||||||
if (!HTMLCanvasElement.prototype.toBlob) {
|
|
||||||
Object.defineProperty(HTMLCanvasElement.prototype, "toBlob", {
|
|
||||||
value: function (callback, mimeType, quality) {
|
|
||||||
const binStr = atob(this.toDataURL(mimeType, quality).split(',')[1]);
|
|
||||||
const len = binStr.length;
|
|
||||||
const arr = new Uint8Array(len);
|
|
||||||
|
|
||||||
for (let i = 0; i < len; i++) {
|
|
||||||
arr[i] = binStr.charCodeAt(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
callback(new Blob([arr], {type: mimeType || "image/png"}));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
polyfillToBlob();
|
|
||||||
|
|
||||||
return ExportImageService;
|
|
||||||
}
|
|
||||||
);
|
|
@ -46,8 +46,6 @@ class ActionCollection extends EventEmitter {
|
|||||||
this._observeObjectPath();
|
this._observeObjectPath();
|
||||||
this.openmct.editor.on('isEditing', this._updateActions);
|
this.openmct.editor.on('isEditing', this._updateActions);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._initializeActions();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
disable(actionKeys) {
|
disable(actionKeys) {
|
||||||
@ -156,19 +154,10 @@ class ActionCollection extends EventEmitter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_initializeActions() {
|
|
||||||
Object.keys(this.applicableActions).forEach(key => {
|
|
||||||
this.applicableActions[key].callBack = () => {
|
|
||||||
return this.applicableActions[key].invoke(this.objectPath, this.view);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_updateActions() {
|
_updateActions() {
|
||||||
let newApplicableActions = this.openmct.actions._applicableActions(this.objectPath, this.view);
|
let newApplicableActions = this.openmct.actions._applicableActions(this.objectPath, this.view);
|
||||||
|
|
||||||
this.applicableActions = this._mergeOldAndNewActions(this.applicableActions, newApplicableActions);
|
this.applicableActions = this._mergeOldAndNewActions(this.applicableActions, newApplicableActions);
|
||||||
this._initializeActions();
|
|
||||||
this._update();
|
this._update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ class ActionsAPI extends EventEmitter {
|
|||||||
this._groupOrder = ['windowing', 'undefined', 'view', 'action', 'json'];
|
this._groupOrder = ['windowing', 'undefined', 'view', 'action', 'json'];
|
||||||
|
|
||||||
this.register = this.register.bind(this);
|
this.register = this.register.bind(this);
|
||||||
this.get = this.get.bind(this);
|
this.getActionsCollection = this.getActionsCollection.bind(this);
|
||||||
this._applicableActions = this._applicableActions.bind(this);
|
this._applicableActions = this._applicableActions.bind(this);
|
||||||
this._updateCachedActionCollections = this._updateCachedActionCollections.bind(this);
|
this._updateCachedActionCollections = this._updateCachedActionCollections.bind(this);
|
||||||
}
|
}
|
||||||
@ -43,12 +43,14 @@ class ActionsAPI extends EventEmitter {
|
|||||||
this._allActions[actionDefinition.key] = actionDefinition;
|
this._allActions[actionDefinition.key] = actionDefinition;
|
||||||
}
|
}
|
||||||
|
|
||||||
get(objectPath, view) {
|
getAction(key) {
|
||||||
if (view) {
|
return this._allActions[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
getActionsCollection(objectPath, view) {
|
||||||
|
if (view) {
|
||||||
return this._getCachedActionCollection(objectPath, view) || this._newActionCollection(objectPath, view, true);
|
return this._getCachedActionCollection(objectPath, view) || this._newActionCollection(objectPath, view, true);
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
return this._newActionCollection(objectPath, view, true);
|
return this._newActionCollection(objectPath, view, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -57,25 +59,24 @@ class ActionsAPI extends EventEmitter {
|
|||||||
this._groupOrder = groupArray;
|
this._groupOrder = groupArray;
|
||||||
}
|
}
|
||||||
|
|
||||||
_get(objectPath, view) {
|
|
||||||
let actionCollection = this._newActionCollection(objectPath, view);
|
|
||||||
|
|
||||||
this._actionCollections.set(view, actionCollection);
|
|
||||||
actionCollection.on('destroy', this._updateCachedActionCollections);
|
|
||||||
|
|
||||||
return actionCollection;
|
|
||||||
}
|
|
||||||
|
|
||||||
_getCachedActionCollection(objectPath, view) {
|
_getCachedActionCollection(objectPath, view) {
|
||||||
let cachedActionCollection = this._actionCollections.get(view);
|
return this._actionCollections.get(view);
|
||||||
|
|
||||||
return cachedActionCollection;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_newActionCollection(objectPath, view, skipEnvironmentObservers) {
|
_newActionCollection(objectPath, view, skipEnvironmentObservers) {
|
||||||
let applicableActions = this._applicableActions(objectPath, view);
|
let applicableActions = this._applicableActions(objectPath, view);
|
||||||
|
|
||||||
return new ActionCollection(applicableActions, objectPath, view, this._openmct, skipEnvironmentObservers);
|
const actionCollection = new ActionCollection(applicableActions, objectPath, view, this._openmct, skipEnvironmentObservers);
|
||||||
|
if (view) {
|
||||||
|
this._cacheActionCollection(view, actionCollection);
|
||||||
|
}
|
||||||
|
|
||||||
|
return actionCollection;
|
||||||
|
}
|
||||||
|
|
||||||
|
_cacheActionCollection(view, actionCollection) {
|
||||||
|
this._actionCollections.set(view, actionCollection);
|
||||||
|
actionCollection.on('destroy', this._updateCachedActionCollections);
|
||||||
}
|
}
|
||||||
|
|
||||||
_updateCachedActionCollections(key) {
|
_updateCachedActionCollections(key) {
|
||||||
|
@ -106,7 +106,7 @@ describe('The Actions API', () => {
|
|||||||
it("adds action to ActionsAPI", () => {
|
it("adds action to ActionsAPI", () => {
|
||||||
actionsAPI.register(mockAction);
|
actionsAPI.register(mockAction);
|
||||||
|
|
||||||
let actionCollection = actionsAPI.get(mockObjectPath, mockViewContext1);
|
let actionCollection = actionsAPI.getActionsCollection(mockObjectPath, mockViewContext1);
|
||||||
let action = actionCollection.getActionsObject()[mockAction.key];
|
let action = actionCollection.getActionsObject()[mockAction.key];
|
||||||
|
|
||||||
expect(action.key).toEqual(mockAction.key);
|
expect(action.key).toEqual(mockAction.key);
|
||||||
@ -121,21 +121,21 @@ describe('The Actions API', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("returns an ActionCollection when invoked with an objectPath only", () => {
|
it("returns an ActionCollection when invoked with an objectPath only", () => {
|
||||||
let actionCollection = actionsAPI.get(mockObjectPath);
|
let actionCollection = actionsAPI.getActionsCollection(mockObjectPath);
|
||||||
let instanceOfActionCollection = actionCollection instanceof ActionCollection;
|
let instanceOfActionCollection = actionCollection instanceof ActionCollection;
|
||||||
|
|
||||||
expect(instanceOfActionCollection).toBeTrue();
|
expect(instanceOfActionCollection).toBeTrue();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns an ActionCollection when invoked with an objectPath and view", () => {
|
it("returns an ActionCollection when invoked with an objectPath and view", () => {
|
||||||
let actionCollection = actionsAPI.get(mockObjectPath, mockViewContext1);
|
let actionCollection = actionsAPI.getActionsCollection(mockObjectPath, mockViewContext1);
|
||||||
let instanceOfActionCollection = actionCollection instanceof ActionCollection;
|
let instanceOfActionCollection = actionCollection instanceof ActionCollection;
|
||||||
|
|
||||||
expect(instanceOfActionCollection).toBeTrue();
|
expect(instanceOfActionCollection).toBeTrue();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns relevant actions when invoked with objectPath only", () => {
|
it("returns relevant actions when invoked with objectPath only", () => {
|
||||||
let actionCollection = actionsAPI.get(mockObjectPath);
|
let actionCollection = actionsAPI.getActionsCollection(mockObjectPath);
|
||||||
let action = actionCollection.getActionsObject()[mockObjectPathAction.key];
|
let action = actionCollection.getActionsObject()[mockObjectPathAction.key];
|
||||||
|
|
||||||
expect(action.key).toEqual(mockObjectPathAction.key);
|
expect(action.key).toEqual(mockObjectPathAction.key);
|
||||||
@ -143,7 +143,7 @@ describe('The Actions API', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("returns relevant actions when invoked with objectPath and view", () => {
|
it("returns relevant actions when invoked with objectPath and view", () => {
|
||||||
let actionCollection = actionsAPI.get(mockObjectPath, mockViewContext1);
|
let actionCollection = actionsAPI.getActionsCollection(mockObjectPath, mockViewContext1);
|
||||||
let action = actionCollection.getActionsObject()[mockAction.key];
|
let action = actionCollection.getActionsObject()[mockAction.key];
|
||||||
|
|
||||||
expect(action.key).toEqual(mockAction.key);
|
expect(action.key).toEqual(mockAction.key);
|
||||||
|
@ -46,7 +46,7 @@ define([
|
|||||||
StatusAPI
|
StatusAPI
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
TimeAPI: TimeAPI,
|
TimeAPI: TimeAPI.default,
|
||||||
ObjectAPI: ObjectAPI,
|
ObjectAPI: ObjectAPI,
|
||||||
CompositionAPI: CompositionAPI,
|
CompositionAPI: CompositionAPI,
|
||||||
TypeRegistry: TypeRegistry,
|
TypeRegistry: TypeRegistry,
|
||||||
|
@ -37,7 +37,7 @@ import Menu, { MENU_PLACEMENT } from './menu.js';
|
|||||||
* @property {Boolean} isDisabled adds disable class if true
|
* @property {Boolean} isDisabled adds disable class if true
|
||||||
* @property {String} name Menu item text
|
* @property {String} name Menu item text
|
||||||
* @property {String} description Menu item description
|
* @property {String} description Menu item description
|
||||||
* @property {Function} callBack callback function: invoked when item is clicked
|
* @property {Function} onItemClicked callback function: invoked when item is clicked
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -66,12 +66,27 @@ class MenuAPI {
|
|||||||
* @param {Array.<Action>|Array.<Array.<Action>>} actions collection of actions{@link Action} or collection of groups of actions {@link Action}
|
* @param {Array.<Action>|Array.<Array.<Action>>} actions collection of actions{@link Action} or collection of groups of actions {@link Action}
|
||||||
* @param {MenuOptions} [menuOptions] [Optional] The {@link MenuOptions} options for Menu
|
* @param {MenuOptions} [menuOptions] [Optional] The {@link MenuOptions} options for Menu
|
||||||
*/
|
*/
|
||||||
showMenu(x, y, actions, menuOptions) {
|
showMenu(x, y, items, menuOptions) {
|
||||||
this._createMenuComponent(x, y, actions, menuOptions);
|
this._createMenuComponent(x, y, items, menuOptions);
|
||||||
|
|
||||||
this.menuComponent.showMenu();
|
this.menuComponent.showMenu();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actionsToMenuItems(actions, objectPath, view) {
|
||||||
|
return actions.map(action => {
|
||||||
|
const isActionGroup = Array.isArray(action);
|
||||||
|
if (isActionGroup) {
|
||||||
|
action = this.actionsToMenuItems(action, objectPath, view);
|
||||||
|
} else {
|
||||||
|
action.onItemClicked = () => {
|
||||||
|
action.invoke(objectPath, view);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return action;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show popup menu with description of item on hover
|
* Show popup menu with description of item on hover
|
||||||
* @param {number} x x-coordinates for popup
|
* @param {number} x x-coordinates for popup
|
||||||
|
@ -57,7 +57,7 @@ describe ('The Menu API', () => {
|
|||||||
name: 'Test Action 1',
|
name: 'Test Action 1',
|
||||||
cssClass: 'icon-clock',
|
cssClass: 'icon-clock',
|
||||||
description: 'This is a test action',
|
description: 'This is a test action',
|
||||||
callBack: () => {
|
onItemClicked: () => {
|
||||||
result = 'Test Action 1 Invoked';
|
result = 'Test Action 1 Invoked';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -66,7 +66,7 @@ describe ('The Menu API', () => {
|
|||||||
name: 'Test Action 2',
|
name: 'Test Action 2',
|
||||||
cssClass: 'icon-clock',
|
cssClass: 'icon-clock',
|
||||||
description: 'This is a test action',
|
description: 'This is a test action',
|
||||||
callBack: () => {
|
onItemClicked: () => {
|
||||||
result = 'Test Action 2 Invoked';
|
result = 'Test Action 2 Invoked';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
:key="action.name"
|
:key="action.name"
|
||||||
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
|
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
|
||||||
:title="action.description"
|
:title="action.description"
|
||||||
@click="action.callBack"
|
@click="action.onItemClicked"
|
||||||
>
|
>
|
||||||
{{ action.name }}
|
{{ action.name }}
|
||||||
</li>
|
</li>
|
||||||
@ -36,7 +36,7 @@
|
|||||||
:key="action.name"
|
:key="action.name"
|
||||||
:class="action.cssClass"
|
:class="action.cssClass"
|
||||||
:title="action.description"
|
:title="action.description"
|
||||||
@click="action.callBack"
|
@click="action.onItemClicked"
|
||||||
>
|
>
|
||||||
{{ action.name }}
|
{{ action.name }}
|
||||||
</li>
|
</li>
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
:key="action.name"
|
:key="action.name"
|
||||||
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
|
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
|
||||||
:title="action.description"
|
:title="action.description"
|
||||||
@click="action.callBack"
|
@click="action.onItemClicked"
|
||||||
@mouseover="toggleItemDescription(action)"
|
@mouseover="toggleItemDescription(action)"
|
||||||
@mouseleave="toggleItemDescription()"
|
@mouseleave="toggleItemDescription()"
|
||||||
>
|
>
|
||||||
@ -42,7 +42,7 @@
|
|||||||
:key="action.name"
|
:key="action.name"
|
||||||
:class="action.cssClass"
|
:class="action.cssClass"
|
||||||
:title="action.description"
|
:title="action.description"
|
||||||
@click="action.callBack"
|
@click="action.onItemClicked"
|
||||||
@mouseover="toggleItemDescription(action)"
|
@mouseover="toggleItemDescription(action)"
|
||||||
@mouseleave="toggleItemDescription()"
|
@mouseleave="toggleItemDescription()"
|
||||||
>
|
>
|
||||||
|
@ -71,12 +71,12 @@ class Menu extends EventEmitter {
|
|||||||
|
|
||||||
showMenu() {
|
showMenu() {
|
||||||
this.component = new Vue({
|
this.component = new Vue({
|
||||||
provide: {
|
|
||||||
options: this.options
|
|
||||||
},
|
|
||||||
components: {
|
components: {
|
||||||
MenuComponent
|
MenuComponent
|
||||||
},
|
},
|
||||||
|
provide: {
|
||||||
|
options: this.options
|
||||||
|
},
|
||||||
template: '<menu-component />'
|
template: '<menu-component />'
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -85,12 +85,12 @@ class Menu extends EventEmitter {
|
|||||||
|
|
||||||
showSuperMenu() {
|
showSuperMenu() {
|
||||||
this.component = new Vue({
|
this.component = new Vue({
|
||||||
provide: {
|
|
||||||
options: this.options
|
|
||||||
},
|
|
||||||
components: {
|
components: {
|
||||||
SuperMenuComponent
|
SuperMenuComponent
|
||||||
},
|
},
|
||||||
|
provide: {
|
||||||
|
options: this.options
|
||||||
|
},
|
||||||
template: '<super-menu-component />'
|
template: '<super-menu-component />'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@ import EventEmitter from 'EventEmitter';
|
|||||||
*
|
*
|
||||||
* @typedef {object} NotificationModel
|
* @typedef {object} NotificationModel
|
||||||
* @property {string} message The message to be displayed by the notification
|
* @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'.
|
* with the string literal 'unknown'.
|
||||||
* @property {string} [progressText] A message conveying progress of some ongoing task.
|
* @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.
|
* Present an alert to the user.
|
||||||
* @param {string} message The message to display to the user.
|
* @param {string} message The message to display to the user.
|
||||||
* @param {Object} [options] object with following properties
|
* @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
|
* link: {Object} Add a link to notifications for navigation
|
||||||
* onClick: callback function
|
* onClick: callback function
|
||||||
* cssClass: css class name to add style on link
|
* 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
|
* Present an error message to the user
|
||||||
* @param {string} message
|
* @param {string} message
|
||||||
* @param {Object} [options] object with following properties
|
* @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
|
* link: {Object} Add a link to notifications for navigation
|
||||||
* onClick: callback function
|
* onClick: callback function
|
||||||
* cssClass: css class name to add style on link
|
* cssClass: css class name to add style on link
|
||||||
|
@ -358,6 +358,20 @@ ObjectAPI.prototype.applyGetInterceptors = function (identifier, domainObject) {
|
|||||||
return 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.
|
* Modify a domain object.
|
||||||
* @param {module:openmct.DomainObject} object the object to mutate
|
* @param {module:openmct.DomainObject} object the object to mutate
|
||||||
|
@ -10,28 +10,37 @@ const cssClasses = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
class Overlay extends EventEmitter {
|
class Overlay extends EventEmitter {
|
||||||
constructor(options) {
|
constructor({
|
||||||
|
buttons,
|
||||||
|
autoHide = true,
|
||||||
|
dismissable = true,
|
||||||
|
element,
|
||||||
|
onDestroy,
|
||||||
|
size
|
||||||
|
} = {}) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.dismissable = options.dismissable !== false;
|
|
||||||
this.container = document.createElement('div');
|
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({
|
this.component = new Vue({
|
||||||
provide: {
|
|
||||||
dismiss: this.dismiss.bind(this),
|
|
||||||
element: options.element,
|
|
||||||
buttons: options.buttons,
|
|
||||||
dismissable: this.dismissable
|
|
||||||
},
|
|
||||||
components: {
|
components: {
|
||||||
OverlayComponent: OverlayComponent
|
OverlayComponent: OverlayComponent
|
||||||
},
|
},
|
||||||
|
provide: {
|
||||||
|
dismiss: this.dismiss.bind(this),
|
||||||
|
element,
|
||||||
|
buttons,
|
||||||
|
dismissable: this.dismissable
|
||||||
|
},
|
||||||
template: '<overlay-component></overlay-component>'
|
template: '<overlay-component></overlay-component>'
|
||||||
});
|
});
|
||||||
|
|
||||||
if (options.onDestroy) {
|
if (onDestroy) {
|
||||||
this.once('destroy', options.onDestroy);
|
this.once('destroy', onDestroy);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,7 +30,10 @@ class OverlayAPI {
|
|||||||
*/
|
*/
|
||||||
showOverlay(overlay) {
|
showOverlay(overlay) {
|
||||||
if (this.activeOverlays.length) {
|
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);
|
this.activeOverlays.push(overlay);
|
||||||
@ -60,7 +63,7 @@ class OverlayAPI {
|
|||||||
* A description of option properties that can be passed into the overlay
|
* A description of option properties that can be passed into the overlay
|
||||||
* @typedef options
|
* @typedef options
|
||||||
* @property {object} element DOMElement that is to be inserted/shown on the overlay
|
* @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 {array} buttons optional button objects with label and callback properties
|
||||||
* @property {function} onDestroy callback to be called when overlay is destroyed
|
* @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
|
* @property {boolean} dismissable allow user to dismiss overlay by using esc, and clicking away
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
></button>
|
></button>
|
||||||
<div
|
<div
|
||||||
ref="element"
|
ref="element"
|
||||||
class="c-overlay__contents"
|
class="c-overlay__contents js-notebook-snapshot-item-wrapper"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
></div>
|
></div>
|
||||||
<div
|
<div
|
||||||
|
@ -20,6 +20,8 @@
|
|||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
|
const { TelemetryCollection } = require("./TelemetryCollection");
|
||||||
|
|
||||||
define([
|
define([
|
||||||
'../../plugins/displayLayout/CustomStringFormatter',
|
'../../plugins/displayLayout/CustomStringFormatter',
|
||||||
'./TelemetryMetadataManager',
|
'./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.
|
* Request historical telemetry for a domain object.
|
||||||
* The `options` argument allows you to specify filters
|
* The `options` argument allows you to specify filters
|
||||||
|
File diff suppressed because it is too large
Load Diff
396
src/api/telemetry/TelemetryCollection.js
Normal file
396
src/api/telemetry/TelemetryCollection.js
Normal file
@ -0,0 +1,396 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* 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 {
|
||||||
|
this.requestAbort = new AbortController();
|
||||||
|
this.options.signal = this.requestAbort.signal;
|
||||||
|
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.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);
|
||||||
|
}
|
||||||
|
}
|
106
src/api/time/GlobalTimeContext.js
Normal file
106
src/api/time/GlobalTimeContext.js
Normal 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;
|
94
src/api/time/IndependentTimeContext.js
Normal file
94
src/api/time/IndependentTimeContext.js
Normal 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;
|
@ -20,51 +20,35 @@
|
|||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
define(['EventEmitter'], function (EventEmitter) {
|
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 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);
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
* A TimeSystem provides meaning to the values returned by the TimeAPI. Open
|
||||||
* MCT supports multiple different types of time values, although all are
|
* MCT supports multiple different types of time values, although all are
|
||||||
@ -94,16 +78,16 @@ define(['EventEmitter'], function (EventEmitter) {
|
|||||||
* @memberof module:openmct.TimeAPI#
|
* @memberof module:openmct.TimeAPI#
|
||||||
* @param {TimeSystem} timeSystem A time system object.
|
* @param {TimeSystem} timeSystem A time system object.
|
||||||
*/
|
*/
|
||||||
TimeAPI.prototype.addTimeSystem = function (timeSystem) {
|
addTimeSystem(timeSystem) {
|
||||||
this.timeSystems.set(timeSystem.key, timeSystem);
|
this.timeSystems.set(timeSystem.key, timeSystem);
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {TimeSystem[]}
|
* @returns {TimeSystem[]}
|
||||||
*/
|
*/
|
||||||
TimeAPI.prototype.getAllTimeSystems = function () {
|
getAllTimeSystems() {
|
||||||
return Array.from(this.timeSystems.values());
|
return Array.from(this.timeSystems.values());
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clocks provide a timing source that is used to
|
* Clocks provide a timing source that is used to
|
||||||
@ -126,340 +110,81 @@ define(['EventEmitter'], function (EventEmitter) {
|
|||||||
* @memberof module:openmct.TimeAPI#
|
* @memberof module:openmct.TimeAPI#
|
||||||
* @param {Clock} clock
|
* @param {Clock} clock
|
||||||
*/
|
*/
|
||||||
TimeAPI.prototype.addClock = function (clock) {
|
addClock(clock) {
|
||||||
this.clocks.set(clock.key, clock);
|
this.clocks.set(clock.key, clock);
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @memberof module:openmct.TimeAPI#
|
* @memberof module:openmct.TimeAPI#
|
||||||
* @returns {Clock[]}
|
* @returns {Clock[]}
|
||||||
* @memberof module:openmct.TimeAPI#
|
* @memberof module:openmct.TimeAPI#
|
||||||
*/
|
*/
|
||||||
TimeAPI.prototype.getAllClocks = function () {
|
getAllClocks() {
|
||||||
return Array.from(this.clocks.values());
|
return Array.from(this.clocks.values());
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate the given bounds. This can be used for pre-validation of bounds,
|
* Get or set an independent time context which follows the TimeAPI timeSystem,
|
||||||
* for example by views validating user inputs.
|
* but with different offsets for a given domain object
|
||||||
* @param {TimeBounds} bounds The start and end time of the conductor.
|
* @param {key | string} key The identifier key of the domain object these offsets are set for
|
||||||
* @returns {string | true} A validation error, or true if valid
|
* @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#
|
* @memberof module:openmct.TimeAPI#
|
||||||
* @method validateBounds
|
* @method addIndependentTimeContext
|
||||||
*/
|
*/
|
||||||
TimeAPI.prototype.validateBounds = function (bounds) {
|
addIndependentContext(key, value, clockKey) {
|
||||||
if ((bounds.start === undefined)
|
let timeContext = this.independentContexts.get(key);
|
||||||
|| (bounds.end === undefined)
|
if (!timeContext) {
|
||||||
|| isNaN(bounds.start)
|
timeContext = new IndependentTimeContext(this, key);
|
||||||
|| isNaN(bounds.end)
|
this.independentContexts.set(key, timeContext);
|
||||||
) {
|
|
||||||
return "Start and end must be specified as integer values";
|
|
||||||
} else if (bounds.start > bounds.end) {
|
|
||||||
return "Specified start date exceeds end bound";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
if (clockKey) {
|
||||||
};
|
timeContext.clock(clockKey, value);
|
||||||
|
} else {
|
||||||
/**
|
timeContext.stopClock();
|
||||||
* Validate the given offsets. This can be used for pre-validation of
|
timeContext.bounds(value);
|
||||||
* 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";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
this.emit('timeContext', key);
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
return () => {
|
||||||
* @typedef {Object} TimeBounds
|
this.independentContexts.delete(key);
|
||||||
* @property {number} start The start time displayed by the time conductor
|
timeContext.emit('timeContext', key);
|
||||||
* 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
|
|
||||||
};
|
};
|
||||||
|
}
|
||||||
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
|
* Get the independent time context which follows the TimeAPI timeSystem,
|
||||||
* and ticking will begin. Offsets from 'now' must also be provided. A clock
|
* but with different offsets.
|
||||||
* can be unset by calling {@link stopClock}.
|
* @param {key | string} key The identifier key of the domain object these offsets
|
||||||
*
|
* @memberof module:openmct.TimeAPI#
|
||||||
* @param {Clock || string} The clock to activate, or its key
|
* @method getIndependentTimeContext
|
||||||
* @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;
|
|
||||||
*/
|
*/
|
||||||
TimeAPI.prototype.clock = function (keyOrClock, offsets) {
|
getIndependentContext(key) {
|
||||||
if (arguments.length === 2) {
|
return this.independentContexts.get(key);
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clock offsets are used to calculate temporal bounds when the system is
|
* 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.
|
||||||
* ticking on a clock source.
|
* Otherwise, the global time context will be returned.
|
||||||
*
|
* @param { Array } objectPath The view's objectPath
|
||||||
* @typedef {object} ClockOffsets
|
* @memberof module:openmct.TimeAPI#
|
||||||
* @property {number} start A time span relative to the current value of the
|
* @method getContextForView
|
||||||
* 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.
|
|
||||||
*/
|
*/
|
||||||
/**
|
getContextForView(objectPath) {
|
||||||
* Get or set the currently applied clock offsets. If no parameter is provided,
|
let timeContext = this;
|
||||||
* 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) {
|
|
||||||
|
|
||||||
const validationResult = this.validateOffsets(offsets);
|
objectPath.forEach(item => {
|
||||||
if (validationResult !== true) {
|
const key = this.openmct.objects.makeKeyString(item.identifier);
|
||||||
throw new Error(validationResult);
|
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);
|
export default TimeAPI;
|
||||||
|
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
});
|
|
||||||
|
@ -19,241 +19,243 @@
|
|||||||
* this source code distribution or the Licensing information page available
|
* this source code distribution or the Licensing information page available
|
||||||
* at runtime from the About dialog for additional information.
|
* 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 () {
|
||||||
describe("The Time API", function () {
|
let api;
|
||||||
let api;
|
let timeSystemKey;
|
||||||
let timeSystemKey;
|
let timeSystem;
|
||||||
let timeSystem;
|
let clockKey;
|
||||||
let clockKey;
|
let clock;
|
||||||
let clock;
|
let bounds;
|
||||||
let bounds;
|
let eventListener;
|
||||||
let eventListener;
|
let toi;
|
||||||
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 () {
|
beforeEach(function () {
|
||||||
api = new TimeAPI();
|
mockTickSource = jasmine.createSpyObj("clock", [
|
||||||
timeSystemKey = "timeSystemKey";
|
|
||||||
timeSystem = {key: timeSystemKey};
|
|
||||||
clockKey = "someClockKey";
|
|
||||||
clock = jasmine.createSpyObj("clock", [
|
|
||||||
"on",
|
"on",
|
||||||
"off",
|
"off",
|
||||||
"currentValue"
|
"currentValue"
|
||||||
]);
|
]);
|
||||||
clock.currentValue.and.returnValue(100);
|
mockTickSource.currentValue.and.returnValue(10);
|
||||||
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.key = "mts";
|
mockTickSource.key = "mts";
|
||||||
|
|
||||||
|
anotherMockTickSource = jasmine.createSpyObj("clock", [
|
||||||
|
"on",
|
||||||
|
"off",
|
||||||
|
"currentValue"
|
||||||
|
]);
|
||||||
|
anotherMockTickSource.key = "amts";
|
||||||
|
anotherMockTickSource.currentValue.and.returnValue(10);
|
||||||
|
|
||||||
api.addClock(mockTickSource);
|
api.addClock(mockTickSource);
|
||||||
api.clock("mts", clockOffsets);
|
api.addClock(anotherMockTickSource);
|
||||||
|
|
||||||
api.on("bounds", boundsCallback);
|
|
||||||
|
|
||||||
tickCallback = mockTickSource.on.calls.mostRecent().args[1];
|
|
||||||
tickCallback(1000);
|
|
||||||
expect(boundsCallback).toHaveBeenCalledWith({
|
|
||||||
start: 900,
|
|
||||||
end: 1100
|
|
||||||
}, true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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
360
src/api/time/TimeContext.js
Normal 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;
|
155
src/api/time/independentTimeAPISpec.js
Normal file
155
src/api/time/independentTimeAPISpec.js
Normal 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();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
189
src/exporters/ImageExporter.js
Normal file
189
src/exporters/ImageExporter.js
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* 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.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class defining an image exporter for JPG/PNG output.
|
||||||
|
* Originally created by hudsonfoo on 09/02/16
|
||||||
|
*/
|
||||||
|
|
||||||
|
function replaceDotsWithUnderscores(filename) {
|
||||||
|
const regex = /\./gi;
|
||||||
|
|
||||||
|
return filename.replace(regex, '_');
|
||||||
|
}
|
||||||
|
|
||||||
|
import {saveAs} from 'file-saver/FileSaver';
|
||||||
|
import html2canvas from 'html2canvas';
|
||||||
|
import uuid from 'uuid';
|
||||||
|
|
||||||
|
class ImageExporter {
|
||||||
|
constructor(openmct) {
|
||||||
|
this.openmct = openmct;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Converts an HTML element into a PNG or JPG Blob.
|
||||||
|
* @private
|
||||||
|
* @param {node} element that will be converted to an image
|
||||||
|
* @param {object} options Image options.
|
||||||
|
* @returns {promise}
|
||||||
|
*/
|
||||||
|
renderElement(element, { imageType, className, thumbnailSize }) {
|
||||||
|
const self = this;
|
||||||
|
const overlays = this.openmct.overlays;
|
||||||
|
const dialog = overlays.dialog({
|
||||||
|
iconClass: 'info',
|
||||||
|
message: 'Caputuring an image',
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
label: 'Cancel',
|
||||||
|
emphasis: true,
|
||||||
|
callback: function () {
|
||||||
|
dialog.dismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
let mimeType = 'image/png';
|
||||||
|
if (imageType === 'jpg') {
|
||||||
|
mimeType = 'image/jpeg';
|
||||||
|
}
|
||||||
|
|
||||||
|
let exportId = undefined;
|
||||||
|
let oldId = undefined;
|
||||||
|
if (className) {
|
||||||
|
const newUUID = uuid();
|
||||||
|
exportId = `$export-element-${newUUID}`;
|
||||||
|
oldId = element.id;
|
||||||
|
element.id = exportId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html2canvas(element, {
|
||||||
|
useCORS: true,
|
||||||
|
allowTaint: true,
|
||||||
|
logging: false,
|
||||||
|
onclone: function (document) {
|
||||||
|
if (className) {
|
||||||
|
const clonedElement = document.getElementById(exportId);
|
||||||
|
clonedElement.classList.add(className);
|
||||||
|
}
|
||||||
|
|
||||||
|
element.id = oldId;
|
||||||
|
},
|
||||||
|
removeContainer: true // Set to false to debug what html2canvas renders
|
||||||
|
}).then(canvas => {
|
||||||
|
dialog.dismiss();
|
||||||
|
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
if (thumbnailSize) {
|
||||||
|
const thumbnail = self.getThumbnail(canvas, mimeType, thumbnailSize);
|
||||||
|
|
||||||
|
return canvas.toBlob(blob => resolve({
|
||||||
|
blob,
|
||||||
|
thumbnail
|
||||||
|
}), mimeType);
|
||||||
|
}
|
||||||
|
|
||||||
|
return canvas.toBlob(blob => resolve({ blob }), mimeType);
|
||||||
|
});
|
||||||
|
}).catch(error => {
|
||||||
|
dialog.dismiss();
|
||||||
|
|
||||||
|
console.error('error capturing image', error);
|
||||||
|
const errorDialog = overlays.dialog({
|
||||||
|
iconClass: 'error',
|
||||||
|
message: 'Image was not captured successfully!',
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
label: "OK",
|
||||||
|
emphasis: true,
|
||||||
|
callback: function () {
|
||||||
|
errorDialog.dismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getThumbnail(canvas, mimeType, size) {
|
||||||
|
const thumbnailCanvas = document.createElement('canvas');
|
||||||
|
thumbnailCanvas.setAttribute('width', size.width);
|
||||||
|
thumbnailCanvas.setAttribute('height', size.height);
|
||||||
|
const ctx = thumbnailCanvas.getContext('2d');
|
||||||
|
ctx.globalCompositeOperation = "copy";
|
||||||
|
ctx.drawImage(canvas, 0, 0, canvas.width, canvas.height, 0, 0, size.width, size.height);
|
||||||
|
|
||||||
|
return thumbnailCanvas.toDataURL(mimeType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes a screenshot of a DOM node and exports to JPG.
|
||||||
|
* @param {node} element to be exported
|
||||||
|
* @param {string} filename the exported image
|
||||||
|
* @param {string} className to be added to element before capturing (optional)
|
||||||
|
* @returns {promise}
|
||||||
|
*/
|
||||||
|
async exportJPG(element, filename, className) {
|
||||||
|
const processedFilename = replaceDotsWithUnderscores(filename);
|
||||||
|
|
||||||
|
const img = await this.renderElement(element, {
|
||||||
|
imageType: 'jpg',
|
||||||
|
className
|
||||||
|
});
|
||||||
|
saveAs(img.blob, processedFilename);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes a screenshot of a DOM node and exports to PNG.
|
||||||
|
* @param {node} element to be exported
|
||||||
|
* @param {string} filename the exported image
|
||||||
|
* @param {string} className to be added to element before capturing (optional)
|
||||||
|
* @returns {promise}
|
||||||
|
*/
|
||||||
|
async exportPNG(element, filename, className) {
|
||||||
|
const processedFilename = replaceDotsWithUnderscores(filename);
|
||||||
|
|
||||||
|
const img = await this.renderElement(element, {
|
||||||
|
imageType: 'png',
|
||||||
|
className
|
||||||
|
});
|
||||||
|
saveAs(img.blob, processedFilename);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes a screenshot of a DOM node in PNG format.
|
||||||
|
* @param {node} element to be exported
|
||||||
|
* @param {string} filename the exported image
|
||||||
|
* @returns {promise}
|
||||||
|
*/
|
||||||
|
|
||||||
|
exportPNGtoSRC(element, options) {
|
||||||
|
return this.renderElement(element, {
|
||||||
|
imageType: 'png',
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ImageExporter;
|
||||||
|
|
58
src/exporters/ImageExporterSpec.js
Normal file
58
src/exporters/ImageExporterSpec.js
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* 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 ImageExporter from './ImageExporter';
|
||||||
|
import { createOpenMct, resetApplicationState } from '../utils/testing';
|
||||||
|
|
||||||
|
describe('The Image Exporter', () => {
|
||||||
|
let openmct;
|
||||||
|
let imageExporter;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
openmct = createOpenMct();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
return resetApplicationState(openmct);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("basic instatation", () => {
|
||||||
|
it("can be instatiated", () => {
|
||||||
|
imageExporter = new ImageExporter(openmct);
|
||||||
|
|
||||||
|
expect(imageExporter).not.toEqual(null);
|
||||||
|
});
|
||||||
|
it("can render an element to a blob", async () => {
|
||||||
|
const mockHeadElement = document.createElement("h1");
|
||||||
|
const mockTextNode = document.createTextNode('foo bar');
|
||||||
|
mockHeadElement.appendChild(mockTextNode);
|
||||||
|
document.body.appendChild(mockHeadElement);
|
||||||
|
imageExporter = new ImageExporter(openmct);
|
||||||
|
const returnedBlob = await imageExporter.renderElement(document.body, {
|
||||||
|
imageType: 'png'
|
||||||
|
});
|
||||||
|
expect(returnedBlob).not.toEqual(null);
|
||||||
|
expect(returnedBlob.blob).not.toEqual(null);
|
||||||
|
expect(returnedBlob.blob).toBeInstanceOf(Blob);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -38,8 +38,6 @@ const DEFAULTS = [
|
|||||||
'platform/exporters',
|
'platform/exporters',
|
||||||
'platform/telemetry',
|
'platform/telemetry',
|
||||||
'platform/features/clock',
|
'platform/features/clock',
|
||||||
'platform/features/hyperlink',
|
|
||||||
'platform/features/timeline',
|
|
||||||
'platform/forms',
|
'platform/forms',
|
||||||
'platform/identity',
|
'platform/identity',
|
||||||
'platform/persistence/aggregator',
|
'platform/persistence/aggregator',
|
||||||
@ -82,9 +80,7 @@ define([
|
|||||||
'../platform/exporters/bundle',
|
'../platform/exporters/bundle',
|
||||||
'../platform/features/clock/bundle',
|
'../platform/features/clock/bundle',
|
||||||
'../platform/features/my-items/bundle',
|
'../platform/features/my-items/bundle',
|
||||||
'../platform/features/hyperlink/bundle',
|
|
||||||
'../platform/features/static-markup/bundle',
|
'../platform/features/static-markup/bundle',
|
||||||
'../platform/features/timeline/bundle',
|
|
||||||
'../platform/forms/bundle',
|
'../platform/forms/bundle',
|
||||||
'../platform/framework/bundle',
|
'../platform/framework/bundle',
|
||||||
'../platform/framework/src/load/Bundle',
|
'../platform/framework/src/load/Bundle',
|
||||||
|
32
src/plugins/DeviceClassifier/plugin.js
Normal file
32
src/plugins/DeviceClassifier/plugin.js
Normal 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);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
72
src/plugins/DeviceClassifier/src/DeviceClassifier.js
Normal file
72
src/plugins/DeviceClassifier/src/DeviceClassifier.js
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
105
src/plugins/DeviceClassifier/src/DeviceClassifierSpec.js
Normal file
105
src/plugins/DeviceClassifier/src/DeviceClassifierSpec.js
Normal 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
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
57
src/plugins/DeviceClassifier/src/DeviceMatchers.js
Normal file
57
src/plugins/DeviceClassifier/src/DeviceMatchers.js
Normal 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();
|
||||||
|
}
|
||||||
|
};
|
65
src/plugins/DeviceClassifier/src/DeviceMatchersSpec.js
Normal file
65
src/plugins/DeviceClassifier/src/DeviceMatchersSpec.js
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -19,8 +19,8 @@
|
|||||||
* this source code distribution or the Licensing information page available
|
* this source code distribution or the Licensing information page available
|
||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
import LadTableSet from './components/LadTableSet.vue';
|
|
||||||
import Vue from 'vue';
|
import LadTableSetView from './LadTableSetView';
|
||||||
|
|
||||||
export default function LADTableSetViewProvider(openmct) {
|
export default function LADTableSetViewProvider(openmct) {
|
||||||
return {
|
return {
|
||||||
@ -34,32 +34,7 @@ export default function LADTableSetViewProvider(openmct) {
|
|||||||
return domainObject.type === 'LadTableSet';
|
return domainObject.type === 'LadTableSet';
|
||||||
},
|
},
|
||||||
view: function (domainObject, objectPath) {
|
view: function (domainObject, objectPath) {
|
||||||
let component;
|
return new LadTableSetView(openmct, domainObject, objectPath);
|
||||||
|
|
||||||
return {
|
|
||||||
show: function (element) {
|
|
||||||
component = new Vue({
|
|
||||||
el: element,
|
|
||||||
components: {
|
|
||||||
LadTableSet: LadTableSet
|
|
||||||
},
|
|
||||||
provide: {
|
|
||||||
openmct,
|
|
||||||
objectPath
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
domainObject
|
|
||||||
};
|
|
||||||
},
|
|
||||||
template: '<lad-table-set :domain-object="domainObject"></lad-table-set>'
|
|
||||||
});
|
|
||||||
},
|
|
||||||
destroy: function (element) {
|
|
||||||
component.$destroy();
|
|
||||||
component = undefined;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
priority: function () {
|
priority: function () {
|
||||||
return 1;
|
return 1;
|
||||||
|
45
src/plugins/LADTable/LADTableView.js
Normal file
45
src/plugins/LADTable/LADTableView.js
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import LadTable from './components/LADTable.vue';
|
||||||
|
|
||||||
|
import Vue from 'vue';
|
||||||
|
|
||||||
|
export default class LADTableView {
|
||||||
|
constructor(openmct, domainObject, objectPath) {
|
||||||
|
this.openmct = openmct;
|
||||||
|
this.domainObject = domainObject;
|
||||||
|
this.objectPath = objectPath;
|
||||||
|
this.component = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
show(element) {
|
||||||
|
this.component = new Vue({
|
||||||
|
el: element,
|
||||||
|
components: {
|
||||||
|
LadTable
|
||||||
|
},
|
||||||
|
provide: {
|
||||||
|
openmct: this.openmct,
|
||||||
|
currentView: this
|
||||||
|
},
|
||||||
|
data: () => {
|
||||||
|
return {
|
||||||
|
domainObject: this.domainObject,
|
||||||
|
objectPath: this.objectPath
|
||||||
|
};
|
||||||
|
},
|
||||||
|
template: '<lad-table ref="ladTable" :domain-object="domainObject" :object-path="objectPath"></lad-table>'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getViewContext() {
|
||||||
|
if (!this.component) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.component.$refs.ladTable.getViewContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(element) {
|
||||||
|
this.component.$destroy();
|
||||||
|
this.component = undefined;
|
||||||
|
}
|
||||||
|
}
|
@ -19,50 +19,30 @@
|
|||||||
* this source code distribution or the Licensing information page available
|
* this source code distribution or the Licensing information page available
|
||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
import LadTable from './components/LADTable.vue';
|
|
||||||
import Vue from 'vue';
|
|
||||||
|
|
||||||
export default function LADTableViewProvider(openmct) {
|
import LADTableView from './LADTableView';
|
||||||
return {
|
|
||||||
key: 'LadTable',
|
|
||||||
name: 'LAD Table',
|
|
||||||
cssClass: 'icon-tabular-lad',
|
|
||||||
canView: function (domainObject) {
|
|
||||||
return domainObject.type === 'LadTable';
|
|
||||||
},
|
|
||||||
canEdit: function (domainObject) {
|
|
||||||
return domainObject.type === 'LadTable';
|
|
||||||
},
|
|
||||||
view: function (domainObject, objectPath) {
|
|
||||||
let component;
|
|
||||||
|
|
||||||
return {
|
export default class LADTableViewProvider {
|
||||||
show: function (element) {
|
constructor(openmct) {
|
||||||
component = new Vue({
|
this.openmct = openmct;
|
||||||
el: element,
|
this.name = 'LAD Table';
|
||||||
components: {
|
this.key = 'LadTable';
|
||||||
LadTableComponent: LadTable
|
this.cssClass = 'icon-tabular-lad';
|
||||||
},
|
}
|
||||||
provide: {
|
|
||||||
openmct
|
canView(domainObject) {
|
||||||
},
|
return domainObject.type === 'LadTable';
|
||||||
data: () => {
|
}
|
||||||
return {
|
|
||||||
domainObject,
|
canEdit(domainObject) {
|
||||||
objectPath
|
return domainObject.type === 'LadTable';
|
||||||
};
|
}
|
||||||
},
|
|
||||||
template: '<lad-table-component :domain-object="domainObject" :object-path="objectPath"></lad-table-component>'
|
view(domainObject, objectPath) {
|
||||||
});
|
return new LADTableView(this.openmct, domainObject, objectPath);
|
||||||
},
|
}
|
||||||
destroy: function (element) {
|
|
||||||
component.$destroy();
|
priority(domainObject) {
|
||||||
component = undefined;
|
return 1;
|
||||||
}
|
}
|
||||||
};
|
|
||||||
},
|
|
||||||
priority: function () {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
45
src/plugins/LADTable/LadTableSetView.js
Normal file
45
src/plugins/LADTable/LadTableSetView.js
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import LadTableSet from './components/LadTableSet.vue';
|
||||||
|
|
||||||
|
import Vue from 'vue';
|
||||||
|
|
||||||
|
export default class LadTableSetView {
|
||||||
|
constructor(openmct, domainObject, objectPath) {
|
||||||
|
this.openmct = openmct;
|
||||||
|
this.domainObject = domainObject;
|
||||||
|
this.objectPath = objectPath;
|
||||||
|
this.component = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
show(element) {
|
||||||
|
this.component = new Vue({
|
||||||
|
el: element,
|
||||||
|
components: {
|
||||||
|
LadTableSet
|
||||||
|
},
|
||||||
|
provide: {
|
||||||
|
openmct: this.openmct,
|
||||||
|
objectPath: this.objectPath,
|
||||||
|
currentView: this
|
||||||
|
},
|
||||||
|
data: () => {
|
||||||
|
return {
|
||||||
|
domainObject: this.domainObject
|
||||||
|
};
|
||||||
|
},
|
||||||
|
template: '<lad-table-set ref="ladTableSet" :domain-object="domainObject"></lad-table-set>'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getViewContext() {
|
||||||
|
if (!this.component) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.component.$refs.ladTableSet.getViewContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(element) {
|
||||||
|
this.component.$destroy();
|
||||||
|
this.component = undefined;
|
||||||
|
}
|
||||||
|
}
|
@ -50,7 +50,7 @@ const CONTEXT_MENU_ACTIONS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
inject: ['openmct'],
|
inject: ['openmct', 'currentView'],
|
||||||
props: {
|
props: {
|
||||||
domainObject: {
|
domainObject: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@ -167,25 +167,23 @@ export default {
|
|||||||
this.resetValues();
|
this.resetValues();
|
||||||
this.timestampKey = timeSystem.key;
|
this.timestampKey = timeSystem.key;
|
||||||
},
|
},
|
||||||
getView() {
|
updateViewContext() {
|
||||||
return {
|
this.$emit('rowContextClick', {
|
||||||
getViewContext: () => {
|
viewHistoricalData: true,
|
||||||
return {
|
viewDatumAction: true,
|
||||||
viewHistoricalData: true,
|
getDatum: () => {
|
||||||
viewDatumAction: true,
|
return this.datum;
|
||||||
getDatum: () => {
|
|
||||||
return this.datum;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
},
|
},
|
||||||
showContextMenu(event) {
|
showContextMenu(event) {
|
||||||
let actionCollection = this.openmct.actions.get(this.objectPath, this.getView());
|
this.updateViewContext();
|
||||||
let allActions = actionCollection.getActionsObject();
|
|
||||||
let applicableActions = CONTEXT_MENU_ACTIONS.map(key => allActions[key]);
|
|
||||||
|
|
||||||
this.openmct.menus.showMenu(event.x, event.y, applicableActions);
|
const actions = CONTEXT_MENU_ACTIONS.map(key => this.openmct.actions.getAction(key));
|
||||||
|
const menuItems = this.openmct.menus.actionsToMenuItems(actions, this.objectPath, this.currentView);
|
||||||
|
if (menuItems.length) {
|
||||||
|
this.openmct.menus.showMenu(event.x, event.y, menuItems);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
resetValues() {
|
resetValues() {
|
||||||
this.value = '---';
|
this.value = '---';
|
||||||
|
@ -38,6 +38,7 @@
|
|||||||
:domain-object="ladRow.domainObject"
|
:domain-object="ladRow.domainObject"
|
||||||
:path-to-table="objectPath"
|
:path-to-table="objectPath"
|
||||||
:has-units="hasUnits"
|
:has-units="hasUnits"
|
||||||
|
@rowContextClick="updateViewContext"
|
||||||
/>
|
/>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@ -51,7 +52,7 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
LadRow
|
LadRow
|
||||||
},
|
},
|
||||||
inject: ['openmct'],
|
inject: ['openmct', 'currentView'],
|
||||||
props: {
|
props: {
|
||||||
domainObject: {
|
domainObject: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@ -64,7 +65,8 @@ export default {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
items: []
|
items: [],
|
||||||
|
viewContext: {}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -114,6 +116,12 @@ export default {
|
|||||||
let metadataWithUnits = valueMetadatas.filter(metadatum => metadatum.unit);
|
let metadataWithUnits = valueMetadatas.filter(metadatum => metadatum.unit);
|
||||||
|
|
||||||
return metadataWithUnits.length > 0;
|
return metadataWithUnits.length > 0;
|
||||||
|
},
|
||||||
|
updateViewContext(rowContext) {
|
||||||
|
this.viewContext.row = rowContext;
|
||||||
|
},
|
||||||
|
getViewContext() {
|
||||||
|
return this.viewContext;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -48,6 +48,7 @@
|
|||||||
:domain-object="ladRow.domainObject"
|
:domain-object="ladRow.domainObject"
|
||||||
:path-to-table="ladTable.objectPath"
|
:path-to-table="ladTable.objectPath"
|
||||||
:has-units="hasUnits"
|
:has-units="hasUnits"
|
||||||
|
@rowContextClick="updateViewContext"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</tbody>
|
</tbody>
|
||||||
@ -61,7 +62,7 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
LadRow
|
LadRow
|
||||||
},
|
},
|
||||||
inject: ['openmct', 'objectPath'],
|
inject: ['openmct', 'objectPath', 'currentView'],
|
||||||
props: {
|
props: {
|
||||||
domainObject: {
|
domainObject: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@ -72,7 +73,8 @@ export default {
|
|||||||
return {
|
return {
|
||||||
ladTableObjects: [],
|
ladTableObjects: [],
|
||||||
ladTelemetryObjects: {},
|
ladTelemetryObjects: {},
|
||||||
compositions: []
|
compositions: [],
|
||||||
|
viewContext: {}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -166,6 +168,12 @@ export default {
|
|||||||
|
|
||||||
this.$set(this.ladTelemetryObjects, ladTable.key, telemetryObjects);
|
this.$set(this.ladTelemetryObjects, ladTable.key, telemetryObjects);
|
||||||
};
|
};
|
||||||
|
},
|
||||||
|
updateViewContext(rowContext) {
|
||||||
|
this.viewContext.row = rowContext;
|
||||||
|
},
|
||||||
|
getViewContext() {
|
||||||
|
return this.viewContext;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -45,6 +45,7 @@ export default class URLTimeSettingsSynchronizer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
initialize() {
|
initialize() {
|
||||||
|
this.updateTimeSettings();
|
||||||
this.openmct.router.on('change:params', this.updateTimeSettings);
|
this.openmct.router.on('change:params', this.updateTimeSettings);
|
||||||
|
|
||||||
TIME_EVENTS.forEach(event => {
|
TIME_EVENTS.forEach(event => {
|
||||||
|
@ -20,33 +20,34 @@
|
|||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
define([
|
import { BAR_GRAPH_KEY } from './BarGraphConstants';
|
||||||
"./res/templates/deprecated-timeline-message.html"
|
|
||||||
], function (
|
export default function BarGraphCompositionPolicy(openmct) {
|
||||||
deprecatedTimelineMessage
|
function hasRange(metadata) {
|
||||||
) {
|
const rangeValues = metadata.valuesForHints(['range']);
|
||||||
|
|
||||||
|
return rangeValues.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasBarGraphTelemetry(domainObject) {
|
||||||
|
if (!openmct.telemetry.isTelemetryObject(domainObject)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let metadata = openmct.telemetry.getMetadata(domainObject);
|
||||||
|
|
||||||
|
return metadata.values().length > 0 && hasRange(metadata);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: 'platform/features/timeline',
|
allow: function (parent, child) {
|
||||||
definition: {
|
if ((parent.type === BAR_GRAPH_KEY)
|
||||||
extensions: {
|
&& ((child.type !== 'telemetry.plot.overlay') && (hasBarGraphTelemetry(child) === false))
|
||||||
types: [
|
) {
|
||||||
{
|
return false;
|
||||||
key: "timeline",
|
|
||||||
name: "Timeline",
|
|
||||||
description: "Timeline, Activity and Activity Mode objects have been deprecated and will no longer be supported. (07/18/2018)",
|
|
||||||
priority: 502
|
|
||||||
}
|
|
||||||
],
|
|
||||||
views: [
|
|
||||||
{
|
|
||||||
key: "timeline",
|
|
||||||
name: "Timeline",
|
|
||||||
type: "timeline",
|
|
||||||
description: "Timeline, Activity and Activity Mode objects have been deprecated and will no longer be supported. (07/18/2018)",
|
|
||||||
template: deprecatedTimelineMessage
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
}
|
346
src/plugins/charts/barGraph/BarGraphCompositionPolicySpec.js
Normal file
346
src/plugins/charts/barGraph/BarGraphCompositionPolicySpec.js
Normal file
@ -0,0 +1,346 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* 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 BarGraphCompositionPolicy from "./BarGraphCompositionPolicy";
|
||||||
|
import { createOpenMct } from "utils/testing";
|
||||||
|
|
||||||
|
describe("The bar graph composition policy", () => {
|
||||||
|
let openmct;
|
||||||
|
const mockMetaDataWithNoRangeHints = {
|
||||||
|
"period": 10,
|
||||||
|
"amplitude": 1,
|
||||||
|
"offset": 0,
|
||||||
|
"dataRateInHz": 1,
|
||||||
|
"phase": 0,
|
||||||
|
"randomness": 0,
|
||||||
|
valuesForHints: () => {
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
"key": "name",
|
||||||
|
"name": "Name",
|
||||||
|
"format": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "utc",
|
||||||
|
"name": "Time",
|
||||||
|
"format": "utc",
|
||||||
|
"hints": {
|
||||||
|
"domain": 1,
|
||||||
|
"priority": 1
|
||||||
|
},
|
||||||
|
"source": "utc"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
const mockMetaDataWithRangeHints = {
|
||||||
|
"period": 10,
|
||||||
|
"amplitude": 1,
|
||||||
|
"offset": 0,
|
||||||
|
"dataRateInHz": 1,
|
||||||
|
"phase": 0,
|
||||||
|
"randomness": 0,
|
||||||
|
"wavelength": 0,
|
||||||
|
valuesForHints: () => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"key": "sin",
|
||||||
|
"name": "Sine",
|
||||||
|
"unit": "Hz",
|
||||||
|
"formatString": "%0.2f",
|
||||||
|
"hints": {
|
||||||
|
"range": 1,
|
||||||
|
"priority": 4
|
||||||
|
},
|
||||||
|
"source": "sin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "cos",
|
||||||
|
"name": "Cosine",
|
||||||
|
"unit": "deg",
|
||||||
|
"formatString": "%0.2f",
|
||||||
|
"hints": {
|
||||||
|
"range": 2,
|
||||||
|
"priority": 5
|
||||||
|
},
|
||||||
|
"source": "cos"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
},
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
"key": "name",
|
||||||
|
"name": "Name",
|
||||||
|
"format": "string",
|
||||||
|
"source": "name",
|
||||||
|
"hints": {
|
||||||
|
"priority": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "utc",
|
||||||
|
"name": "Time",
|
||||||
|
"format": "utc",
|
||||||
|
"hints": {
|
||||||
|
"domain": 1,
|
||||||
|
"priority": 1
|
||||||
|
},
|
||||||
|
"source": "utc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "yesterday",
|
||||||
|
"name": "Yesterday",
|
||||||
|
"format": "utc",
|
||||||
|
"hints": {
|
||||||
|
"domain": 2,
|
||||||
|
"priority": 2
|
||||||
|
},
|
||||||
|
"source": "yesterday"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "sin",
|
||||||
|
"name": "Sine",
|
||||||
|
"unit": "Hz",
|
||||||
|
"formatString": "%0.2f",
|
||||||
|
"hints": {
|
||||||
|
"range": 1,
|
||||||
|
"spectralAttribute": true
|
||||||
|
},
|
||||||
|
"source": "sin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "cos",
|
||||||
|
"name": "Cosine",
|
||||||
|
"unit": "deg",
|
||||||
|
"formatString": "%0.2f",
|
||||||
|
"hints": {
|
||||||
|
"range": 2,
|
||||||
|
"priority": 5
|
||||||
|
},
|
||||||
|
"source": "cos"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
openmct = createOpenMct();
|
||||||
|
const mockTypeDef = {
|
||||||
|
telemetry: mockMetaDataWithRangeHints
|
||||||
|
};
|
||||||
|
const mockTypeService = {
|
||||||
|
getType: () => {
|
||||||
|
return {
|
||||||
|
typeDef: mockTypeDef
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
openmct.$injector = {
|
||||||
|
get: () => {
|
||||||
|
return mockTypeService;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
openmct.telemetry.isTelemetryObject = function (domainObject) {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it("exists", () => {
|
||||||
|
expect(BarGraphCompositionPolicy(openmct).allow).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
xit("allow composition for telemetry that provides/supports bar graph meta data", () => {
|
||||||
|
const parent = {
|
||||||
|
"composition": [],
|
||||||
|
"configuration": {},
|
||||||
|
"name": "Some Bar Graph",
|
||||||
|
"type": "telemetry.plot.bar-graph",
|
||||||
|
"location": "mine",
|
||||||
|
"modified": 1631005183584,
|
||||||
|
"persisted": 1631005183502,
|
||||||
|
"identifier": {
|
||||||
|
"namespace": "",
|
||||||
|
"key": "b78e7e23-f2b8-4776-b1f0-3ff778f5c8a9"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const child = {
|
||||||
|
"telemetry": {
|
||||||
|
"period": 10,
|
||||||
|
"amplitude": 1,
|
||||||
|
"offset": 0,
|
||||||
|
"dataRateInHz": 1,
|
||||||
|
"phase": 0,
|
||||||
|
"randomness": 0
|
||||||
|
},
|
||||||
|
"name": "Unnamed Sine Wave Generator",
|
||||||
|
"type": "generator",
|
||||||
|
"location": "mine",
|
||||||
|
"modified": 1630399715531,
|
||||||
|
"persisted": 1630399715531,
|
||||||
|
"identifier": {
|
||||||
|
"namespace": "",
|
||||||
|
"key": "21d61f2d-6d2d-4bea-8b0a-7f59fd504c6c"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expect(BarGraphCompositionPolicy(openmct).allow(parent, child)).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows composition for telemetry that contain at least one range", () => {
|
||||||
|
const mockTypeDef = {
|
||||||
|
telemetry: mockMetaDataWithRangeHints
|
||||||
|
};
|
||||||
|
const mockTypeService = {
|
||||||
|
getType: () => {
|
||||||
|
return {
|
||||||
|
typeDef: mockTypeDef
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
openmct.$injector = {
|
||||||
|
get: () => {
|
||||||
|
return mockTypeService;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const parent = {
|
||||||
|
"composition": [],
|
||||||
|
"configuration": {},
|
||||||
|
"name": "Some Bar Graph",
|
||||||
|
"type": "telemetry.plot.bar-graph",
|
||||||
|
"location": "mine",
|
||||||
|
"modified": 1631005183584,
|
||||||
|
"persisted": 1631005183502,
|
||||||
|
"identifier": {
|
||||||
|
"namespace": "",
|
||||||
|
"key": "b78e7e23-f2b8-4776-b1f0-3ff778f5c8a9"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const child = {
|
||||||
|
"telemetry": {
|
||||||
|
"period": 10,
|
||||||
|
"amplitude": 1,
|
||||||
|
"offset": 0,
|
||||||
|
"dataRateInHz": 1,
|
||||||
|
"phase": 0,
|
||||||
|
"randomness": 0
|
||||||
|
},
|
||||||
|
"name": "Unnamed Sine Wave Generator",
|
||||||
|
"type": "generator",
|
||||||
|
"location": "mine",
|
||||||
|
"modified": 1630399715531,
|
||||||
|
"persisted": 1630399715531,
|
||||||
|
"identifier": {
|
||||||
|
"namespace": "",
|
||||||
|
"key": "21d61f2d-6d2d-4bea-8b0a-7f59fd504c6c"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expect(BarGraphCompositionPolicy(openmct).allow(parent, child)).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("disallows composition for telemetry that don't contain any range hints", () => {
|
||||||
|
const mockTypeDef = {
|
||||||
|
telemetry: mockMetaDataWithNoRangeHints
|
||||||
|
};
|
||||||
|
const mockTypeService = {
|
||||||
|
getType: () => {
|
||||||
|
return {
|
||||||
|
typeDef: mockTypeDef
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
openmct.$injector = {
|
||||||
|
get: () => {
|
||||||
|
return mockTypeService;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const parent = {
|
||||||
|
"composition": [],
|
||||||
|
"configuration": {},
|
||||||
|
"name": "Some Bar Graph",
|
||||||
|
"type": "telemetry.plot.bar-graph",
|
||||||
|
"location": "mine",
|
||||||
|
"modified": 1631005183584,
|
||||||
|
"persisted": 1631005183502,
|
||||||
|
"identifier": {
|
||||||
|
"namespace": "",
|
||||||
|
"key": "b78e7e23-f2b8-4776-b1f0-3ff778f5c8a9"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const child = {
|
||||||
|
"telemetry": {
|
||||||
|
"period": 10,
|
||||||
|
"amplitude": 1,
|
||||||
|
"offset": 0,
|
||||||
|
"dataRateInHz": 1,
|
||||||
|
"phase": 0,
|
||||||
|
"randomness": 0
|
||||||
|
},
|
||||||
|
"name": "Unnamed Sine Wave Generator",
|
||||||
|
"type": "generator",
|
||||||
|
"location": "mine",
|
||||||
|
"modified": 1630399715531,
|
||||||
|
"persisted": 1630399715531,
|
||||||
|
"identifier": {
|
||||||
|
"namespace": "",
|
||||||
|
"key": "21d61f2d-6d2d-4bea-8b0a-7f59fd504c6c"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expect(BarGraphCompositionPolicy(openmct).allow(parent, child)).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("passthrough for composition for non bar graph plots", () => {
|
||||||
|
const parent = {
|
||||||
|
"composition": [],
|
||||||
|
"configuration": {},
|
||||||
|
"name": "Some Stacked Plot",
|
||||||
|
"type": "telemetry.plot.stacked",
|
||||||
|
"location": "mine",
|
||||||
|
"modified": 1631005183584,
|
||||||
|
"persisted": 1631005183502,
|
||||||
|
"identifier": {
|
||||||
|
"namespace": "",
|
||||||
|
"key": "b78e7e23-f2b8-4776-b1f0-3ff778f5c8a9"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const child = {
|
||||||
|
"telemetry": {
|
||||||
|
"period": 10,
|
||||||
|
"amplitude": 1,
|
||||||
|
"offset": 0,
|
||||||
|
"dataRateInHz": 1,
|
||||||
|
"phase": 0,
|
||||||
|
"randomness": 0
|
||||||
|
},
|
||||||
|
"name": "Unnamed Sine Wave Generator",
|
||||||
|
"type": "generator",
|
||||||
|
"location": "mine",
|
||||||
|
"modified": 1630399715531,
|
||||||
|
"persisted": 1630399715531,
|
||||||
|
"identifier": {
|
||||||
|
"namespace": "",
|
||||||
|
"key": "21d61f2d-6d2d-4bea-8b0a-7f59fd504c6c"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expect(BarGraphCompositionPolicy(openmct).allow(parent, child)).toEqual(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
3
src/plugins/charts/barGraph/BarGraphConstants.js
Normal file
3
src/plugins/charts/barGraph/BarGraphConstants.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export const BAR_GRAPH_VIEW = 'bar-graph.view';
|
||||||
|
export const BAR_GRAPH_KEY = 'telemetry.plot.bar-graph';
|
||||||
|
export const BAR_GRAPH_INSPECTOR_KEY = 'telemetry.plot.bar-graph.inspector';
|
289
src/plugins/charts/barGraph/BarGraphPlot.vue
Normal file
289
src/plugins/charts/barGraph/BarGraphPlot.vue
Normal file
@ -0,0 +1,289 @@
|
|||||||
|
<template>
|
||||||
|
<div ref="plotWrapper"
|
||||||
|
class="has-local-controls"
|
||||||
|
:class="{ 's-unsynced' : isZoomed }"
|
||||||
|
>
|
||||||
|
<div v-if="isZoomed"
|
||||||
|
class="l-state-indicators"
|
||||||
|
>
|
||||||
|
<span class="l-state-indicators__alert-no-lad t-object-alert t-alert-unsynced icon-alert-triangle"
|
||||||
|
title="This plot is not currently displaying the latest data. Reset pan/zoom to view latest data."
|
||||||
|
></span>
|
||||||
|
</div>
|
||||||
|
<div ref="plot"
|
||||||
|
class="c-bar-chart"
|
||||||
|
@plotly_relayout="zoom"
|
||||||
|
></div>
|
||||||
|
<div v-if="false"
|
||||||
|
ref="localControl"
|
||||||
|
class="gl-plot__local-controls h-local-controls h-local-controls--overlay-content c-local-controls--show-on-hover"
|
||||||
|
>
|
||||||
|
<button v-if="data.length"
|
||||||
|
class="c-button icon-reset"
|
||||||
|
:disabled="!isZoomed"
|
||||||
|
title="Reset pan/zoom"
|
||||||
|
@click="reset()"
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import Plotly from 'plotly-basic';
|
||||||
|
|
||||||
|
const MULTI_AXES_X_PADDING_PERCENT = {
|
||||||
|
LEFT: 8,
|
||||||
|
RIGHT: 94
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
inject: ['openmct', 'domainObject'],
|
||||||
|
props: {
|
||||||
|
data: {
|
||||||
|
type: Array,
|
||||||
|
default() {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plotAxisTitle: {
|
||||||
|
type: Object,
|
||||||
|
default() {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isZoomed: false,
|
||||||
|
primaryYAxisRange: {
|
||||||
|
min: '',
|
||||||
|
max: ''
|
||||||
|
},
|
||||||
|
xAxisRange: {
|
||||||
|
min: '',
|
||||||
|
max: ''
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
data: {
|
||||||
|
immediate: false,
|
||||||
|
handler: 'updateData'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
Plotly.newPlot(this.$refs.plot, Array.from(this.data), this.getLayout(), {
|
||||||
|
responsive: true,
|
||||||
|
displayModeBar: false
|
||||||
|
});
|
||||||
|
this.registerListeners();
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
if (this.plotResizeObserver) {
|
||||||
|
this.plotResizeObserver.unobserve(this.$refs.plotWrapper);
|
||||||
|
clearTimeout(this.resizeTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.removeBarColorListener) {
|
||||||
|
this.removeBarColorListener();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getAxisMinMax(axis) {
|
||||||
|
const min = axis.autoSize
|
||||||
|
? ''
|
||||||
|
: axis.min;
|
||||||
|
const max = axis.autoSize
|
||||||
|
? ''
|
||||||
|
: axis.max;
|
||||||
|
|
||||||
|
return {
|
||||||
|
min,
|
||||||
|
max
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getLayout() {
|
||||||
|
const yAxesMeta = this.getYAxisMeta();
|
||||||
|
const primaryYaxis = this.getYaxisLayout(yAxesMeta['1']);
|
||||||
|
const xAxisDomain = this.getXAxisDomain(yAxesMeta);
|
||||||
|
|
||||||
|
return {
|
||||||
|
autosize: true,
|
||||||
|
showlegend: false,
|
||||||
|
textposition: 'auto',
|
||||||
|
font: {
|
||||||
|
family: 'Helvetica Neue, Helvetica, Arial, sans-serif',
|
||||||
|
size: '12px',
|
||||||
|
color: '#666'
|
||||||
|
},
|
||||||
|
xaxis: {
|
||||||
|
domain: xAxisDomain,
|
||||||
|
range: [this.xAxisRange.min, this.xAxisRange.max],
|
||||||
|
title: this.plotAxisTitle.xAxisTitle,
|
||||||
|
automargin: true,
|
||||||
|
fixedrange: true
|
||||||
|
},
|
||||||
|
yaxis: primaryYaxis,
|
||||||
|
margin: {
|
||||||
|
l: 5,
|
||||||
|
r: 5,
|
||||||
|
t: 5,
|
||||||
|
b: 0
|
||||||
|
},
|
||||||
|
paper_bgcolor: 'transparent',
|
||||||
|
plot_bgcolor: 'transparent'
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getYAxisMeta() {
|
||||||
|
const yAxisMeta = {};
|
||||||
|
|
||||||
|
this.data.forEach(d => {
|
||||||
|
const yAxisMetadata = d.yAxisMetadata;
|
||||||
|
const range = '1';
|
||||||
|
const side = 'left';
|
||||||
|
const name = '';
|
||||||
|
const unit = yAxisMetadata.units;
|
||||||
|
|
||||||
|
yAxisMeta[range] = {
|
||||||
|
range,
|
||||||
|
side,
|
||||||
|
name,
|
||||||
|
unit
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return yAxisMeta;
|
||||||
|
},
|
||||||
|
getXAxisDomain(yAxisMeta) {
|
||||||
|
let leftPaddingPerc = 0;
|
||||||
|
let rightPaddingPerc = 100;
|
||||||
|
let rightSide = yAxisMeta && Object.values(yAxisMeta).filter((axisMeta => axisMeta.side === 'right'));
|
||||||
|
let leftSide = yAxisMeta && Object.values(yAxisMeta).filter((axisMeta => axisMeta.side === 'left'));
|
||||||
|
if (yAxisMeta && rightSide.length > 1) {
|
||||||
|
rightPaddingPerc = MULTI_AXES_X_PADDING_PERCENT.RIGHT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (yAxisMeta && leftSide.length > 1) {
|
||||||
|
leftPaddingPerc = MULTI_AXES_X_PADDING_PERCENT.LEFT;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [leftPaddingPerc / 100, rightPaddingPerc / 100];
|
||||||
|
},
|
||||||
|
getYaxisLayout(yAxisMeta) {
|
||||||
|
if (!yAxisMeta) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const { name, range, side = 'left', unit } = yAxisMeta;
|
||||||
|
const title = `${name} ${unit ? '(' + unit + ')' : ''}`;
|
||||||
|
const yaxis = {
|
||||||
|
automargin: true,
|
||||||
|
fixedrange: true,
|
||||||
|
title
|
||||||
|
};
|
||||||
|
|
||||||
|
let yAxistype = this.primaryYAxisRange;
|
||||||
|
if (range === '1') {
|
||||||
|
yaxis.range = [yAxistype.min, yAxistype.max];
|
||||||
|
|
||||||
|
return yaxis;
|
||||||
|
}
|
||||||
|
|
||||||
|
yaxis.range = [yAxistype.min, yAxistype.max];
|
||||||
|
yaxis.anchor = side.toLowerCase() === 'left'
|
||||||
|
? 'free'
|
||||||
|
: 'x';
|
||||||
|
yaxis.showline = side.toLowerCase() === 'left';
|
||||||
|
yaxis.side = side.toLowerCase();
|
||||||
|
yaxis.overlaying = 'y';
|
||||||
|
yaxis.position = 0.01;
|
||||||
|
|
||||||
|
return yaxis;
|
||||||
|
},
|
||||||
|
registerListeners() {
|
||||||
|
this.removeBarColorListener = this.openmct.objects.observe(
|
||||||
|
this.domainObject,
|
||||||
|
'configuration.barStyles',
|
||||||
|
this.barColorChanged
|
||||||
|
);
|
||||||
|
this.resizeTimer = false;
|
||||||
|
if (window.ResizeObserver) {
|
||||||
|
this.plotResizeObserver = new ResizeObserver(() => {
|
||||||
|
// debounce and trigger window resize so that plotly can resize the plot
|
||||||
|
clearTimeout(this.resizeTimer);
|
||||||
|
this.resizeTimer = setTimeout(() => {
|
||||||
|
window.dispatchEvent(new Event('resize'));
|
||||||
|
}, 250);
|
||||||
|
});
|
||||||
|
this.plotResizeObserver.observe(this.$refs.plotWrapper);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
reset() {
|
||||||
|
this.updatePlot();
|
||||||
|
|
||||||
|
this.isZoomed = false;
|
||||||
|
this.$emit('subscribe');
|
||||||
|
},
|
||||||
|
barColorChanged() {
|
||||||
|
const colors = [];
|
||||||
|
const indices = [];
|
||||||
|
this.data.forEach((item, index) => {
|
||||||
|
const key = item.key;
|
||||||
|
const color = this.domainObject.configuration.barStyles[key] && this.domainObject.configuration.barStyles[key].color;
|
||||||
|
indices.push(index);
|
||||||
|
if (color) {
|
||||||
|
colors.push();
|
||||||
|
} else {
|
||||||
|
colors.push(item.marker.color);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const plotUpdate = {
|
||||||
|
'marker.color': colors
|
||||||
|
};
|
||||||
|
Plotly.restyle(this.$refs.plot, plotUpdate, indices);
|
||||||
|
},
|
||||||
|
updateData() {
|
||||||
|
this.updatePlot();
|
||||||
|
},
|
||||||
|
updateLocalControlPosition() {
|
||||||
|
const localControl = this.$refs.localControl;
|
||||||
|
localControl.style.display = 'none';
|
||||||
|
|
||||||
|
const plot = this.$refs.plot;
|
||||||
|
const bgLayer = this.$el.querySelector('.bglayer');
|
||||||
|
|
||||||
|
const plotBoundingRect = plot.getBoundingClientRect();
|
||||||
|
const bgLayerBoundingRect = bgLayer.getBoundingClientRect();
|
||||||
|
|
||||||
|
const top = bgLayerBoundingRect.top - plotBoundingRect.top + 5;
|
||||||
|
const left = bgLayerBoundingRect.left - plotBoundingRect.left + 5;
|
||||||
|
|
||||||
|
localControl.style.top = `${top}px`;
|
||||||
|
localControl.style.left = `${left}px`;
|
||||||
|
localControl.style.display = 'block';
|
||||||
|
},
|
||||||
|
updatePlot() {
|
||||||
|
if (!this.$refs || !this.$refs.plot) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Plotly.react(this.$refs.plot, Array.from(this.data), this.getLayout());
|
||||||
|
},
|
||||||
|
zoom(eventData) {
|
||||||
|
const autorange = eventData['xaxis.autorange'];
|
||||||
|
const { autosize } = eventData;
|
||||||
|
|
||||||
|
if (autosize || autorange) {
|
||||||
|
this.isZoomed = false;
|
||||||
|
this.reset();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isZoomed = true;
|
||||||
|
this.$emit('unsubscribe');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
281
src/plugins/charts/barGraph/BarGraphView.vue
Normal file
281
src/plugins/charts/barGraph/BarGraphView.vue
Normal file
@ -0,0 +1,281 @@
|
|||||||
|
<!--
|
||||||
|
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>
|
||||||
|
<BarGraph ref="barGraph"
|
||||||
|
class="c-plot c-bar-chart-view"
|
||||||
|
:data="trace"
|
||||||
|
:plot-axis-title="plotAxisTitle"
|
||||||
|
@subscribe="subscribeToAll"
|
||||||
|
@unsubscribe="removeAllSubscriptions"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ColorPalette from '../../plot/lib/ColorPalette';
|
||||||
|
import BarGraph from './BarGraphPlot.vue';
|
||||||
|
import Color from "@/plugins/plot/lib/Color";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
BarGraph
|
||||||
|
},
|
||||||
|
inject: ['openmct', 'domainObject'],
|
||||||
|
data() {
|
||||||
|
this.telemetryObjects = {};
|
||||||
|
this.telemetryObjectFormats = {};
|
||||||
|
this.subscriptions = [];
|
||||||
|
this.composition = {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
trace: []
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
plotAxisTitle() {
|
||||||
|
const { xAxisMetadata = {}, yAxisMetadata = {} } = this.trace[0] || {};
|
||||||
|
const xAxisUnit = xAxisMetadata.units ? `(${xAxisMetadata.units})` : '';
|
||||||
|
const yAxisUnit = yAxisMetadata.units ? `(${yAxisMetadata.units})` : '';
|
||||||
|
|
||||||
|
return {
|
||||||
|
xAxisTitle: `${xAxisMetadata.name || ''} ${xAxisUnit}`,
|
||||||
|
yAxisTitle: `${yAxisMetadata.name || ''} ${yAxisUnit}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.colorPalette = new ColorPalette();
|
||||||
|
this.loadComposition();
|
||||||
|
|
||||||
|
this.openmct.time.on('bounds', this.refreshData);
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.openmct.time.off('bounds', this.refreshData);
|
||||||
|
|
||||||
|
this.removeAllSubscriptions();
|
||||||
|
|
||||||
|
if (!this.composition) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.composition.off('add', this.addTelemetryObject);
|
||||||
|
this.composition.off('remove', this.removeTelemetryObject);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
addTelemetryObject(telemetryObject) {
|
||||||
|
const key = this.openmct.objects.makeKeyString(telemetryObject.identifier);
|
||||||
|
|
||||||
|
// check to see if we've set a bar color
|
||||||
|
if (!this.domainObject.configuration.barStyles[key] || !this.domainObject.configuration.barStyles[key].color) {
|
||||||
|
const color = this.colorPalette.getNextColor().asHexString();
|
||||||
|
this.openmct.objects.mutate(
|
||||||
|
this.domainObject,
|
||||||
|
`configuration.barStyles[${key}]`,
|
||||||
|
{
|
||||||
|
name: telemetryObject.name,
|
||||||
|
color
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
let color = this.domainObject.configuration.barStyles[key].color;
|
||||||
|
if (!(color instanceof Color)) {
|
||||||
|
color = Color.fromHexString(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.colorPalette.remove(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.telemetryObjects[key] = telemetryObject;
|
||||||
|
const metadata = this.openmct.telemetry.getMetadata(telemetryObject);
|
||||||
|
const formats = this.openmct.telemetry.getFormatMap(metadata);
|
||||||
|
this.telemetryObjectFormats[key] = formats;
|
||||||
|
|
||||||
|
this.requestDataFor(telemetryObject);
|
||||||
|
this.subscribeToObject(telemetryObject);
|
||||||
|
},
|
||||||
|
addTrace(trace, key) {
|
||||||
|
if (!this.trace.length) {
|
||||||
|
this.trace = this.trace.concat([trace]);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let isInTrace = false;
|
||||||
|
const newTrace = this.trace.map((currentTrace, index) => {
|
||||||
|
if (currentTrace.key !== key) {
|
||||||
|
return currentTrace;
|
||||||
|
}
|
||||||
|
|
||||||
|
isInTrace = true;
|
||||||
|
|
||||||
|
return trace;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.trace = isInTrace ? newTrace : newTrace.concat([trace]);
|
||||||
|
},
|
||||||
|
getAxisMetadata(telemetryObject) {
|
||||||
|
const metadata = this.openmct.telemetry.getMetadata(telemetryObject);
|
||||||
|
const yAxisMetadata = metadata.valuesForHints(['range'])[0];
|
||||||
|
//Exclude 'name' and 'time' based metadata specifically, from the x-Axis values by using range hints only
|
||||||
|
const xAxisMetadata = metadata.valuesForHints(['range']);
|
||||||
|
|
||||||
|
return {
|
||||||
|
xAxisMetadata,
|
||||||
|
yAxisMetadata
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getOptions() {
|
||||||
|
const { start, end } = this.openmct.time.bounds();
|
||||||
|
|
||||||
|
return {
|
||||||
|
end,
|
||||||
|
start
|
||||||
|
};
|
||||||
|
},
|
||||||
|
loadComposition() {
|
||||||
|
this.composition = this.openmct.composition.get(this.domainObject);
|
||||||
|
|
||||||
|
if (!this.composition) {
|
||||||
|
this.addTelemetryObject(this.domainObject);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.composition.on('add', this.addTelemetryObject);
|
||||||
|
this.composition.on('remove', this.removeTelemetryObject);
|
||||||
|
this.composition.load();
|
||||||
|
},
|
||||||
|
refreshData(bounds, isTick) {
|
||||||
|
if (!isTick) {
|
||||||
|
const telemetryObjects = Object.values(this.telemetryObjects);
|
||||||
|
telemetryObjects.forEach(this.requestDataFor);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removeAllSubscriptions() {
|
||||||
|
this.subscriptions.forEach(subscription => subscription.unsubscribe());
|
||||||
|
this.subscriptions = [];
|
||||||
|
},
|
||||||
|
removeSubscription(key) {
|
||||||
|
const found = this.subscriptions.findIndex(subscription => subscription.key === key);
|
||||||
|
if (found > -1) {
|
||||||
|
this.subscriptions[found].unsubscribe();
|
||||||
|
this.subscriptions.splice(found, 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removeTelemetryObject(identifier) {
|
||||||
|
const key = this.openmct.objects.makeKeyString(identifier);
|
||||||
|
delete this.telemetryObjects[key];
|
||||||
|
delete this.this.telemetryObjectFormats[key];
|
||||||
|
if (this.domainObject.configuration.barStyles[key]) {
|
||||||
|
delete this.domainObject.configuration.barStyles[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.removeSubscription(key);
|
||||||
|
|
||||||
|
this.trace = this.trace.filter(t => t.key !== key);
|
||||||
|
},
|
||||||
|
addDataToGraph(telemetryObject, data, axisMetadata) {
|
||||||
|
const key = this.openmct.objects.makeKeyString(telemetryObject.identifier);
|
||||||
|
|
||||||
|
if (data.message) {
|
||||||
|
this.openmct.notifications.alert(data.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.isDataInTimeRange(data)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let xValues = [];
|
||||||
|
let yValues = [];
|
||||||
|
|
||||||
|
//populate X and Y values for plotly
|
||||||
|
axisMetadata.xAxisMetadata.forEach((metadata) => {
|
||||||
|
xValues.push(metadata.name);
|
||||||
|
if (data[metadata.key]) {
|
||||||
|
const formattedValue = this.format(key, metadata.key, data);
|
||||||
|
yValues.push(formattedValue);
|
||||||
|
} else {
|
||||||
|
yValues.push(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const trace = {
|
||||||
|
key,
|
||||||
|
name: telemetryObject.name,
|
||||||
|
x: xValues,
|
||||||
|
y: yValues,
|
||||||
|
text: yValues.map(String),
|
||||||
|
xAxisMetadata: axisMetadata.xAxisMetadata,
|
||||||
|
yAxisMetadata: axisMetadata.yAxisMetadata,
|
||||||
|
type: 'bar',
|
||||||
|
marker: {
|
||||||
|
color: this.domainObject.configuration.barStyles[key].color
|
||||||
|
},
|
||||||
|
hoverinfo: 'skip'
|
||||||
|
};
|
||||||
|
|
||||||
|
this.addTrace(trace, key);
|
||||||
|
},
|
||||||
|
isDataInTimeRange(data) {
|
||||||
|
const timeSystemKey = this.openmct.time.timeSystem().key;
|
||||||
|
const currentTimestamp = data[timeSystemKey];
|
||||||
|
|
||||||
|
return currentTimestamp && this.openmct.time.bounds().end >= currentTimestamp;
|
||||||
|
},
|
||||||
|
format(telemetryObjectKey, metadataKey, data) {
|
||||||
|
const formats = this.telemetryObjectFormats[telemetryObjectKey];
|
||||||
|
|
||||||
|
return formats[metadataKey].format(data);
|
||||||
|
},
|
||||||
|
requestDataFor(telemetryObject) {
|
||||||
|
const axisMetadata = this.getAxisMetadata(telemetryObject);
|
||||||
|
this.openmct.telemetry.request(telemetryObject)
|
||||||
|
.then(data => {
|
||||||
|
data.forEach((datum) => {
|
||||||
|
this.addDataToGraph(telemetryObject, datum, axisMetadata);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
subscribeToObject(telemetryObject) {
|
||||||
|
const key = this.openmct.objects.makeKeyString(telemetryObject.identifier);
|
||||||
|
|
||||||
|
this.removeSubscription(key);
|
||||||
|
|
||||||
|
const options = this.getOptions();
|
||||||
|
const axisMetadata = this.getAxisMetadata(telemetryObject);
|
||||||
|
const unsubscribe = this.openmct.telemetry.subscribe(telemetryObject,
|
||||||
|
data => this.addDataToGraph(telemetryObject, data, axisMetadata)
|
||||||
|
, options);
|
||||||
|
|
||||||
|
this.subscriptions.push({
|
||||||
|
key,
|
||||||
|
unsubscribe
|
||||||
|
});
|
||||||
|
},
|
||||||
|
subscribeToAll() {
|
||||||
|
const telemetryObjects = Object.values(this.telemetryObjects);
|
||||||
|
telemetryObjects.forEach(this.subscribeToObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
76
src/plugins/charts/barGraph/BarGraphViewProvider.js
Normal file
76
src/plugins/charts/barGraph/BarGraphViewProvider.js
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* 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 BarGraphView from './BarGraphView.vue';
|
||||||
|
import { BAR_GRAPH_KEY, BAR_GRAPH_VIEW } from './BarGraphConstants';
|
||||||
|
import Vue from 'vue';
|
||||||
|
|
||||||
|
export default function BarGraphViewProvider(openmct) {
|
||||||
|
function isCompactView(objectPath) {
|
||||||
|
return objectPath.find(object => object.type === 'time-strip');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: BAR_GRAPH_VIEW,
|
||||||
|
name: 'Spectral Aggregate Plot',
|
||||||
|
cssClass: 'icon-telemetry',
|
||||||
|
canView(domainObject, objectPath) {
|
||||||
|
return domainObject && domainObject.type === BAR_GRAPH_KEY;
|
||||||
|
},
|
||||||
|
|
||||||
|
canEdit(domainObject, objectPath) {
|
||||||
|
return domainObject && domainObject.type === BAR_GRAPH_KEY;
|
||||||
|
},
|
||||||
|
|
||||||
|
view: function (domainObject, objectPath) {
|
||||||
|
let component;
|
||||||
|
|
||||||
|
return {
|
||||||
|
show: function (element) {
|
||||||
|
let isCompact = isCompactView(objectPath);
|
||||||
|
component = new Vue({
|
||||||
|
el: element,
|
||||||
|
components: {
|
||||||
|
BarGraphView
|
||||||
|
},
|
||||||
|
provide: {
|
||||||
|
openmct,
|
||||||
|
domainObject
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
options: {
|
||||||
|
compact: isCompact
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
template: '<bar-graph-view :options="options"></bar-graph-view>'
|
||||||
|
});
|
||||||
|
},
|
||||||
|
destroy: function () {
|
||||||
|
component.$destroy();
|
||||||
|
component = undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
import { BAR_GRAPH_INSPECTOR_KEY, BAR_GRAPH_KEY } from '../BarGraphConstants';
|
||||||
|
import Vue from 'vue';
|
||||||
|
import Options from "./Options.vue";
|
||||||
|
|
||||||
|
export default function BarGraphInspectorViewProvider(openmct) {
|
||||||
|
return {
|
||||||
|
key: BAR_GRAPH_INSPECTOR_KEY,
|
||||||
|
name: 'Bar Graph Inspector View',
|
||||||
|
canView: function (selection) {
|
||||||
|
if (selection.length === 0 || selection[0].length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let object = selection[0][0].context.item;
|
||||||
|
|
||||||
|
return object
|
||||||
|
&& object.type === BAR_GRAPH_KEY;
|
||||||
|
},
|
||||||
|
view: function (selection) {
|
||||||
|
let component;
|
||||||
|
|
||||||
|
return {
|
||||||
|
show: function (element) {
|
||||||
|
component = new Vue({
|
||||||
|
el: element,
|
||||||
|
components: {
|
||||||
|
Options
|
||||||
|
},
|
||||||
|
provide: {
|
||||||
|
openmct,
|
||||||
|
domainObject: selection[0][0].context.item
|
||||||
|
},
|
||||||
|
template: '<options></options>'
|
||||||
|
});
|
||||||
|
},
|
||||||
|
destroy: function () {
|
||||||
|
if (component) {
|
||||||
|
component.$destroy();
|
||||||
|
component = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
priority: function () {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
107
src/plugins/charts/barGraph/inspector/BarGraphOptions.vue
Normal file
107
src/plugins/charts/barGraph/inspector/BarGraphOptions.vue
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
<!--
|
||||||
|
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.
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<ul>
|
||||||
|
<li class="c-tree__item menus-to-left">
|
||||||
|
<span class="c-disclosure-triangle is-enabled flex-elem"
|
||||||
|
:class="expandedCssClass"
|
||||||
|
@click="expanded = !expanded"
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<div class="c-object-label__name">{{ name }}</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<ColorSwatch v-if="expanded"
|
||||||
|
:current-color="currentColor"
|
||||||
|
title="Manually set the color for this bar graph."
|
||||||
|
edit-title="Manually set the color for this bar graph"
|
||||||
|
view-title="The color for this bar graph."
|
||||||
|
short-label="Color"
|
||||||
|
class="grid-properties"
|
||||||
|
@colorSet="setColor"
|
||||||
|
/>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ColorSwatch from '../../../plot/ColorSwatch.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
ColorSwatch
|
||||||
|
},
|
||||||
|
inject: ['openmct', 'domainObject'],
|
||||||
|
props: {
|
||||||
|
item: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
currentColor: undefined,
|
||||||
|
name: '',
|
||||||
|
expanded: false
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
expandedCssClass() {
|
||||||
|
return this.expanded ? 'c-disclosure-triangle--expanded' : '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
item: {
|
||||||
|
handler() {
|
||||||
|
this.initColor();
|
||||||
|
},
|
||||||
|
deep: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.key = this.openmct.objects.makeKeyString(this.item);
|
||||||
|
this.initColor();
|
||||||
|
this.unObserve = this.openmct.objects.observe(this.domainObject, `this.domainObject.configuration.barStyles[${this.key}]`, this.initColor);
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
if (this.unObserve) {
|
||||||
|
this.unObserve();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
initColor() {
|
||||||
|
if (this.domainObject.configuration.barStyles && this.domainObject.configuration.barStyles[this.key]) {
|
||||||
|
this.currentColor = this.domainObject.configuration.barStyles[this.key].color;
|
||||||
|
this.name = this.domainObject.configuration.barStyles[this.key].name;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setColor(chosenColor) {
|
||||||
|
this.currentColor = chosenColor.asHexString();
|
||||||
|
this.openmct.objects.mutate(
|
||||||
|
this.domainObject,
|
||||||
|
`configuration.barStyles[${this.key}].color`,
|
||||||
|
this.currentColor
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
63
src/plugins/charts/barGraph/inspector/Options.vue
Normal file
63
src/plugins/charts/barGraph/inspector/Options.vue
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
<!--
|
||||||
|
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.
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<ul class="c-tree">
|
||||||
|
<li v-for="series in domainObject.composition"
|
||||||
|
:key="series.key"
|
||||||
|
>
|
||||||
|
<bar-graph-options :item="series" />
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import BarGraphOptions from "./BarGraphOptions.vue";
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
BarGraphOptions
|
||||||
|
},
|
||||||
|
inject: ['openmct', 'domainObject'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isEditing: this.openmct.editor.isEditing()
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
canEdit() {
|
||||||
|
return this.isEditing && !this.domainObject.locked;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.openmct.editor.on('isEditing', this.setEditState);
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.openmct.editor.off('isEditing', this.setEditState);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setEditState(isEditing) {
|
||||||
|
this.isEditing = isEditing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
49
src/plugins/charts/plugin.js
Normal file
49
src/plugins/charts/plugin.js
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* 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 { BAR_GRAPH_KEY } from '../charts/barGraph/BarGraphConstants';
|
||||||
|
import BarGraphViewProvider from '../charts/barGraph/BarGraphViewProvider';
|
||||||
|
import BarGraphInspectorViewProvider from '../charts/barGraph/inspector/BarGraphInspectorViewProvider';
|
||||||
|
import BarGraphCompositionPolicy from '../charts/barGraph/BarGraphCompositionPolicy';
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
return function install(openmct) {
|
||||||
|
openmct.types.addType(BAR_GRAPH_KEY, {
|
||||||
|
key: BAR_GRAPH_KEY,
|
||||||
|
name: "Bar Graph",
|
||||||
|
cssClass: "icon-bar-chart",
|
||||||
|
description: "View data as a bar graph. Can be added to Display Layouts.",
|
||||||
|
creatable: true,
|
||||||
|
initialize: function (domainObject) {
|
||||||
|
domainObject.composition = [];
|
||||||
|
domainObject.configuration = {
|
||||||
|
barStyles: {}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
priority: 891
|
||||||
|
});
|
||||||
|
|
||||||
|
openmct.objectViews.addProvider(new BarGraphViewProvider(openmct));
|
||||||
|
openmct.inspectorViews.addProvider(new BarGraphInspectorViewProvider(openmct));
|
||||||
|
openmct.composition.addPolicy(new BarGraphCompositionPolicy(openmct).allow);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
151
src/plugins/charts/pluginSpec.js
Normal file
151
src/plugins/charts/pluginSpec.js
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* 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 { BAR_GRAPH_VIEW, BAR_GRAPH_KEY } from '../charts/barGraph/BarGraphConstants';
|
||||||
|
import BarGraphPlugin from './plugin';
|
||||||
|
|
||||||
|
describe("the plugin", function () {
|
||||||
|
let element;
|
||||||
|
let child;
|
||||||
|
let openmct;
|
||||||
|
let telemetryPromise;
|
||||||
|
let telemetryPromiseResolve;
|
||||||
|
let mockObjectPath;
|
||||||
|
|
||||||
|
beforeEach((done) => {
|
||||||
|
mockObjectPath = [
|
||||||
|
{
|
||||||
|
name: 'mock folder',
|
||||||
|
type: 'fake-folder',
|
||||||
|
identifier: {
|
||||||
|
key: 'mock-folder',
|
||||||
|
namespace: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'mock parent folder',
|
||||||
|
type: 'time-strip',
|
||||||
|
identifier: {
|
||||||
|
key: 'mock-parent-folder',
|
||||||
|
namespace: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
const testTelemetry = [
|
||||||
|
{
|
||||||
|
'utc': 1,
|
||||||
|
'some-key': 'some-value 1',
|
||||||
|
'some-other-key': 'some-other-value 1'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'utc': 2,
|
||||||
|
'some-key': 'some-value 2',
|
||||||
|
'some-other-key': 'some-other-value 2'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'utc': 3,
|
||||||
|
'some-key': 'some-value 3',
|
||||||
|
'some-other-key': 'some-other-value 3'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
openmct = createOpenMct();
|
||||||
|
|
||||||
|
telemetryPromise = new Promise((resolve) => {
|
||||||
|
telemetryPromiseResolve = resolve;
|
||||||
|
});
|
||||||
|
|
||||||
|
spyOn(openmct.telemetry, 'request').and.callFake(() => {
|
||||||
|
telemetryPromiseResolve(testTelemetry);
|
||||||
|
|
||||||
|
return telemetryPromise;
|
||||||
|
});
|
||||||
|
|
||||||
|
openmct.install(new BarGraphPlugin());
|
||||||
|
|
||||||
|
element = document.createElement("div");
|
||||||
|
element.style.width = "640px";
|
||||||
|
element.style.height = "480px";
|
||||||
|
child = document.createElement("div");
|
||||||
|
child.style.width = "640px";
|
||||||
|
child.style.height = "480px";
|
||||||
|
element.appendChild(child);
|
||||||
|
document.body.appendChild(element);
|
||||||
|
|
||||||
|
spyOn(window, 'ResizeObserver').and.returnValue({
|
||||||
|
observe() {},
|
||||||
|
disconnect() {}
|
||||||
|
});
|
||||||
|
|
||||||
|
openmct.time.timeSystem("utc", {
|
||||||
|
start: 0,
|
||||||
|
end: 4
|
||||||
|
});
|
||||||
|
|
||||||
|
openmct.types.addType("test-object", {
|
||||||
|
creatable: true
|
||||||
|
});
|
||||||
|
|
||||||
|
openmct.on("start", done);
|
||||||
|
openmct.startHeadless();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach((done) => {
|
||||||
|
openmct.time.timeSystem('utc', {
|
||||||
|
start: 0,
|
||||||
|
end: 1
|
||||||
|
});
|
||||||
|
resetApplicationState(openmct).then(done).catch(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("the bar graph", () => {
|
||||||
|
const mockObject = {
|
||||||
|
name: 'A bar graph',
|
||||||
|
key: BAR_GRAPH_KEY,
|
||||||
|
creatable: true
|
||||||
|
};
|
||||||
|
|
||||||
|
it('defines a bar graph type with the correct key', () => {
|
||||||
|
const objectDef = openmct.types.get(BAR_GRAPH_KEY).definition;
|
||||||
|
expect(objectDef.key).toEqual(mockObject.key);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is creatable', () => {
|
||||||
|
const objectDef = openmct.types.get(BAR_GRAPH_KEY).definition;
|
||||||
|
expect(objectDef.creatable).toEqual(mockObject.creatable);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("the bar graph view", () => {
|
||||||
|
it("provides view for objects with telemetry", () => {
|
||||||
|
const testObject = {
|
||||||
|
id: "test-object",
|
||||||
|
type: BAR_GRAPH_KEY
|
||||||
|
};
|
||||||
|
|
||||||
|
const applicableViews = openmct.objectViews.get(testObject, mockObjectPath);
|
||||||
|
let plotView = applicableViews.find((viewProvider) => viewProvider.key === BAR_GRAPH_VIEW);
|
||||||
|
expect(plotView).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
78
src/plugins/clearData/ClearDataAction.js
Normal file
78
src/plugins/clearData/ClearDataAction.js
Normal 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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -22,7 +22,7 @@
|
|||||||
|
|
||||||
define([
|
define([
|
||||||
'./components/globalClearIndicator.vue',
|
'./components/globalClearIndicator.vue',
|
||||||
'./clearDataAction',
|
'./ClearDataAction',
|
||||||
'vue'
|
'vue'
|
||||||
], function (
|
], function (
|
||||||
GlobaClearIndicator,
|
GlobaClearIndicator,
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user