Compare commits

..

18 Commits

Author SHA1 Message Date
fd4dcc8513 Add check for destroy method on store.
Change failing test to use new data getter.
2021-08-10 09:55:22 -07:00
9ebd18318b Remve console log 2021-08-10 09:55:22 -07:00
4a89b81f4f Logging for beforeunload events 2021-08-10 09:55:22 -07:00
98e1abd7b1 Don't draw points if the count is 0 2021-08-10 09:55:22 -07:00
56c25762ac Fix resize handler for plots 2021-08-10 09:55:22 -07:00
5c8e726b87 Fix data store id 2021-08-10 09:55:22 -07:00
d80f4a1f7d Revert commented out code 2021-08-10 09:55:22 -07:00
3fe4c7a954 Revert eslint changes 2021-08-10 09:55:22 -07:00
676ef60128 Revert eslint changes 2021-08-10 09:55:22 -07:00
5a90d28450 Separate plot series data from the configuration (like it should be!) 2021-08-10 09:55:22 -07:00
2bb6822e6b Draft 2021-08-10 09:55:22 -07:00
383b4c0d8d Fix no mutating props violation for Browsebar and StyleEditor 2021-08-10 09:55:22 -07:00
404ab720ad Enable no mutating props vue lint configuration. Fix error for plots 2021-08-10 09:55:22 -07:00
259ab53060 Refactor clock object and clock indicator to remove AngularJS dependency (#4094)
* To enable clock indicator, pass in the following configuration { enableClockIndicator: true }.
2021-08-09 14:29:45 -07:00
1db7ac55b4 [Imagery] Click on image to get a large view #3582 (#4085)
fixed issue where large imagery view opens only once.
2021-08-04 15:44:50 -07:00
82b3383834 Set the yKey value on the series when it's changed (#4083) 2021-08-04 13:56:37 -07:00
ac240d524c Add check for stop observing before calling it (#4080) 2021-08-04 10:40:16 -07:00
1b034f6125 remove can edit from hyperlink (#4076) 2021-08-03 16:01:29 -07:00
233 changed files with 4488 additions and 10368 deletions

View File

@ -42,7 +42,6 @@ jobs:
- ~/.npm - ~/.npm
- ~/.cache - ~/.cache
- node_modules - node_modules
- run: npm run lint
- run: npm run test:coverage -- --browsers=<<parameters.browser>> || <<parameters.always-pass>> - run: npm run test:coverage -- --browsers=<<parameters.browser>> || <<parameters.always-pass>>
- store_test_results: - store_test_results:
path: dist/reports/tests/ path: dist/reports/tests/
@ -57,38 +56,14 @@ workflows:
browser: ChromeHeadless browser: ChromeHeadless
always-pass: false always-pass: false
- test: - test:
name: node12-firefoxESR-build-only name: node12-firefoxESR
node-version: lts/erbium node-version: lts/erbium
browser: FirefoxESR browser: FirefoxESR
always-pass: true always-pass: true
- test: - test:
name: node14-chrome-build-only name: node14-chrome
node-version: lts/fermium node-version: lts/fermium
browser: ChromeHeadless browser: ChromeHeadless
always-pass: true 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

View File

@ -2,7 +2,6 @@
* [ ] Have you followed the guidelines in our [Contributing document](https://github.com/nasa/openmct/blob/master/CONTRIBUTING.md)? * [ ] Have you followed the guidelines in our [Contributing document](https://github.com/nasa/openmct/blob/master/CONTRIBUTING.md)?
* [ ] Have you checked to ensure there aren't other open [Pull Requests](https://github.com/nasa/openmct/pulls) for the same update/change? * [ ] Have you checked to ensure there aren't other open [Pull Requests](https://github.com/nasa/openmct/pulls) for the same update/change?
* [ ] Is this change backwards compatible? For example, developers won't need to change how they are calling the API or how they've extended core plugins such as Tables or Plots.
### Author Checklist ### Author Checklist

View File

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

1
.npmrc
View File

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

2
API.md
View File

@ -996,7 +996,7 @@ reveal additional information when the mouse cursor is hovered over it.
A common use case for indicators is to convey the state of some external system such as a 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
returning a 2xx status code. The URL Status Indicator is made available as a default plugin. See returing 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.

View File

@ -317,7 +317,6 @@ checklist).
### Reviewer Checklist ### Reviewer Checklist
* [ ] Changes appear to address issue? * [ ] Changes appear to address issue?
* [ ] Changes appear not to be breaking changes?
* [ ] Appropriate unit tests included? * [ ] Appropriate unit tests included?
* [ ] Code style and in-line documentation are appropriate? * [ ] Code style and in-line documentation are appropriate?
* [ ] Commit messages meet standards? * [ ] Commit messages meet standards?

View File

@ -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 usability of documentation (`typeService` is an * Improves useability 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

View File

@ -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 transition) may be useful as well: (which is supported during the transtion) 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.

View File

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

View File

@ -1,86 +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([
], function (
) {
function SpectralAggregateGeneratorProvider() {
}
function pointForTimestamp(timestamp, count, name) {
return {
name: name,
utc: String(Math.floor(timestamp / count) * count),
ch1: String(Math.floor(timestamp / count) % 1),
ch2: String(Math.floor(timestamp / count) % 2),
ch3: String(Math.floor(timestamp / count) % 3),
ch4: String(Math.floor(timestamp / count) % 4),
ch5: String(Math.floor(timestamp / count) % 5)
};
}
SpectralAggregateGeneratorProvider.prototype.supportsSubscribe = function (domainObject) {
return domainObject.type === 'example.spectral-aggregate-generator';
};
SpectralAggregateGeneratorProvider.prototype.subscribe = function (domainObject, callback) {
var count = 5000;
var interval = setInterval(function () {
var now = Date.now();
var datum = pointForTimestamp(now, count, domainObject.name);
callback(datum);
}, count);
return function () {
clearInterval(interval);
};
};
SpectralAggregateGeneratorProvider.prototype.supportsRequest = function (domainObject, options) {
return domainObject.type === 'example.spectral-aggregate-generator';
};
SpectralAggregateGeneratorProvider.prototype.request = function (domainObject, options) {
var start = options.start;
var end = Math.min(Date.now(), options.end); // no future values
var count = 5000;
if (options.strategy === 'latest' || options.size === 1) {
start = end;
}
var data = [];
while (start <= end && data.length < 5000) {
data.push(pointForTimestamp(start, count, domainObject.name));
start += count;
}
return Promise.resolve(data);
};
return SpectralAggregateGeneratorProvider;
});

View File

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

View File

@ -63,7 +63,7 @@ define([
StateGeneratorProvider.prototype.request = function (domainObject, options) { StateGeneratorProvider.prototype.request = function (domainObject, options) {
var start = options.start; var start = options.start;
var end = Math.min(Date.now(), options.end); // no future values var end = options.end;
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;

View File

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

View File

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

View File

@ -152,7 +152,7 @@
<h2>How to Use Glyphs</h2> <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 pseudo <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 psuedo <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>

View File

@ -25,7 +25,7 @@
const devMode = process.env.NODE_ENV !== 'production'; const devMode = process.env.NODE_ENV !== 'production';
const browsers = [process.env.NODE_ENV === 'debug' ? 'ChromeDebugging' : 'ChromeHeadless']; const browsers = [process.env.NODE_ENV === 'debug' ? 'ChromeDebugging' : 'ChromeHeadless'];
const coverageEnabled = process.env.COVERAGE === 'true'; const coverageEnabled = process.env.COVERAGE === 'true';
const reporters = ['spec', 'html', 'junit']; const reporters = ['progress', 'html', 'junit'];
if (coverageEnabled) { if (coverageEnabled) {
reporters.push('coverage-istanbul'); reporters.push('coverage-istanbul');
@ -60,7 +60,7 @@ module.exports = (config) => {
client: { client: {
jasmine: { jasmine: {
random: false, random: false,
timeoutInterval: 5000 timeoutInterval: 30000
} }
}, },
customLaunchers: { customLaunchers: {
@ -88,6 +88,11 @@ module.exports = (config) => {
outputFile: "test-results.xml", outputFile: "test-results.xml",
useBrowserName: false 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
@ -100,15 +105,6 @@ module.exports = (config) => {
} }
} }
}, },
specReporter: {
maxLogLines: 5,
suppressErrorSummary: true,
suppressFailed: false,
suppressPassed: false,
suppressSkipped: true,
showSpecTiming: true,
failFast: false
},
preprocessors: { preprocessors: {
'indexTest.js': ['webpack', 'sourcemap'] 'indexTest.js': ['webpack', 'sourcemap']
}, },

View File

@ -1,7 +1,8 @@
{ {
"name": "openmct", "name": "openmct",
"version": "1.7.8-SNAPSHOT", "version": "1.7.6-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",
@ -11,9 +12,16 @@
"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",
@ -33,15 +41,14 @@
"jsdoc": "^3.3.2", "jsdoc": "^3.3.2",
"karma": "6.3.4", "karma": "6.3.4",
"karma-chrome-launcher": "3.1.0", "karma-chrome-launcher": "3.1.0",
"karma-firefox-launcher": "2.1.1",
"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": "2.1.1", "karma-junit-reporter": "2.0.1",
"karma-html-reporter": "0.2.7", "karma-html-reporter": "0.2.7",
"karma-jasmine": "4.0.1", "karma-jasmine": "4.0.1",
"karma-junit-reporter": "2.0.1",
"karma-sourcemap-loader": "0.3.8", "karma-sourcemap-loader": "0.3.8",
"karma-spec-reporter": "0.0.32",
"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",
@ -55,8 +62,6 @@
"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",
@ -65,7 +70,6 @@
"uuid": "^3.3.3", "uuid": "^3.3.3",
"v8-compile-cache": "^1.1.0", "v8-compile-cache": "^1.1.0",
"vue": "2.5.6", "vue": "2.5.6",
"vue-eslint-parser": "7.11.0",
"vue-loader": "^15.2.6", "vue-loader": "^15.2.6",
"vue-template-compiler": "2.5.6", "vue-template-compiler": "2.5.6",
"webpack": "^4.16.2", "webpack": "^4.16.2",

View File

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

View File

@ -50,6 +50,8 @@ define(
* or finish() are called. * or finish() are called.
*/ */
EditorCapability.prototype.edit = function () { EditorCapability.prototype.edit = function () {
console.warn('DEPRECATED: cannot edit via edit capability, use openmct.editor instead.');
if (!this.openmct.editor.isEditing()) { if (!this.openmct.editor.isEditing()) {
this.openmct.editor.edit(); this.openmct.editor.edit();
this.domainObject.getCapability('status').set('editing', true); this.domainObject.getCapability('status').set('editing', true);
@ -80,6 +82,8 @@ define(
* @returns {*} * @returns {*}
*/ */
EditorCapability.prototype.save = function () { EditorCapability.prototype.save = function () {
console.warn('DEPRECATED: cannot save via edit capability, use openmct.editor instead.');
return Promise.resolve(); return Promise.resolve();
}; };
@ -91,6 +95,8 @@ define(
* @returns {*} * @returns {*}
*/ */
EditorCapability.prototype.finish = function () { EditorCapability.prototype.finish = function () {
console.warn('DEPRECATED: cannot finish via edit capability, use openmct.editor instead.');
return Promise.resolve(); return Promise.resolve();
}; };

View File

@ -25,8 +25,9 @@ define([
], function ( ], function (
moment moment
) { ) {
const DATE_FORMAT = "YYYY-MM-DD HH:mm:ss.SSS";
const DATE_FORMATS = [ var DATE_FORMAT = "YYYY-MM-DD HH:mm:ss.SSS",
DATE_FORMATS = [
DATE_FORMAT, DATE_FORMAT,
DATE_FORMAT + "Z", DATE_FORMAT + "Z",
"YYYY-MM-DD HH:mm:ss", "YYYY-MM-DD HH:mm:ss",
@ -52,27 +53,15 @@ define([
this.key = "utc"; this.key = "utc";
} }
/**
* @param {string} formatString
* @returns the value of formatString if the value is a string type and exists in the DATE_FORMATS array; otherwise the DATE_FORMAT value.
*/
function validateFormatString(formatString) {
return typeof formatString === 'string' && DATE_FORMATS.includes(formatString) ? formatString : DATE_FORMAT;
}
/** /**
* @param {number} value The value to format. * @param {number} value The value to format.
* @param {string} formatString The string format to format. Default "YYYY-MM-DD HH:mm:ss.SSS" + "Z" * @returns {string} the formatted date(s). If multiple values were requested, then an array of
* @returns {string} the formatted date(s) according to the proper parameter of formatString or the default value of "YYYY-MM-DD HH:mm:ss.SSS" + "Z".
* If multiple values were requested, then an array of
* formatted values will be returned. Where a value could not be formatted, `undefined` will be returned at its position * formatted values will be returned. Where a value could not be formatted, `undefined` will be returned at its position
* in the array. * in the array.
*/ */
UTCTimeFormat.prototype.format = function (value, formatString) { UTCTimeFormat.prototype.format = function (value) {
if (value !== undefined) { if (value !== undefined) {
const format = validateFormatString(formatString); return moment.utc(value).format(DATE_FORMAT) + "Z";
return moment.utc(value).format(format) + (formatString ? '' : 'Z');
} else { } else {
return value; return value;
} }

View File

@ -21,14 +21,28 @@
*****************************************************************************/ *****************************************************************************/
define([ define([
"./src/AgentService" "./src/MCTDevice",
"./src/AgentService",
"./src/DeviceClassifier"
], function ( ], function (
AgentService MCTDevice,
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",
@ -37,6 +51,15 @@ define([
"$window" "$window"
] ]
} }
],
"runs": [
{
"implementation": DeviceClassifier,
"depends": [
"agentService",
"$document"
]
}
] ]
} }
} }

View File

@ -20,12 +20,122 @@
* 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) { /**
function AngularAgentServiceWrapper(window) { * Provides features which support variant behavior on mobile devices.
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);
} }
return AngularAgentServiceWrapper; /**
}); * 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;
}
);

View File

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

View File

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

View File

@ -19,35 +19,40 @@
* 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.
*****************************************************************************/ *****************************************************************************/
define(function () {
import { BAR_GRAPH_KEY } from './BarGraphConstants'; /**
* An object containing key-value pairs, where keys are symbolic of
export default function BarGraphCompositionPolicy(openmct) { * device attributes, and values are functions that take the
function hasAggregateDomainAndRange(metadata) { * `agentService` as inputs and return boolean values indicating
const rangeValues = metadata.valuesForHints(['range']); * whether or not the current device has these attributes.
*
return rangeValues.length > 0; * For internal use by the mobile support bundle.
} *
* @memberof platform/commonUI/mobile
function hasBarGraphTelemetry(domainObject) { * @private
if (!Object.prototype.hasOwnProperty.call(domainObject, 'telemetry')) { */
return false;
}
let metadata = openmct.telemetry.getMetadata(domainObject);
return metadata.values().length > 0 && hasAggregateDomainAndRange(metadata);
}
return { return {
allow: function (parent, child) { mobile: function (agentService) {
if ((parent.type === BAR_GRAPH_KEY) return agentService.isMobile();
&& ((child.type !== 'telemetry.plot.overlay') && (hasBarGraphTelemetry(child) === false)) },
) { phone: function (agentService) {
return false; return agentService.isPhone();
} },
tablet: function (agentService) {
return true; return agentService.isTablet();
},
desktop: function (agentService) {
return !agentService.isMobile();
},
portrait: function (agentService) {
return agentService.isPortrait();
},
landscape: function (agentService) {
return agentService.isLandscape();
},
touch: function (agentService) {
return agentService.isTouch();
} }
}; };
} });

View File

@ -0,0 +1,88 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
['./DeviceMatchers'],
function (DeviceMatchers) {
/**
* The `mct-device` directive, when applied as an attribute,
* only includes the element when the device being used matches
* a set of characteristics required.
*
* Required characteristics are given as space-separated strings
* as the value to this attribute, e.g.:
*
* <span mct-device="mobile portrait">Hello world!</span>
*
* ...will only show Hello world! when viewed on a mobile device
* in the portrait orientation.
*
* Valid device characteristics to detect are:
*
* * `mobile`: Phones or tablets.
* * `phone`: Phones specifically.
* * `tablet`: Tablets specifically.
* * `desktop`: Non-mobile devices.
* * `portrait`: Devices in a portrait-style orientation.
* * `landscape`: Devices in a landscape-style orientation.
* * `touch`: Device supports touch events.
*
* @param {AgentService} agentService used to detect device type
* based on information about the user agent
*/
function MCTDevice(agentService) {
function deviceMatches(tokens) {
tokens = tokens || "";
return tokens.split(" ").every(function (token) {
var fn = DeviceMatchers[token];
return fn && fn(agentService);
});
}
function link(scope, element, attrs, ctrl, transclude) {
if (deviceMatches(attrs.mctDevice)) {
transclude(function (clone) {
element.replaceWith(clone);
});
}
}
return {
link: link,
// We are transcluding the whole element (like ng-if)
transclude: 'element',
// 1 more than ng-if
priority: 601,
// Also terminal, since element will be transcluded
terminal: true,
// Only apply as an attribute
restrict: "A"
};
}
return MCTDevice;
}
);

View File

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

View File

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

View File

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

View File

@ -0,0 +1,168 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
['../src/MCTDevice'],
function (MCTDevice) {
var JQLITE_METHODS = ['replaceWith'];
describe("The mct-device directive", function () {
var mockAgentService,
mockTransclude,
mockElement,
mockClone,
testAttrs,
directive;
function link() {
directive.link(null, mockElement, testAttrs, null, mockTransclude);
}
beforeEach(function () {
mockAgentService = jasmine.createSpyObj(
"agentService",
["isMobile", "isPhone", "isTablet", "isPortrait", "isLandscape"]
);
mockTransclude = jasmine.createSpy("$transclude");
mockElement = jasmine.createSpyObj(name, JQLITE_METHODS);
mockClone = jasmine.createSpyObj(name, JQLITE_METHODS);
mockTransclude.and.callFake(function (fn) {
fn(mockClone);
});
// Look desktop-like by default
mockAgentService.isLandscape.and.returnValue(true);
testAttrs = {};
directive = new MCTDevice(mockAgentService);
});
function expectInclusion() {
expect(mockElement.replaceWith)
.toHaveBeenCalledWith(mockClone);
}
function expectExclusion() {
expect(mockElement.replaceWith).not.toHaveBeenCalled();
}
it("is applicable at the attribute level", function () {
expect(directive.restrict).toEqual("A");
});
it("transcludes at the element level", function () {
expect(directive.transclude).toEqual('element');
});
it("has a greater priority number than ng-if", function () {
expect(directive.priority > 600).toBeTruthy();
});
it("restricts element inclusion for mobile devices", function () {
testAttrs.mctDevice = "mobile";
link();
expectExclusion();
mockAgentService.isMobile.and.returnValue(true);
link();
expectInclusion();
});
it("restricts element inclusion for tablet devices", function () {
testAttrs.mctDevice = "tablet";
mockAgentService.isMobile.and.returnValue(true);
link();
expectExclusion();
mockAgentService.isTablet.and.returnValue(true);
link();
expectInclusion();
});
it("restricts element inclusion for phone devices", function () {
testAttrs.mctDevice = "phone";
mockAgentService.isMobile.and.returnValue(true);
link();
expectExclusion();
mockAgentService.isPhone.and.returnValue(true);
link();
expectInclusion();
});
it("restricts element inclusion for desktop devices", function () {
testAttrs.mctDevice = "desktop";
mockAgentService.isMobile.and.returnValue(true);
link();
expectExclusion();
mockAgentService.isMobile.and.returnValue(false);
link();
expectInclusion();
});
it("restricts element inclusion for portrait orientation", function () {
testAttrs.mctDevice = "portrait";
link();
expectExclusion();
mockAgentService.isPortrait.and.returnValue(true);
link();
expectInclusion();
});
it("restricts element inclusion for landscape orientation", function () {
testAttrs.mctDevice = "landscape";
mockAgentService.isLandscape.and.returnValue(false);
mockAgentService.isPortrait.and.returnValue(true);
link();
expectExclusion();
mockAgentService.isLandscape.and.returnValue(true);
link();
expectInclusion();
});
it("allows multiple device characteristics to be requested", function () {
// Won't try to test every permutation here, just
// make sure the multi-characteristic feature has support.
testAttrs.mctDevice = "portrait mobile";
link();
// Neither portrait nor mobile, not called
expectExclusion();
mockAgentService.isPortrait.and.returnValue(true);
link();
// Was portrait, but not mobile, so no
expectExclusion();
mockAgentService.isMobile.and.returnValue(true);
link();
expectInclusion();
});
});
}
);

View File

@ -379,7 +379,7 @@ define([
{ {
"name": "Math.uuid.js", "name": "Math.uuid.js",
"version": "1.4.7", "version": "1.4.7",
"description": "Unique identifier generation (code adapted.)", "description": "Unique identifer 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",

View File

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

View File

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

View File

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

View File

@ -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 {string} path the path to domain objects within ElasticSearch * @param {stirng} 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];

View File

@ -136,7 +136,7 @@ define([
* @memberof module:openmct.MCT# * @memberof module:openmct.MCT#
* @name conductor * @name conductor
*/ */
this.time = new api.TimeAPI(this); this.time = new api.TimeAPI();
/** /**
* An interface for interacting with the composition of domain objects. * An interface for interacting with the composition of domain objects.
@ -287,7 +287,6 @@ define([
this.install(this.plugins.ViewLargeAction()); 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);

View File

@ -28,6 +28,8 @@ export default function LegacyActionAdapter(openmct, legacyActions) {
return true; return true;
} }
console.warn(`DEPRECATION WARNING: Action ${action.definition.key} in bundle ${action.bundle.path} is non-contextual and should be migrated.`);
return false; return false;
} }

View File

@ -29,6 +29,7 @@ define([
'./capabilities/APICapabilityDecorator', './capabilities/APICapabilityDecorator',
'./policies/AdaptedViewPolicy', './policies/AdaptedViewPolicy',
'./runs/AlternateCompositionInitializer', './runs/AlternateCompositionInitializer',
'./runs/TypeDeprecationChecker',
'./runs/LegacyTelemetryProvider', './runs/LegacyTelemetryProvider',
'./runs/RegisterLegacyTypes', './runs/RegisterLegacyTypes',
'./services/LegacyObjectAPIInterceptor', './services/LegacyObjectAPIInterceptor',
@ -45,6 +46,7 @@ define([
APICapabilityDecorator, APICapabilityDecorator,
AdaptedViewPolicy, AdaptedViewPolicy,
AlternateCompositionInitializer, AlternateCompositionInitializer,
TypeDeprecationChecker,
LegacyTelemetryProvider, LegacyTelemetryProvider,
RegisterLegacyTypes, RegisterLegacyTypes,
LegacyObjectAPIInterceptor, LegacyObjectAPIInterceptor,
@ -133,6 +135,10 @@ define([
} }
], ],
runs: [ runs: [
{
implementation: TypeDeprecationChecker,
depends: ["types[]"]
},
{ {
implementation: AlternateCompositionInitializer, implementation: AlternateCompositionInitializer,
depends: ["openmct"] depends: ["openmct"]

View File

@ -4,6 +4,12 @@ define([
) { ) {
function RegisterLegacyTypes(types, openmct) { function RegisterLegacyTypes(types, openmct) {
types.forEach(function (legacyDefinition) {
if (!openmct.types.get(legacyDefinition.key)) {
console.warn(`DEPRECATION WARNING: Migrate type ${legacyDefinition.key} from ${legacyDefinition.bundle.path} to use the new Types API. Legacy type support will be removed soon.`);
}
});
openmct.types.importLegacyTypes(types); openmct.types.importLegacyTypes(types);
} }

View File

@ -1,9 +1,9 @@
/***************************************************************************** /*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government * Open openmct, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space * as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved. * Administration. All rights reserved.
* *
* Open MCT is licensed under the Apache License, Version 2.0 (the * Open openmct is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License. * "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0. * http://www.apache.org/licenses/LICENSE-2.0.
@ -14,27 +14,33 @@
* License for the specific language governing permissions and limitations * License for the specific language governing permissions and limitations
* under the License. * under the License.
* *
* Open MCT includes source code licensed under additional open source * Open openmct includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with * licenses. See the Open Source Licenses file (LICENSES.md) included with
* 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.
*****************************************************************************/ *****************************************************************************/
export default function timelineInterceptor(openmct) { define([
openmct.objects.addGetInterceptor({ ], function (
appliesTo: (identifier, domainObject) => {
return domainObject && domainObject.type === 'time-strip';
},
invoke: (identifier, object) => {
if (object && object.configuration === undefined) { ) {
object.configuration = {
useIndependentTime: true function checkForDeprecatedFunctionality(typeDef) {
}; if (Object.prototype.hasOwnProperty.call(typeDef, 'telemetry')) {
console.warn(
'DEPRECATION WARNING: Telemetry data on type '
+ 'registrations will be deprecated in a future version, '
+ 'please convert to a custom telemetry metadata provider '
+ 'for type: ' + typeDef.key
);
}
} }
return object; function TypeDeprecationChecker(types) {
types.forEach(checkForDeprecatedFunctionality);
} }
return TypeDeprecationChecker;
}); });
}

View File

@ -15,6 +15,8 @@ define([
}; };
function LegacyViewProvider(legacyView, openmct, convertToLegacyObject) { function LegacyViewProvider(legacyView, openmct, convertToLegacyObject) {
console.warn(`DEPRECATION WARNING: Migrate ${legacyView.key} from ${legacyView.bundle.path} to use the new View APIs. Legacy view support will be removed soon.`);
return { return {
key: legacyView.key, key: legacyView.key,
name: legacyView.name, name: legacyView.name,

View File

@ -4,6 +4,7 @@ define([
) { ) {
function TypeInspectorViewProvider(typeDefinition, openmct, convertToLegacyObject) { function TypeInspectorViewProvider(typeDefinition, openmct, convertToLegacyObject) {
console.warn(`DEPRECATION WARNING: Migrate ${typeDefinition.key} from ${typeDefinition.bundle.path} to use the new Inspector View APIs. Legacy Inspector view support will be removed soon.`);
let representation = openmct.$injector.get('representations[]') let representation = openmct.$injector.get('representations[]')
.filter((r) => r.key === typeDefinition.inspector)[0]; .filter((r) => r.key === typeDefinition.inspector)[0];

View File

@ -60,7 +60,9 @@ class ActionsAPI extends EventEmitter {
} }
_getCachedActionCollection(objectPath, view) { _getCachedActionCollection(objectPath, view) {
return this._actionCollections.get(view); let cachedActionCollection = this._actionCollections.get(view);
return cachedActionCollection;
} }
_newActionCollection(objectPath, view, skipEnvironmentObservers) { _newActionCollection(objectPath, view, skipEnvironmentObservers) {

View File

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

View File

@ -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 progress of some ongoing task. Should be a number between 0 and 100, or * @property {number | 'unknown'} [progress] The progres 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 milliseconds to automatically dismisses notification * autoDismissTimeout: {number} in miliseconds 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 milliseconds to automatically dismisses notification * autoDismissTimeout: {number} in miliseconds 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

View File

@ -182,12 +182,6 @@ ObjectAPI.prototype.get = function (identifier, abortSignal) {
let objectPromise = provider.get(identifier, abortSignal).then(result => { let objectPromise = provider.get(identifier, abortSignal).then(result => {
delete this.cache[keystring]; delete this.cache[keystring];
result = this.applyGetInterceptors(identifier, result); result = this.applyGetInterceptors(identifier, result);
if (result.isMutable) {
result.$refresh(result);
} else {
let mutableDomainObject = this._toMutable(result);
mutableDomainObject.$refresh(result);
}
return result; return result;
}); });
@ -304,15 +298,10 @@ ObjectAPI.prototype.save = function (domainObject) {
savedResolve = resolve; savedResolve = resolve;
}); });
domainObject.persisted = persistedTime; domainObject.persisted = persistedTime;
const newObjectPromise = provider.create(domainObject); provider.create(domainObject).then((response) => {
if (newObjectPromise) {
newObjectPromise.then(response => {
this.mutate(domainObject, 'persisted', persistedTime); this.mutate(domainObject, 'persisted', persistedTime);
savedResolve(response); savedResolve(response);
}); });
} else {
result = Promise.reject(`[ObjectAPI][save] Object provider returned ${newObjectPromise} when creating new object.`);
}
} else { } else {
domainObject.persisted = persistedTime; domainObject.persisted = persistedTime;
this.mutate(domainObject, 'persisted', persistedTime); this.mutate(domainObject, 'persisted', persistedTime);
@ -369,20 +358,6 @@ 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

View File

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

View File

@ -30,10 +30,7 @@ class OverlayAPI {
*/ */
showOverlay(overlay) { showOverlay(overlay) {
if (this.activeOverlays.length) { if (this.activeOverlays.length) {
const previousOverlay = this.activeOverlays[this.activeOverlays.length - 1]; this.activeOverlays[this.activeOverlays.length - 1].container.classList.add('invisible');
if (previousOverlay.autoHide) {
previousOverlay.container.classList.add('invisible');
}
} }
this.activeOverlays.push(overlay); this.activeOverlays.push(overlay);
@ -63,7 +60,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 preferred size of the overlay (large, small, fit) * @property {string} size prefered 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

View File

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

View File

@ -20,8 +20,6 @@
* 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',
@ -180,6 +178,12 @@ define([
* @memberof module:openmct.TelemetryAPI~TelemetryProvider# * @memberof module:openmct.TelemetryAPI~TelemetryProvider#
*/ */
TelemetryAPI.prototype.canProvideTelemetry = function (domainObject) { TelemetryAPI.prototype.canProvideTelemetry = function (domainObject) {
console.warn(
'DEPRECATION WARNING: openmct.telemetry.canProvideTelemetry '
+ 'will not be supported in future versions of Open MCT. Please '
+ 'use openmct.telemetry.isTelemetryObject instead.'
);
return Boolean(this.findSubscriptionProvider(domainObject)) return Boolean(this.findSubscriptionProvider(domainObject))
|| Boolean(this.findRequestProvider(domainObject)); || Boolean(this.findRequestProvider(domainObject));
}; };
@ -269,28 +273,6 @@ 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

View File

@ -19,11 +19,13 @@
* 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 TelemetryAPI from './TelemetryAPI';
const { TelemetryCollection } = require("./TelemetryCollection");
describe('Telemetry API', function () { define([
const NO_PROVIDER = 'No provider found'; './TelemetryAPI'
], function (
TelemetryAPI
) {
xdescribe('Telemetry API', function () {
let openmct; let openmct;
let telemetryAPI; let telemetryAPI;
let mockTypeService; let mockTypeService;
@ -71,23 +73,17 @@ describe('Telemetry API', function () {
}; };
}); });
it('provides consistent results without providers', function (done) { it('provides consistent results without providers', function () {
const unsubscribe = telemetryAPI.subscribe(domainObject); const unsubscribe = telemetryAPI.subscribe(domainObject);
expect(unsubscribe).toEqual(jasmine.any(Function)); expect(unsubscribe).toEqual(jasmine.any(Function));
telemetryAPI.request(domainObject).then( const response = telemetryAPI.request(domainObject);
() => {}, expect(response).toEqual(jasmine.any(Promise));
(error) => {
expect(error).toBe(NO_PROVIDER);
}
).finally(done);
}); });
it('skips providers that do not match', function (done) { it('skips providers that do not match', function () {
telemetryProvider.supportsSubscribe.and.returnValue(false); telemetryProvider.supportsSubscribe.and.returnValue(false);
telemetryProvider.supportsRequest.and.returnValue(false); telemetryProvider.supportsRequest.and.returnValue(false);
telemetryProvider.request.and.returnValue(Promise.resolve([]));
telemetryAPI.addProvider(telemetryProvider); telemetryAPI.addProvider(telemetryProvider);
const callback = jasmine.createSpy('callback'); const callback = jasmine.createSpy('callback');
@ -97,13 +93,11 @@ describe('Telemetry API', function () {
expect(telemetryProvider.subscribe).not.toHaveBeenCalled(); expect(telemetryProvider.subscribe).not.toHaveBeenCalled();
expect(unsubscribe).toEqual(jasmine.any(Function)); expect(unsubscribe).toEqual(jasmine.any(Function));
telemetryAPI.request(domainObject).then((response) => { const response = telemetryAPI.request(domainObject);
expect(telemetryProvider.supportsRequest) expect(telemetryProvider.supportsRequest)
.toHaveBeenCalledWith(domainObject, jasmine.any(Object)); .toHaveBeenCalledWith(domainObject, jasmine.any(Object));
expect(telemetryProvider.request).not.toHaveBeenCalled(); expect(telemetryProvider.request).not.toHaveBeenCalled();
}, (error) => { expect(response).toEqual(jasmine.any(Promise));
expect(error).toBe(NO_PROVIDER);
}).finally(done);
}); });
it('sends subscribe calls to matching providers', function () { it('sends subscribe calls to matching providers', function () {
@ -119,7 +113,7 @@ describe('Telemetry API', function () {
.toHaveBeenCalledWith(domainObject); .toHaveBeenCalledWith(domainObject);
expect(telemetryProvider.subscribe.calls.count()).toBe(1); expect(telemetryProvider.subscribe.calls.count()).toBe(1);
expect(telemetryProvider.subscribe) expect(telemetryProvider.subscribe)
.toHaveBeenCalledWith(domainObject, jasmine.any(Function), undefined); .toHaveBeenCalledWith(domainObject, jasmine.any(Function));
const notify = telemetryProvider.subscribe.calls.mostRecent().args[1]; const notify = telemetryProvider.subscribe.calls.mostRecent().args[1];
notify('someValue'); notify('someValue');
@ -244,13 +238,14 @@ describe('Telemetry API', function () {
expect(unsubFuncs[1]).toHaveBeenCalled(); expect(unsubFuncs[1]).toHaveBeenCalled();
}); });
it('sends requests to matching providers', function (done) { it('sends requests to matching providers', function () {
const telemPromise = Promise.resolve([]); const telemPromise = Promise.resolve([]);
telemetryProvider.supportsRequest.and.returnValue(true); telemetryProvider.supportsRequest.and.returnValue(true);
telemetryProvider.request.and.returnValue(telemPromise); telemetryProvider.request.and.returnValue(telemPromise);
telemetryAPI.addProvider(telemetryProvider); telemetryAPI.addProvider(telemetryProvider);
telemetryAPI.request(domainObject).then(() => { const result = telemetryAPI.request(domainObject);
expect(result).toBe(telemPromise);
expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith( expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(
domainObject, domainObject,
jasmine.any(Object) jasmine.any(Object)
@ -259,15 +254,13 @@ describe('Telemetry API', function () {
domainObject, domainObject,
jasmine.any(Object) jasmine.any(Object)
); );
}).finally(done);
}); });
it('generates default request options', function (done) { it('generates default request options', function () {
telemetryProvider.supportsRequest.and.returnValue(true); telemetryProvider.supportsRequest.and.returnValue(true);
telemetryProvider.request.and.returnValue(Promise.resolve([]));
telemetryAPI.addProvider(telemetryProvider); telemetryAPI.addProvider(telemetryProvider);
telemetryAPI.request(domainObject).then(() => { telemetryAPI.request(domainObject);
expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith( expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(
jasmine.any(Object), jasmine.any(Object),
{ {
@ -289,7 +282,7 @@ describe('Telemetry API', function () {
telemetryProvider.supportsRequest.calls.reset(); telemetryProvider.supportsRequest.calls.reset();
telemetryProvider.request.calls.reset(); telemetryProvider.request.calls.reset();
telemetryAPI.request(domainObject, {}).then(() => { telemetryAPI.request(domainObject, {});
expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith( expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(
jasmine.any(Object), jasmine.any(Object),
{ {
@ -308,20 +301,17 @@ describe('Telemetry API', function () {
} }
); );
}); });
}).finally(done);
}); it('does not overwrite existing request options', function () {
it('do not overwrite existing request options', function (done) {
telemetryProvider.supportsRequest.and.returnValue(true); telemetryProvider.supportsRequest.and.returnValue(true);
telemetryProvider.request.and.returnValue(Promise.resolve([]));
telemetryAPI.addProvider(telemetryProvider); telemetryAPI.addProvider(telemetryProvider);
telemetryAPI.request(domainObject, { telemetryAPI.request(domainObject, {
start: 20, start: 20,
end: 30, end: 30,
domain: 'someDomain' domain: 'someDomain'
}).then(() => { });
expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith( expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(
jasmine.any(Object), jasmine.any(Object),
{ {
@ -339,11 +329,8 @@ describe('Telemetry API', function () {
domain: 'someDomain' domain: 'someDomain'
} }
); );
}).finally(done);
}); });
}); });
describe('metadata', function () { describe('metadata', function () {
let mockMetadata = {}; let mockMetadata = {};
let mockObjectType = { let mockObjectType = {
@ -361,7 +348,6 @@ describe('Telemetry API', function () {
}); });
mockTypeService.getType.and.returnValue(mockObjectType); mockTypeService.getType.and.returnValue(mockObjectType);
}); });
it('respects explicit priority', function () { it('respects explicit priority', function () {
mockMetadata.values = [ mockMetadata.values = [
{ {
@ -573,41 +559,5 @@ describe('Telemetry API', function () {
}); });
}); });
}); });
describe('telemetry collections', () => {
let domainObject;
let mockMetadata = {};
let mockObjectType = {
typeDef: {}
};
beforeEach(function () {
openmct.telemetry = telemetryAPI;
telemetryAPI.addProvider({
key: 'mockMetadataProvider',
supportsMetadata() {
return true;
},
getMetadata() {
return mockMetadata;
}
});
mockTypeService.getType.and.returnValue(mockObjectType);
domainObject = {
identifier: {
key: 'a',
namespace: 'b'
},
type: 'sample-type'
};
});
it('when requested, returns an instance of telemetry collection', () => {
const telemetryCollection = telemetryAPI.requestCollection(domainObject);
expect(telemetryCollection).toBeInstanceOf(TelemetryCollection);
});
}); });
}); });

View File

@ -1,396 +0,0 @@
/*****************************************************************************
* 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);
}
}

View File

@ -31,6 +31,11 @@ define([
valueMetadata.hints = valueMetadata.hints || {}; valueMetadata.hints = valueMetadata.hints || {};
if (Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'x')) { if (Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'x')) {
console.warn(
'DEPRECATION WARNING: `x` hints should be replaced with '
+ '`domain` hints moving forward. '
+ 'https://github.com/nasa/openmct/issues/1546'
);
if (!Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'domain')) { if (!Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'domain')) {
valueMetadata.hints.domain = valueMetadata.hints.x; valueMetadata.hints.domain = valueMetadata.hints.x;
} }
@ -39,6 +44,11 @@ define([
} }
if (Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'y')) { if (Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'y')) {
console.warn(
'DEPRECATION WARNING: `y` hints should be replaced with '
+ '`range` hints moving forward. '
+ 'https://github.com/nasa/openmct/issues/1546'
);
if (!Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'range')) { if (!Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'range')) {
valueMetadata.hints.range = valueMetadata.hints.y; valueMetadata.hints.range = valueMetadata.hints.y;
} }

View File

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

View File

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

View File

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

View File

@ -19,9 +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 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;
@ -31,11 +30,9 @@ describe("The Time API", function () {
let bounds; let bounds;
let eventListener; let eventListener;
let toi; let toi;
let openmct;
beforeEach(function () { beforeEach(function () {
openmct = createOpenMct(); api = new TimeAPI();
api = new TimeAPI(openmct);
timeSystemKey = "timeSystemKey"; timeSystemKey = "timeSystemKey";
timeSystem = {key: timeSystemKey}; timeSystem = {key: timeSystemKey};
clockKey = "someClockKey"; clockKey = "someClockKey";
@ -259,3 +256,4 @@ describe("The Time API", function () {
}, true); }, true);
}); });
}); });
});

View File

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

View File

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

View File

@ -63,6 +63,12 @@ define(['./Type'], function (Type) {
*/ */
TypeRegistry.prototype.standardizeType = function (typeDef) { TypeRegistry.prototype.standardizeType = function (typeDef) {
if (Object.prototype.hasOwnProperty.call(typeDef, 'label')) { if (Object.prototype.hasOwnProperty.call(typeDef, 'label')) {
console.warn(
'DEPRECATION WARNING typeDef: ' + typeDef.label + '. '
+ '`label` is deprecated in type definitions. Please use '
+ '`name` instead. This will cause errors in a future version '
+ 'of Open MCT. For more information, see '
+ 'https://github.com/nasa/openmct/issues/1568');
if (!typeDef.name) { if (!typeDef.name) {
typeDef.name = typeDef.label; typeDef.name = typeDef.label;
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -20,24 +20,22 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
<template> export default class ClearDataAction {
<li class="c-inspect-properties__row"> constructor(openmct, appliesToObjects) {
<div class="c-inspect-properties__label"> this.name = 'Clear Data for Object';
{{ detail.name }} this.key = 'clear-data-action';
</div> this.description = 'Clears current data for object, unsubscribes and resubscribes to data';
<div class="c-inspect-properties__value"> this.cssClass = 'icon-clear-data';
{{ detail.value }}
</div>
</li>
</template>
<script> this._openmct = openmct;
export default { this._appliesToObjects = appliesToObjects;
props: { }
detail: { invoke(objectPath) {
type: Object, this._openmct.objectViews.emit('clearData', objectPath[0]);
required: true }
appliesTo(objectPath) {
let contextualDomainObject = objectPath[0];
return this._appliesToObjects.filter(type => contextualDomainObject.type === type).length;
} }
} }
};
</script>

View File

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

View File

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

View File

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

View File

@ -46,7 +46,7 @@ export default function ClockViewProvider(openmct) {
openmct, openmct,
domainObject domainObject
}, },
template: '<clock />' template: '<clock></clock>'
}); });
}, },
destroy: function () { destroy: function () {

View File

@ -99,7 +99,7 @@ export default function ClockPlugin(options) {
}); });
openmct.objectViews.addProvider(new ClockViewProvider(openmct)); openmct.objectViews.addProvider(new ClockViewProvider(openmct));
if (options && options.enableClockIndicator === true) { if (options && options.enableClockIndicator) {
const clockIndicator = new Vue ({ const clockIndicator = new Vue ({
components: { components: {
ClockIndicator ClockIndicator
@ -112,7 +112,7 @@ export default function ClockPlugin(options) {
indicatorFormat: CLOCK_INDICATOR_FORMAT indicatorFormat: CLOCK_INDICATOR_FORMAT
}; };
}, },
template: '<ClockIndicator :indicator-format="indicatorFormat" />' template: '<ClockIndicator :indicator-format="indicatorFormat"></ClockIndicator>'
}); });
const indicator = { const indicator = {
element: clockIndicator.$mount().$el, element: clockIndicator.$mount().$el,

View File

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

View File

@ -30,7 +30,7 @@
<div v-if="staticStyle" <div v-if="staticStyle"
class="c-inspect-styles__style" class="c-inspect-styles__style"
> >
<StyleEditor class="c-inspect-styles__editor" <style-editor class="c-inspect-styles__editor"
:style-item="staticStyle" :style-item="staticStyle"
:is-editing="isEditing" :is-editing="isEditing"
@persist="updateStaticStyle" @persist="updateStaticStyle"
@ -87,7 +87,7 @@
<condition-description :show-label="true" <condition-description :show-label="true"
:condition="getCondition(conditionStyle.conditionId)" :condition="getCondition(conditionStyle.conditionId)"
/> />
<StyleEditor class="c-inspect-styles__editor" <style-editor class="c-inspect-styles__editor"
:style-item="conditionStyle" :style-item="conditionStyle"
:is-editing="isEditing" :is-editing="isEditing"
@persist="updateConditionalStyle" @persist="updateConditionalStyle"
@ -240,10 +240,10 @@ export default {
} }
let vm = new Vue({ let vm = new Vue({
components: {ConditionSetSelectorDialog},
provide: { provide: {
openmct: this.openmct openmct: this.openmct
}, },
components: {ConditionSetSelectorDialog},
data() { data() {
return { return {
handleItemSelection handleItemSelection
@ -273,7 +273,10 @@ export default {
this.openmct.objects.getOriginalPath(this.conditionSetDomainObject.identifier).then( this.openmct.objects.getOriginalPath(this.conditionSetDomainObject.identifier).then(
(objectPath) => { (objectPath) => {
this.objectPath = objectPath; this.objectPath = objectPath;
this.navigateToPath = '#/browse/' + this.openmct.objects.getRelativePath(this.objectPath); this.navigateToPath = '#/browse/' + this.objectPath
.map(o => o && this.openmct.objects.makeKeyString(o.identifier))
.reverse()
.join('/');
} }
); );
}, },

View File

@ -40,7 +40,7 @@
<div v-if="staticStyle" <div v-if="staticStyle"
class="c-inspect-styles__style" class="c-inspect-styles__style"
> >
<StyleEditor class="c-inspect-styles__editor" <style-editor class="c-inspect-styles__editor"
:style-item="staticStyle" :style-item="staticStyle"
:is-editing="allowEditing" :is-editing="allowEditing"
:mixed-styles="mixedStyles" :mixed-styles="mixedStyles"
@ -108,7 +108,7 @@
<condition-description :show-label="true" <condition-description :show-label="true"
:condition="getCondition(conditionStyle.conditionId)" :condition="getCondition(conditionStyle.conditionId)"
/> />
<StyleEditor class="c-inspect-styles__editor" <style-editor class="c-inspect-styles__editor"
:style-item="conditionStyle" :style-item="conditionStyle"
:non-specific-font-properties="nonSpecificFontProperties" :non-specific-font-properties="nonSpecificFontProperties"
:is-editing="allowEditing" :is-editing="allowEditing"
@ -141,7 +141,6 @@ const NON_STYLEABLE_CONTAINER_TYPES = [
const NON_STYLEABLE_LAYOUT_ITEM_TYPES = [ const NON_STYLEABLE_LAYOUT_ITEM_TYPES = [
'line-view', 'line-view',
'box-view', 'box-view',
'ellipse-view',
'image-view' 'image-view'
]; ];
@ -297,7 +296,10 @@ export default {
this.openmct.objects.getOriginalPath(this.conditionSetDomainObject.identifier).then( this.openmct.objects.getOriginalPath(this.conditionSetDomainObject.identifier).then(
(objectPath) => { (objectPath) => {
this.objectPath = objectPath; this.objectPath = objectPath;
this.navigateToPath = '#/browse/' + this.openmct.objects.getRelativePath(this.objectPath); this.navigateToPath = '#/browse/' + this.objectPath
.map(o => o && this.openmct.objects.makeKeyString(o.identifier))
.reverse()
.join('/');
} }
); );
}, },
@ -319,7 +321,7 @@ export default {
if (item) { if (item) {
const type = this.openmct.types.get(item.type); const type = this.openmct.types.get(item.type);
if (type && type.definition) { if (type && type.definition) {
creatable = (type.definition.creatable !== undefined && (type.definition.creatable === 'true' || type.definition.creatable === true)); creatable = (type.definition.creatable === true);
} }
} }
@ -556,10 +558,10 @@ export default {
} }
let vm = new Vue({ let vm = new Vue({
components: {ConditionSetSelectorDialog},
provide: { provide: {
openmct: this.openmct openmct: this.openmct
}, },
components: {ConditionSetSelectorDialog},
data() { data() {
return { return {
handleItemSelection handleItemSelection

View File

@ -230,7 +230,7 @@ describe('the plugin', function () {
}; };
const staticStyle = { const staticStyle = {
"style": { "style": {
"backgroundColor": "#666666", "backgroundColor": "#717171",
"border": "1px solid #00ffff" "border": "1px solid #00ffff"
} }
}; };
@ -238,7 +238,7 @@ describe('the plugin', function () {
"conditionId": "39584410-cbf9-499e-96dc-76f27e69885d", "conditionId": "39584410-cbf9-499e-96dc-76f27e69885d",
"style": { "style": {
"isStyleInvisible": "", "isStyleInvisible": "",
"backgroundColor": "#666666", "backgroundColor": "#717171",
"border": "1px solid #ffff00" "border": "1px solid #ffff00"
} }
}; };
@ -250,7 +250,7 @@ describe('the plugin', function () {
"configuration": { "configuration": {
"items": [ "items": [
{ {
"fill": "#666666", "fill": "#717171",
"stroke": "", "stroke": "",
"x": 1, "x": 1,
"y": 1, "y": 1,
@ -259,22 +259,12 @@ describe('the plugin', function () {
"type": "box-view", "type": "box-view",
"id": "89b88746-d325-487b-aec4-11b79afff9e8" "id": "89b88746-d325-487b-aec4-11b79afff9e8"
}, },
{
"fill": "#666666",
"stroke": "",
"x": 1,
"y": 1,
"width": 10,
"height": 5,
"type": "ellipse-view",
"id": "19b88746-d325-487b-aec4-11b79afff9z8"
},
{ {
"x": 18, "x": 18,
"y": 9, "y": 9,
"x2": 23, "x2": 23,
"y2": 4, "y2": 4,
"stroke": "#666666", "stroke": "#717171",
"type": "line-view", "type": "line-view",
"id": "57d49a28-7863-43bd-9593-6570758916f0" "id": "57d49a28-7863-43bd-9593-6570758916f0"
}, },
@ -309,12 +299,12 @@ describe('the plugin', function () {
"y": 9, "y": 9,
"x2": 23, "x2": 23,
"y2": 4, "y2": 4,
"stroke": "#666666", "stroke": "#717171",
"type": "line-view", "type": "line-view",
"id": "57d49a28-7863-43bd-9593-6570758916f0" "id": "57d49a28-7863-43bd-9593-6570758916f0"
}; };
boxLayoutItem = { boxLayoutItem = {
"fill": "#666666", "fill": "#717171",
"stroke": "", "stroke": "",
"x": 1, "x": 1,
"y": 1, "y": 1,

View File

@ -31,7 +31,6 @@ const styleProps = {
return !type ? true : (type === 'text-view' return !type ? true : (type === 'text-view'
|| type === 'telemetry-view' || type === 'telemetry-view'
|| type === 'box-view' || type === 'box-view'
|| type === 'ellipse-view'
|| type === 'subobject-view'); || type === 'subobject-view');
} }
}, },
@ -42,7 +41,6 @@ const styleProps = {
return !type ? true : (type === 'text-view' return !type ? true : (type === 'text-view'
|| type === 'telemetry-view' || type === 'telemetry-view'
|| type === 'box-view' || type === 'box-view'
|| type === 'ellipse-view'
|| type === 'image-view' || type === 'image-view'
|| type === 'line-view' || type === 'line-view'
|| type === 'subobject-view'); || type === 'subobject-view');

View File

@ -38,8 +38,7 @@ a.c-condition-widget {
// Make Condition Widget expand when in a hidden frame Layout context // Make Condition Widget expand when in a hidden frame Layout context
// For both static and Flexible Layouts // For both static and Flexible Layouts
.c-so-view--conditionWidget.c-so-view--no-frame { .c-so-view--no-frame > .c-so-view__object-view > .c-condition-widget {
.c-condition-widget {
@include abs(); @include abs();
display: flex; display: flex;
align-items: center; align-items: center;
@ -47,9 +46,6 @@ a.c-condition-widget {
padding: 0; padding: 0;
} }
.c-so-view__frame-controls { display: none; }
}
// Add some margin when a Condition Widget is in a Flexible Layout // Add some margin when a Condition Widget is in a Flexible Layout
.c-fl .c-so-view--no-frame .c-condition-widget { .c-fl .c-so-view--no-frame .c-condition-widget {
@include abs(1px); @include abs(1px);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -47,8 +47,8 @@
.is-editing { .is-editing {
.l-shell__main-container { .l-shell__main-container {
[s-selected], &[s-selected],
[s-selected-parent] { &[s-selected-parent] {
// Display grid and allow edit marquee to display in main layout holder when editing // Display grid and allow edit marquee to display in main layout holder when editing
> .l-layout { > .l-layout {
background: $editUIGridColorBg; background: $editUIGridColorBg;

View File

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

View File

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

View File

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

View File

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

View File

@ -39,13 +39,10 @@ describe("The Compass component", () => {
sunAngle: 30 sunAngle: 30
}; };
let propsData = { let propsData = {
containerWidth: 600,
containerHeight: 600,
naturalAspectRatio: 0.9, naturalAspectRatio: 0.9,
image: imageDatum, image: imageDatum
sizedImageDimensions: {
width: 100,
height: 100
},
compassRoseSizingClasses: '--rose-small --rose-min'
}; };
app = new Vue({ app = new Vue({
@ -54,13 +51,13 @@ describe("The Compass component", () => {
return propsData; return propsData;
}, },
template: `<Compass template: `<Compass
:compass-rose-sizing-classes="compassRoseSizingClasses" :container-width="containerWidth"
:image="image" :container-height="containerHeight"
:natural-aspect-ratio="naturalAspectRatio" :natural-aspect-ratio="naturalAspectRatio"
:sized-image-dimensions="sizedImageDimensions" :image="image" />`
/>`
}); });
instance = app.$mount(); instance = app.$mount();
}); });
afterAll(() => { afterAll(() => {

View File

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

View File

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

View File

@ -285,17 +285,17 @@
} }
} }
.c-imagery__prev-next-button { .c-imagery__prev-next-buttons {
pointer-events: all; display: flex;
width: 100%;
justify-content: space-between;
pointer-events: none;
position: absolute; position: absolute;
top: 50%; top: 50%;
transform: translateY(-75%); // 75% due to transform: rotation approach to the button transform: translateY(-75%);
&.c-nav { .c-nav {
position: absolute; pointer-events: all;
&--prev { left: 0; }
&--next { right: 0; }
} }
.s-status-taking-snapshot & { .s-status-taking-snapshot & {
@ -312,34 +312,3 @@
@include cArrowButtonSizing($dimOuter: 32px); @include cArrowButtonSizing($dimOuter: 32px);
} }
} }
/*************************************** IMAGERY IN TIMESTRIP VIEWS */
.c-imagery-tsv {
g.c-imagery-tsv__image-wrapper {
cursor: pointer;
&.is-hovered {
filter: brightness(1) contrast(1) !important;
[class*='__image-handle'] {
fill: $colorBodyFg;
}
}
}
&__no-items {
fill: $colorBodyFg !important;
}
&__image-handle {
fill: rgba($colorBodyFg, 0.5);
}
&__image-placeholder {
fill: pushBack($colorBodyBg, 0.3);
}
&:hover g.c-imagery-tsv__image-wrapper {
// TODO CH: convert to theme constants
filter: brightness(0.5) contrast(0.7);
}
}

View File

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

View File

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

View File

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

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