mirror of
https://github.com/nasa/openmct.git
synced 2025-06-26 19:12:02 +00:00
Compare commits
29 Commits
icon-telem
...
vue-hack
Author | SHA1 | Date | |
---|---|---|---|
62ff404bb3 | |||
8243cf5d7b | |||
c4c1fea17f | |||
5e920e90ce | |||
886db23eb6 | |||
0ccb546a2e | |||
271f8ed38f | |||
650f84e95c | |||
b70af5a1bb | |||
0af21632db | |||
e2f1ff5442 | |||
c4b9be18f1 | |||
eabdf6cd04 | |||
e56c673005 | |||
dad9f12a5c | |||
aa5edb0b83 | |||
b315803180 | |||
b27317631b | |||
953a9daafb | |||
63f9cd449f | |||
54220f547b | |||
93d967c2b3 | |||
1226459c6f | |||
d7c9c9cb98 | |||
2131ef2397 | |||
48c22369a1 | |||
6506077f4d | |||
b1b4266ff3 | |||
42b0148f93 |
33
.github/workflows/codeql-analysis.yml
vendored
Normal file
33
.github/workflows/codeql-analysis.yml
vendored
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
|
||||||
|
name: "CodeQL"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master ]
|
||||||
|
schedule:
|
||||||
|
- cron: '28 21 * * 3'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
name: Analyze
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
contents: read
|
||||||
|
security-events: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
# Initializes the CodeQL tools for scanning.
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v1
|
||||||
|
with:
|
||||||
|
languages: javascript
|
||||||
|
|
||||||
|
- name: Autobuild
|
||||||
|
uses: github/codeql-action/autobuild@v1
|
||||||
|
|
||||||
|
- name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@v1
|
@ -28,6 +28,15 @@ 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",
|
||||||
@ -109,6 +118,100 @@ define([
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
'example.spectral-generator': {
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
key: "name",
|
||||||
|
name: "Name",
|
||||||
|
format: "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "utc",
|
||||||
|
name: "Time",
|
||||||
|
format: "utc",
|
||||||
|
hints: {
|
||||||
|
domain: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "wavelength",
|
||||||
|
name: "Wavelength",
|
||||||
|
unit: "Hz",
|
||||||
|
formatString: '%0.2f',
|
||||||
|
hints: {
|
||||||
|
domain: 2,
|
||||||
|
spectralAttribute: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "cos",
|
||||||
|
name: "Cosine",
|
||||||
|
unit: "deg",
|
||||||
|
formatString: '%0.2f',
|
||||||
|
hints: {
|
||||||
|
range: 2,
|
||||||
|
spectralAttribute: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'example.spectral-aggregate-generator': {
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
key: "name",
|
||||||
|
name: "Name",
|
||||||
|
format: "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "utc",
|
||||||
|
name: "Time",
|
||||||
|
format: "utc",
|
||||||
|
hints: {
|
||||||
|
domain: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "ch1",
|
||||||
|
name: "Channel 1",
|
||||||
|
format: "string",
|
||||||
|
hints: {
|
||||||
|
range: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "ch2",
|
||||||
|
name: "Channel 2",
|
||||||
|
format: "string",
|
||||||
|
hints: {
|
||||||
|
range: 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "ch3",
|
||||||
|
name: "Channel 3",
|
||||||
|
format: "string",
|
||||||
|
hints: {
|
||||||
|
range: 3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "ch4",
|
||||||
|
name: "Channel 4",
|
||||||
|
format: "string",
|
||||||
|
hints: {
|
||||||
|
range: 4
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "ch5",
|
||||||
|
name: "Channel 5",
|
||||||
|
format: "string",
|
||||||
|
hints: {
|
||||||
|
range: 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
86
example/generator/SpectralAggregateGeneratorProvider.js
Normal file
86
example/generator/SpectralAggregateGeneratorProvider.js
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* Open MCT includes source code licensed under additional open source
|
||||||
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
* this source code distribution or the Licensing information page available
|
||||||
|
* at runtime from the About dialog for additional information.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
define([
|
||||||
|
|
||||||
|
], function (
|
||||||
|
|
||||||
|
) {
|
||||||
|
|
||||||
|
function SpectralAggregateGeneratorProvider() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function pointForTimestamp(timestamp, count, name) {
|
||||||
|
return {
|
||||||
|
name: name,
|
||||||
|
utc: String(Math.floor(timestamp / count) * count),
|
||||||
|
ch1: String(Math.floor(timestamp / count) % 1),
|
||||||
|
ch2: String(Math.floor(timestamp / count) % 2),
|
||||||
|
ch3: String(Math.floor(timestamp / count) % 3),
|
||||||
|
ch4: String(Math.floor(timestamp / count) % 4),
|
||||||
|
ch5: String(Math.floor(timestamp / count) % 5)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
SpectralAggregateGeneratorProvider.prototype.supportsSubscribe = function (domainObject) {
|
||||||
|
return domainObject.type === 'example.spectral-aggregate-generator';
|
||||||
|
};
|
||||||
|
|
||||||
|
SpectralAggregateGeneratorProvider.prototype.subscribe = function (domainObject, callback) {
|
||||||
|
var count = 5000;
|
||||||
|
|
||||||
|
var interval = setInterval(function () {
|
||||||
|
var now = Date.now();
|
||||||
|
var datum = pointForTimestamp(now, count, domainObject.name);
|
||||||
|
callback(datum);
|
||||||
|
}, count);
|
||||||
|
|
||||||
|
return function () {
|
||||||
|
clearInterval(interval);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
SpectralAggregateGeneratorProvider.prototype.supportsRequest = function (domainObject, options) {
|
||||||
|
return domainObject.type === 'example.spectral-aggregate-generator';
|
||||||
|
};
|
||||||
|
|
||||||
|
SpectralAggregateGeneratorProvider.prototype.request = function (domainObject, options) {
|
||||||
|
var start = options.start;
|
||||||
|
var end = Math.min(Date.now(), options.end); // no future values
|
||||||
|
var count = 5000;
|
||||||
|
if (options.strategy === 'latest' || options.size === 1) {
|
||||||
|
start = end;
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = [];
|
||||||
|
while (start <= end && data.length < 5000) {
|
||||||
|
data.push(pointForTimestamp(start, count, domainObject.name));
|
||||||
|
start += count;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
return SpectralAggregateGeneratorProvider;
|
||||||
|
|
||||||
|
});
|
102
example/generator/SpectralGeneratorProvider.js
Normal file
102
example/generator/SpectralGeneratorProvider.js
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* Open MCT includes source code licensed under additional open source
|
||||||
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
* this source code distribution or the Licensing information page available
|
||||||
|
* at runtime from the About dialog for additional information.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
define([
|
||||||
|
'./WorkerInterface'
|
||||||
|
], function (
|
||||||
|
WorkerInterface
|
||||||
|
) {
|
||||||
|
|
||||||
|
var REQUEST_DEFAULTS = {
|
||||||
|
amplitude: 1,
|
||||||
|
wavelength: 1,
|
||||||
|
period: 10,
|
||||||
|
offset: 0,
|
||||||
|
dataRateInHz: 1,
|
||||||
|
randomness: 0,
|
||||||
|
phase: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
function SpectralGeneratorProvider() {
|
||||||
|
this.workerInterface = new WorkerInterface();
|
||||||
|
}
|
||||||
|
|
||||||
|
SpectralGeneratorProvider.prototype.canProvideTelemetry = function (domainObject) {
|
||||||
|
return domainObject.type === 'example.spectral-generator';
|
||||||
|
};
|
||||||
|
|
||||||
|
SpectralGeneratorProvider.prototype.supportsRequest =
|
||||||
|
SpectralGeneratorProvider.prototype.supportsSubscribe =
|
||||||
|
SpectralGeneratorProvider.prototype.canProvideTelemetry;
|
||||||
|
|
||||||
|
SpectralGeneratorProvider.prototype.makeWorkerRequest = function (domainObject, request = {}) {
|
||||||
|
var props = [
|
||||||
|
'amplitude',
|
||||||
|
'wavelength',
|
||||||
|
'period',
|
||||||
|
'offset',
|
||||||
|
'dataRateInHz',
|
||||||
|
'phase',
|
||||||
|
'randomness'
|
||||||
|
];
|
||||||
|
|
||||||
|
var workerRequest = {};
|
||||||
|
|
||||||
|
props.forEach(function (prop) {
|
||||||
|
if (domainObject.telemetry && Object.prototype.hasOwnProperty.call(domainObject.telemetry, prop)) {
|
||||||
|
workerRequest[prop] = domainObject.telemetry[prop];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request && Object.prototype.hasOwnProperty.call(request, prop)) {
|
||||||
|
workerRequest[prop] = request[prop];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(workerRequest, prop)) {
|
||||||
|
workerRequest[prop] = REQUEST_DEFAULTS[prop];
|
||||||
|
}
|
||||||
|
|
||||||
|
workerRequest[prop] = Number(workerRequest[prop]);
|
||||||
|
});
|
||||||
|
|
||||||
|
workerRequest.name = domainObject.name;
|
||||||
|
|
||||||
|
return workerRequest;
|
||||||
|
};
|
||||||
|
|
||||||
|
SpectralGeneratorProvider.prototype.request = function (domainObject, request) {
|
||||||
|
var workerRequest = this.makeWorkerRequest(domainObject, request);
|
||||||
|
workerRequest.start = request.start;
|
||||||
|
workerRequest.end = request.end;
|
||||||
|
workerRequest.spectra = true;
|
||||||
|
|
||||||
|
return this.workerInterface.request(workerRequest);
|
||||||
|
};
|
||||||
|
|
||||||
|
SpectralGeneratorProvider.prototype.subscribe = function (domainObject, callback) {
|
||||||
|
var workerRequest = this.makeWorkerRequest(domainObject, {});
|
||||||
|
workerRequest.spectra = true;
|
||||||
|
|
||||||
|
return this.workerInterface.subscribe(workerRequest, callback);
|
||||||
|
};
|
||||||
|
|
||||||
|
return SpectralGeneratorProvider;
|
||||||
|
});
|
@ -54,23 +54,38 @@
|
|||||||
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;
|
||||||
|
}
|
||||||
|
|
||||||
function work(now) {
|
return nextStep;
|
||||||
while (nextStep < now) {
|
};
|
||||||
self.postMessage({
|
} else {
|
||||||
id: message.id,
|
work = function (now) {
|
||||||
data: {
|
while (nextStep < now) {
|
||||||
name: data.name,
|
self.postMessage({
|
||||||
utc: nextStep,
|
id: message.id,
|
||||||
yesterday: nextStep - 60 * 60 * 24 * 1000,
|
data: {
|
||||||
sin: sin(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness),
|
name: data.name,
|
||||||
cos: cos(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness)
|
utc: nextStep,
|
||||||
}
|
yesterday: nextStep - 60 * 60 * 24 * 1000,
|
||||||
});
|
sin: sin(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness),
|
||||||
nextStep += step;
|
wavelength: wavelength(start, nextStep),
|
||||||
}
|
cos: cos(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
nextStep += step;
|
||||||
|
}
|
||||||
|
|
||||||
return nextStep;
|
return nextStep;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
subscriptions[message.id] = work;
|
subscriptions[message.id] = work;
|
||||||
@ -111,13 +126,21 @@
|
|||||||
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: data
|
data: request.spectra ? {
|
||||||
|
wavelength: data.map((item) => {
|
||||||
|
return item.wavelength;
|
||||||
|
}),
|
||||||
|
cos: data.map((item) => {
|
||||||
|
return item.cos;
|
||||||
|
})
|
||||||
|
} : data
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,6 +154,10 @@
|
|||||||
* Math.sin(phase + (timestamp / period / 1000 * Math.PI * 2)) + (amplitude * Math.random() * randomness) + offset;
|
* 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,
|
||||||
|
@ -24,11 +24,15 @@ define([
|
|||||||
"./GeneratorProvider",
|
"./GeneratorProvider",
|
||||||
"./SinewaveLimitProvider",
|
"./SinewaveLimitProvider",
|
||||||
"./StateGeneratorProvider",
|
"./StateGeneratorProvider",
|
||||||
|
"./SpectralGeneratorProvider",
|
||||||
|
"./SpectralAggregateGeneratorProvider",
|
||||||
"./GeneratorMetadataProvider"
|
"./GeneratorMetadataProvider"
|
||||||
], function (
|
], function (
|
||||||
GeneratorProvider,
|
GeneratorProvider,
|
||||||
SinewaveLimitProvider,
|
SinewaveLimitProvider,
|
||||||
StateGeneratorProvider,
|
StateGeneratorProvider,
|
||||||
|
SpectralGeneratorProvider,
|
||||||
|
SpectralAggregateGeneratorProvider,
|
||||||
GeneratorMetadataProvider
|
GeneratorMetadataProvider
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@ -61,6 +65,37 @@ 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.",
|
||||||
|
@ -195,6 +195,7 @@
|
|||||||
['table', 'telemetry.plot.overlay', 'telemetry.plot.stacked'],
|
['table', 'telemetry.plot.overlay', 'telemetry.plot.stacked'],
|
||||||
{indicator: true}
|
{indicator: true}
|
||||||
));
|
));
|
||||||
|
openmct.install(openmct.plugins.Clock({ enableClockIndicator: true }));
|
||||||
openmct.start();
|
openmct.start();
|
||||||
</script>
|
</script>
|
||||||
</html>
|
</html>
|
||||||
|
14
package.json
14
package.json
@ -2,7 +2,6 @@
|
|||||||
"name": "openmct",
|
"name": "openmct",
|
||||||
"version": "1.7.8-SNAPSHOT",
|
"version": "1.7.8-SNAPSHOT",
|
||||||
"description": "The Open MCT core platform",
|
"description": "The Open MCT core platform",
|
||||||
"dependencies": {},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"angular": ">=1.8.0",
|
"angular": ">=1.8.0",
|
||||||
"angular-route": "1.4.14",
|
"angular-route": "1.4.14",
|
||||||
@ -12,16 +11,9 @@
|
|||||||
"copy-webpack-plugin": "^4.5.2",
|
"copy-webpack-plugin": "^4.5.2",
|
||||||
"cross-env": "^6.0.3",
|
"cross-env": "^6.0.3",
|
||||||
"css-loader": "^1.0.0",
|
"css-loader": "^1.0.0",
|
||||||
"d3-array": "1.2.x",
|
|
||||||
"d3-axis": "1.0.x",
|
"d3-axis": "1.0.x",
|
||||||
"d3-collection": "1.0.x",
|
|
||||||
"d3-color": "1.0.x",
|
|
||||||
"d3-format": "1.2.x",
|
|
||||||
"d3-interpolate": "1.1.x",
|
|
||||||
"d3-scale": "1.0.x",
|
"d3-scale": "1.0.x",
|
||||||
"d3-selection": "1.3.x",
|
"d3-selection": "1.3.x",
|
||||||
"d3-time": "1.0.x",
|
|
||||||
"d3-time-format": "2.1.x",
|
|
||||||
"eslint": "7.0.0",
|
"eslint": "7.0.0",
|
||||||
"eslint-plugin-vue": "^7.5.0",
|
"eslint-plugin-vue": "^7.5.0",
|
||||||
"eslint-plugin-you-dont-need-lodash-underscore": "^6.10.0",
|
"eslint-plugin-you-dont-need-lodash-underscore": "^6.10.0",
|
||||||
@ -41,13 +33,13 @@
|
|||||||
"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-junit-reporter": "2.0.1",
|
"karma-firefox-launcher": "2.1.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-webpack": "4.0.2",
|
"karma-webpack": "4.0.2",
|
||||||
"location-bar": "^3.0.1",
|
"location-bar": "^3.0.1",
|
||||||
@ -62,6 +54,8 @@
|
|||||||
"node-bourbon": "^4.2.3",
|
"node-bourbon": "^4.2.3",
|
||||||
"node-sass": "^4.14.1",
|
"node-sass": "^4.14.1",
|
||||||
"painterro": "^1.2.56",
|
"painterro": "^1.2.56",
|
||||||
|
"plotly.js-basic-dist": "^2.5.0",
|
||||||
|
"plotly.js-gl2d-dist": "^2.5.0",
|
||||||
"printj": "^1.2.1",
|
"printj": "^1.2.1",
|
||||||
"raw-loader": "^0.5.1",
|
"raw-loader": "^0.5.1",
|
||||||
"request": "^2.69.0",
|
"request": "^2.69.0",
|
||||||
|
@ -21,32 +21,24 @@
|
|||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
define([
|
define([
|
||||||
"moment-timezone",
|
|
||||||
"./src/indicators/ClockIndicator",
|
|
||||||
"./src/services/TickerService",
|
"./src/services/TickerService",
|
||||||
"./src/services/TimerService",
|
"./src/services/TimerService",
|
||||||
"./src/controllers/ClockController",
|
|
||||||
"./src/controllers/TimerController",
|
"./src/controllers/TimerController",
|
||||||
"./src/controllers/RefreshingController",
|
"./src/controllers/RefreshingController",
|
||||||
"./src/actions/StartTimerAction",
|
"./src/actions/StartTimerAction",
|
||||||
"./src/actions/RestartTimerAction",
|
"./src/actions/RestartTimerAction",
|
||||||
"./src/actions/StopTimerAction",
|
"./src/actions/StopTimerAction",
|
||||||
"./src/actions/PauseTimerAction",
|
"./src/actions/PauseTimerAction",
|
||||||
"./res/templates/clock.html",
|
|
||||||
"./res/templates/timer.html"
|
"./res/templates/timer.html"
|
||||||
], function (
|
], function (
|
||||||
MomentTimezone,
|
|
||||||
ClockIndicator,
|
|
||||||
TickerService,
|
TickerService,
|
||||||
TimerService,
|
TimerService,
|
||||||
ClockController,
|
|
||||||
TimerController,
|
TimerController,
|
||||||
RefreshingController,
|
RefreshingController,
|
||||||
StartTimerAction,
|
StartTimerAction,
|
||||||
RestartTimerAction,
|
RestartTimerAction,
|
||||||
StopTimerAction,
|
StopTimerAction,
|
||||||
PauseTimerAction,
|
PauseTimerAction,
|
||||||
clockTemplate,
|
|
||||||
timerTemplate
|
timerTemplate
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
@ -73,16 +65,6 @@ define([
|
|||||||
"value": "YYYY/MM/DD HH:mm:ss"
|
"value": "YYYY/MM/DD HH:mm:ss"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"indicators": [
|
|
||||||
{
|
|
||||||
"implementation": ClockIndicator,
|
|
||||||
"depends": [
|
|
||||||
"tickerService",
|
|
||||||
"CLOCK_INDICATOR_FORMAT"
|
|
||||||
],
|
|
||||||
"priority": "preferred"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"services": [
|
"services": [
|
||||||
{
|
{
|
||||||
"key": "tickerService",
|
"key": "tickerService",
|
||||||
@ -99,14 +81,6 @@ define([
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"controllers": [
|
"controllers": [
|
||||||
{
|
|
||||||
"key": "ClockController",
|
|
||||||
"implementation": ClockController,
|
|
||||||
"depends": [
|
|
||||||
"$scope",
|
|
||||||
"tickerService"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"key": "TimerController",
|
"key": "TimerController",
|
||||||
"implementation": TimerController,
|
"implementation": TimerController,
|
||||||
@ -126,12 +100,6 @@ define([
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"views": [
|
"views": [
|
||||||
{
|
|
||||||
"key": "clock",
|
|
||||||
"type": "clock",
|
|
||||||
"editable": false,
|
|
||||||
"template": clockTemplate
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"key": "timer",
|
"key": "timer",
|
||||||
"type": "timer",
|
"type": "timer",
|
||||||
@ -186,70 +154,6 @@ define([
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"types": [
|
"types": [
|
||||||
{
|
|
||||||
"key": "clock",
|
|
||||||
"name": "Clock",
|
|
||||||
"cssClass": "icon-clock",
|
|
||||||
"description": "A UTC-based clock that supports a variety of display formats. Clocks can be added to Display Layouts.",
|
|
||||||
"priority": 101,
|
|
||||||
"features": [
|
|
||||||
"creation"
|
|
||||||
],
|
|
||||||
"properties": [
|
|
||||||
{
|
|
||||||
"key": "clockFormat",
|
|
||||||
"name": "Display Format",
|
|
||||||
"control": "composite",
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"control": "select",
|
|
||||||
"options": [
|
|
||||||
{
|
|
||||||
"value": "YYYY/MM/DD hh:mm:ss",
|
|
||||||
"name": "YYYY/MM/DD hh:mm:ss"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"value": "YYYY/DDD hh:mm:ss",
|
|
||||||
"name": "YYYY/DDD hh:mm:ss"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"value": "hh:mm:ss",
|
|
||||||
"name": "hh:mm:ss"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"cssClass": "l-inline"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"control": "select",
|
|
||||||
"options": [
|
|
||||||
{
|
|
||||||
"value": "clock12",
|
|
||||||
"name": "12hr"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"value": "clock24",
|
|
||||||
"name": "24hr"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"cssClass": "l-inline"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "timezone",
|
|
||||||
"name": "Timezone",
|
|
||||||
"control": "autocomplete",
|
|
||||||
"options": MomentTimezone.tz.names()
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"model": {
|
|
||||||
"clockFormat": [
|
|
||||||
"YYYY/MM/DD hh:mm:ss",
|
|
||||||
"clock12"
|
|
||||||
],
|
|
||||||
"timezone": "UTC"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"key": "timer",
|
"key": "timer",
|
||||||
"name": "Timer",
|
"name": "Timer",
|
||||||
|
@ -1,32 +0,0 @@
|
|||||||
<!--
|
|
||||||
Open MCT, Copyright (c) 2009-2016, United States Government
|
|
||||||
as represented by the Administrator of the National Aeronautics and Space
|
|
||||||
Administration. All rights reserved.
|
|
||||||
|
|
||||||
Open MCT is licensed under the Apache License, Version 2.0 (the
|
|
||||||
"License"); you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0.
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
License for the specific language governing permissions and limitations
|
|
||||||
under the License.
|
|
||||||
|
|
||||||
Open MCT includes source code licensed under additional open source
|
|
||||||
licenses. See the Open Source Licenses file (LICENSES.md) included with
|
|
||||||
this source code distribution or the Licensing information page available
|
|
||||||
at runtime from the About dialog for additional information.
|
|
||||||
-->
|
|
||||||
<div class="c-clock l-time-display u-style-receiver js-style-receiver" ng-controller="ClockController as clock">
|
|
||||||
<div class="c-clock__timezone">
|
|
||||||
{{clock.zone()}}
|
|
||||||
</div>
|
|
||||||
<div class="c-clock__value">
|
|
||||||
{{clock.text()}}
|
|
||||||
</div>
|
|
||||||
<div class="c-clock__ampm">
|
|
||||||
{{clock.ampm()}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@ -1,110 +0,0 @@
|
|||||||
/*****************************************************************************
|
|
||||||
* Open MCT, Copyright (c) 2009-2016, United States Government
|
|
||||||
* as represented by the Administrator of the National Aeronautics and Space
|
|
||||||
* Administration. All rights reserved.
|
|
||||||
*
|
|
||||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
* License for the specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*
|
|
||||||
* Open MCT includes source code licensed under additional open source
|
|
||||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
|
||||||
* this source code distribution or the Licensing information page available
|
|
||||||
* at runtime from the About dialog for additional information.
|
|
||||||
*****************************************************************************/
|
|
||||||
|
|
||||||
define([
|
|
||||||
'moment',
|
|
||||||
'moment-timezone'
|
|
||||||
],
|
|
||||||
function (
|
|
||||||
moment,
|
|
||||||
momentTimezone
|
|
||||||
) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Controller for views of a Clock domain object.
|
|
||||||
*
|
|
||||||
* @constructor
|
|
||||||
* @memberof platform/features/clock
|
|
||||||
* @param {angular.Scope} $scope the Angular scope
|
|
||||||
* @param {platform/features/clock.TickerService} tickerService
|
|
||||||
* a service used to align behavior with clock ticks
|
|
||||||
*/
|
|
||||||
function ClockController($scope, tickerService) {
|
|
||||||
var lastTimestamp,
|
|
||||||
unlisten,
|
|
||||||
timeFormat,
|
|
||||||
zoneName,
|
|
||||||
self = this;
|
|
||||||
|
|
||||||
function update() {
|
|
||||||
var m = zoneName
|
|
||||||
? moment.utc(lastTimestamp).tz(zoneName) : moment.utc(lastTimestamp);
|
|
||||||
self.zoneAbbr = m.zoneAbbr();
|
|
||||||
self.textValue = timeFormat && m.format(timeFormat);
|
|
||||||
self.ampmValue = m.format("A"); // Just the AM or PM part
|
|
||||||
}
|
|
||||||
|
|
||||||
function tick(timestamp) {
|
|
||||||
lastTimestamp = timestamp;
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateModel(model) {
|
|
||||||
var baseFormat;
|
|
||||||
if (model !== undefined) {
|
|
||||||
baseFormat = model.clockFormat[0];
|
|
||||||
|
|
||||||
self.use24 = model.clockFormat[1] === 'clock24';
|
|
||||||
timeFormat = self.use24
|
|
||||||
? baseFormat.replace('hh', "HH") : baseFormat;
|
|
||||||
// If wrong timezone is provided, the UTC will be used
|
|
||||||
zoneName = momentTimezone.tz.names().includes(model.timezone)
|
|
||||||
? model.timezone : "UTC";
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pull in the model (clockFormat and timezone) from the domain object model
|
|
||||||
$scope.$watch('model', updateModel);
|
|
||||||
|
|
||||||
// Listen for clock ticks ... and stop listening on destroy
|
|
||||||
unlisten = tickerService.listen(tick);
|
|
||||||
$scope.$on('$destroy', unlisten);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the clock's time zone, as displayable text.
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
ClockController.prototype.zone = function () {
|
|
||||||
return this.zoneAbbr;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the current time, as displayable text.
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
ClockController.prototype.text = function () {
|
|
||||||
return this.textValue;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the text to display to qualify a time as AM or PM.
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
ClockController.prototype.ampm = function () {
|
|
||||||
return this.use24 ? '' : this.ampmValue;
|
|
||||||
};
|
|
||||||
|
|
||||||
return ClockController;
|
|
||||||
}
|
|
||||||
);
|
|
@ -1,65 +0,0 @@
|
|||||||
/*****************************************************************************
|
|
||||||
* Open MCT, Copyright (c) 2009-2016, United States Government
|
|
||||||
* as represented by the Administrator of the National Aeronautics and Space
|
|
||||||
* Administration. All rights reserved.
|
|
||||||
*
|
|
||||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
* License for the specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*
|
|
||||||
* Open MCT includes source code licensed under additional open source
|
|
||||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
|
||||||
* this source code distribution or the Licensing information page available
|
|
||||||
* at runtime from the About dialog for additional information.
|
|
||||||
*****************************************************************************/
|
|
||||||
|
|
||||||
define(
|
|
||||||
['moment'],
|
|
||||||
function (moment) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicator that displays the current UTC time in the status area.
|
|
||||||
* @implements {Indicator}
|
|
||||||
* @memberof platform/features/clock
|
|
||||||
* @param {platform/features/clock.TickerService} tickerService
|
|
||||||
* a service used to align behavior with clock ticks
|
|
||||||
* @param {string} indicatorFormat format string for timestamps
|
|
||||||
* shown in this indicator
|
|
||||||
*/
|
|
||||||
function ClockIndicator(tickerService, indicatorFormat) {
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
this.text = "";
|
|
||||||
|
|
||||||
tickerService.listen(function (timestamp) {
|
|
||||||
self.text = moment.utc(timestamp)
|
|
||||||
.format(indicatorFormat) + " UTC";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
ClockIndicator.prototype.getGlyphClass = function () {
|
|
||||||
return "";
|
|
||||||
};
|
|
||||||
|
|
||||||
ClockIndicator.prototype.getCssClass = function () {
|
|
||||||
return "t-indicator-clock icon-clock no-minify c-indicator--not-clickable";
|
|
||||||
};
|
|
||||||
|
|
||||||
ClockIndicator.prototype.getText = function () {
|
|
||||||
return this.text;
|
|
||||||
};
|
|
||||||
|
|
||||||
ClockIndicator.prototype.getDescription = function () {
|
|
||||||
return "";
|
|
||||||
};
|
|
||||||
|
|
||||||
return ClockIndicator;
|
|
||||||
}
|
|
||||||
);
|
|
@ -1,107 +0,0 @@
|
|||||||
/*****************************************************************************
|
|
||||||
* Open MCT, Copyright (c) 2009-2017, United States Government
|
|
||||||
* as represented by the Administrator of the National Aeronautics and Space
|
|
||||||
* Administration. All rights reserved.
|
|
||||||
*
|
|
||||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
* License for the specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*
|
|
||||||
* Open MCT includes source code licensed under additional open source
|
|
||||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
|
||||||
* this source code distribution or the Licensing information page available
|
|
||||||
* at runtime from the About dialog for additional information.
|
|
||||||
*****************************************************************************/
|
|
||||||
|
|
||||||
define(
|
|
||||||
["../../src/controllers/ClockController"],
|
|
||||||
function (ClockController) {
|
|
||||||
|
|
||||||
// Wed, 03 Jun 2015 17:56:14 GMT
|
|
||||||
var TEST_TIMESTAMP = 1433354174000;
|
|
||||||
|
|
||||||
describe("A clock view's controller", function () {
|
|
||||||
var mockScope,
|
|
||||||
mockTicker,
|
|
||||||
mockUnticker,
|
|
||||||
controller;
|
|
||||||
|
|
||||||
beforeEach(function () {
|
|
||||||
mockScope = jasmine.createSpyObj('$scope', ['$watch', '$on']);
|
|
||||||
mockTicker = jasmine.createSpyObj('ticker', ['listen']);
|
|
||||||
mockUnticker = jasmine.createSpy('unticker');
|
|
||||||
|
|
||||||
mockTicker.listen.and.returnValue(mockUnticker);
|
|
||||||
|
|
||||||
controller = new ClockController(mockScope, mockTicker);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("watches for model (clockFormat and timezone) from the domain object model", function () {
|
|
||||||
expect(mockScope.$watch).toHaveBeenCalledWith(
|
|
||||||
"model",
|
|
||||||
jasmine.any(Function)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("subscribes to clock ticks", function () {
|
|
||||||
expect(mockTicker.listen)
|
|
||||||
.toHaveBeenCalledWith(jasmine.any(Function));
|
|
||||||
});
|
|
||||||
|
|
||||||
it("unsubscribes to ticks when destroyed", function () {
|
|
||||||
// Make sure $destroy is being listened for...
|
|
||||||
expect(mockScope.$on.calls.mostRecent().args[0]).toEqual('$destroy');
|
|
||||||
expect(mockUnticker).not.toHaveBeenCalled();
|
|
||||||
|
|
||||||
// ...and makes sure that its listener unsubscribes from ticker
|
|
||||||
mockScope.$on.calls.mostRecent().args[1]();
|
|
||||||
expect(mockUnticker).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("formats using the format string from the model", function () {
|
|
||||||
mockTicker.listen.calls.mostRecent().args[0](TEST_TIMESTAMP);
|
|
||||||
mockScope.$watch.calls.mostRecent().args[1]({
|
|
||||||
"clockFormat": [
|
|
||||||
"YYYY-DDD hh:mm:ss",
|
|
||||||
"clock24"
|
|
||||||
],
|
|
||||||
"timezone": "Canada/Eastern"
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(controller.zone()).toEqual("EDT");
|
|
||||||
expect(controller.text()).toEqual("2015-154 13:56:14");
|
|
||||||
expect(controller.ampm()).toEqual("");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("formats 12-hour time", function () {
|
|
||||||
mockTicker.listen.calls.mostRecent().args[0](TEST_TIMESTAMP);
|
|
||||||
mockScope.$watch.calls.mostRecent().args[1]({
|
|
||||||
"clockFormat": [
|
|
||||||
"YYYY-DDD hh:mm:ss",
|
|
||||||
"clock12"
|
|
||||||
],
|
|
||||||
"timezone": ""
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(controller.zone()).toEqual("UTC");
|
|
||||||
expect(controller.text()).toEqual("2015-154 05:56:14");
|
|
||||||
expect(controller.ampm()).toEqual("PM");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does not throw exceptions when model is undefined", function () {
|
|
||||||
mockTicker.listen.calls.mostRecent().args[0](TEST_TIMESTAMP);
|
|
||||||
expect(function () {
|
|
||||||
mockScope.$watch.calls.mostRecent().args[1](undefined);
|
|
||||||
}).not.toThrow();
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
@ -1,58 +0,0 @@
|
|||||||
/*****************************************************************************
|
|
||||||
* Open MCT, Copyright (c) 2009-2016, United States Government
|
|
||||||
* as represented by the Administrator of the National Aeronautics and Space
|
|
||||||
* Administration. All rights reserved.
|
|
||||||
*
|
|
||||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
* License for the specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*
|
|
||||||
* Open MCT includes source code licensed under additional open source
|
|
||||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
|
||||||
* this source code distribution or the Licensing information page available
|
|
||||||
* at runtime from the About dialog for additional information.
|
|
||||||
*****************************************************************************/
|
|
||||||
|
|
||||||
define(
|
|
||||||
["../../src/indicators/ClockIndicator"],
|
|
||||||
function (ClockIndicator) {
|
|
||||||
|
|
||||||
// Wed, 03 Jun 2015 17:56:14 GMT
|
|
||||||
var TEST_TIMESTAMP = 1433354174000,
|
|
||||||
TEST_FORMAT = "YYYY-DDD HH:mm:ss";
|
|
||||||
|
|
||||||
describe("The clock indicator", function () {
|
|
||||||
var mockTicker,
|
|
||||||
mockUnticker,
|
|
||||||
indicator;
|
|
||||||
|
|
||||||
beforeEach(function () {
|
|
||||||
mockTicker = jasmine.createSpyObj('ticker', ['listen']);
|
|
||||||
mockUnticker = jasmine.createSpy('unticker');
|
|
||||||
|
|
||||||
mockTicker.listen.and.returnValue(mockUnticker);
|
|
||||||
|
|
||||||
indicator = new ClockIndicator(mockTicker, TEST_FORMAT);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("displays the current time", function () {
|
|
||||||
mockTicker.listen.calls.mostRecent().args[0](TEST_TIMESTAMP);
|
|
||||||
expect(indicator.getText()).toEqual("2015-154 17:56:14 UTC");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("implements the Indicator interface", function () {
|
|
||||||
expect(indicator.getCssClass()).toEqual(jasmine.any(String));
|
|
||||||
expect(indicator.getText()).toEqual(jasmine.any(String));
|
|
||||||
expect(indicator.getDescription()).toEqual(jasmine.any(String));
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
@ -44,9 +44,11 @@ define(
|
|||||||
setText(result.name);
|
setText(result.name);
|
||||||
scope.ngModel[scope.field] = result;
|
scope.ngModel[scope.field] = result;
|
||||||
control.$setValidity("file-input", true);
|
control.$setValidity("file-input", true);
|
||||||
|
scope.$digest();
|
||||||
}, function () {
|
}, function () {
|
||||||
setText('Select File');
|
setText('Select File');
|
||||||
control.$setValidity("file-input", false);
|
control.$setValidity("file-input", false);
|
||||||
|
scope.$digest();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,8 +30,8 @@ define([
|
|||||||
|
|
||||||
return function ImportExportPlugin() {
|
return function ImportExportPlugin() {
|
||||||
return function (openmct) {
|
return function (openmct) {
|
||||||
ExportAsJSONAction.appliesTo = function (context) {
|
ExportAsJSONAction.prototype.appliesTo = function (context) {
|
||||||
return openmct.$injector.get('policyService')
|
return this.openmct.$injector.get('policyService')
|
||||||
.allow("creation", context.domainObject.getCapability("type")
|
.allow("creation", context.domainObject.getCapability("type")
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -29,7 +29,7 @@ define(
|
|||||||
],
|
],
|
||||||
function (ExportAsJSONAction, domainObjectFactory, MCT, AdapterCapability) {
|
function (ExportAsJSONAction, domainObjectFactory, MCT, AdapterCapability) {
|
||||||
|
|
||||||
xdescribe("The export JSON action", function () {
|
describe("The export JSON action", function () {
|
||||||
|
|
||||||
var context,
|
var context,
|
||||||
action,
|
action,
|
||||||
@ -102,7 +102,7 @@ define(
|
|||||||
expect(action).toBeDefined();
|
expect(action).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("doesn't export non-creatable objects in tree", function () {
|
xit("doesn't export non-creatable objects in tree", function () {
|
||||||
var nonCreatableType = {
|
var nonCreatableType = {
|
||||||
hasFeature:
|
hasFeature:
|
||||||
function (feature) {
|
function (feature) {
|
||||||
@ -149,7 +149,7 @@ define(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("can export self-containing objects", function () {
|
xit("can export self-containing objects", function () {
|
||||||
var parent = domainObjectFactory({
|
var parent = domainObjectFactory({
|
||||||
name: 'parent',
|
name: 'parent',
|
||||||
model: {
|
model: {
|
||||||
@ -191,7 +191,7 @@ define(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("exports links to external objects as new objects", function () {
|
xit("exports links to external objects as new objects", function () {
|
||||||
var parent = domainObjectFactory({
|
var parent = domainObjectFactory({
|
||||||
name: 'parent',
|
name: 'parent',
|
||||||
model: {
|
model: {
|
||||||
|
@ -27,7 +27,7 @@ define(
|
|||||||
],
|
],
|
||||||
function (ImportAsJSONAction, domainObjectFactory) {
|
function (ImportAsJSONAction, domainObjectFactory) {
|
||||||
|
|
||||||
xdescribe("The import JSON action", function () {
|
describe("The import JSON action", function () {
|
||||||
|
|
||||||
var context = {};
|
var context = {};
|
||||||
var action,
|
var action,
|
||||||
@ -146,7 +146,7 @@ define(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("can import self-containing objects", function () {
|
xit("can import self-containing objects", function () {
|
||||||
var compDomainObject = domainObjectFactory({
|
var compDomainObject = domainObjectFactory({
|
||||||
name: 'compObject',
|
name: 'compObject',
|
||||||
model: { name: 'compObject'},
|
model: { name: 'compObject'},
|
||||||
@ -198,7 +198,7 @@ define(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("assigns new ids to each imported object", function () {
|
xit("assigns new ids to each imported object", function () {
|
||||||
dialogService.getUserInput.and.returnValue(Promise.resolve(
|
dialogService.getUserInput.and.returnValue(Promise.resolve(
|
||||||
{
|
{
|
||||||
selectFile: {
|
selectFile: {
|
||||||
|
@ -136,7 +136,7 @@ define([
|
|||||||
* @memberof module:openmct.MCT#
|
* @memberof module:openmct.MCT#
|
||||||
* @name conductor
|
* @name conductor
|
||||||
*/
|
*/
|
||||||
this.time = new api.TimeAPI();
|
this.time = new api.TimeAPI(this);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An interface for interacting with the composition of domain objects.
|
* An interface for interacting with the composition of domain objects.
|
||||||
@ -288,6 +288,8 @@ define([
|
|||||||
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());
|
this.install(this.plugins.DeviceClassifier());
|
||||||
|
|
||||||
|
this._isVue = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
MCT.prototype = Object.create(EventEmitter.prototype);
|
MCT.prototype = Object.create(EventEmitter.prototype);
|
||||||
|
@ -81,14 +81,8 @@ define([
|
|||||||
return models;
|
return models;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.apiFetch(missingIds)
|
//Temporary fix for missing models - don't retry using this.apiFetch
|
||||||
.then(function (apiResults) {
|
return models;
|
||||||
Object.keys(apiResults).forEach(function (k) {
|
|
||||||
models[k] = apiResults[k];
|
|
||||||
});
|
|
||||||
|
|
||||||
return models;
|
|
||||||
});
|
|
||||||
}.bind(this));
|
}.bind(this));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -110,7 +110,7 @@ class ActionsAPI extends EventEmitter {
|
|||||||
return actionsObject;
|
return actionsObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
_groupAndSortActions(actionsArray) {
|
_groupAndSortActions(actionsArray = []) {
|
||||||
if (!Array.isArray(actionsArray) && typeof actionsArray === 'object') {
|
if (!Array.isArray(actionsArray) && typeof actionsArray === 'object') {
|
||||||
actionsArray = Object.keys(actionsArray).map(key => actionsArray[key]);
|
actionsArray = Object.keys(actionsArray).map(key => actionsArray[key]);
|
||||||
}
|
}
|
||||||
|
@ -46,7 +46,7 @@ define([
|
|||||||
StatusAPI
|
StatusAPI
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
TimeAPI: TimeAPI,
|
TimeAPI: TimeAPI.default,
|
||||||
ObjectAPI: ObjectAPI,
|
ObjectAPI: ObjectAPI,
|
||||||
CompositionAPI: CompositionAPI,
|
CompositionAPI: CompositionAPI,
|
||||||
TypeRegistry: TypeRegistry,
|
TypeRegistry: TypeRegistry,
|
||||||
|
2
src/api/objects/ConflictError.js
Normal file
2
src/api/objects/ConflictError.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export default class ConflictError extends Error {
|
||||||
|
}
|
@ -26,6 +26,7 @@ import RootRegistry from './RootRegistry';
|
|||||||
import RootObjectProvider from './RootObjectProvider';
|
import RootObjectProvider from './RootObjectProvider';
|
||||||
import EventEmitter from 'EventEmitter';
|
import EventEmitter from 'EventEmitter';
|
||||||
import InterceptorRegistry from './InterceptorRegistry';
|
import InterceptorRegistry from './InterceptorRegistry';
|
||||||
|
import ConflictError from './ConflictError';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utilities for loading, saving, and manipulating domain objects.
|
* Utilities for loading, saving, and manipulating domain objects.
|
||||||
@ -34,6 +35,7 @@ import InterceptorRegistry from './InterceptorRegistry';
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
function ObjectAPI(typeRegistry, openmct) {
|
function ObjectAPI(typeRegistry, openmct) {
|
||||||
|
this.openmct = openmct;
|
||||||
this.typeRegistry = typeRegistry;
|
this.typeRegistry = typeRegistry;
|
||||||
this.eventEmitter = new EventEmitter();
|
this.eventEmitter = new EventEmitter();
|
||||||
this.providers = {};
|
this.providers = {};
|
||||||
@ -47,6 +49,10 @@ function ObjectAPI(typeRegistry, openmct) {
|
|||||||
this.interceptorRegistry = new InterceptorRegistry();
|
this.interceptorRegistry = new InterceptorRegistry();
|
||||||
|
|
||||||
this.SYNCHRONIZED_OBJECT_TYPES = ['notebook', 'plan'];
|
this.SYNCHRONIZED_OBJECT_TYPES = ['notebook', 'plan'];
|
||||||
|
|
||||||
|
this.errors = {
|
||||||
|
Conflict: ConflictError
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -181,8 +187,17 @@ ObjectAPI.prototype.get = function (identifier, abortSignal) {
|
|||||||
|
|
||||||
let objectPromise = provider.get(identifier, abortSignal).then(result => {
|
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);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}).catch((result) => {
|
||||||
|
console.warn(`Failed to retrieve ${keystring}:`, result);
|
||||||
|
|
||||||
|
delete this.cache[keystring];
|
||||||
|
|
||||||
|
result = this.applyGetInterceptors(identifier);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -285,6 +300,7 @@ ObjectAPI.prototype.isPersistable = function (idOrKeyString) {
|
|||||||
ObjectAPI.prototype.save = function (domainObject) {
|
ObjectAPI.prototype.save = function (domainObject) {
|
||||||
let provider = this.getProvider(domainObject.identifier);
|
let provider = this.getProvider(domainObject.identifier);
|
||||||
let savedResolve;
|
let savedResolve;
|
||||||
|
let savedReject;
|
||||||
let result;
|
let result;
|
||||||
|
|
||||||
if (!this.isPersistable(domainObject.identifier)) {
|
if (!this.isPersistable(domainObject.identifier)) {
|
||||||
@ -294,14 +310,18 @@ ObjectAPI.prototype.save = function (domainObject) {
|
|||||||
} else {
|
} else {
|
||||||
const persistedTime = Date.now();
|
const persistedTime = Date.now();
|
||||||
if (domainObject.persisted === undefined) {
|
if (domainObject.persisted === undefined) {
|
||||||
result = new Promise((resolve) => {
|
result = new Promise((resolve, reject) => {
|
||||||
savedResolve = resolve;
|
savedResolve = resolve;
|
||||||
|
savedReject = reject;
|
||||||
});
|
});
|
||||||
domainObject.persisted = persistedTime;
|
domainObject.persisted = persistedTime;
|
||||||
provider.create(domainObject).then((response) => {
|
provider.create(domainObject)
|
||||||
this.mutate(domainObject, 'persisted', persistedTime);
|
.then((response) => {
|
||||||
savedResolve(response);
|
this.mutate(domainObject, 'persisted', persistedTime);
|
||||||
});
|
savedResolve(response);
|
||||||
|
}).catch((error) => {
|
||||||
|
savedReject(error);
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
domainObject.persisted = persistedTime;
|
domainObject.persisted = persistedTime;
|
||||||
this.mutate(domainObject, 'persisted', persistedTime);
|
this.mutate(domainObject, 'persisted', persistedTime);
|
||||||
@ -358,6 +378,20 @@ ObjectAPI.prototype.applyGetInterceptors = function (identifier, domainObject) {
|
|||||||
return domainObject;
|
return domainObject;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return relative url path from a given object path
|
||||||
|
* eg: #/browse/mine/cb56f6bf-c900-43b7-b923-2e3b64b412db/6e89e858-77ce-46e4-a1ad-749240286497/....
|
||||||
|
* @param {Array} objectPath
|
||||||
|
* @returns {string} relative url for object
|
||||||
|
*/
|
||||||
|
ObjectAPI.prototype.getRelativePath = function (objectPath) {
|
||||||
|
return objectPath
|
||||||
|
.map(p => this.makeKeyString(p.identifier))
|
||||||
|
.reverse()
|
||||||
|
.join('/')
|
||||||
|
;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Modify a domain object.
|
* Modify a domain object.
|
||||||
* @param {module:openmct.DomainObject} object the object to mutate
|
* @param {module:openmct.DomainObject} object the object to mutate
|
||||||
|
@ -10,28 +10,37 @@ const cssClasses = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
class Overlay extends EventEmitter {
|
class Overlay extends EventEmitter {
|
||||||
constructor(options) {
|
constructor({
|
||||||
|
buttons,
|
||||||
|
autoHide = true,
|
||||||
|
dismissable = true,
|
||||||
|
element,
|
||||||
|
onDestroy,
|
||||||
|
size
|
||||||
|
} = {}) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.dismissable = options.dismissable !== false;
|
|
||||||
this.container = document.createElement('div');
|
this.container = document.createElement('div');
|
||||||
this.container.classList.add('l-overlay-wrapper', cssClasses[options.size]);
|
this.container.classList.add('l-overlay-wrapper', cssClasses[size]);
|
||||||
|
|
||||||
|
this.autoHide = autoHide;
|
||||||
|
this.dismissable = dismissable !== false;
|
||||||
|
|
||||||
this.component = new Vue({
|
this.component = new Vue({
|
||||||
provide: {
|
|
||||||
dismiss: this.dismiss.bind(this),
|
|
||||||
element: options.element,
|
|
||||||
buttons: options.buttons,
|
|
||||||
dismissable: this.dismissable
|
|
||||||
},
|
|
||||||
components: {
|
components: {
|
||||||
OverlayComponent: OverlayComponent
|
OverlayComponent: OverlayComponent
|
||||||
},
|
},
|
||||||
|
provide: {
|
||||||
|
dismiss: this.dismiss.bind(this),
|
||||||
|
element,
|
||||||
|
buttons,
|
||||||
|
dismissable: this.dismissable
|
||||||
|
},
|
||||||
template: '<overlay-component></overlay-component>'
|
template: '<overlay-component></overlay-component>'
|
||||||
});
|
});
|
||||||
|
|
||||||
if (options.onDestroy) {
|
if (onDestroy) {
|
||||||
this.once('destroy', options.onDestroy);
|
this.once('destroy', onDestroy);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,7 +30,10 @@ class OverlayAPI {
|
|||||||
*/
|
*/
|
||||||
showOverlay(overlay) {
|
showOverlay(overlay) {
|
||||||
if (this.activeOverlays.length) {
|
if (this.activeOverlays.length) {
|
||||||
this.activeOverlays[this.activeOverlays.length - 1].container.classList.add('invisible');
|
const previousOverlay = this.activeOverlays[this.activeOverlays.length - 1];
|
||||||
|
if (previousOverlay.autoHide) {
|
||||||
|
previousOverlay.container.classList.add('invisible');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.activeOverlays.push(overlay);
|
this.activeOverlays.push(overlay);
|
||||||
|
@ -483,6 +483,10 @@ define([
|
|||||||
* @returns {Object<String, {TelemetryValueFormatter}>}
|
* @returns {Object<String, {TelemetryValueFormatter}>}
|
||||||
*/
|
*/
|
||||||
TelemetryAPI.prototype.getFormatMap = function (metadata) {
|
TelemetryAPI.prototype.getFormatMap = function (metadata) {
|
||||||
|
if (!metadata) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.formatMapCache.has(metadata)) {
|
if (!this.formatMapCache.has(metadata)) {
|
||||||
const formatMap = metadata.values().reduce(function (map, valueMetadata) {
|
const formatMap = metadata.values().reduce(function (map, valueMetadata) {
|
||||||
map[valueMetadata.key] = this.getValueFormatter(valueMetadata);
|
map[valueMetadata.key] = this.getValueFormatter(valueMetadata);
|
||||||
|
@ -115,6 +115,7 @@ export class TelemetryCollection extends EventEmitter {
|
|||||||
|
|
||||||
this._requestHistoricalTelemetry();
|
this._requestHistoricalTelemetry();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If a historical provider exists, then historical requests will be made
|
* If a historical provider exists, then historical requests will be made
|
||||||
* @private
|
* @private
|
||||||
@ -126,20 +127,31 @@ export class TelemetryCollection extends EventEmitter {
|
|||||||
|
|
||||||
let historicalData;
|
let historicalData;
|
||||||
|
|
||||||
|
this.options.onPartialResponse = this._processNewTelemetry.bind(this);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (this.requestAbort) {
|
||||||
|
this.requestAbort.abort();
|
||||||
|
}
|
||||||
|
|
||||||
this.requestAbort = new AbortController();
|
this.requestAbort = new AbortController();
|
||||||
this.options.signal = this.requestAbort.signal;
|
this.options.signal = this.requestAbort.signal;
|
||||||
|
this.emit('requestStarted');
|
||||||
historicalData = await this.historicalProvider.request(this.domainObject, this.options);
|
historicalData = await this.historicalProvider.request(this.domainObject, this.options);
|
||||||
this.requestAbort = undefined;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error requesting telemetry data...');
|
if (error.name !== 'AbortError') {
|
||||||
this.requestAbort = undefined;
|
console.error('Error requesting telemetry data...');
|
||||||
this._error(error);
|
this._error(error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.emit('requestEnded');
|
||||||
|
this.requestAbort = undefined;
|
||||||
|
|
||||||
this._processNewTelemetry(historicalData);
|
this._processNewTelemetry(historicalData);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This uses the built in subscription function from Telemetry API
|
* This uses the built in subscription function from Telemetry API
|
||||||
* @private
|
* @private
|
||||||
@ -342,6 +354,8 @@ export class TelemetryCollection extends EventEmitter {
|
|||||||
this.boundedTelemetry = [];
|
this.boundedTelemetry = [];
|
||||||
this.futureBuffer = [];
|
this.futureBuffer = [];
|
||||||
|
|
||||||
|
this.emit('clear');
|
||||||
|
|
||||||
this._requestHistoricalTelemetry();
|
this._requestHistoricalTelemetry();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
106
src/api/time/GlobalTimeContext.js
Normal file
106
src/api/time/GlobalTimeContext.js
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* Open MCT includes source code licensed under additional open source
|
||||||
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
* this source code distribution or the Licensing information page available
|
||||||
|
* at runtime from the About dialog for additional information.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
import TimeContext from "./TimeContext";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The GlobalContext handles getting and setting time of the openmct application in general.
|
||||||
|
* Views will use this context unless they specify an alternate/independent time context
|
||||||
|
*/
|
||||||
|
class GlobalTimeContext extends TimeContext {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
//The Time Of Interest
|
||||||
|
this.toi = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get or set the start and end time of the time conductor. Basic validation
|
||||||
|
* of bounds is performed.
|
||||||
|
*
|
||||||
|
* @param {module:openmct.TimeAPI~TimeConductorBounds} newBounds
|
||||||
|
* @throws {Error} Validation error
|
||||||
|
* @fires module:openmct.TimeAPI~bounds
|
||||||
|
* @returns {module:openmct.TimeAPI~TimeConductorBounds}
|
||||||
|
* @memberof module:openmct.TimeAPI#
|
||||||
|
* @method bounds
|
||||||
|
*/
|
||||||
|
bounds(newBounds) {
|
||||||
|
if (arguments.length > 0) {
|
||||||
|
super.bounds.call(this, ...arguments);
|
||||||
|
// If a bounds change results in a TOI outside of the current
|
||||||
|
// bounds, unset it
|
||||||
|
if (this.toi < newBounds.start || this.toi > newBounds.end) {
|
||||||
|
this.timeOfInterest(undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Return a copy to prevent direct mutation of time conductor bounds.
|
||||||
|
return JSON.parse(JSON.stringify(this.boundsVal));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update bounds based on provided time and current offsets
|
||||||
|
* @private
|
||||||
|
* @param {number} timestamp A time from which bounds will be calculated
|
||||||
|
* using current offsets.
|
||||||
|
*/
|
||||||
|
tick(timestamp) {
|
||||||
|
super.tick.call(this, ...arguments);
|
||||||
|
|
||||||
|
// If a bounds change results in a TOI outside of the current
|
||||||
|
// bounds, unset it
|
||||||
|
if (this.toi < this.boundsVal.start || this.toi > this.boundsVal.end) {
|
||||||
|
this.timeOfInterest(undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get or set the Time of Interest. The Time of Interest is a single point
|
||||||
|
* in time, and constitutes the temporal focus of application views. It can
|
||||||
|
* be manipulated by the user from the time conductor or from other views.
|
||||||
|
* The time of interest can effectively be unset by assigning a value of
|
||||||
|
* 'undefined'.
|
||||||
|
* @fires module:openmct.TimeAPI~timeOfInterest
|
||||||
|
* @param newTOI
|
||||||
|
* @returns {number} the current time of interest
|
||||||
|
* @memberof module:openmct.TimeAPI#
|
||||||
|
* @method timeOfInterest
|
||||||
|
*/
|
||||||
|
timeOfInterest(newTOI) {
|
||||||
|
if (arguments.length > 0) {
|
||||||
|
this.toi = newTOI;
|
||||||
|
/**
|
||||||
|
* The Time of Interest has moved.
|
||||||
|
* @event timeOfInterest
|
||||||
|
* @memberof module:openmct.TimeAPI~
|
||||||
|
* @property {number} Current time of interest
|
||||||
|
*/
|
||||||
|
this.emit('timeOfInterest', this.toi);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.toi;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GlobalTimeContext;
|
94
src/api/time/IndependentTimeContext.js
Normal file
94
src/api/time/IndependentTimeContext.js
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* Open MCT includes source code licensed under additional open source
|
||||||
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
* this source code distribution or the Licensing information page available
|
||||||
|
* at runtime from the About dialog for additional information.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
import TimeContext from "./TimeContext";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The IndependentTimeContext handles getting and setting time of the openmct application in general.
|
||||||
|
* Views will use the GlobalTimeContext unless they specify an alternate/independent time context here.
|
||||||
|
*/
|
||||||
|
class IndependentTimeContext extends TimeContext {
|
||||||
|
constructor(globalTimeContext, key) {
|
||||||
|
super();
|
||||||
|
this.key = key;
|
||||||
|
|
||||||
|
this.globalTimeContext = globalTimeContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the active clock. Tick source will be immediately subscribed to
|
||||||
|
* and ticking will begin. Offsets from 'now' must also be provided. A clock
|
||||||
|
* can be unset by calling {@link stopClock}.
|
||||||
|
*
|
||||||
|
* @param {Clock || string} keyOrClock The clock to activate, or its key
|
||||||
|
* @param {ClockOffsets} offsets on each tick these will be used to calculate
|
||||||
|
* the start and end bounds. This maintains a sliding time window of a fixed
|
||||||
|
* width that automatically updates.
|
||||||
|
* @fires module:openmct.TimeAPI~clock
|
||||||
|
* @return {Clock} the currently active clock;
|
||||||
|
*/
|
||||||
|
clock(keyOrClock, offsets) {
|
||||||
|
if (arguments.length === 2) {
|
||||||
|
let clock;
|
||||||
|
|
||||||
|
if (typeof keyOrClock === 'string') {
|
||||||
|
clock = this.globalTimeContext.clocks.get(keyOrClock);
|
||||||
|
if (clock === undefined) {
|
||||||
|
throw "Unknown clock '" + keyOrClock + "'. Has it been registered with 'addClock'?";
|
||||||
|
}
|
||||||
|
} else if (typeof keyOrClock === 'object') {
|
||||||
|
clock = keyOrClock;
|
||||||
|
if (!this.globalTimeContext.clocks.has(clock.key)) {
|
||||||
|
throw "Unknown clock '" + keyOrClock.key + "'. Has it been registered with 'addClock'?";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const previousClock = this.activeClock;
|
||||||
|
if (previousClock !== undefined) {
|
||||||
|
previousClock.off("tick", this.tick);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.activeClock = clock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The active clock has changed. Clock can be unset by calling {@link stopClock}
|
||||||
|
* @event clock
|
||||||
|
* @memberof module:openmct.TimeAPI~
|
||||||
|
* @property {Clock} clock The newly activated clock, or undefined
|
||||||
|
* if the system is no longer following a clock source
|
||||||
|
*/
|
||||||
|
this.emit("clock", this.activeClock);
|
||||||
|
|
||||||
|
if (this.activeClock !== undefined) {
|
||||||
|
this.clockOffsets(offsets);
|
||||||
|
this.activeClock.on("tick", this.tick);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (arguments.length === 1) {
|
||||||
|
throw "When setting the clock, clock offsets must also be provided";
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.activeClock;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IndependentTimeContext;
|
@ -20,51 +20,35 @@
|
|||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
define(['EventEmitter'], function (EventEmitter) {
|
import GlobalTimeContext from "./GlobalTimeContext";
|
||||||
|
import IndependentTimeContext from "@/api/time/IndependentTimeContext";
|
||||||
/**
|
|
||||||
* The public API for setting and querying the temporal state of the
|
|
||||||
* application. The concept of time is integral to Open MCT, and at least
|
|
||||||
* one {@link TimeSystem}, as well as some default time bounds must be
|
|
||||||
* registered and enabled via {@link TimeAPI.addTimeSystem} and
|
|
||||||
* {@link TimeAPI.timeSystem} respectively for Open MCT to work.
|
|
||||||
*
|
|
||||||
* Time-sensitive views will typically respond to changes to bounds or other
|
|
||||||
* properties of the time conductor and update the data displayed based on
|
|
||||||
* the temporal state of the application. The current time bounds are also
|
|
||||||
* used in queries for historical data.
|
|
||||||
*
|
|
||||||
* The TimeAPI extends the EventEmitter class. A number of events are
|
|
||||||
* fired when properties of the time conductor change, which are documented
|
|
||||||
* below.
|
|
||||||
*
|
|
||||||
* @interface
|
|
||||||
* @memberof module:openmct
|
|
||||||
*/
|
|
||||||
function TimeAPI() {
|
|
||||||
EventEmitter.call(this);
|
|
||||||
|
|
||||||
//The Time System
|
|
||||||
this.system = undefined;
|
|
||||||
//The Time Of Interest
|
|
||||||
this.toi = undefined;
|
|
||||||
|
|
||||||
this.boundsVal = {
|
|
||||||
start: undefined,
|
|
||||||
end: undefined
|
|
||||||
};
|
|
||||||
|
|
||||||
this.timeSystems = new Map();
|
|
||||||
this.clocks = new Map();
|
|
||||||
this.activeClock = undefined;
|
|
||||||
this.offsets = undefined;
|
|
||||||
|
|
||||||
this.tick = this.tick.bind(this);
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The public API for setting and querying the temporal state of the
|
||||||
|
* application. The concept of time is integral to Open MCT, and at least
|
||||||
|
* one {@link TimeSystem}, as well as some default time bounds must be
|
||||||
|
* registered and enabled via {@link TimeAPI.addTimeSystem} and
|
||||||
|
* {@link TimeAPI.timeSystem} respectively for Open MCT to work.
|
||||||
|
*
|
||||||
|
* Time-sensitive views will typically respond to changes to bounds or other
|
||||||
|
* properties of the time conductor and update the data displayed based on
|
||||||
|
* the temporal state of the application. The current time bounds are also
|
||||||
|
* used in queries for historical data.
|
||||||
|
*
|
||||||
|
* The TimeAPI extends the GlobalTimeContext which in turn extends the TimeContext/EventEmitter class. A number of events are
|
||||||
|
* fired when properties of the time conductor change, which are documented
|
||||||
|
* below.
|
||||||
|
*
|
||||||
|
* @interface
|
||||||
|
* @memberof module:openmct
|
||||||
|
*/
|
||||||
|
class TimeAPI extends GlobalTimeContext {
|
||||||
|
constructor(openmct) {
|
||||||
|
super();
|
||||||
|
this.openmct = openmct;
|
||||||
|
this.independentContexts = new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
TimeAPI.prototype = Object.create(EventEmitter.prototype);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A TimeSystem provides meaning to the values returned by the TimeAPI. Open
|
* A TimeSystem provides meaning to the values returned by the TimeAPI. Open
|
||||||
* MCT supports multiple different types of time values, although all are
|
* MCT supports multiple different types of time values, although all are
|
||||||
@ -94,16 +78,16 @@ define(['EventEmitter'], function (EventEmitter) {
|
|||||||
* @memberof module:openmct.TimeAPI#
|
* @memberof module:openmct.TimeAPI#
|
||||||
* @param {TimeSystem} timeSystem A time system object.
|
* @param {TimeSystem} timeSystem A time system object.
|
||||||
*/
|
*/
|
||||||
TimeAPI.prototype.addTimeSystem = function (timeSystem) {
|
addTimeSystem(timeSystem) {
|
||||||
this.timeSystems.set(timeSystem.key, timeSystem);
|
this.timeSystems.set(timeSystem.key, timeSystem);
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {TimeSystem[]}
|
* @returns {TimeSystem[]}
|
||||||
*/
|
*/
|
||||||
TimeAPI.prototype.getAllTimeSystems = function () {
|
getAllTimeSystems() {
|
||||||
return Array.from(this.timeSystems.values());
|
return Array.from(this.timeSystems.values());
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clocks provide a timing source that is used to
|
* Clocks provide a timing source that is used to
|
||||||
@ -126,340 +110,81 @@ define(['EventEmitter'], function (EventEmitter) {
|
|||||||
* @memberof module:openmct.TimeAPI#
|
* @memberof module:openmct.TimeAPI#
|
||||||
* @param {Clock} clock
|
* @param {Clock} clock
|
||||||
*/
|
*/
|
||||||
TimeAPI.prototype.addClock = function (clock) {
|
addClock(clock) {
|
||||||
this.clocks.set(clock.key, clock);
|
this.clocks.set(clock.key, clock);
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @memberof module:openmct.TimeAPI#
|
* @memberof module:openmct.TimeAPI#
|
||||||
* @returns {Clock[]}
|
* @returns {Clock[]}
|
||||||
* @memberof module:openmct.TimeAPI#
|
* @memberof module:openmct.TimeAPI#
|
||||||
*/
|
*/
|
||||||
TimeAPI.prototype.getAllClocks = function () {
|
getAllClocks() {
|
||||||
return Array.from(this.clocks.values());
|
return Array.from(this.clocks.values());
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate the given bounds. This can be used for pre-validation of bounds,
|
* Get or set an independent time context which follows the TimeAPI timeSystem,
|
||||||
* for example by views validating user inputs.
|
* but with different offsets for a given domain object
|
||||||
* @param {TimeBounds} bounds The start and end time of the conductor.
|
* @param {key | string} key The identifier key of the domain object these offsets are set for
|
||||||
* @returns {string | true} A validation error, or true if valid
|
* @param {ClockOffsets | TimeBounds} value This maintains a sliding time window of a fixed width that automatically updates
|
||||||
|
* @param {key | string} clockKey the real time clock key currently in use
|
||||||
* @memberof module:openmct.TimeAPI#
|
* @memberof module:openmct.TimeAPI#
|
||||||
* @method validateBounds
|
* @method addIndependentTimeContext
|
||||||
*/
|
*/
|
||||||
TimeAPI.prototype.validateBounds = function (bounds) {
|
addIndependentContext(key, value, clockKey) {
|
||||||
if ((bounds.start === undefined)
|
let timeContext = this.independentContexts.get(key);
|
||||||
|| (bounds.end === undefined)
|
if (!timeContext) {
|
||||||
|| isNaN(bounds.start)
|
timeContext = new IndependentTimeContext(this, key);
|
||||||
|| isNaN(bounds.end)
|
this.independentContexts.set(key, timeContext);
|
||||||
) {
|
|
||||||
return "Start and end must be specified as integer values";
|
|
||||||
} else if (bounds.start > bounds.end) {
|
|
||||||
return "Specified start date exceeds end bound";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
if (clockKey) {
|
||||||
};
|
timeContext.clock(clockKey, value);
|
||||||
|
} else {
|
||||||
/**
|
timeContext.stopClock();
|
||||||
* Validate the given offsets. This can be used for pre-validation of
|
timeContext.bounds(value);
|
||||||
* offsets, for example by views validating user inputs.
|
|
||||||
* @param {ClockOffsets} offsets The start and end offsets from a 'now' value.
|
|
||||||
* @returns {string | true} A validation error, or true if valid
|
|
||||||
* @memberof module:openmct.TimeAPI#
|
|
||||||
* @method validateBounds
|
|
||||||
*/
|
|
||||||
TimeAPI.prototype.validateOffsets = function (offsets) {
|
|
||||||
if ((offsets.start === undefined)
|
|
||||||
|| (offsets.end === undefined)
|
|
||||||
|| isNaN(offsets.start)
|
|
||||||
|| isNaN(offsets.end)
|
|
||||||
) {
|
|
||||||
return "Start and end offsets must be specified as integer values";
|
|
||||||
} else if (offsets.start >= offsets.end) {
|
|
||||||
return "Specified start offset must be < end offset";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
this.emit('timeContext', key);
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
return () => {
|
||||||
* @typedef {Object} TimeBounds
|
this.independentContexts.delete(key);
|
||||||
* @property {number} start The start time displayed by the time conductor
|
timeContext.emit('timeContext', key);
|
||||||
* in ms since epoch. Epoch determined by currently active time system
|
|
||||||
* @property {number} end The end time displayed by the time conductor in ms
|
|
||||||
* since epoch.
|
|
||||||
* @memberof module:openmct.TimeAPI~
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get or set the start and end time of the time conductor. Basic validation
|
|
||||||
* of bounds is performed.
|
|
||||||
*
|
|
||||||
* @param {module:openmct.TimeAPI~TimeConductorBounds} newBounds
|
|
||||||
* @throws {Error} Validation error
|
|
||||||
* @fires module:openmct.TimeAPI~bounds
|
|
||||||
* @returns {module:openmct.TimeAPI~TimeConductorBounds}
|
|
||||||
* @memberof module:openmct.TimeAPI#
|
|
||||||
* @method bounds
|
|
||||||
*/
|
|
||||||
TimeAPI.prototype.bounds = function (newBounds) {
|
|
||||||
if (arguments.length > 0) {
|
|
||||||
const validationResult = this.validateBounds(newBounds);
|
|
||||||
if (validationResult !== true) {
|
|
||||||
throw new Error(validationResult);
|
|
||||||
}
|
|
||||||
|
|
||||||
//Create a copy to avoid direct mutation of conductor bounds
|
|
||||||
this.boundsVal = JSON.parse(JSON.stringify(newBounds));
|
|
||||||
/**
|
|
||||||
* The start time, end time, or both have been updated.
|
|
||||||
* @event bounds
|
|
||||||
* @memberof module:openmct.TimeAPI~
|
|
||||||
* @property {TimeConductorBounds} bounds The newly updated bounds
|
|
||||||
* @property {boolean} [tick] `true` if the bounds update was due to
|
|
||||||
* a "tick" event (ie. was an automatic update), false otherwise.
|
|
||||||
*/
|
|
||||||
this.emit('bounds', this.boundsVal, false);
|
|
||||||
|
|
||||||
// If a bounds change results in a TOI outside of the current
|
|
||||||
// bounds, unset it
|
|
||||||
if (this.toi < newBounds.start || this.toi > newBounds.end) {
|
|
||||||
this.timeOfInterest(undefined);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//Return a copy to prevent direct mutation of time conductor bounds.
|
|
||||||
return JSON.parse(JSON.stringify(this.boundsVal));
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get or set the time system of the TimeAPI.
|
|
||||||
* @param {TimeSystem | string} timeSystem
|
|
||||||
* @param {module:openmct.TimeAPI~TimeConductorBounds} bounds
|
|
||||||
* @fires module:openmct.TimeAPI~timeSystem
|
|
||||||
* @returns {TimeSystem} The currently applied time system
|
|
||||||
* @memberof module:openmct.TimeAPI#
|
|
||||||
* @method timeSystem
|
|
||||||
*/
|
|
||||||
TimeAPI.prototype.timeSystem = function (timeSystemOrKey, bounds) {
|
|
||||||
if (arguments.length >= 1) {
|
|
||||||
if (arguments.length === 1 && !this.activeClock) {
|
|
||||||
throw new Error(
|
|
||||||
"Must specify bounds when changing time system without "
|
|
||||||
+ "an active clock."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let timeSystem;
|
|
||||||
|
|
||||||
if (timeSystemOrKey === undefined) {
|
|
||||||
throw "Please provide a time system";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof timeSystemOrKey === 'string') {
|
|
||||||
timeSystem = this.timeSystems.get(timeSystemOrKey);
|
|
||||||
|
|
||||||
if (timeSystem === undefined) {
|
|
||||||
throw "Unknown time system " + timeSystemOrKey + ". Has it been registered with 'addTimeSystem'?";
|
|
||||||
}
|
|
||||||
} else if (typeof timeSystemOrKey === 'object') {
|
|
||||||
timeSystem = timeSystemOrKey;
|
|
||||||
|
|
||||||
if (!this.timeSystems.has(timeSystem.key)) {
|
|
||||||
throw "Unknown time system " + timeSystem.key + ". Has it been registered with 'addTimeSystem'?";
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw "Attempt to set invalid time system in Time API. Please provide a previously registered time system object or key";
|
|
||||||
}
|
|
||||||
|
|
||||||
this.system = timeSystem;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The time system used by the time
|
|
||||||
* conductor has changed. A change in Time System will always be
|
|
||||||
* followed by a bounds event specifying new query bounds.
|
|
||||||
*
|
|
||||||
* @event module:openmct.TimeAPI~timeSystem
|
|
||||||
* @property {TimeSystem} The value of the currently applied
|
|
||||||
* Time System
|
|
||||||
* */
|
|
||||||
this.emit('timeSystem', this.system);
|
|
||||||
if (bounds) {
|
|
||||||
this.bounds(bounds);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.system;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get or set the Time of Interest. The Time of Interest is a single point
|
|
||||||
* in time, and constitutes the temporal focus of application views. It can
|
|
||||||
* be manipulated by the user from the time conductor or from other views.
|
|
||||||
* The time of interest can effectively be unset by assigning a value of
|
|
||||||
* 'undefined'.
|
|
||||||
* @fires module:openmct.TimeAPI~timeOfInterest
|
|
||||||
* @param newTOI
|
|
||||||
* @returns {number} the current time of interest
|
|
||||||
* @memberof module:openmct.TimeAPI#
|
|
||||||
* @method timeOfInterest
|
|
||||||
*/
|
|
||||||
TimeAPI.prototype.timeOfInterest = function (newTOI) {
|
|
||||||
if (arguments.length > 0) {
|
|
||||||
this.toi = newTOI;
|
|
||||||
/**
|
|
||||||
* The Time of Interest has moved.
|
|
||||||
* @event timeOfInterest
|
|
||||||
* @memberof module:openmct.TimeAPI~
|
|
||||||
* @property {number} Current time of interest
|
|
||||||
*/
|
|
||||||
this.emit('timeOfInterest', this.toi);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.toi;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update bounds based on provided time and current offsets
|
|
||||||
* @private
|
|
||||||
* @param {number} timestamp A time from which boudns will be calculated
|
|
||||||
* using current offsets.
|
|
||||||
*/
|
|
||||||
TimeAPI.prototype.tick = function (timestamp) {
|
|
||||||
const newBounds = {
|
|
||||||
start: timestamp + this.offsets.start,
|
|
||||||
end: timestamp + this.offsets.end
|
|
||||||
};
|
};
|
||||||
|
}
|
||||||
this.boundsVal = newBounds;
|
|
||||||
this.emit('bounds', this.boundsVal, true);
|
|
||||||
|
|
||||||
// If a bounds change results in a TOI outside of the current
|
|
||||||
// bounds, unset it
|
|
||||||
if (this.toi < newBounds.start || this.toi > newBounds.end) {
|
|
||||||
this.timeOfInterest(undefined);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the active clock. Tick source will be immediately subscribed to
|
* Get the independent time context which follows the TimeAPI timeSystem,
|
||||||
* and ticking will begin. Offsets from 'now' must also be provided. A clock
|
* but with different offsets.
|
||||||
* can be unset by calling {@link stopClock}.
|
* @param {key | string} key The identifier key of the domain object these offsets
|
||||||
*
|
* @memberof module:openmct.TimeAPI#
|
||||||
* @param {Clock || string} The clock to activate, or its key
|
* @method getIndependentTimeContext
|
||||||
* @param {ClockOffsets} offsets on each tick these will be used to calculate
|
|
||||||
* the start and end bounds. This maintains a sliding time window of a fixed
|
|
||||||
* width that automatically updates.
|
|
||||||
* @fires module:openmct.TimeAPI~clock
|
|
||||||
* @return {Clock} the currently active clock;
|
|
||||||
*/
|
*/
|
||||||
TimeAPI.prototype.clock = function (keyOrClock, offsets) {
|
getIndependentContext(key) {
|
||||||
if (arguments.length === 2) {
|
return this.independentContexts.get(key);
|
||||||
let clock;
|
}
|
||||||
|
|
||||||
if (typeof keyOrClock === 'string') {
|
|
||||||
clock = this.clocks.get(keyOrClock);
|
|
||||||
if (clock === undefined) {
|
|
||||||
throw "Unknown clock '" + keyOrClock + "'. Has it been registered with 'addClock'?";
|
|
||||||
}
|
|
||||||
} else if (typeof keyOrClock === 'object') {
|
|
||||||
clock = keyOrClock;
|
|
||||||
if (!this.clocks.has(clock.key)) {
|
|
||||||
throw "Unknown clock '" + keyOrClock.key + "'. Has it been registered with 'addClock'?";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const previousClock = this.activeClock;
|
|
||||||
if (previousClock !== undefined) {
|
|
||||||
previousClock.off("tick", this.tick);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.activeClock = clock;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The active clock has changed. Clock can be unset by calling {@link stopClock}
|
|
||||||
* @event clock
|
|
||||||
* @memberof module:openmct.TimeAPI~
|
|
||||||
* @property {Clock} clock The newly activated clock, or undefined
|
|
||||||
* if the system is no longer following a clock source
|
|
||||||
*/
|
|
||||||
this.emit("clock", this.activeClock);
|
|
||||||
|
|
||||||
if (this.activeClock !== undefined) {
|
|
||||||
this.clockOffsets(offsets);
|
|
||||||
this.activeClock.on("tick", this.tick);
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if (arguments.length === 1) {
|
|
||||||
throw "When setting the clock, clock offsets must also be provided";
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.activeClock;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clock offsets are used to calculate temporal bounds when the system is
|
* Get the a timeContext for a view based on it's objectPath. If there is any object in the objectPath with an independent time context, it will be returned.
|
||||||
* ticking on a clock source.
|
* Otherwise, the global time context will be returned.
|
||||||
*
|
* @param { Array } objectPath The view's objectPath
|
||||||
* @typedef {object} ClockOffsets
|
* @memberof module:openmct.TimeAPI#
|
||||||
* @property {number} start A time span relative to the current value of the
|
* @method getContextForView
|
||||||
* ticking clock, from which start bounds will be calculated. This value must
|
|
||||||
* be < 0. When a clock is active, bounds will be calculated automatically
|
|
||||||
* based on the value provided by the clock, and the defined clock offsets.
|
|
||||||
* @property {number} end A time span relative to the current value of the
|
|
||||||
* ticking clock, from which end bounds will be calculated. This value must
|
|
||||||
* be >= 0.
|
|
||||||
*/
|
*/
|
||||||
/**
|
getContextForView(objectPath) {
|
||||||
* Get or set the currently applied clock offsets. If no parameter is provided,
|
let timeContext = this;
|
||||||
* the current value will be returned. If provided, the new value will be
|
|
||||||
* used as the new clock offsets.
|
|
||||||
* @param {ClockOffsets} offsets
|
|
||||||
* @returns {ClockOffsets}
|
|
||||||
*/
|
|
||||||
TimeAPI.prototype.clockOffsets = function (offsets) {
|
|
||||||
if (arguments.length > 0) {
|
|
||||||
|
|
||||||
const validationResult = this.validateOffsets(offsets);
|
objectPath.forEach(item => {
|
||||||
if (validationResult !== true) {
|
const key = this.openmct.objects.makeKeyString(item.identifier);
|
||||||
throw new Error(validationResult);
|
if (this.independentContexts.get(key)) {
|
||||||
|
timeContext = this.independentContexts.get(key);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.offsets = offsets;
|
return timeContext;
|
||||||
|
}
|
||||||
|
|
||||||
const currentValue = this.activeClock.currentValue();
|
}
|
||||||
const newBounds = {
|
|
||||||
start: currentValue + offsets.start,
|
|
||||||
end: currentValue + offsets.end
|
|
||||||
};
|
|
||||||
|
|
||||||
this.bounds(newBounds);
|
export default TimeAPI;
|
||||||
|
|
||||||
/**
|
|
||||||
* Event that is triggered when clock offsets change.
|
|
||||||
* @event clockOffsets
|
|
||||||
* @memberof module:openmct.TimeAPI~
|
|
||||||
* @property {ClockOffsets} clockOffsets The newly activated clock
|
|
||||||
* offsets.
|
|
||||||
*/
|
|
||||||
this.emit("clockOffsets", offsets);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.offsets;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stop the currently active clock from ticking, and unset it. This will
|
|
||||||
* revert all views to showing a static time frame defined by the current
|
|
||||||
* bounds.
|
|
||||||
*/
|
|
||||||
TimeAPI.prototype.stopClock = function () {
|
|
||||||
if (this.activeClock) {
|
|
||||||
this.clock(undefined, undefined);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return TimeAPI;
|
|
||||||
});
|
|
||||||
|
@ -19,241 +19,243 @@
|
|||||||
* this source code distribution or the Licensing information page available
|
* this source code distribution or the Licensing information page available
|
||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
import TimeAPI from "./TimeAPI";
|
||||||
|
import {createOpenMct} from "utils/testing";
|
||||||
|
|
||||||
define(['./TimeAPI'], function (TimeAPI) {
|
describe("The Time API", function () {
|
||||||
describe("The Time API", function () {
|
let api;
|
||||||
let api;
|
let timeSystemKey;
|
||||||
let timeSystemKey;
|
let timeSystem;
|
||||||
let timeSystem;
|
let clockKey;
|
||||||
let clockKey;
|
let clock;
|
||||||
let clock;
|
let bounds;
|
||||||
let bounds;
|
let eventListener;
|
||||||
let eventListener;
|
let toi;
|
||||||
let toi;
|
let openmct;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
openmct = createOpenMct();
|
||||||
|
api = new TimeAPI(openmct);
|
||||||
|
timeSystemKey = "timeSystemKey";
|
||||||
|
timeSystem = {key: timeSystemKey};
|
||||||
|
clockKey = "someClockKey";
|
||||||
|
clock = jasmine.createSpyObj("clock", [
|
||||||
|
"on",
|
||||||
|
"off",
|
||||||
|
"currentValue"
|
||||||
|
]);
|
||||||
|
clock.currentValue.and.returnValue(100);
|
||||||
|
clock.key = clockKey;
|
||||||
|
bounds = {
|
||||||
|
start: 0,
|
||||||
|
end: 1
|
||||||
|
};
|
||||||
|
eventListener = jasmine.createSpy("eventListener");
|
||||||
|
toi = 111;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Supports setting and querying of time of interest", function () {
|
||||||
|
expect(api.timeOfInterest()).not.toBe(toi);
|
||||||
|
api.timeOfInterest(toi);
|
||||||
|
expect(api.timeOfInterest()).toBe(toi);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Allows setting of valid bounds", function () {
|
||||||
|
bounds = {
|
||||||
|
start: 0,
|
||||||
|
end: 1
|
||||||
|
};
|
||||||
|
expect(api.bounds()).not.toBe(bounds);
|
||||||
|
expect(api.bounds.bind(api, bounds)).not.toThrow();
|
||||||
|
expect(api.bounds()).toEqual(bounds);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Disallows setting of invalid bounds", function () {
|
||||||
|
bounds = {
|
||||||
|
start: 1,
|
||||||
|
end: 0
|
||||||
|
};
|
||||||
|
expect(api.bounds()).not.toEqual(bounds);
|
||||||
|
expect(api.bounds.bind(api, bounds)).toThrow();
|
||||||
|
expect(api.bounds()).not.toEqual(bounds);
|
||||||
|
|
||||||
|
bounds = {start: 1};
|
||||||
|
expect(api.bounds()).not.toEqual(bounds);
|
||||||
|
expect(api.bounds.bind(api, bounds)).toThrow();
|
||||||
|
expect(api.bounds()).not.toEqual(bounds);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Allows setting of previously registered time system with bounds", function () {
|
||||||
|
api.addTimeSystem(timeSystem);
|
||||||
|
expect(api.timeSystem()).not.toBe(timeSystem);
|
||||||
|
expect(function () {
|
||||||
|
api.timeSystem(timeSystem, bounds);
|
||||||
|
}).not.toThrow();
|
||||||
|
expect(api.timeSystem()).toBe(timeSystem);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Disallows setting of time system without bounds", function () {
|
||||||
|
api.addTimeSystem(timeSystem);
|
||||||
|
expect(api.timeSystem()).not.toBe(timeSystem);
|
||||||
|
expect(function () {
|
||||||
|
api.timeSystem(timeSystemKey);
|
||||||
|
}).toThrow();
|
||||||
|
expect(api.timeSystem()).not.toBe(timeSystem);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows setting of timesystem without bounds with clock", function () {
|
||||||
|
api.addTimeSystem(timeSystem);
|
||||||
|
api.addClock(clock);
|
||||||
|
api.clock(clockKey, {
|
||||||
|
start: 0,
|
||||||
|
end: 1
|
||||||
|
});
|
||||||
|
expect(api.timeSystem()).not.toBe(timeSystem);
|
||||||
|
expect(function () {
|
||||||
|
api.timeSystem(timeSystemKey);
|
||||||
|
}).not.toThrow();
|
||||||
|
expect(api.timeSystem()).toBe(timeSystem);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Emits an event when time system changes", function () {
|
||||||
|
api.addTimeSystem(timeSystem);
|
||||||
|
expect(eventListener).not.toHaveBeenCalled();
|
||||||
|
api.on("timeSystem", eventListener);
|
||||||
|
api.timeSystem(timeSystemKey, bounds);
|
||||||
|
expect(eventListener).toHaveBeenCalledWith(timeSystem);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Emits an event when time of interest changes", function () {
|
||||||
|
expect(eventListener).not.toHaveBeenCalled();
|
||||||
|
api.on("timeOfInterest", eventListener);
|
||||||
|
api.timeOfInterest(toi);
|
||||||
|
expect(eventListener).toHaveBeenCalledWith(toi);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Emits an event when bounds change", function () {
|
||||||
|
expect(eventListener).not.toHaveBeenCalled();
|
||||||
|
api.on("bounds", eventListener);
|
||||||
|
api.bounds(bounds);
|
||||||
|
expect(eventListener).toHaveBeenCalledWith(bounds, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("If bounds are set and TOI lies inside them, do not change TOI", function () {
|
||||||
|
api.timeOfInterest(6);
|
||||||
|
api.bounds({
|
||||||
|
start: 1,
|
||||||
|
end: 10
|
||||||
|
});
|
||||||
|
expect(api.timeOfInterest()).toEqual(6);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("If bounds are set and TOI lies outside them, reset TOI", function () {
|
||||||
|
api.timeOfInterest(11);
|
||||||
|
api.bounds({
|
||||||
|
start: 1,
|
||||||
|
end: 10
|
||||||
|
});
|
||||||
|
expect(api.timeOfInterest()).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Maintains delta during tick", function () {
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Allows registered time system to be activated", function () {
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Allows a registered tick source to be activated", function () {
|
||||||
|
const mockTickSource = jasmine.createSpyObj("mockTickSource", [
|
||||||
|
"on",
|
||||||
|
"off",
|
||||||
|
"currentValue"
|
||||||
|
]);
|
||||||
|
mockTickSource.key = 'mockTickSource';
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(" when enabling a tick source", function () {
|
||||||
|
let mockTickSource;
|
||||||
|
let anotherMockTickSource;
|
||||||
|
const mockOffsets = {
|
||||||
|
start: 0,
|
||||||
|
end: 1
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
api = new TimeAPI();
|
mockTickSource = jasmine.createSpyObj("clock", [
|
||||||
timeSystemKey = "timeSystemKey";
|
|
||||||
timeSystem = {key: timeSystemKey};
|
|
||||||
clockKey = "someClockKey";
|
|
||||||
clock = jasmine.createSpyObj("clock", [
|
|
||||||
"on",
|
"on",
|
||||||
"off",
|
"off",
|
||||||
"currentValue"
|
"currentValue"
|
||||||
]);
|
]);
|
||||||
clock.currentValue.and.returnValue(100);
|
mockTickSource.currentValue.and.returnValue(10);
|
||||||
clock.key = clockKey;
|
|
||||||
bounds = {
|
|
||||||
start: 0,
|
|
||||||
end: 1
|
|
||||||
};
|
|
||||||
eventListener = jasmine.createSpy("eventListener");
|
|
||||||
toi = 111;
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Supports setting and querying of time of interest", function () {
|
|
||||||
expect(api.timeOfInterest()).not.toBe(toi);
|
|
||||||
api.timeOfInterest(toi);
|
|
||||||
expect(api.timeOfInterest()).toBe(toi);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Allows setting of valid bounds", function () {
|
|
||||||
bounds = {
|
|
||||||
start: 0,
|
|
||||||
end: 1
|
|
||||||
};
|
|
||||||
expect(api.bounds()).not.toBe(bounds);
|
|
||||||
expect(api.bounds.bind(api, bounds)).not.toThrow();
|
|
||||||
expect(api.bounds()).toEqual(bounds);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Disallows setting of invalid bounds", function () {
|
|
||||||
bounds = {
|
|
||||||
start: 1,
|
|
||||||
end: 0
|
|
||||||
};
|
|
||||||
expect(api.bounds()).not.toEqual(bounds);
|
|
||||||
expect(api.bounds.bind(api, bounds)).toThrow();
|
|
||||||
expect(api.bounds()).not.toEqual(bounds);
|
|
||||||
|
|
||||||
bounds = {start: 1};
|
|
||||||
expect(api.bounds()).not.toEqual(bounds);
|
|
||||||
expect(api.bounds.bind(api, bounds)).toThrow();
|
|
||||||
expect(api.bounds()).not.toEqual(bounds);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Allows setting of previously registered time system with bounds", function () {
|
|
||||||
api.addTimeSystem(timeSystem);
|
|
||||||
expect(api.timeSystem()).not.toBe(timeSystem);
|
|
||||||
expect(function () {
|
|
||||||
api.timeSystem(timeSystem, bounds);
|
|
||||||
}).not.toThrow();
|
|
||||||
expect(api.timeSystem()).toBe(timeSystem);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Disallows setting of time system without bounds", function () {
|
|
||||||
api.addTimeSystem(timeSystem);
|
|
||||||
expect(api.timeSystem()).not.toBe(timeSystem);
|
|
||||||
expect(function () {
|
|
||||||
api.timeSystem(timeSystemKey);
|
|
||||||
}).toThrow();
|
|
||||||
expect(api.timeSystem()).not.toBe(timeSystem);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("allows setting of timesystem without bounds with clock", function () {
|
|
||||||
api.addTimeSystem(timeSystem);
|
|
||||||
api.addClock(clock);
|
|
||||||
api.clock(clockKey, {
|
|
||||||
start: 0,
|
|
||||||
end: 1
|
|
||||||
});
|
|
||||||
expect(api.timeSystem()).not.toBe(timeSystem);
|
|
||||||
expect(function () {
|
|
||||||
api.timeSystem(timeSystemKey);
|
|
||||||
}).not.toThrow();
|
|
||||||
expect(api.timeSystem()).toBe(timeSystem);
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Emits an event when time system changes", function () {
|
|
||||||
api.addTimeSystem(timeSystem);
|
|
||||||
expect(eventListener).not.toHaveBeenCalled();
|
|
||||||
api.on("timeSystem", eventListener);
|
|
||||||
api.timeSystem(timeSystemKey, bounds);
|
|
||||||
expect(eventListener).toHaveBeenCalledWith(timeSystem);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Emits an event when time of interest changes", function () {
|
|
||||||
expect(eventListener).not.toHaveBeenCalled();
|
|
||||||
api.on("timeOfInterest", eventListener);
|
|
||||||
api.timeOfInterest(toi);
|
|
||||||
expect(eventListener).toHaveBeenCalledWith(toi);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Emits an event when bounds change", function () {
|
|
||||||
expect(eventListener).not.toHaveBeenCalled();
|
|
||||||
api.on("bounds", eventListener);
|
|
||||||
api.bounds(bounds);
|
|
||||||
expect(eventListener).toHaveBeenCalledWith(bounds, false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("If bounds are set and TOI lies inside them, do not change TOI", function () {
|
|
||||||
api.timeOfInterest(6);
|
|
||||||
api.bounds({
|
|
||||||
start: 1,
|
|
||||||
end: 10
|
|
||||||
});
|
|
||||||
expect(api.timeOfInterest()).toEqual(6);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("If bounds are set and TOI lies outside them, reset TOI", function () {
|
|
||||||
api.timeOfInterest(11);
|
|
||||||
api.bounds({
|
|
||||||
start: 1,
|
|
||||||
end: 10
|
|
||||||
});
|
|
||||||
expect(api.timeOfInterest()).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Maintains delta during tick", function () {
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Allows registered time system to be activated", function () {
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Allows a registered tick source to be activated", function () {
|
|
||||||
const mockTickSource = jasmine.createSpyObj("mockTickSource", [
|
|
||||||
"on",
|
|
||||||
"off",
|
|
||||||
"currentValue"
|
|
||||||
]);
|
|
||||||
mockTickSource.key = 'mockTickSource';
|
|
||||||
});
|
|
||||||
|
|
||||||
describe(" when enabling a tick source", function () {
|
|
||||||
let mockTickSource;
|
|
||||||
let anotherMockTickSource;
|
|
||||||
const mockOffsets = {
|
|
||||||
start: 0,
|
|
||||||
end: 1
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(function () {
|
|
||||||
mockTickSource = jasmine.createSpyObj("clock", [
|
|
||||||
"on",
|
|
||||||
"off",
|
|
||||||
"currentValue"
|
|
||||||
]);
|
|
||||||
mockTickSource.currentValue.and.returnValue(10);
|
|
||||||
mockTickSource.key = "mts";
|
|
||||||
|
|
||||||
anotherMockTickSource = jasmine.createSpyObj("clock", [
|
|
||||||
"on",
|
|
||||||
"off",
|
|
||||||
"currentValue"
|
|
||||||
]);
|
|
||||||
anotherMockTickSource.key = "amts";
|
|
||||||
anotherMockTickSource.currentValue.and.returnValue(10);
|
|
||||||
|
|
||||||
api.addClock(mockTickSource);
|
|
||||||
api.addClock(anotherMockTickSource);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("sets bounds based on current value", function () {
|
|
||||||
api.clock("mts", mockOffsets);
|
|
||||||
expect(api.bounds()).toEqual({
|
|
||||||
start: 10,
|
|
||||||
end: 11
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("a new tick listener is registered", function () {
|
|
||||||
api.clock("mts", mockOffsets);
|
|
||||||
expect(mockTickSource.on).toHaveBeenCalledWith("tick", jasmine.any(Function));
|
|
||||||
});
|
|
||||||
|
|
||||||
it("listener of existing tick source is reregistered", function () {
|
|
||||||
api.clock("mts", mockOffsets);
|
|
||||||
api.clock("amts", mockOffsets);
|
|
||||||
expect(mockTickSource.off).toHaveBeenCalledWith("tick", jasmine.any(Function));
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Allows the active clock to be set and unset", function () {
|
|
||||||
expect(api.clock()).toBeUndefined();
|
|
||||||
api.clock("mts", mockOffsets);
|
|
||||||
expect(api.clock()).toBeDefined();
|
|
||||||
api.stopClock();
|
|
||||||
expect(api.clock()).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
it("on tick, observes offsets, and indicates tick in bounds callback", function () {
|
|
||||||
const mockTickSource = jasmine.createSpyObj("clock", [
|
|
||||||
"on",
|
|
||||||
"off",
|
|
||||||
"currentValue"
|
|
||||||
]);
|
|
||||||
mockTickSource.currentValue.and.returnValue(100);
|
|
||||||
let tickCallback;
|
|
||||||
const boundsCallback = jasmine.createSpy("boundsCallback");
|
|
||||||
const clockOffsets = {
|
|
||||||
start: -100,
|
|
||||||
end: 100
|
|
||||||
};
|
|
||||||
mockTickSource.key = "mts";
|
mockTickSource.key = "mts";
|
||||||
|
|
||||||
|
anotherMockTickSource = jasmine.createSpyObj("clock", [
|
||||||
|
"on",
|
||||||
|
"off",
|
||||||
|
"currentValue"
|
||||||
|
]);
|
||||||
|
anotherMockTickSource.key = "amts";
|
||||||
|
anotherMockTickSource.currentValue.and.returnValue(10);
|
||||||
|
|
||||||
api.addClock(mockTickSource);
|
api.addClock(mockTickSource);
|
||||||
api.clock("mts", clockOffsets);
|
api.addClock(anotherMockTickSource);
|
||||||
|
|
||||||
api.on("bounds", boundsCallback);
|
|
||||||
|
|
||||||
tickCallback = mockTickSource.on.calls.mostRecent().args[1];
|
|
||||||
tickCallback(1000);
|
|
||||||
expect(boundsCallback).toHaveBeenCalledWith({
|
|
||||||
start: 900,
|
|
||||||
end: 1100
|
|
||||||
}, true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("sets bounds based on current value", function () {
|
||||||
|
api.clock("mts", mockOffsets);
|
||||||
|
expect(api.bounds()).toEqual({
|
||||||
|
start: 10,
|
||||||
|
end: 11
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("a new tick listener is registered", function () {
|
||||||
|
api.clock("mts", mockOffsets);
|
||||||
|
expect(mockTickSource.on).toHaveBeenCalledWith("tick", jasmine.any(Function));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("listener of existing tick source is reregistered", function () {
|
||||||
|
api.clock("mts", mockOffsets);
|
||||||
|
api.clock("amts", mockOffsets);
|
||||||
|
expect(mockTickSource.off).toHaveBeenCalledWith("tick", jasmine.any(Function));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Allows the active clock to be set and unset", function () {
|
||||||
|
expect(api.clock()).toBeUndefined();
|
||||||
|
api.clock("mts", mockOffsets);
|
||||||
|
expect(api.clock()).toBeDefined();
|
||||||
|
api.stopClock();
|
||||||
|
expect(api.clock()).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it("on tick, observes offsets, and indicates tick in bounds callback", function () {
|
||||||
|
const mockTickSource = jasmine.createSpyObj("clock", [
|
||||||
|
"on",
|
||||||
|
"off",
|
||||||
|
"currentValue"
|
||||||
|
]);
|
||||||
|
mockTickSource.currentValue.and.returnValue(100);
|
||||||
|
let tickCallback;
|
||||||
|
const boundsCallback = jasmine.createSpy("boundsCallback");
|
||||||
|
const clockOffsets = {
|
||||||
|
start: -100,
|
||||||
|
end: 100
|
||||||
|
};
|
||||||
|
mockTickSource.key = "mts";
|
||||||
|
|
||||||
|
api.addClock(mockTickSource);
|
||||||
|
api.clock("mts", clockOffsets);
|
||||||
|
|
||||||
|
api.on("bounds", boundsCallback);
|
||||||
|
|
||||||
|
tickCallback = mockTickSource.on.calls.mostRecent().args[1];
|
||||||
|
tickCallback(1000);
|
||||||
|
expect(boundsCallback).toHaveBeenCalledWith({
|
||||||
|
start: 900,
|
||||||
|
end: 1100
|
||||||
|
}, true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
360
src/api/time/TimeContext.js
Normal file
360
src/api/time/TimeContext.js
Normal file
@ -0,0 +1,360 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* Open MCT includes source code licensed under additional open source
|
||||||
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
* this source code distribution or the Licensing information page available
|
||||||
|
* at runtime from the About dialog for additional information.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
import EventEmitter from 'EventEmitter';
|
||||||
|
|
||||||
|
class TimeContext extends EventEmitter {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
//The Time System
|
||||||
|
this.timeSystems = new Map();
|
||||||
|
|
||||||
|
this.system = undefined;
|
||||||
|
|
||||||
|
this.clocks = new Map();
|
||||||
|
|
||||||
|
this.boundsVal = {
|
||||||
|
start: undefined,
|
||||||
|
end: undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
this.activeClock = undefined;
|
||||||
|
this.offsets = undefined;
|
||||||
|
|
||||||
|
this.tick = this.tick.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get or set the time system of the TimeAPI.
|
||||||
|
* @param {TimeSystem | string} timeSystem
|
||||||
|
* @param {module:openmct.TimeAPI~TimeConductorBounds} bounds
|
||||||
|
* @fires module:openmct.TimeAPI~timeSystem
|
||||||
|
* @returns {TimeSystem} The currently applied time system
|
||||||
|
* @memberof module:openmct.TimeAPI#
|
||||||
|
* @method timeSystem
|
||||||
|
*/
|
||||||
|
timeSystem(timeSystemOrKey, bounds) {
|
||||||
|
if (arguments.length >= 1) {
|
||||||
|
if (arguments.length === 1 && !this.activeClock) {
|
||||||
|
throw new Error(
|
||||||
|
"Must specify bounds when changing time system without "
|
||||||
|
+ "an active clock."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let timeSystem;
|
||||||
|
|
||||||
|
if (timeSystemOrKey === undefined) {
|
||||||
|
throw "Please provide a time system";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof timeSystemOrKey === 'string') {
|
||||||
|
timeSystem = this.timeSystems.get(timeSystemOrKey);
|
||||||
|
|
||||||
|
if (timeSystem === undefined) {
|
||||||
|
throw "Unknown time system " + timeSystemOrKey + ". Has it been registered with 'addTimeSystem'?";
|
||||||
|
}
|
||||||
|
} else if (typeof timeSystemOrKey === 'object') {
|
||||||
|
timeSystem = timeSystemOrKey;
|
||||||
|
|
||||||
|
if (!this.timeSystems.has(timeSystem.key)) {
|
||||||
|
throw "Unknown time system " + timeSystem.key + ". Has it been registered with 'addTimeSystem'?";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw "Attempt to set invalid time system in Time API. Please provide a previously registered time system object or key";
|
||||||
|
}
|
||||||
|
|
||||||
|
this.system = timeSystem;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The time system used by the time
|
||||||
|
* conductor has changed. A change in Time System will always be
|
||||||
|
* followed by a bounds event specifying new query bounds.
|
||||||
|
*
|
||||||
|
* @event module:openmct.TimeAPI~timeSystem
|
||||||
|
* @property {TimeSystem} The value of the currently applied
|
||||||
|
* Time System
|
||||||
|
* */
|
||||||
|
this.emit('timeSystem', this.system);
|
||||||
|
if (bounds) {
|
||||||
|
this.bounds(bounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.system;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clock offsets are used to calculate temporal bounds when the system is
|
||||||
|
* ticking on a clock source.
|
||||||
|
*
|
||||||
|
* @typedef {object} ValidationResult
|
||||||
|
* @property {boolean} valid Result of the validation - true or false.
|
||||||
|
* @property {string} message An error message if valid is false.
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Validate the given bounds. This can be used for pre-validation of bounds,
|
||||||
|
* for example by views validating user inputs.
|
||||||
|
* @param {TimeBounds} bounds The start and end time of the conductor.
|
||||||
|
* @returns {ValidationResult} A validation error, or true if valid
|
||||||
|
* @memberof module:openmct.TimeAPI#
|
||||||
|
* @method validateBounds
|
||||||
|
*/
|
||||||
|
validateBounds(bounds) {
|
||||||
|
if ((bounds.start === undefined)
|
||||||
|
|| (bounds.end === undefined)
|
||||||
|
|| isNaN(bounds.start)
|
||||||
|
|| isNaN(bounds.end)
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: "Start and end must be specified as integer values"
|
||||||
|
};
|
||||||
|
} else if (bounds.start > bounds.end) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: "Specified start date exceeds end bound"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: true,
|
||||||
|
message: ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get or set the start and end time of the time conductor. Basic validation
|
||||||
|
* of bounds is performed.
|
||||||
|
*
|
||||||
|
* @param {module:openmct.TimeAPI~TimeConductorBounds} newBounds
|
||||||
|
* @throws {Error} Validation error
|
||||||
|
* @fires module:openmct.TimeAPI~bounds
|
||||||
|
* @returns {module:openmct.TimeAPI~TimeConductorBounds}
|
||||||
|
* @memberof module:openmct.TimeAPI#
|
||||||
|
* @method bounds
|
||||||
|
*/
|
||||||
|
bounds(newBounds) {
|
||||||
|
if (arguments.length > 0) {
|
||||||
|
const validationResult = this.validateBounds(newBounds);
|
||||||
|
if (validationResult.valid !== true) {
|
||||||
|
throw new Error(validationResult.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Create a copy to avoid direct mutation of conductor bounds
|
||||||
|
this.boundsVal = JSON.parse(JSON.stringify(newBounds));
|
||||||
|
/**
|
||||||
|
* The start time, end time, or both have been updated.
|
||||||
|
* @event bounds
|
||||||
|
* @memberof module:openmct.TimeAPI~
|
||||||
|
* @property {TimeConductorBounds} bounds The newly updated bounds
|
||||||
|
* @property {boolean} [tick] `true` if the bounds update was due to
|
||||||
|
* a "tick" event (ie. was an automatic update), false otherwise.
|
||||||
|
*/
|
||||||
|
this.emit('bounds', this.boundsVal, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Return a copy to prevent direct mutation of time conductor bounds.
|
||||||
|
return JSON.parse(JSON.stringify(this.boundsVal));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the given offsets. This can be used for pre-validation of
|
||||||
|
* offsets, for example by views validating user inputs.
|
||||||
|
* @param {ClockOffsets} offsets The start and end offsets from a 'now' value.
|
||||||
|
* @returns { ValidationResult } A validation error, and true/false if valid or not
|
||||||
|
* @memberof module:openmct.TimeAPI#
|
||||||
|
* @method validateOffsets
|
||||||
|
*/
|
||||||
|
validateOffsets(offsets) {
|
||||||
|
if ((offsets.start === undefined)
|
||||||
|
|| (offsets.end === undefined)
|
||||||
|
|| isNaN(offsets.start)
|
||||||
|
|| isNaN(offsets.end)
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: "Start and end offsets must be specified as integer values"
|
||||||
|
};
|
||||||
|
} else if (offsets.start >= offsets.end) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: "Specified start offset must be < end offset"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: true,
|
||||||
|
message: ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} TimeBounds
|
||||||
|
* @property {number} start The start time displayed by the time conductor
|
||||||
|
* in ms since epoch. Epoch determined by currently active time system
|
||||||
|
* @property {number} end The end time displayed by the time conductor in ms
|
||||||
|
* since epoch.
|
||||||
|
* @memberof module:openmct.TimeAPI~
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clock offsets are used to calculate temporal bounds when the system is
|
||||||
|
* ticking on a clock source.
|
||||||
|
*
|
||||||
|
* @typedef {object} ClockOffsets
|
||||||
|
* @property {number} start A time span relative to the current value of the
|
||||||
|
* ticking clock, from which start bounds will be calculated. This value must
|
||||||
|
* be < 0. When a clock is active, bounds will be calculated automatically
|
||||||
|
* based on the value provided by the clock, and the defined clock offsets.
|
||||||
|
* @property {number} end A time span relative to the current value of the
|
||||||
|
* ticking clock, from which end bounds will be calculated. This value must
|
||||||
|
* be >= 0.
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Get or set the currently applied clock offsets. If no parameter is provided,
|
||||||
|
* the current value will be returned. If provided, the new value will be
|
||||||
|
* used as the new clock offsets.
|
||||||
|
* @param {ClockOffsets} offsets
|
||||||
|
* @returns {ClockOffsets}
|
||||||
|
*/
|
||||||
|
clockOffsets(offsets) {
|
||||||
|
if (arguments.length > 0) {
|
||||||
|
|
||||||
|
const validationResult = this.validateOffsets(offsets);
|
||||||
|
if (validationResult.valid !== true) {
|
||||||
|
throw new Error(validationResult.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.offsets = offsets;
|
||||||
|
|
||||||
|
const currentValue = this.activeClock.currentValue();
|
||||||
|
const newBounds = {
|
||||||
|
start: currentValue + offsets.start,
|
||||||
|
end: currentValue + offsets.end
|
||||||
|
};
|
||||||
|
|
||||||
|
this.bounds(newBounds);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event that is triggered when clock offsets change.
|
||||||
|
* @event clockOffsets
|
||||||
|
* @memberof module:openmct.TimeAPI~
|
||||||
|
* @property {ClockOffsets} clockOffsets The newly activated clock
|
||||||
|
* offsets.
|
||||||
|
*/
|
||||||
|
this.emit("clockOffsets", offsets);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.offsets;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the currently active clock from ticking, and unset it. This will
|
||||||
|
* revert all views to showing a static time frame defined by the current
|
||||||
|
* bounds.
|
||||||
|
*/
|
||||||
|
stopClock() {
|
||||||
|
if (this.activeClock) {
|
||||||
|
this.clock(undefined, undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the active clock. Tick source will be immediately subscribed to
|
||||||
|
* and ticking will begin. Offsets from 'now' must also be provided. A clock
|
||||||
|
* can be unset by calling {@link stopClock}.
|
||||||
|
*
|
||||||
|
* @param {Clock || string} keyOrClock The clock to activate, or its key
|
||||||
|
* @param {ClockOffsets} offsets on each tick these will be used to calculate
|
||||||
|
* the start and end bounds. This maintains a sliding time window of a fixed
|
||||||
|
* width that automatically updates.
|
||||||
|
* @fires module:openmct.TimeAPI~clock
|
||||||
|
* @return {Clock} the currently active clock;
|
||||||
|
*/
|
||||||
|
clock(keyOrClock, offsets) {
|
||||||
|
if (arguments.length === 2) {
|
||||||
|
let clock;
|
||||||
|
|
||||||
|
if (typeof keyOrClock === 'string') {
|
||||||
|
clock = this.clocks.get(keyOrClock);
|
||||||
|
if (clock === undefined) {
|
||||||
|
throw "Unknown clock '" + keyOrClock + "'. Has it been registered with 'addClock'?";
|
||||||
|
}
|
||||||
|
} else if (typeof keyOrClock === 'object') {
|
||||||
|
clock = keyOrClock;
|
||||||
|
if (!this.clocks.has(clock.key)) {
|
||||||
|
throw "Unknown clock '" + keyOrClock.key + "'. Has it been registered with 'addClock'?";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const previousClock = this.activeClock;
|
||||||
|
if (previousClock !== undefined) {
|
||||||
|
previousClock.off("tick", this.tick);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.activeClock = clock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The active clock has changed. Clock can be unset by calling {@link stopClock}
|
||||||
|
* @event clock
|
||||||
|
* @memberof module:openmct.TimeAPI~
|
||||||
|
* @property {Clock} clock The newly activated clock, or undefined
|
||||||
|
* if the system is no longer following a clock source
|
||||||
|
*/
|
||||||
|
this.emit("clock", this.activeClock);
|
||||||
|
|
||||||
|
if (this.activeClock !== undefined) {
|
||||||
|
this.clockOffsets(offsets);
|
||||||
|
this.activeClock.on("tick", this.tick);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (arguments.length === 1) {
|
||||||
|
throw "When setting the clock, clock offsets must also be provided";
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.activeClock;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update bounds based on provided time and current offsets
|
||||||
|
* @param {number} timestamp A time from which bounds will be calculated
|
||||||
|
* using current offsets.
|
||||||
|
*/
|
||||||
|
tick(timestamp) {
|
||||||
|
if (!this.activeClock) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newBounds = {
|
||||||
|
start: timestamp + this.offsets.start,
|
||||||
|
end: timestamp + this.offsets.end
|
||||||
|
};
|
||||||
|
|
||||||
|
this.boundsVal = newBounds;
|
||||||
|
this.emit('bounds', this.boundsVal, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TimeContext;
|
155
src/api/time/independentTimeAPISpec.js
Normal file
155
src/api/time/independentTimeAPISpec.js
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* Open MCT includes source code licensed under additional open source
|
||||||
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
* this source code distribution or the Licensing information page available
|
||||||
|
* at runtime from the About dialog for additional information.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
import TimeAPI from "./TimeAPI";
|
||||||
|
import {createOpenMct} from "utils/testing";
|
||||||
|
describe("The Independent Time API", function () {
|
||||||
|
let api;
|
||||||
|
let domainObjectKey;
|
||||||
|
let clockKey;
|
||||||
|
let clock;
|
||||||
|
let bounds;
|
||||||
|
let independentBounds;
|
||||||
|
let eventListener;
|
||||||
|
let openmct;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
openmct = createOpenMct();
|
||||||
|
api = new TimeAPI(openmct);
|
||||||
|
clockKey = "someClockKey";
|
||||||
|
clock = jasmine.createSpyObj("clock", [
|
||||||
|
"on",
|
||||||
|
"off",
|
||||||
|
"currentValue"
|
||||||
|
]);
|
||||||
|
clock.currentValue.and.returnValue(100);
|
||||||
|
clock.key = clockKey;
|
||||||
|
api.addClock(clock);
|
||||||
|
domainObjectKey = 'test-key';
|
||||||
|
bounds = {
|
||||||
|
start: 0,
|
||||||
|
end: 1
|
||||||
|
};
|
||||||
|
api.bounds(bounds);
|
||||||
|
independentBounds = {
|
||||||
|
start: 10,
|
||||||
|
end: 11
|
||||||
|
};
|
||||||
|
eventListener = jasmine.createSpy("eventListener");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Creates an independent time context", () => {
|
||||||
|
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
|
||||||
|
let timeContext = api.getIndependentContext(domainObjectKey);
|
||||||
|
expect(timeContext.bounds()).toEqual(independentBounds);
|
||||||
|
destroyTimeContext();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Gets an independent time context given the objectPath", () => {
|
||||||
|
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
|
||||||
|
let timeContext = api.getContextForView([{
|
||||||
|
identifier: {
|
||||||
|
namespace: '',
|
||||||
|
key: 'blah'
|
||||||
|
}
|
||||||
|
}, { identifier: domainObjectKey }]);
|
||||||
|
expect(timeContext.bounds()).toEqual(independentBounds);
|
||||||
|
destroyTimeContext();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("defaults to the global time context given the objectPath", () => {
|
||||||
|
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
|
||||||
|
let timeContext = api.getContextForView([{
|
||||||
|
identifier: {
|
||||||
|
namespace: '',
|
||||||
|
key: 'blah'
|
||||||
|
}
|
||||||
|
}]);
|
||||||
|
expect(timeContext.bounds()).toEqual(bounds);
|
||||||
|
destroyTimeContext();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Allows setting of valid bounds", function () {
|
||||||
|
bounds = {
|
||||||
|
start: 0,
|
||||||
|
end: 1
|
||||||
|
};
|
||||||
|
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
|
||||||
|
let timeContext = api.getContextForView([{identifier: domainObjectKey}]);
|
||||||
|
expect(timeContext.bounds()).not.toEqual(bounds);
|
||||||
|
timeContext.bounds(bounds);
|
||||||
|
expect(timeContext.bounds()).toEqual(bounds);
|
||||||
|
destroyTimeContext();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Disallows setting of invalid bounds", function () {
|
||||||
|
bounds = {
|
||||||
|
start: 1,
|
||||||
|
end: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
|
||||||
|
let timeContext = api.getContextForView([{identifier: domainObjectKey}]);
|
||||||
|
expect(timeContext.bounds()).not.toBe(bounds);
|
||||||
|
|
||||||
|
expect(timeContext.bounds.bind(timeContext, bounds)).toThrow();
|
||||||
|
expect(timeContext.bounds()).not.toEqual(bounds);
|
||||||
|
|
||||||
|
bounds = {start: 1};
|
||||||
|
expect(timeContext.bounds()).not.toEqual(bounds);
|
||||||
|
expect(timeContext.bounds.bind(timeContext, bounds)).toThrow();
|
||||||
|
expect(timeContext.bounds()).not.toEqual(bounds);
|
||||||
|
destroyTimeContext();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Emits an event when bounds change", function () {
|
||||||
|
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
|
||||||
|
let timeContext = api.getContextForView([{identifier: domainObjectKey}]);
|
||||||
|
expect(eventListener).not.toHaveBeenCalled();
|
||||||
|
timeContext.on('bounds', eventListener);
|
||||||
|
timeContext.bounds(bounds);
|
||||||
|
expect(eventListener).toHaveBeenCalledWith(bounds, false);
|
||||||
|
destroyTimeContext();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(" when using real time clock", function () {
|
||||||
|
const mockOffsets = {
|
||||||
|
start: 10,
|
||||||
|
end: 11
|
||||||
|
};
|
||||||
|
|
||||||
|
it("Emits an event when bounds change based on current value", function () {
|
||||||
|
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
|
||||||
|
let timeContext = api.getContextForView([{identifier: domainObjectKey}]);
|
||||||
|
expect(eventListener).not.toHaveBeenCalled();
|
||||||
|
timeContext.clock('someClockKey', mockOffsets);
|
||||||
|
timeContext.on('bounds', eventListener);
|
||||||
|
timeContext.tick(10);
|
||||||
|
expect(eventListener).toHaveBeenCalledWith({
|
||||||
|
start: 20,
|
||||||
|
end: 21
|
||||||
|
}, true);
|
||||||
|
destroyTimeContext();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
@ -90,7 +90,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(function (canvas) {
|
}).then(canvas => {
|
||||||
dialog.dismiss();
|
dialog.dismiss();
|
||||||
|
|
||||||
return new Promise(function (resolve, reject) {
|
return new Promise(function (resolve, reject) {
|
||||||
@ -105,9 +105,10 @@ class ImageExporter {
|
|||||||
|
|
||||||
return canvas.toBlob(blob => resolve({ blob }), mimeType);
|
return canvas.toBlob(blob => resolve({ blob }), mimeType);
|
||||||
});
|
});
|
||||||
}, function (error) {
|
}).catch(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!',
|
||||||
|
@ -41,7 +41,6 @@ const DEFAULTS = [
|
|||||||
'platform/forms',
|
'platform/forms',
|
||||||
'platform/identity',
|
'platform/identity',
|
||||||
'platform/persistence/aggregator',
|
'platform/persistence/aggregator',
|
||||||
'platform/persistence/queue',
|
|
||||||
'platform/policy',
|
'platform/policy',
|
||||||
'platform/entanglement',
|
'platform/entanglement',
|
||||||
'platform/search',
|
'platform/search',
|
||||||
|
@ -32,7 +32,7 @@ describe('the plugin', function () {
|
|||||||
let openmct;
|
let openmct;
|
||||||
let composition;
|
let composition;
|
||||||
|
|
||||||
beforeEach((done) => {
|
beforeEach(() => {
|
||||||
|
|
||||||
openmct = createOpenMct();
|
openmct = createOpenMct();
|
||||||
|
|
||||||
@ -47,11 +47,6 @@ describe('the plugin', function () {
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
openmct.on('start', done);
|
|
||||||
openmct.startHeadless();
|
|
||||||
|
|
||||||
composition = openmct.composition.get({identifier});
|
|
||||||
|
|
||||||
spyOn(couchPlugin.couchProvider, 'getObjectsByFilter').and.returnValue(Promise.resolve([
|
spyOn(couchPlugin.couchProvider, 'getObjectsByFilter').and.returnValue(Promise.resolve([
|
||||||
{
|
{
|
||||||
identifier: {
|
identifier: {
|
||||||
@ -66,6 +61,19 @@ describe('the plugin', function () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
]));
|
]));
|
||||||
|
|
||||||
|
spyOn(couchPlugin.couchProvider, "get").and.callFake((id) => {
|
||||||
|
return Promise.resolve({
|
||||||
|
identifier: id
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
openmct.once('start', resolve);
|
||||||
|
openmct.startHeadless();
|
||||||
|
}).then(() => {
|
||||||
|
composition = openmct.composition.get({identifier});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
@ -96,11 +96,11 @@ export default {
|
|||||||
|
|
||||||
this.timestampKey = this.openmct.time.timeSystem().key;
|
this.timestampKey = this.openmct.time.timeSystem().key;
|
||||||
|
|
||||||
this.valueMetadata = this
|
this.valueMetadata = this.metadata ? this
|
||||||
.metadata
|
.metadata
|
||||||
.valuesForHints(['range'])[0];
|
.valuesForHints(['range'])[0] : undefined;
|
||||||
|
|
||||||
this.valueKey = this.valueMetadata.key;
|
this.valueKey = this.valueMetadata ? this.valueMetadata.key : undefined;
|
||||||
|
|
||||||
this.unsubscribe = this.openmct
|
this.unsubscribe = this.openmct
|
||||||
.telemetry
|
.telemetry
|
||||||
@ -151,7 +151,10 @@ export default {
|
|||||||
size: 1,
|
size: 1,
|
||||||
strategy: 'latest'
|
strategy: 'latest'
|
||||||
})
|
})
|
||||||
.then((array) => this.updateValues(array[array.length - 1]));
|
.then((array) => this.updateValues(array[array.length - 1]))
|
||||||
|
.catch((error) => {
|
||||||
|
console.warn('Error fetching data', error);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
updateBounds(bounds, isTick) {
|
updateBounds(bounds, isTick) {
|
||||||
this.bounds = bounds;
|
this.bounds = bounds;
|
||||||
|
@ -73,8 +73,9 @@ export default {
|
|||||||
hasUnits() {
|
hasUnits() {
|
||||||
let itemsWithUnits = this.items.filter((item) => {
|
let itemsWithUnits = this.items.filter((item) => {
|
||||||
let metadata = this.openmct.telemetry.getMetadata(item.domainObject);
|
let metadata = this.openmct.telemetry.getMetadata(item.domainObject);
|
||||||
|
const valueMetadatas = metadata ? metadata.valueMetadatas : [];
|
||||||
|
|
||||||
return this.metadataHasUnits(metadata.valueMetadatas);
|
return this.metadataHasUnits(valueMetadatas);
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
78
src/plugins/clearData/ClearDataAction.js
Normal file
78
src/plugins/clearData/ClearDataAction.js
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* Open MCT includes source code licensed under additional open source
|
||||||
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
* this source code distribution or the Licensing information page available
|
||||||
|
* at runtime from the About dialog for additional information.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
function inSelectionPath(openmct, domainObject) {
|
||||||
|
const domainObjectIdentifier = domainObject.identifier;
|
||||||
|
|
||||||
|
return openmct.selection.get().some(selectionPath => {
|
||||||
|
return selectionPath.some(objectInPath => {
|
||||||
|
const objectInPathIdentifier = objectInPath.context.item.identifier;
|
||||||
|
|
||||||
|
return openmct.objects.areIdsEqual(objectInPathIdentifier, domainObjectIdentifier);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ClearDataAction {
|
||||||
|
constructor(openmct, appliesToObjects) {
|
||||||
|
this.name = 'Clear Data for Object';
|
||||||
|
this.key = 'clear-data-action';
|
||||||
|
this.description = 'Clears current data for object, unsubscribes and resubscribes to data';
|
||||||
|
this.cssClass = 'icon-clear-data';
|
||||||
|
|
||||||
|
this._openmct = openmct;
|
||||||
|
this._appliesToObjects = appliesToObjects;
|
||||||
|
}
|
||||||
|
invoke(objectPath) {
|
||||||
|
let domainObject = null;
|
||||||
|
if (objectPath) {
|
||||||
|
domainObject = objectPath[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
this._openmct.objectViews.emit('clearData', domainObject);
|
||||||
|
}
|
||||||
|
appliesTo(objectPath) {
|
||||||
|
if (!objectPath) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const contextualDomainObject = objectPath[0];
|
||||||
|
// first check to see if this action applies to this sort of object at all
|
||||||
|
const appliesToThisObject = this._appliesToObjects.some(type => {
|
||||||
|
return contextualDomainObject.type === type;
|
||||||
|
});
|
||||||
|
if (!appliesToThisObject) {
|
||||||
|
// we've selected something not applicable
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const objectInSelectionPath = inSelectionPath(this._openmct, contextualDomainObject);
|
||||||
|
if (objectInSelectionPath) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
// if this it doesn't match up, check to see if we're in a composition (i.e., layout)
|
||||||
|
const routerPath = this._openmct.router.path[0];
|
||||||
|
|
||||||
|
return routerPath.type === 'layout';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -22,7 +22,7 @@
|
|||||||
|
|
||||||
define([
|
define([
|
||||||
'./components/globalClearIndicator.vue',
|
'./components/globalClearIndicator.vue',
|
||||||
'./clearDataAction',
|
'./ClearDataAction',
|
||||||
'vue'
|
'vue'
|
||||||
], function (
|
], function (
|
||||||
GlobaClearIndicator,
|
GlobaClearIndicator,
|
||||||
|
140
src/plugins/clearData/test/ClearDataActionSpec.js
Normal file
140
src/plugins/clearData/test/ClearDataActionSpec.js
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* Open MCT includes source code licensed under additional open source
|
||||||
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
* this source code distribution or the Licensing information page available
|
||||||
|
* at runtime from the About dialog for additional information.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
import ClearDataActionPlugin from '../plugin.js';
|
||||||
|
import ClearDataAction from '../ClearDataAction.js';
|
||||||
|
|
||||||
|
describe('When the Clear Data Plugin is installed,', () => {
|
||||||
|
const mockObjectViews = jasmine.createSpyObj('objectViews', ['emit']);
|
||||||
|
const mockIndicatorProvider = jasmine.createSpyObj('indicators', ['add']);
|
||||||
|
const mockActionsProvider = jasmine.createSpyObj('actions', ['register']);
|
||||||
|
const goodMockSelectionPath = [[{
|
||||||
|
context: {
|
||||||
|
item: {
|
||||||
|
identifier: {
|
||||||
|
key: 'apple',
|
||||||
|
namespace: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]];
|
||||||
|
|
||||||
|
const openmct = {
|
||||||
|
objectViews: mockObjectViews,
|
||||||
|
indicators: mockIndicatorProvider,
|
||||||
|
actions: mockActionsProvider,
|
||||||
|
install: function (plugin) {
|
||||||
|
plugin(this);
|
||||||
|
},
|
||||||
|
selection: {
|
||||||
|
get: function () {
|
||||||
|
return goodMockSelectionPath;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
objects: {
|
||||||
|
areIdsEqual: function (obj1, obj2) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockObjectPath = [
|
||||||
|
{
|
||||||
|
name: 'mockObject1',
|
||||||
|
type: 'apple'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'mockObject2',
|
||||||
|
type: 'banana'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
it('Global Clear Indicator is installed', () => {
|
||||||
|
openmct.install(ClearDataActionPlugin([]));
|
||||||
|
|
||||||
|
expect(mockIndicatorProvider.add).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Clear Data context menu action is installed', () => {
|
||||||
|
openmct.install(ClearDataActionPlugin([]));
|
||||||
|
|
||||||
|
expect(mockActionsProvider.register).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clear data action emits a clearData event when invoked', () => {
|
||||||
|
const action = new ClearDataAction(openmct);
|
||||||
|
|
||||||
|
action.invoke(mockObjectPath);
|
||||||
|
|
||||||
|
expect(mockObjectViews.emit).toHaveBeenCalledWith('clearData', mockObjectPath[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clears data on applicable objects', () => {
|
||||||
|
let action = new ClearDataAction(openmct, ['apple']);
|
||||||
|
|
||||||
|
const actionApplies = action.appliesTo(mockObjectPath);
|
||||||
|
|
||||||
|
expect(actionApplies).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not clear data on inapplicable objects', () => {
|
||||||
|
let action = new ClearDataAction(openmct, ['pineapple']);
|
||||||
|
|
||||||
|
const actionApplies = action.appliesTo(mockObjectPath);
|
||||||
|
|
||||||
|
expect(actionApplies).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not clear data if not in the selection path and not a layout', () => {
|
||||||
|
openmct.objects = {
|
||||||
|
areIdsEqual: function (obj1, obj2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
openmct.router = {
|
||||||
|
path: [{type: 'not-a-layout'}]
|
||||||
|
};
|
||||||
|
|
||||||
|
let action = new ClearDataAction(openmct, ['apple']);
|
||||||
|
|
||||||
|
const actionApplies = action.appliesTo(mockObjectPath);
|
||||||
|
|
||||||
|
expect(actionApplies).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does clear data if not in the selection path and is a layout', () => {
|
||||||
|
openmct.objects = {
|
||||||
|
areIdsEqual: function (obj1, obj2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
openmct.router = {
|
||||||
|
path: [{type: 'layout'}]
|
||||||
|
};
|
||||||
|
|
||||||
|
let action = new ClearDataAction(openmct, ['apple']);
|
||||||
|
|
||||||
|
const actionApplies = action.appliesTo(mockObjectPath);
|
||||||
|
|
||||||
|
expect(actionApplies).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
@ -1,64 +0,0 @@
|
|||||||
/*****************************************************************************
|
|
||||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
|
||||||
* as represented by the Administrator of the National Aeronautics and Space
|
|
||||||
* Administration. All rights reserved.
|
|
||||||
*
|
|
||||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
* License for the specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*
|
|
||||||
* Open MCT includes source code licensed under additional open source
|
|
||||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
|
||||||
* this source code distribution or the Licensing information page available
|
|
||||||
* at runtime from the About dialog for additional information.
|
|
||||||
*****************************************************************************/
|
|
||||||
|
|
||||||
import ClearDataActionPlugin from '../plugin.js';
|
|
||||||
import ClearDataAction from '../clearDataAction.js';
|
|
||||||
|
|
||||||
describe('When the Clear Data Plugin is installed,', function () {
|
|
||||||
const mockObjectViews = jasmine.createSpyObj('objectViews', ['emit']);
|
|
||||||
const mockIndicatorProvider = jasmine.createSpyObj('indicators', ['add']);
|
|
||||||
const mockActionsProvider = jasmine.createSpyObj('actions', ['register']);
|
|
||||||
|
|
||||||
const openmct = {
|
|
||||||
objectViews: mockObjectViews,
|
|
||||||
indicators: mockIndicatorProvider,
|
|
||||||
actions: mockActionsProvider,
|
|
||||||
install: function (plugin) {
|
|
||||||
plugin(this);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockObjectPath = [
|
|
||||||
{name: 'mockObject1'},
|
|
||||||
{name: 'mockObject2'}
|
|
||||||
];
|
|
||||||
|
|
||||||
it('Global Clear Indicator is installed', function () {
|
|
||||||
openmct.install(ClearDataActionPlugin([]));
|
|
||||||
|
|
||||||
expect(mockIndicatorProvider.add).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Clear Data context menu action is installed', function () {
|
|
||||||
openmct.install(ClearDataActionPlugin([]));
|
|
||||||
|
|
||||||
expect(mockActionsProvider.register).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('clear data action emits a clearData event when invoked', function () {
|
|
||||||
let action = new ClearDataAction(openmct);
|
|
||||||
|
|
||||||
action.invoke(mockObjectPath);
|
|
||||||
|
|
||||||
expect(mockObjectViews.emit).toHaveBeenCalledWith('clearData', mockObjectPath[0]);
|
|
||||||
});
|
|
||||||
});
|
|
59
src/plugins/clock/ClockViewProvider.js
Normal file
59
src/plugins/clock/ClockViewProvider.js
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* 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 Clock from './components/Clock.vue';
|
||||||
|
import Vue from 'vue';
|
||||||
|
|
||||||
|
export default function ClockViewProvider(openmct) {
|
||||||
|
return {
|
||||||
|
key: 'clock.view',
|
||||||
|
name: 'Clock',
|
||||||
|
cssClass: 'icon-clock',
|
||||||
|
canView(domainObject) {
|
||||||
|
return domainObject.type === 'clock';
|
||||||
|
},
|
||||||
|
|
||||||
|
view: function (domainObject) {
|
||||||
|
let component;
|
||||||
|
|
||||||
|
return {
|
||||||
|
show: function (element) {
|
||||||
|
component = new Vue({
|
||||||
|
el: element,
|
||||||
|
components: {
|
||||||
|
Clock
|
||||||
|
},
|
||||||
|
provide: {
|
||||||
|
openmct,
|
||||||
|
domainObject
|
||||||
|
},
|
||||||
|
template: '<clock />'
|
||||||
|
});
|
||||||
|
},
|
||||||
|
destroy: function () {
|
||||||
|
component.$destroy();
|
||||||
|
component = undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
99
src/plugins/clock/components/Clock.vue
Normal file
99
src/plugins/clock/components/Clock.vue
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
<!--
|
||||||
|
Open MCT, Copyright (c) 2014-2021, United States Government
|
||||||
|
as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
Administration. All rights reserved.
|
||||||
|
|
||||||
|
Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
"License"); you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
License for the specific language governing permissions and limitations
|
||||||
|
under the License.
|
||||||
|
|
||||||
|
Open MCT includes source code licensed under additional open source
|
||||||
|
licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
this source code distribution or the Licensing information page available
|
||||||
|
at runtime from the About dialog for additional information.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="l-angular-ov-wrapper">
|
||||||
|
<div class="u-contents">
|
||||||
|
<div class="c-clock l-time-display u-style-receiver js-style-receiver">
|
||||||
|
<div class="c-clock__timezone">
|
||||||
|
{{ timeZoneAbbr }}
|
||||||
|
</div>
|
||||||
|
<div class="c-clock__value">
|
||||||
|
{{ timeTextValue }}
|
||||||
|
</div>
|
||||||
|
<div class="c-clock__ampm">
|
||||||
|
{{ timeAmPm }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import moment from 'moment';
|
||||||
|
import momentTimezone from 'moment-timezone';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
inject: ['openmct', 'domainObject'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
lastTimestamp: null
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
configuration() {
|
||||||
|
return this.domainObject.configuration;
|
||||||
|
},
|
||||||
|
baseFormat() {
|
||||||
|
return this.configuration.baseFormat;
|
||||||
|
},
|
||||||
|
use24() {
|
||||||
|
return this.configuration.use24 === 'clock24';
|
||||||
|
},
|
||||||
|
timezone() {
|
||||||
|
return this.configuration.timezone;
|
||||||
|
},
|
||||||
|
timeFormat() {
|
||||||
|
return this.use24 ? this.baseFormat.replace('hh', "HH") : this.baseFormat;
|
||||||
|
},
|
||||||
|
zoneName() {
|
||||||
|
return momentTimezone.tz.names().includes(this.timezone) ? this.timezone : "UTC";
|
||||||
|
},
|
||||||
|
momentTime() {
|
||||||
|
return this.zoneName ? moment.utc(this.lastTimestamp).tz(this.zoneName) : moment.utc(this.lastTimestamp);
|
||||||
|
},
|
||||||
|
timeZoneAbbr() {
|
||||||
|
return this.momentTime.zoneAbbr();
|
||||||
|
},
|
||||||
|
timeTextValue() {
|
||||||
|
return this.timeFormat && this.momentTime.format(this.timeFormat);
|
||||||
|
},
|
||||||
|
timeAmPm() {
|
||||||
|
return this.use24 ? '' : this.momentTime.format("A");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
const TickerService = this.openmct.$injector.get('tickerService');
|
||||||
|
this.unlisten = TickerService.listen(this.tick);
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
if (this.unlisten) {
|
||||||
|
this.unlisten();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
tick(timestamp) {
|
||||||
|
this.lastTimestamp = timestamp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
64
src/plugins/clock/components/ClockIndicator.vue
Normal file
64
src/plugins/clock/components/ClockIndicator.vue
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
<!--
|
||||||
|
Open MCT, Copyright (c) 2014-2021, United States Government
|
||||||
|
as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
Administration. All rights reserved.
|
||||||
|
|
||||||
|
Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
"License"); you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
License for the specific language governing permissions and limitations
|
||||||
|
under the License.
|
||||||
|
|
||||||
|
Open MCT includes source code licensed under additional open source
|
||||||
|
licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
this source code distribution or the Licensing information page available
|
||||||
|
at runtime from the About dialog for additional information.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="c-indicator t-indicator-clock icon-clock no-minify c-indicator--not-clickable">
|
||||||
|
<span class="label c-indicator__label">
|
||||||
|
{{ timeTextValue }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
inject: ['openmct'],
|
||||||
|
props: {
|
||||||
|
indicatorFormat: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
timeTextValue: null
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.openmct.on('start', () => {
|
||||||
|
const TickerService = this.openmct.$injector.get('tickerService');
|
||||||
|
this.unlisten = TickerService.listen(this.tick);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
if (this.unlisten) {
|
||||||
|
this.unlisten();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
tick(timestamp) {
|
||||||
|
this.timeTextValue = `${moment.utc(timestamp).format(this.indicatorFormat)} UTC`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
154
src/plugins/clock/plugin.js
Normal file
154
src/plugins/clock/plugin.js
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
|
||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* Open MCT includes source code licensed under additional open source
|
||||||
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
* this source code distribution or the Licensing information page available
|
||||||
|
* at runtime from the About dialog for additional information.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
import ClockViewProvider from './ClockViewProvider';
|
||||||
|
import ClockIndicator from './components/ClockIndicator.vue';
|
||||||
|
|
||||||
|
import momentTimezone from 'moment-timezone';
|
||||||
|
import Vue from 'vue';
|
||||||
|
|
||||||
|
export default function ClockPlugin(options) {
|
||||||
|
return function install(openmct) {
|
||||||
|
const CLOCK_INDICATOR_FORMAT = 'YYYY/MM/DD HH:mm:ss';
|
||||||
|
openmct.types.addType('clock', {
|
||||||
|
name: 'Clock',
|
||||||
|
description: 'A UTC-based clock that supports a variety of display formats. Clocks can be added to Display Layouts.',
|
||||||
|
creatable: true,
|
||||||
|
cssClass: 'icon-clock',
|
||||||
|
initialize: function (domainObject) {
|
||||||
|
domainObject.configuration = {
|
||||||
|
baseFormat: 'YYYY/MM/DD hh:mm:ss',
|
||||||
|
use24: 'clock12',
|
||||||
|
timezone: 'UTC'
|
||||||
|
};
|
||||||
|
},
|
||||||
|
"form": [
|
||||||
|
{
|
||||||
|
"key": "displayFormat",
|
||||||
|
"name": "Display Format",
|
||||||
|
control: 'select',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: 'YYYY/MM/DD hh:mm:ss',
|
||||||
|
name: 'YYYY/MM/DD hh:mm:ss'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'YYYY/DDD hh:mm:ss',
|
||||||
|
name: 'YYYY/DDD hh:mm:ss'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'hh:mm:ss',
|
||||||
|
name: 'hh:mm:ss'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
cssClass: 'l-inline',
|
||||||
|
property: [
|
||||||
|
'configuration',
|
||||||
|
'baseFormat'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
control: 'select',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: 'clock12',
|
||||||
|
name: '12hr'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'clock24',
|
||||||
|
name: '24hr'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
cssClass: 'l-inline',
|
||||||
|
property: [
|
||||||
|
'configuration',
|
||||||
|
'use24'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "timezone",
|
||||||
|
"name": "Timezone",
|
||||||
|
"control": "autocomplete",
|
||||||
|
"options": momentTimezone.tz.names(),
|
||||||
|
property: [
|
||||||
|
'configuration',
|
||||||
|
'timezone'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
openmct.objectViews.addProvider(new ClockViewProvider(openmct));
|
||||||
|
|
||||||
|
if (options && options.enableClockIndicator === true) {
|
||||||
|
const clockIndicator = new Vue ({
|
||||||
|
components: {
|
||||||
|
ClockIndicator
|
||||||
|
},
|
||||||
|
provide: {
|
||||||
|
openmct
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
indicatorFormat: CLOCK_INDICATOR_FORMAT
|
||||||
|
};
|
||||||
|
},
|
||||||
|
template: '<ClockIndicator :indicator-format="indicatorFormat" />'
|
||||||
|
});
|
||||||
|
const indicator = {
|
||||||
|
element: clockIndicator.$mount().$el,
|
||||||
|
key: 'clock-indicator'
|
||||||
|
};
|
||||||
|
|
||||||
|
openmct.indicators.add(indicator);
|
||||||
|
}
|
||||||
|
|
||||||
|
openmct.objects.addGetInterceptor({
|
||||||
|
appliesTo: (identifier, domainObject) => {
|
||||||
|
return domainObject && domainObject.type === 'clock';
|
||||||
|
},
|
||||||
|
invoke: (identifier, domainObject) => {
|
||||||
|
if (domainObject.configuration) {
|
||||||
|
return domainObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (domainObject.clockFormat
|
||||||
|
&& domainObject.timezone) {
|
||||||
|
const baseFormat = domainObject.clockFormat[0];
|
||||||
|
const use24 = domainObject.clockFormat[1];
|
||||||
|
const timezone = domainObject.timezone;
|
||||||
|
|
||||||
|
domainObject.configuration = {
|
||||||
|
baseFormat,
|
||||||
|
use24,
|
||||||
|
timezone
|
||||||
|
};
|
||||||
|
|
||||||
|
openmct.objects.mutate(domainObject, 'configuration', domainObject.configuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
return domainObject;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
231
src/plugins/clock/pluginSpec.js
Normal file
231
src/plugins/clock/pluginSpec.js
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* Open MCT includes source code licensed under additional open source
|
||||||
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
* this source code distribution or the Licensing information page available
|
||||||
|
* at runtime from the About dialog for additional information.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
import { createOpenMct, resetApplicationState } from 'utils/testing';
|
||||||
|
import clockPlugin from './plugin';
|
||||||
|
|
||||||
|
import Vue from 'vue';
|
||||||
|
|
||||||
|
describe("Clock plugin:", () => {
|
||||||
|
let openmct;
|
||||||
|
let clockDefinition;
|
||||||
|
let element;
|
||||||
|
let child;
|
||||||
|
let appHolder;
|
||||||
|
|
||||||
|
let clockDomainObject;
|
||||||
|
|
||||||
|
function setupClock(enableClockIndicator) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
clockDomainObject = {
|
||||||
|
identifier: {
|
||||||
|
key: 'clock',
|
||||||
|
namespace: 'test-namespace'
|
||||||
|
},
|
||||||
|
type: 'clock'
|
||||||
|
};
|
||||||
|
|
||||||
|
appHolder = document.createElement('div');
|
||||||
|
appHolder.style.width = '640px';
|
||||||
|
appHolder.style.height = '480px';
|
||||||
|
document.body.appendChild(appHolder);
|
||||||
|
|
||||||
|
openmct = createOpenMct();
|
||||||
|
|
||||||
|
element = document.createElement('div');
|
||||||
|
child = document.createElement('div');
|
||||||
|
element.appendChild(child);
|
||||||
|
|
||||||
|
openmct.install(clockPlugin({ enableClockIndicator }));
|
||||||
|
|
||||||
|
clockDefinition = openmct.types.get('clock').definition;
|
||||||
|
clockDefinition.initialize(clockDomainObject);
|
||||||
|
|
||||||
|
openmct.on('start', resolve);
|
||||||
|
openmct.start(appHolder);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Clock view:", () => {
|
||||||
|
let clockViewProvider;
|
||||||
|
let clockView;
|
||||||
|
let clockViewObject;
|
||||||
|
let mutableClockObject;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await setupClock(true);
|
||||||
|
|
||||||
|
clockViewObject = {
|
||||||
|
...clockDomainObject,
|
||||||
|
id: "test-object",
|
||||||
|
name: 'Clock',
|
||||||
|
configuration: {
|
||||||
|
baseFormat: 'YYYY/MM/DD hh:mm:ss',
|
||||||
|
use24: 'clock12',
|
||||||
|
timezone: 'UTC'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve(clockViewObject));
|
||||||
|
spyOn(openmct.objects, 'save').and.returnValue(Promise.resolve(true));
|
||||||
|
|
||||||
|
const applicableViews = openmct.objectViews.get(clockViewObject, [clockViewObject]);
|
||||||
|
clockViewProvider = applicableViews.find(viewProvider => viewProvider.key === 'clock.view');
|
||||||
|
|
||||||
|
mutableClockObject = await openmct.objects.getMutable(clockViewObject.identifier);
|
||||||
|
|
||||||
|
clockView = clockViewProvider.view(mutableClockObject);
|
||||||
|
clockView.show(child);
|
||||||
|
|
||||||
|
await Vue.nextTick();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
clockView.destroy();
|
||||||
|
openmct.objects.destroyMutable(mutableClockObject);
|
||||||
|
if (appHolder) {
|
||||||
|
appHolder.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
return resetApplicationState(openmct);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("has name as Clock", () => {
|
||||||
|
expect(clockDefinition.name).toEqual('Clock');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is creatable", () => {
|
||||||
|
expect(clockDefinition.creatable).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("provides clock view", () => {
|
||||||
|
expect(clockViewProvider).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders clock element", () => {
|
||||||
|
const clockElement = element.querySelectorAll('.c-clock');
|
||||||
|
expect(clockElement.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders major elements", () => {
|
||||||
|
const clockElement = element.querySelector('.c-clock');
|
||||||
|
const timezone = clockElement.querySelector('.c-clock__timezone');
|
||||||
|
const time = clockElement.querySelector('.c-clock__value');
|
||||||
|
const amPm = clockElement.querySelector('.c-clock__ampm');
|
||||||
|
const hasMajorElements = Boolean(timezone && time && amPm);
|
||||||
|
|
||||||
|
expect(hasMajorElements).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders time in UTC", () => {
|
||||||
|
const clockElement = element.querySelector('.c-clock');
|
||||||
|
const timezone = clockElement.querySelector('.c-clock__timezone').textContent.trim();
|
||||||
|
|
||||||
|
expect(timezone).toBe('UTC');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("updates the 24 hour option in the configuration", (done) => {
|
||||||
|
expect(clockDomainObject.configuration.use24).toBe('clock12');
|
||||||
|
const new24Option = 'clock24';
|
||||||
|
|
||||||
|
openmct.objects.observe(clockViewObject, 'configuration', (changedDomainObject) => {
|
||||||
|
expect(changedDomainObject.use24).toBe(new24Option);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
openmct.objects.mutate(clockViewObject, 'configuration.use24', new24Option);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("updates the timezone option in the configuration", (done) => {
|
||||||
|
expect(clockDomainObject.configuration.timezone).toBe('UTC');
|
||||||
|
const newZone = 'CST6CDT';
|
||||||
|
|
||||||
|
openmct.objects.observe(clockViewObject, 'configuration', (changedDomainObject) => {
|
||||||
|
expect(changedDomainObject.timezone).toBe(newZone);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
openmct.objects.mutate(clockViewObject, 'configuration.timezone', newZone);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("updates the time format option in the configuration", (done) => {
|
||||||
|
expect(clockDomainObject.configuration.baseFormat).toBe('YYYY/MM/DD hh:mm:ss');
|
||||||
|
const newFormat = 'hh:mm:ss';
|
||||||
|
|
||||||
|
openmct.objects.observe(clockViewObject, 'configuration', (changedDomainObject) => {
|
||||||
|
expect(changedDomainObject.baseFormat).toBe(newFormat);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
openmct.objects.mutate(clockViewObject, 'configuration.baseFormat', newFormat);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Clock Indicator view:", () => {
|
||||||
|
let clockIndicator;
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (clockIndicator) {
|
||||||
|
clockIndicator.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
clockIndicator = undefined;
|
||||||
|
if (appHolder) {
|
||||||
|
appHolder.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
return resetApplicationState(openmct);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("doesn't exist", async () => {
|
||||||
|
await setupClock(false);
|
||||||
|
|
||||||
|
clockIndicator = openmct.indicators.indicatorObjects
|
||||||
|
.find(indicator => indicator.key === 'clock-indicator');
|
||||||
|
|
||||||
|
const clockIndicatorMissing = clockIndicator === null || clockIndicator === undefined;
|
||||||
|
expect(clockIndicatorMissing).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("exists", async () => {
|
||||||
|
await setupClock(true);
|
||||||
|
|
||||||
|
clockIndicator = openmct.indicators.indicatorObjects
|
||||||
|
.find(indicator => indicator.key === 'clock-indicator').element;
|
||||||
|
|
||||||
|
const hasClockIndicator = clockIndicator !== null && clockIndicator !== undefined;
|
||||||
|
expect(hasClockIndicator).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("contains text", async () => {
|
||||||
|
await setupClock(true);
|
||||||
|
|
||||||
|
clockIndicator = openmct.indicators.indicatorObjects
|
||||||
|
.find(indicator => indicator.key === 'clock-indicator').element;
|
||||||
|
|
||||||
|
const clockIndicatorText = clockIndicator.textContent.trim();
|
||||||
|
const textIncludesUTC = clockIndicatorText.includes('UTC');
|
||||||
|
|
||||||
|
expect(textIncludesUTC).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -273,10 +273,7 @@ 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.objectPath
|
this.navigateToPath = '#/browse/' + this.openmct.objects.getRelativePath(this.objectPath);
|
||||||
.map(o => o && this.openmct.objects.makeKeyString(o.identifier))
|
|
||||||
.reverse()
|
|
||||||
.join('/');
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -297,10 +297,7 @@ 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.objectPath
|
this.navigateToPath = '#/browse/' + this.openmct.objects.getRelativePath(this.objectPath);
|
||||||
.map(o => o && this.openmct.objects.makeKeyString(o.identifier))
|
|
||||||
.reverse()
|
|
||||||
.join('/');
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -98,6 +98,8 @@ describe('the plugin', function () {
|
|||||||
|
|
||||||
conditionSetDefinition.initialize(mockConditionSetDomainObject);
|
conditionSetDefinition.initialize(mockConditionSetDomainObject);
|
||||||
|
|
||||||
|
spyOn(openmct.objects, "save").and.returnValue(Promise.resolve(true));
|
||||||
|
|
||||||
openmct.on('start', done);
|
openmct.on('start', done);
|
||||||
openmct.startHeadless();
|
openmct.startHeadless();
|
||||||
});
|
});
|
||||||
|
@ -101,7 +101,7 @@ export default {
|
|||||||
addChildren(domainObject) {
|
addChildren(domainObject) {
|
||||||
let keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
|
let keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
|
||||||
let metadata = this.openmct.telemetry.getMetadata(domainObject);
|
let metadata = this.openmct.telemetry.getMetadata(domainObject);
|
||||||
let metadataWithFilters = metadata.valueMetadatas.filter(value => value.filters);
|
let metadataWithFilters = metadata ? metadata.valueMetadatas.filter(value => value.filters) : [];
|
||||||
let hasFiltersWithKeyString = this.persistedFilters[keyString] !== undefined;
|
let hasFiltersWithKeyString = this.persistedFilters[keyString] !== undefined;
|
||||||
let mutateFilters = false;
|
let mutateFilters = false;
|
||||||
let childObject = {
|
let childObject = {
|
||||||
|
73
src/plugins/imagery/ImageryTimestripViewProvider.js
Normal file
73
src/plugins/imagery/ImageryTimestripViewProvider.js
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* Open MCT includes source code licensed under additional open source
|
||||||
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
* this source code distribution or the Licensing information page available
|
||||||
|
* at runtime from the About dialog for additional information.
|
||||||
|
*****************************************************************************/
|
||||||
|
import ImageryTimeView from './components/ImageryTimeView.vue';
|
||||||
|
import Vue from "vue";
|
||||||
|
|
||||||
|
export default function ImageryTimestripViewProvider(openmct) {
|
||||||
|
const type = 'example.imagery.time-strip.view';
|
||||||
|
|
||||||
|
function hasImageTelemetry(domainObject) {
|
||||||
|
const metadata = openmct.telemetry.getMetadata(domainObject);
|
||||||
|
if (!metadata) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return metadata.valuesForHints(['image']).length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: type,
|
||||||
|
name: 'Imagery Timestrip View',
|
||||||
|
cssClass: 'icon-image',
|
||||||
|
canView: function (domainObject, objectPath) {
|
||||||
|
let isChildOfTimeStrip = objectPath.find(object => object.type === 'time-strip');
|
||||||
|
|
||||||
|
return hasImageTelemetry(domainObject) && isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath);
|
||||||
|
},
|
||||||
|
view: function (domainObject, objectPath) {
|
||||||
|
let component;
|
||||||
|
|
||||||
|
return {
|
||||||
|
show: function (element) {
|
||||||
|
component = new Vue({
|
||||||
|
el: element,
|
||||||
|
components: {
|
||||||
|
ImageryTimeView
|
||||||
|
},
|
||||||
|
provide: {
|
||||||
|
openmct: openmct,
|
||||||
|
domainObject: domainObject,
|
||||||
|
objectPath: objectPath
|
||||||
|
},
|
||||||
|
template: '<imagery-time-view></imagery-time-view>'
|
||||||
|
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
destroy: function () {
|
||||||
|
component.$destroy();
|
||||||
|
component = undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import ImageryViewLayout from './components/ImageryViewLayout.vue';
|
import ImageryViewComponent from './components/ImageryView.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: {
|
||||||
ImageryViewLayout
|
'imagery-view': ImageryViewComponent
|
||||||
},
|
},
|
||||||
provide: {
|
provide: {
|
||||||
openmct: this.openmct,
|
openmct: this.openmct,
|
||||||
@ -22,7 +22,8 @@ export default class ImageryView {
|
|||||||
objectPath: this.objectPath,
|
objectPath: this.objectPath,
|
||||||
currentView: this
|
currentView: this
|
||||||
},
|
},
|
||||||
template: '<imagery-view-layout ref="ImageryLayout"></imagery-view-layout>'
|
template: '<imagery-view ref="ImageryContainer"></imagery-view>'
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,8 +37,10 @@ export default function ImageryViewProvider(openmct) {
|
|||||||
key: type,
|
key: type,
|
||||||
name: 'Imagery Layout',
|
name: 'Imagery Layout',
|
||||||
cssClass: 'icon-image',
|
cssClass: 'icon-image',
|
||||||
canView: function (domainObject) {
|
canView: function (domainObject, objectPath) {
|
||||||
return hasImageTelemetry(domainObject);
|
let isChildOfTimeStrip = objectPath.find(object => object.type === 'time-strip');
|
||||||
|
|
||||||
|
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);
|
||||||
|
475
src/plugins/imagery/components/ImageryTimeView.vue
Normal file
475
src/plugins/imagery/components/ImageryTimeView.vue
Normal file
@ -0,0 +1,475 @@
|
|||||||
|
<!--
|
||||||
|
Open MCT, Copyright (c) 2014-2021, United States Government
|
||||||
|
as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
Administration. All rights reserved.
|
||||||
|
|
||||||
|
Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
"License"); you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
License for the specific language governing permissions and limitations
|
||||||
|
under the License.
|
||||||
|
|
||||||
|
Open MCT includes source code licensed under additional open source
|
||||||
|
licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
this source code distribution or the Licensing information page available
|
||||||
|
at runtime from the About dialog for additional information.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div ref="imagery"
|
||||||
|
class="c-imagery-tsv c-timeline-holder"
|
||||||
|
>
|
||||||
|
<div ref="imageryHolder"
|
||||||
|
class="c-imagery-tsv__contents u-contents"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import * as d3Scale from 'd3-scale';
|
||||||
|
import SwimLane from "@/ui/components/swim-lane/SwimLane.vue";
|
||||||
|
import Vue from "vue";
|
||||||
|
import imageryData from "../../imagery/mixins/imageryData";
|
||||||
|
import PreviewAction from "@/ui/preview/PreviewAction";
|
||||||
|
import _ from "lodash";
|
||||||
|
|
||||||
|
const PADDING = 1;
|
||||||
|
const ROW_HEIGHT = 100;
|
||||||
|
const IMAGE_WIDTH_THRESHOLD = 40;
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [imageryData],
|
||||||
|
inject: ['openmct', 'domainObject', 'objectPath'],
|
||||||
|
data() {
|
||||||
|
let timeSystem = this.openmct.time.timeSystem();
|
||||||
|
this.metadata = {};
|
||||||
|
this.requestCount = 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
viewBounds: undefined,
|
||||||
|
height: 0,
|
||||||
|
durationFormatter: undefined,
|
||||||
|
imageHistory: [],
|
||||||
|
timeSystem: timeSystem,
|
||||||
|
keyString: undefined
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
imageHistorySize() {
|
||||||
|
return this.imageHistory.length;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
imageHistorySize(newSize, oldSize) {
|
||||||
|
this.updatePlotImagery();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.previewAction = new PreviewAction(this.openmct);
|
||||||
|
|
||||||
|
this.canvas = this.$refs.imagery.appendChild(document.createElement('canvas'));
|
||||||
|
this.canvas.height = 0;
|
||||||
|
this.canvasContext = this.canvas.getContext('2d');
|
||||||
|
this.setDimensions();
|
||||||
|
|
||||||
|
this.updateViewBounds();
|
||||||
|
|
||||||
|
this.openmct.time.on("timeSystem", this.setScaleAndPlotImagery);
|
||||||
|
this.openmct.time.on("bounds", this.updateViewBounds);
|
||||||
|
|
||||||
|
this.resize = _.debounce(this.resize, 400);
|
||||||
|
this.imageryStripResizeObserver = new ResizeObserver(this.resize);
|
||||||
|
this.imageryStripResizeObserver.observe(this.$refs.imagery);
|
||||||
|
|
||||||
|
this.unlisten = this.openmct.objects.observe(this.domainObject, '*', this.observeForChanges);
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
if (this.unsubscribe) {
|
||||||
|
this.unsubscribe();
|
||||||
|
delete this.unsubscribe;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.imageryStripResizeObserver) {
|
||||||
|
this.imageryStripResizeObserver.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.openmct.time.off("timeSystem", this.setScaleAndPlotImagery);
|
||||||
|
this.openmct.time.off("bounds", this.updateViewBounds);
|
||||||
|
if (this.unlisten) {
|
||||||
|
this.unlisten();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
expand(index) {
|
||||||
|
const path = this.objectPath[0];
|
||||||
|
this.previewAction.invoke([path]);
|
||||||
|
},
|
||||||
|
observeForChanges(mutatedObject) {
|
||||||
|
this.updateViewBounds();
|
||||||
|
},
|
||||||
|
resize() {
|
||||||
|
let clientWidth = this.getClientWidth();
|
||||||
|
if (clientWidth !== this.width) {
|
||||||
|
this.setDimensions();
|
||||||
|
this.updateViewBounds();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getClientWidth() {
|
||||||
|
let clientWidth = this.$refs.imagery.clientWidth;
|
||||||
|
|
||||||
|
if (!clientWidth) {
|
||||||
|
//this is a hack - need a better way to find the parent of this component
|
||||||
|
let parent = this.openmct.layout.$refs.browseObject.$el;
|
||||||
|
if (parent) {
|
||||||
|
clientWidth = parent.getBoundingClientRect().width;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return clientWidth;
|
||||||
|
},
|
||||||
|
updateViewBounds(bounds, isTick) {
|
||||||
|
this.viewBounds = this.openmct.time.bounds();
|
||||||
|
//Add a 50% padding to the end bounds to look ahead
|
||||||
|
let timespan = (this.viewBounds.end - this.viewBounds.start);
|
||||||
|
let padding = timespan / 2;
|
||||||
|
this.viewBounds.end = this.viewBounds.end + padding;
|
||||||
|
|
||||||
|
if (this.timeSystem === undefined) {
|
||||||
|
this.timeSystem = this.openmct.time.timeSystem();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setScaleAndPlotImagery(this.timeSystem, !isTick);
|
||||||
|
|
||||||
|
},
|
||||||
|
setScaleAndPlotImagery(timeSystem, clearAllImagery) {
|
||||||
|
if (timeSystem !== undefined) {
|
||||||
|
this.timeSystem = timeSystem;
|
||||||
|
this.timeFormatter = this.getFormatter(this.timeSystem.key);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setScale(this.timeSystem);
|
||||||
|
this.updatePlotImagery(clearAllImagery);
|
||||||
|
},
|
||||||
|
getFormatter(key) {
|
||||||
|
const metadata = this.openmct.telemetry.getMetadata(this.domainObject);
|
||||||
|
|
||||||
|
let metadataValue = metadata.value(key) || { format: key };
|
||||||
|
let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);
|
||||||
|
|
||||||
|
return valueFormatter;
|
||||||
|
},
|
||||||
|
updatePlotImagery(clearAllImagery) {
|
||||||
|
this.clearPreviousImagery(clearAllImagery);
|
||||||
|
if (this.xScale) {
|
||||||
|
this.drawImagery();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
clearPreviousImagery(clearAllImagery) {
|
||||||
|
//TODO: Only clear items that are out of bounds
|
||||||
|
let noItemsEl = this.$el.querySelectorAll(".c-imagery-tsv__no-items");
|
||||||
|
noItemsEl.forEach(item => {
|
||||||
|
item.remove();
|
||||||
|
});
|
||||||
|
let imagery = this.$el.querySelectorAll(".c-imagery-tsv__image-wrapper");
|
||||||
|
imagery.forEach(item => {
|
||||||
|
if (clearAllImagery) {
|
||||||
|
item.remove();
|
||||||
|
} else {
|
||||||
|
const id = this.getNSAttributesForElement(item, 'id');
|
||||||
|
if (id) {
|
||||||
|
const timestamp = id.replace('id-', '');
|
||||||
|
if (!this.isImageryInBounds({
|
||||||
|
time: timestamp
|
||||||
|
})) {
|
||||||
|
item.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setDimensions() {
|
||||||
|
const imageryHolder = this.$refs.imagery;
|
||||||
|
this.width = this.getClientWidth();
|
||||||
|
|
||||||
|
this.height = Math.round(imageryHolder.getBoundingClientRect().height);
|
||||||
|
},
|
||||||
|
setScale(timeSystem) {
|
||||||
|
if (!this.width) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeSystem === undefined) {
|
||||||
|
timeSystem = this.openmct.time.timeSystem();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeSystem.isUTCBased) {
|
||||||
|
this.xScale = d3Scale.scaleUtc();
|
||||||
|
this.xScale.domain(
|
||||||
|
[new Date(this.viewBounds.start), new Date(this.viewBounds.end)]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.xScale = d3Scale.scaleLinear();
|
||||||
|
this.xScale.domain(
|
||||||
|
[this.viewBounds.start, this.viewBounds.end]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.xScale.range([PADDING, this.width - PADDING * 2]);
|
||||||
|
},
|
||||||
|
isImageryInBounds(imageObj) {
|
||||||
|
return (imageObj.time < this.viewBounds.end) && (imageObj.time > this.viewBounds.start);
|
||||||
|
},
|
||||||
|
getImageryContainer() {
|
||||||
|
let svgHeight = 100;
|
||||||
|
let svgWidth = this.imageHistory.length ? this.width : 200;
|
||||||
|
let groupSVG;
|
||||||
|
|
||||||
|
let existingSVG = this.$el.querySelector(".c-imagery-tsv__contents svg");
|
||||||
|
if (existingSVG) {
|
||||||
|
groupSVG = existingSVG;
|
||||||
|
this.setNSAttributesForElement(groupSVG, {
|
||||||
|
width: svgWidth
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
let component = new Vue({
|
||||||
|
components: {
|
||||||
|
SwimLane
|
||||||
|
},
|
||||||
|
provide: {
|
||||||
|
openmct: this.openmct
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isNested: true,
|
||||||
|
height: svgHeight,
|
||||||
|
width: svgWidth
|
||||||
|
};
|
||||||
|
},
|
||||||
|
template: `<swim-lane :is-nested="isNested" :hide-label="true"><template slot="object"><svg class="c-imagery-tsv-container" :height="height" :width="width"></svg></template></swim-lane>`
|
||||||
|
});
|
||||||
|
|
||||||
|
this.$refs.imageryHolder.appendChild(component.$mount().$el);
|
||||||
|
|
||||||
|
groupSVG = component.$el.querySelector('svg');
|
||||||
|
|
||||||
|
groupSVG.addEventListener('mouseout', (event) => {
|
||||||
|
if (event.target.nodeName === 'svg' || event.target.nodeName === 'use') {
|
||||||
|
this.removeFromForeground();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return groupSVG;
|
||||||
|
},
|
||||||
|
isImageryWidthAcceptable() {
|
||||||
|
// We're calculating if there is enough space between images to show the thumbnails.
|
||||||
|
// This algorithm could probably be enhanced to check the x co-ordinate distance between 2 consecutive images, but
|
||||||
|
// we will go with this for now assuming imagery is not sorted by asc time so it's difficult to calculate.
|
||||||
|
// TODO: Use telemetry.requestCollection to get sorted telemetry
|
||||||
|
const currentStart = this.viewBounds.start;
|
||||||
|
const currentEnd = this.viewBounds.end;
|
||||||
|
const rectX = this.xScale(currentStart);
|
||||||
|
const rectY = this.xScale(currentEnd);
|
||||||
|
const imageContainerWidth = this.imageHistory.length ? (rectY - rectX) / this.imageHistory.length : 0;
|
||||||
|
|
||||||
|
return imageContainerWidth < IMAGE_WIDTH_THRESHOLD;
|
||||||
|
},
|
||||||
|
drawImagery() {
|
||||||
|
let groupSVG = this.getImageryContainer();
|
||||||
|
const showImagePlaceholders = this.isImageryWidthAcceptable();
|
||||||
|
|
||||||
|
if (this.imageHistory.length) {
|
||||||
|
this.imageHistory.forEach((currentImageObject, index) => {
|
||||||
|
if (this.isImageryInBounds(currentImageObject)) {
|
||||||
|
this.plotImagery(currentImageObject, showImagePlaceholders, groupSVG, index);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.plotNoItems(groupSVG);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plotNoItems(svgElement) {
|
||||||
|
let textElement = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
||||||
|
this.setNSAttributesForElement(textElement, {
|
||||||
|
x: "10",
|
||||||
|
y: "20",
|
||||||
|
class: "c-imagery-tsv__no-items"
|
||||||
|
});
|
||||||
|
textElement.innerHTML = 'No images within timeframe';
|
||||||
|
|
||||||
|
svgElement.appendChild(textElement);
|
||||||
|
},
|
||||||
|
setNSAttributesForElement(element, attributes) {
|
||||||
|
Object.keys(attributes).forEach((key) => {
|
||||||
|
if (key === 'url') {
|
||||||
|
element.setAttributeNS('http://www.w3.org/1999/xlink', 'href', attributes[key]);
|
||||||
|
} else {
|
||||||
|
element.setAttributeNS(null, key, attributes[key]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getNSAttributesForElement(element, attribute) {
|
||||||
|
return element.getAttributeNS(null, attribute);
|
||||||
|
},
|
||||||
|
getImageWrapper(item) {
|
||||||
|
const id = `id-${item.time}`;
|
||||||
|
|
||||||
|
return this.$el.querySelector(`.c-imagery-tsv__contents g[id=${id}]`);
|
||||||
|
},
|
||||||
|
plotImagery(item, showImagePlaceholders, svgElement, index) {
|
||||||
|
//TODO: Placeholder image
|
||||||
|
let existingImageWrapper = this.getImageWrapper(item);
|
||||||
|
//imageWrapper wraps the vertical tick rect and the image
|
||||||
|
if (existingImageWrapper) {
|
||||||
|
this.updateExistingImageWrapper(existingImageWrapper, item, showImagePlaceholders);
|
||||||
|
} else {
|
||||||
|
let imageWrapper = this.createImageWrapper(index, item, showImagePlaceholders, svgElement);
|
||||||
|
svgElement.appendChild(imageWrapper);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateExistingImageWrapper(existingImageWrapper, item, showImagePlaceholders) {
|
||||||
|
//Update the x co-ordinates of the handle and image elements and the url of image
|
||||||
|
//this is to avoid tearing down all elements completely and re-drawing them
|
||||||
|
this.setNSAttributesForElement(existingImageWrapper, {
|
||||||
|
showImagePlaceholders
|
||||||
|
});
|
||||||
|
let imageTickElement = existingImageWrapper.querySelector('rect.c-imagery-tsv__image-handle');
|
||||||
|
this.setNSAttributesForElement(imageTickElement, {
|
||||||
|
x: this.xScale(item.time)
|
||||||
|
});
|
||||||
|
|
||||||
|
let imageRect = existingImageWrapper.querySelector('rect.c-imagery-tsv__image-placeholder');
|
||||||
|
this.setNSAttributesForElement(imageRect, {
|
||||||
|
x: this.xScale(item.time) + 2
|
||||||
|
});
|
||||||
|
|
||||||
|
let imageElement = existingImageWrapper.querySelector('image');
|
||||||
|
const selector = `href*=${existingImageWrapper.id}`;
|
||||||
|
let hoverEl = this.$el.querySelector(`.c-imagery-tsv__contents use[${selector}]`);
|
||||||
|
const hideImageUrl = (showImagePlaceholders && !hoverEl);
|
||||||
|
this.setNSAttributesForElement(imageElement, {
|
||||||
|
x: this.xScale(item.time) + 2,
|
||||||
|
url: hideImageUrl ? '' : item.url
|
||||||
|
});
|
||||||
|
},
|
||||||
|
createImageWrapper(index, item, showImagePlaceholders, svgElement) {
|
||||||
|
const id = `id-${item.time}`;
|
||||||
|
const imgSize = String(ROW_HEIGHT - 15);
|
||||||
|
let imageWrapper = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
||||||
|
this.setNSAttributesForElement(imageWrapper, {
|
||||||
|
id,
|
||||||
|
class: 'c-imagery-tsv__image-wrapper',
|
||||||
|
showImagePlaceholders
|
||||||
|
});
|
||||||
|
//create image tick indicator
|
||||||
|
let imageTickElement = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
||||||
|
this.setNSAttributesForElement(imageTickElement, {
|
||||||
|
class: 'c-imagery-tsv__image-handle',
|
||||||
|
x: this.xScale(item.time),
|
||||||
|
y: 5,
|
||||||
|
rx: 0,
|
||||||
|
width: 2,
|
||||||
|
height: String(ROW_HEIGHT - 10)
|
||||||
|
});
|
||||||
|
imageWrapper.appendChild(imageTickElement);
|
||||||
|
|
||||||
|
let imageRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
||||||
|
this.setNSAttributesForElement(imageRect, {
|
||||||
|
class: 'c-imagery-tsv__image-placeholder',
|
||||||
|
x: this.xScale(item.time) + 2,
|
||||||
|
y: 10,
|
||||||
|
rx: 0,
|
||||||
|
width: imgSize,
|
||||||
|
height: imgSize,
|
||||||
|
mask: `#image-${item.time}`
|
||||||
|
});
|
||||||
|
imageWrapper.appendChild(imageRect);
|
||||||
|
|
||||||
|
let imageElement = document.createElementNS('http://www.w3.org/2000/svg', 'image');
|
||||||
|
this.setNSAttributesForElement(imageElement, {
|
||||||
|
id: `image-${item.time}`,
|
||||||
|
x: this.xScale(item.time) + 2,
|
||||||
|
y: 10,
|
||||||
|
rx: 0,
|
||||||
|
width: imgSize,
|
||||||
|
height: imgSize,
|
||||||
|
url: showImagePlaceholders ? '' : item.url
|
||||||
|
});
|
||||||
|
imageWrapper.appendChild(imageElement);
|
||||||
|
|
||||||
|
//TODO: Don't add the hover listener if the width is too small
|
||||||
|
imageWrapper.addEventListener('mouseover', this.bringToForeground.bind(this, svgElement, imageWrapper, index, item.url));
|
||||||
|
|
||||||
|
return imageWrapper;
|
||||||
|
},
|
||||||
|
bringToForeground(svgElement, imageWrapper, index, url, event) {
|
||||||
|
const selector = `href*=${imageWrapper.id}`;
|
||||||
|
let hoverEls = this.$el.querySelectorAll(`.c-imagery-tsv__contents use:not([${selector}])`);
|
||||||
|
if (hoverEls.length > 0) {
|
||||||
|
this.removeFromForeground(hoverEls);
|
||||||
|
}
|
||||||
|
|
||||||
|
hoverEls = this.$el.querySelectorAll(`.c-imagery-tsv__contents use[${selector}]`);
|
||||||
|
if (hoverEls.length) {
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let imageElement = imageWrapper.querySelector('image');
|
||||||
|
this.setNSAttributesForElement(imageElement, {
|
||||||
|
url: url,
|
||||||
|
fill: 'none'
|
||||||
|
});
|
||||||
|
let hoverElement = document.createElementNS('http://www.w3.org/2000/svg', 'use');
|
||||||
|
this.setNSAttributesForElement(hoverElement, {
|
||||||
|
class: 'image-highlight',
|
||||||
|
x: 0,
|
||||||
|
href: `#${imageWrapper.id}`
|
||||||
|
});
|
||||||
|
this.setNSAttributesForElement(imageWrapper, {
|
||||||
|
class: 'c-imagery-tsv__image-wrapper is-hovered'
|
||||||
|
});
|
||||||
|
// We're using mousedown here and not 'click' because 'click' doesn't seem to be triggered reliably
|
||||||
|
hoverElement.addEventListener('mousedown', (e) => {
|
||||||
|
if (e.button === 0) {
|
||||||
|
this.expand(index);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
svgElement.appendChild(hoverElement);
|
||||||
|
|
||||||
|
},
|
||||||
|
removeFromForeground(items) {
|
||||||
|
let hoverEls;
|
||||||
|
if (items) {
|
||||||
|
hoverEls = items;
|
||||||
|
} else {
|
||||||
|
hoverEls = this.$el.querySelectorAll(".c-imagery-tsv__contents use");
|
||||||
|
}
|
||||||
|
|
||||||
|
hoverEls.forEach(item => {
|
||||||
|
let selector = `id*=${this.getNSAttributesForElement(item, 'href').replace('#', '')}`;
|
||||||
|
let imageWrapper = this.$el.querySelector(`.c-imagery-tsv__contents g[${selector}]`);
|
||||||
|
this.setNSAttributesForElement(imageWrapper, {
|
||||||
|
class: 'c-imagery-tsv__image-wrapper'
|
||||||
|
});
|
||||||
|
let showImagePlaceholders = this.getNSAttributesForElement(imageWrapper, 'showImagePlaceholders');
|
||||||
|
if (showImagePlaceholders === 'true') {
|
||||||
|
let imageElement = imageWrapper.querySelector('image');
|
||||||
|
this.setNSAttributesForElement(imageElement, {
|
||||||
|
url: ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
item.remove();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
@ -129,12 +129,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="c-imagery__thumbs-wrapper"
|
||||||
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 }
|
]"
|
||||||
]"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
ref="thumbsWrapper"
|
ref="thumbsWrapper"
|
||||||
@ -175,7 +174,8 @@ 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';
|
||||||
|
|
||||||
const DEFAULT_DURATION_FORMATTER = 'duration';
|
import imageryData from "../../imagery/mixins/imageryData";
|
||||||
|
|
||||||
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,30 +197,29 @@ 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 {
|
||||||
autoScroll: true,
|
|
||||||
durationFormatter: undefined,
|
durationFormatter: undefined,
|
||||||
|
imageHistory: [],
|
||||||
|
timeSystem: timeSystem,
|
||||||
|
keyString: undefined,
|
||||||
|
autoScroll: true,
|
||||||
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,
|
||||||
@ -231,6 +230,9 @@ 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) {
|
||||||
@ -258,9 +260,6 @@ 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;
|
||||||
|
|
||||||
@ -383,6 +382,10 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
imageHistorySize(newSize, oldSize) {
|
||||||
|
this.setFocusedImage(newSize - 1, false);
|
||||||
|
this.scrollToRight();
|
||||||
|
},
|
||||||
focusedImageIndex() {
|
focusedImageIndex() {
|
||||||
this.trackDuration();
|
this.trackDuration();
|
||||||
this.resetAgeCSS();
|
this.resetAgeCSS();
|
||||||
@ -391,18 +394,9 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
// listen
|
//listen
|
||||||
this.openmct.time.on('bounds', this.boundsChange);
|
this.openmct.time.on('timeSystem', this.trackDuration);
|
||||||
this.openmct.time.on('timeSystem', this.timeSystemChange);
|
this.openmct.time.on('clock', this.trackDuration);
|
||||||
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'];
|
||||||
@ -410,56 +404,49 @@ 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();
|
||||||
this.updateRelatedTelemetryForFocusedImage();
|
await 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
|
||||||
_.debounce(this.updateRelatedTelemetryForFocusedImage, 400);
|
this.updateRelatedTelemetryForFocusedImage = _.debounce(this.updateRelatedTelemetryForFocusedImage, 400);
|
||||||
_.debounce(this.resizeImageContainer, 400);
|
|
||||||
|
|
||||||
this.imageContainerResizeObserver = new ResizeObserver(this.resizeImageContainer);
|
// for resizing the object view
|
||||||
this.imageContainerResizeObserver.observe(this.$refs.imageBG);
|
this.resizeImageContainer = _.debounce(this.resizeImageContainer, 400);
|
||||||
|
|
||||||
|
if (this.$refs.imageBG) {
|
||||||
|
this.imageContainerResizeObserver = new ResizeObserver(this.resizeImageContainer);
|
||||||
|
this.imageContainerResizeObserver.observe(this.$refs.imageBG);
|
||||||
|
}
|
||||||
|
|
||||||
// For adjusting scroll bar size and position when resizing thumbs wrapper
|
// 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.observe(this.$refs.thumbsWrapper);
|
||||||
|
}
|
||||||
|
|
||||||
this.thumbWrapperResizeObserver = new ResizeObserver(this.handleThumbWindowResizeStart);
|
|
||||||
this.thumbWrapperResizeObserver.observe(this.$refs.thumbsWrapper);
|
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
if (this.unsubscribe) {
|
this.openmct.time.off('timeSystem', this.trackDuration);
|
||||||
this.unsubscribe();
|
this.openmct.time.off('clock', this.trackDuration);
|
||||||
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) {
|
||||||
@ -576,56 +563,6 @@ 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) {
|
||||||
@ -683,6 +620,10 @@ 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;
|
||||||
}
|
}
|
||||||
@ -693,70 +634,6 @@ 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();
|
||||||
@ -876,6 +753,10 @@ 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;
|
||||||
}
|
}
|
@ -312,3 +312,34 @@
|
|||||||
@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);
|
||||||
|
}
|
||||||
|
}
|
174
src/plugins/imagery/mixins/imageryData.js
Normal file
174
src/plugins/imagery/mixins/imageryData.js
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* Open MCT includes source code licensed under additional open source
|
||||||
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
* this source code distribution or the Licensing information page available
|
||||||
|
* at runtime from the About dialog for additional information.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
const DEFAULT_DURATION_FORMATTER = 'duration';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
inject: ['openmct', 'domainObject', 'objectPath'],
|
||||||
|
mounted() {
|
||||||
|
// listen
|
||||||
|
this.openmct.time.on('bounds', this.boundsChange);
|
||||||
|
this.openmct.time.on('timeSystem', this.timeSystemChange);
|
||||||
|
|
||||||
|
// set
|
||||||
|
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||||
|
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
|
||||||
|
this.imageHints = { ...this.metadata.valuesForHints(['image'])[0] };
|
||||||
|
this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
|
||||||
|
this.imageFormatter = this.openmct.telemetry.getValueFormatter(this.imageHints);
|
||||||
|
this.imageDownloadNameHints = { ...this.metadata.valuesForHints(['imageDownloadName'])[0]};
|
||||||
|
|
||||||
|
// initialize
|
||||||
|
this.timeKey = this.timeSystem.key;
|
||||||
|
this.timeFormatter = this.getFormatter(this.timeKey);
|
||||||
|
|
||||||
|
// kickoff
|
||||||
|
this.subscribe();
|
||||||
|
this.requestHistory();
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
if (this.unsubscribe) {
|
||||||
|
this.unsubscribe();
|
||||||
|
delete this.unsubscribe;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.openmct.time.off('bounds', this.boundsChange);
|
||||||
|
this.openmct.time.off('timeSystem', this.timeSystemChange);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
datumIsNotValid(datum) {
|
||||||
|
if (this.imageHistory.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const datumURL = this.formatImageUrl(datum);
|
||||||
|
const lastHistoryURL = this.formatImageUrl(this.imageHistory.slice(-1)[0]);
|
||||||
|
|
||||||
|
// datum is not valid if it matches the last datum in history,
|
||||||
|
// or it is before the last datum in the history
|
||||||
|
const datumTimeCheck = this.parseTime(datum);
|
||||||
|
const historyTimeCheck = this.parseTime(this.imageHistory.slice(-1)[0]);
|
||||||
|
const matchesLast = (datumTimeCheck === historyTimeCheck) && (datumURL === lastHistoryURL);
|
||||||
|
const isStale = datumTimeCheck < historyTimeCheck;
|
||||||
|
|
||||||
|
return matchesLast || isStale;
|
||||||
|
},
|
||||||
|
formatImageUrl(datum) {
|
||||||
|
if (!datum) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.imageFormatter.format(datum);
|
||||||
|
},
|
||||||
|
formatTime(datum) {
|
||||||
|
if (!datum) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let dateTimeStr = this.timeFormatter.format(datum);
|
||||||
|
|
||||||
|
// Replace ISO "T" with a space to allow wrapping
|
||||||
|
return dateTimeStr.replace("T", " ");
|
||||||
|
},
|
||||||
|
getImageDownloadName(datum) {
|
||||||
|
let imageDownloadName = '';
|
||||||
|
if (datum) {
|
||||||
|
const key = this.imageDownloadNameHints.key;
|
||||||
|
imageDownloadName = datum[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
return imageDownloadName;
|
||||||
|
},
|
||||||
|
parseTime(datum) {
|
||||||
|
if (!datum) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.timeFormatter.parse(datum);
|
||||||
|
},
|
||||||
|
boundsChange(bounds, isTick) {
|
||||||
|
if (!isTick) {
|
||||||
|
this.requestHistory();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async requestHistory() {
|
||||||
|
let bounds = this.openmct.time.bounds();
|
||||||
|
this.requestCount++;
|
||||||
|
const requestId = this.requestCount;
|
||||||
|
this.imageHistory = [];
|
||||||
|
|
||||||
|
let data = await this.openmct.telemetry
|
||||||
|
.request(this.domainObject, bounds) || [];
|
||||||
|
|
||||||
|
if (this.requestCount === requestId) {
|
||||||
|
let imagery = [];
|
||||||
|
data.forEach((datum) => {
|
||||||
|
let image = this.normalizeDatum(datum);
|
||||||
|
if (image) {
|
||||||
|
imagery.push(image);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
//this is to optimize anything that reacts to imageHistory length
|
||||||
|
this.imageHistory = imagery;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
timeSystemChange() {
|
||||||
|
this.timeSystem = this.openmct.time.timeSystem();
|
||||||
|
this.timeKey = this.timeSystem.key;
|
||||||
|
this.timeFormatter = this.getFormatter(this.timeKey);
|
||||||
|
this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
|
||||||
|
},
|
||||||
|
subscribe() {
|
||||||
|
this.unsubscribe = this.openmct.telemetry
|
||||||
|
.subscribe(this.domainObject, (datum) => {
|
||||||
|
let parsedTimestamp = this.parseTime(datum);
|
||||||
|
let bounds = this.openmct.time.bounds();
|
||||||
|
|
||||||
|
if (parsedTimestamp >= bounds.start && parsedTimestamp <= bounds.end) {
|
||||||
|
let image = this.normalizeDatum(datum);
|
||||||
|
if (image) {
|
||||||
|
this.imageHistory.push(image);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
normalizeDatum(datum) {
|
||||||
|
if (this.datumIsNotValid(datum)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let image = { ...datum };
|
||||||
|
image.formattedTime = this.formatTime(datum);
|
||||||
|
image.url = this.formatImageUrl(datum);
|
||||||
|
image.time = this.parseTime(image.formattedTime);
|
||||||
|
image.imageDownloadName = this.getImageDownloadName(datum);
|
||||||
|
|
||||||
|
return image;
|
||||||
|
},
|
||||||
|
getFormatter(key) {
|
||||||
|
let metadataValue = this.metadata.value(key) || { format: key };
|
||||||
|
let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);
|
||||||
|
|
||||||
|
return valueFormatter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
@ -21,10 +21,12 @@
|
|||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
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));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,12 +84,14 @@ function generateTelemetry(start, count) {
|
|||||||
return telemetry;
|
return telemetry;
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("The Imagery View Layout", () => {
|
describe("The Imagery View Layouts", () => {
|
||||||
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;
|
||||||
@ -116,51 +118,51 @@ describe("The Imagery View Layout", () => {
|
|||||||
"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,6 +222,8 @@ describe("The Imagery View Layout", () => {
|
|||||||
spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([]));
|
spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([]));
|
||||||
spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({}));
|
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);
|
||||||
});
|
});
|
||||||
@ -229,10 +233,34 @@ describe("The Imagery View Layout", () => {
|
|||||||
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(
|
||||||
@ -242,6 +270,46 @@ describe("The Imagery View Layout", () => {
|
|||||||
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;
|
||||||
@ -367,18 +435,18 @@ describe("The Imagery View Layout", () => {
|
|||||||
});
|
});
|
||||||
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.ImageryLayout.autoScroll = false;
|
imageryView._getInstance().$refs.ImageryContainer.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.ImageryLayout, 'scrollToRight');
|
spyOn(imageryView._getInstance().$refs.ImageryContainer, 'scrollToRight');
|
||||||
imageryView._getInstance().$refs.ImageryLayout.autoScroll = false;
|
imageryView._getInstance().$refs.ImageryContainer.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.ImageryLayout.scrollToRight).toHaveBeenCalledWith('reset');
|
expect(imageryView._getInstance().$refs.ImageryContainer.scrollToRight).toHaveBeenCalledWith('reset');
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -140,6 +140,7 @@ import SearchResults from './SearchResults.vue';
|
|||||||
import Sidebar from './Sidebar.vue';
|
import Sidebar from './Sidebar.vue';
|
||||||
import { clearDefaultNotebook, getDefaultNotebook, setDefaultNotebook, setDefaultNotebookSectionId, setDefaultNotebookPageId } from '../utils/notebook-storage';
|
import { clearDefaultNotebook, getDefaultNotebook, setDefaultNotebook, setDefaultNotebookSectionId, setDefaultNotebookPageId } from '../utils/notebook-storage';
|
||||||
import { addNotebookEntry, createNewEmbed, getEntryPosById, getNotebookEntries, mutateObject } from '../utils/notebook-entries';
|
import { addNotebookEntry, createNewEmbed, getEntryPosById, getNotebookEntries, mutateObject } from '../utils/notebook-entries';
|
||||||
|
import { saveNotebookImageDomainObject, updateNamespaceOfDomainObject } from '../utils/notebook-image';
|
||||||
import { NOTEBOOK_VIEW_TYPE } from '../notebook-constants';
|
import { NOTEBOOK_VIEW_TYPE } from '../notebook-constants';
|
||||||
import objectUtils from 'objectUtils';
|
import objectUtils from 'objectUtils';
|
||||||
|
|
||||||
@ -385,9 +386,13 @@ export default {
|
|||||||
const snapshotId = event.dataTransfer.getData('openmct/snapshot/id');
|
const snapshotId = event.dataTransfer.getData('openmct/snapshot/id');
|
||||||
if (snapshotId.length) {
|
if (snapshotId.length) {
|
||||||
const snapshot = this.snapshotContainer.getSnapshot(snapshotId);
|
const snapshot = this.snapshotContainer.getSnapshot(snapshotId);
|
||||||
this.newEntry(snapshot);
|
this.newEntry(snapshot.embedObject);
|
||||||
this.snapshotContainer.removeSnapshot(snapshotId);
|
this.snapshotContainer.removeSnapshot(snapshotId);
|
||||||
|
|
||||||
|
const namespace = this.domainObject.identifier.namespace;
|
||||||
|
const notebookImageDomainObject = updateNamespaceOfDomainObject(snapshot.notebookImageDomainObject, namespace);
|
||||||
|
saveNotebookImageDomainObject(this.openmct, notebookImageDomainObject);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -451,12 +456,9 @@ export default {
|
|||||||
: undefined;
|
: undefined;
|
||||||
},
|
},
|
||||||
getDefaultNotebookObject() {
|
getDefaultNotebookObject() {
|
||||||
const oldNotebookStorage = getDefaultNotebook();
|
const defaultNotebook = getDefaultNotebook();
|
||||||
if (!oldNotebookStorage) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.openmct.objects.get(oldNotebookStorage.identifier);
|
return defaultNotebook && this.openmct.objects.get(defaultNotebook.identifier);
|
||||||
},
|
},
|
||||||
getLinktoNotebook() {
|
getLinktoNotebook() {
|
||||||
const objectPath = this.openmct.router.path;
|
const objectPath = this.openmct.router.path;
|
||||||
|
@ -40,7 +40,7 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
PopupMenu
|
PopupMenu
|
||||||
},
|
},
|
||||||
inject: ['openmct'],
|
inject: ['openmct', 'snapshotContainer'],
|
||||||
props: {
|
props: {
|
||||||
embed: {
|
embed: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@ -48,6 +48,12 @@ export default {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
isSnapshotContainer: {
|
||||||
|
type: Boolean,
|
||||||
|
default() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
removeActionString: {
|
removeActionString: {
|
||||||
type: String,
|
type: String,
|
||||||
default() {
|
default() {
|
||||||
@ -135,6 +141,14 @@ export default {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.isSnapshotContainer) {
|
||||||
|
const snapshot = this.snapshotContainer.getSnapshot(this.embed.id);
|
||||||
|
const fullSizeImageURL = snapshot.notebookImageDomainObject.configuration.fullSizeImageURL;
|
||||||
|
painterroInstance.show(fullSizeImageURL);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.openmct.objects.get(fullSizeImageObjectIdentifier)
|
this.openmct.objects.get(fullSizeImageObjectIdentifier)
|
||||||
.then(object => {
|
.then(object => {
|
||||||
painterroInstance.show(object.configuration.fullSizeImageURL);
|
painterroInstance.show(object.configuration.fullSizeImageURL);
|
||||||
@ -166,9 +180,13 @@ export default {
|
|||||||
this.openmct.notifications.alert(message);
|
this.openmct.notifications.alert(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
const relativeHash = hash.slice(hash.indexOf('#'));
|
if (this.openmct.editor.isEditing()) {
|
||||||
const url = new URL(relativeHash, `${location.protocol}//${location.host}${location.pathname}`);
|
this.previewEmbed();
|
||||||
this.openmct.router.navigate(url.hash);
|
} else {
|
||||||
|
const relativeHash = hash.slice(hash.indexOf('#'));
|
||||||
|
const url = new URL(relativeHash, `${location.protocol}//${location.host}${location.pathname}`);
|
||||||
|
this.openmct.router.navigate(url.hash);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
formatTime(unixTime, timeFormat) {
|
formatTime(unixTime, timeFormat) {
|
||||||
return Moment.utc(unixTime).format(timeFormat);
|
return Moment.utc(unixTime).format(timeFormat);
|
||||||
@ -190,6 +208,14 @@ export default {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.isSnapshotContainer) {
|
||||||
|
const snapshot = this.snapshotContainer.getSnapshot(this.embed.id);
|
||||||
|
const fullSizeImageURL = snapshot.notebookImageDomainObject.configuration.fullSizeImageURL;
|
||||||
|
this.openSnapshotOverlay(fullSizeImageURL);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.openmct.objects.get(fullSizeImageObjectIdentifier)
|
this.openmct.objects.get(fullSizeImageObjectIdentifier)
|
||||||
.then(object => {
|
.then(object => {
|
||||||
this.openSnapshotOverlay(object.configuration.fullSizeImageURL);
|
this.openSnapshotOverlay(object.configuration.fullSizeImageURL);
|
||||||
@ -259,8 +285,20 @@ export default {
|
|||||||
updateSnapshot(snapshotObject) {
|
updateSnapshot(snapshotObject) {
|
||||||
this.embed.snapshot.thumbnailImage = snapshotObject.thumbnailImage;
|
this.embed.snapshot.thumbnailImage = snapshotObject.thumbnailImage;
|
||||||
|
|
||||||
updateNotebookImageDomainObject(this.openmct, this.embed.snapshot.fullSizeImageObjectIdentifier, snapshotObject.fullSizeImage);
|
this.updateNotebookImageDomainObjectSnapshot(snapshotObject);
|
||||||
this.updateEmbed(this.embed);
|
this.updateEmbed(this.embed);
|
||||||
|
},
|
||||||
|
updateNotebookImageDomainObjectSnapshot(snapshotObject) {
|
||||||
|
if (this.isSnapshotContainer) {
|
||||||
|
const snapshot = this.snapshotContainer.getSnapshot(this.embed.id);
|
||||||
|
|
||||||
|
snapshot.embedObject.snapshot.thumbnailImage = snapshotObject.thumbnailImage;
|
||||||
|
snapshot.notebookImageDomainObject.configuration.fullSizeImageURL = snapshotObject.fullSizeImage.src;
|
||||||
|
|
||||||
|
this.snapshotContainer.updateSnapshot(snapshot);
|
||||||
|
} else {
|
||||||
|
updateNotebookImageDomainObject(this.openmct, this.embed.snapshot.fullSizeImageObjectIdentifier, snapshotObject.fullSizeImage);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -102,9 +102,11 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import NotebookEmbed from './NotebookEmbed.vue';
|
import NotebookEmbed from './NotebookEmbed.vue';
|
||||||
import { createNewEmbed } from '../utils/notebook-entries';
|
|
||||||
import Moment from 'moment';
|
|
||||||
import TextHighlight from '../../../utils/textHighlight/TextHighlight.vue';
|
import TextHighlight from '../../../utils/textHighlight/TextHighlight.vue';
|
||||||
|
import { createNewEmbed } from '../utils/notebook-entries';
|
||||||
|
import { saveNotebookImageDomainObject, updateNamespaceOfDomainObject } from '../utils/notebook-image';
|
||||||
|
|
||||||
|
import Moment from 'moment';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@ -210,8 +212,12 @@ export default {
|
|||||||
const snapshotId = $event.dataTransfer.getData('openmct/snapshot/id');
|
const snapshotId = $event.dataTransfer.getData('openmct/snapshot/id');
|
||||||
if (snapshotId.length) {
|
if (snapshotId.length) {
|
||||||
const snapshot = this.snapshotContainer.getSnapshot(snapshotId);
|
const snapshot = this.snapshotContainer.getSnapshot(snapshotId);
|
||||||
|
this.entry.embeds.push(snapshot.embedObject);
|
||||||
this.snapshotContainer.removeSnapshot(snapshotId);
|
this.snapshotContainer.removeSnapshot(snapshotId);
|
||||||
this.entry.embeds.push(snapshot);
|
|
||||||
|
const namespace = this.domainObject.identifier.namespace;
|
||||||
|
const notebookImageDomainObject = updateNamespaceOfDomainObject(snapshot.notebookImageDomainObject, namespace);
|
||||||
|
saveNotebookImageDomainObject(this.openmct, notebookImageDomainObject);
|
||||||
} else {
|
} else {
|
||||||
const data = $event.dataTransfer.getData('openmct/domain-object-path');
|
const data = $event.dataTransfer.getData('openmct/domain-object-path');
|
||||||
const objectPath = JSON.parse(data);
|
const objectPath = JSON.parse(data);
|
||||||
|
@ -17,19 +17,26 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Snapshot from '../snapshot';
|
import Snapshot from '../snapshot';
|
||||||
import { getDefaultNotebook, getNotebookSectionAndPage, validateNotebookStorageObject } from '../utils/notebook-storage';
|
import { getDefaultNotebook, validateNotebookStorageObject } from '../utils/notebook-storage';
|
||||||
import { NOTEBOOK_DEFAULT, NOTEBOOK_SNAPSHOT } from '../notebook-constants';
|
import { NOTEBOOK_DEFAULT, NOTEBOOK_SNAPSHOT } from '../notebook-constants';
|
||||||
|
import { getMenuItems } from '../utils/notebook-snapshot-menu';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
inject: ['openmct'],
|
inject: ['openmct'],
|
||||||
props: {
|
props: {
|
||||||
|
currentView: {
|
||||||
|
type: Object,
|
||||||
|
default() {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
},
|
||||||
domainObject: {
|
domainObject: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default() {
|
default() {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
ignoreLink: {
|
isPreview: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default() {
|
default() {
|
||||||
return false;
|
return false;
|
||||||
@ -50,51 +57,40 @@ export default {
|
|||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
validateNotebookStorageObject();
|
validateNotebookStorageObject();
|
||||||
this.getDefaultNotebookObject();
|
|
||||||
|
|
||||||
this.notebookSnapshot = new Snapshot(this.openmct);
|
this.notebookSnapshot = new Snapshot(this.openmct);
|
||||||
this.setDefaultNotebookStatus();
|
this.setDefaultNotebookStatus();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
getDefaultNotebookObject() {
|
getPreviewObjectLink() {
|
||||||
const defaultNotebook = getDefaultNotebook();
|
const relativePath = this.openmct.objects.getRelativePath(this.objectPath);
|
||||||
|
const urlParams = this.openmct.router.getParams();
|
||||||
|
urlParams.view = this.currentView.key;
|
||||||
|
|
||||||
return defaultNotebook && this.openmct.objects.get(defaultNotebook.identifier);
|
const urlParamsString = Object.entries(urlParams)
|
||||||
|
.map(([key, value]) => `${key}=${value}`)
|
||||||
|
.join('&');
|
||||||
|
|
||||||
|
return `#/browse/${relativePath}?${urlParamsString}`;
|
||||||
},
|
},
|
||||||
async showMenu(event) {
|
async showMenu(event) {
|
||||||
const notebookTypes = [];
|
const menuItemOptions = {
|
||||||
|
default: {
|
||||||
|
cssClass: 'icon-notebook',
|
||||||
|
name: `Save to Notebook`,
|
||||||
|
onItemClicked: () => this.snapshot(NOTEBOOK_DEFAULT, event.target)
|
||||||
|
},
|
||||||
|
snapshot: {
|
||||||
|
cssClass: 'icon-camera',
|
||||||
|
name: 'Save to Notebook Snapshots',
|
||||||
|
onItemClicked: () => this.snapshot(NOTEBOOK_SNAPSHOT, event.target)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const notebookTypes = await getMenuItems(this.openmct, menuItemOptions);
|
||||||
const elementBoundingClientRect = this.$el.getBoundingClientRect();
|
const elementBoundingClientRect = this.$el.getBoundingClientRect();
|
||||||
const x = elementBoundingClientRect.x;
|
const x = elementBoundingClientRect.x;
|
||||||
const y = elementBoundingClientRect.y + elementBoundingClientRect.height;
|
const y = elementBoundingClientRect.y + elementBoundingClientRect.height;
|
||||||
|
|
||||||
const defaultNotebookObject = await this.getDefaultNotebookObject();
|
|
||||||
if (defaultNotebookObject) {
|
|
||||||
const defaultNotebook = getDefaultNotebook();
|
|
||||||
const { section, page } = getNotebookSectionAndPage(defaultNotebookObject, defaultNotebook.defaultSectionId, defaultNotebook.defaultPageId);
|
|
||||||
if (section && page) {
|
|
||||||
const name = defaultNotebookObject.name;
|
|
||||||
const sectionName = section.name;
|
|
||||||
const pageName = page.name;
|
|
||||||
const defaultPath = `${name} - ${sectionName} - ${pageName}`;
|
|
||||||
|
|
||||||
notebookTypes.push({
|
|
||||||
cssClass: 'icon-notebook',
|
|
||||||
name: `Save to Notebook ${defaultPath}`,
|
|
||||||
onItemClicked: () => {
|
|
||||||
return this.snapshot(NOTEBOOK_DEFAULT, event.target);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
notebookTypes.push({
|
|
||||||
cssClass: 'icon-camera',
|
|
||||||
name: 'Save to Notebook Snapshots',
|
|
||||||
onItemClicked: () => {
|
|
||||||
return this.snapshot(NOTEBOOK_SNAPSHOT, event.target);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.openmct.menus.showMenu(x, y, notebookTypes);
|
this.openmct.menus.showMenu(x, y, notebookTypes);
|
||||||
},
|
},
|
||||||
snapshot(notebookType, target) {
|
snapshot(notebookType, target) {
|
||||||
@ -102,15 +98,12 @@ export default {
|
|||||||
const wrapper = target && target.closest('.js-notebook-snapshot-item-wrapper')
|
const wrapper = target && target.closest('.js-notebook-snapshot-item-wrapper')
|
||||||
|| document;
|
|| document;
|
||||||
const element = wrapper.querySelector('.js-notebook-snapshot-item');
|
const element = wrapper.querySelector('.js-notebook-snapshot-item');
|
||||||
|
|
||||||
const bounds = this.openmct.time.bounds();
|
|
||||||
const link = !this.ignoreLink
|
|
||||||
? window.location.hash
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const objectPath = this.objectPath || this.openmct.router.path;
|
const objectPath = this.objectPath || this.openmct.router.path;
|
||||||
|
const link = this.isPreview
|
||||||
|
? this.getPreviewObjectLink()
|
||||||
|
: window.location.hash;
|
||||||
const snapshotMeta = {
|
const snapshotMeta = {
|
||||||
bounds,
|
bounds: this.openmct.time.bounds(),
|
||||||
link,
|
link,
|
||||||
objectPath,
|
objectPath,
|
||||||
openmct: this.openmct
|
openmct: this.openmct
|
||||||
|
@ -27,15 +27,15 @@
|
|||||||
</div><!-- closes l-browse-bar -->
|
</div><!-- closes l-browse-bar -->
|
||||||
<div class="c-snapshots">
|
<div class="c-snapshots">
|
||||||
<span v-for="snapshot in snapshots"
|
<span v-for="snapshot in snapshots"
|
||||||
:key="snapshot.id"
|
:key="snapshot.embedObject.id"
|
||||||
draggable="true"
|
draggable="true"
|
||||||
@dragstart="startEmbedDrag(snapshot, $event)"
|
@dragstart="startEmbedDrag(snapshot, $event)"
|
||||||
>
|
>
|
||||||
<NotebookEmbed ref="notebookEmbed"
|
<NotebookEmbed ref="notebookEmbed"
|
||||||
:key="snapshot.id"
|
:key="snapshot.embedObject.id"
|
||||||
:embed="snapshot"
|
:embed="snapshot.embedObject"
|
||||||
|
:is-snapshot-container="true"
|
||||||
:remove-action-string="'Delete Snapshot'"
|
:remove-action-string="'Delete Snapshot'"
|
||||||
@updateEmbed="updateSnapshot"
|
|
||||||
@removeEmbed="removeSnapshot"
|
@removeEmbed="removeSnapshot"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
@ -119,11 +119,8 @@ export default {
|
|||||||
this.snapshots = this.snapshotContainer.getSnapshots();
|
this.snapshots = this.snapshotContainer.getSnapshots();
|
||||||
},
|
},
|
||||||
startEmbedDrag(snapshot, event) {
|
startEmbedDrag(snapshot, event) {
|
||||||
event.dataTransfer.setData('text/plain', snapshot.id);
|
event.dataTransfer.setData('text/plain', snapshot.embedObject.id);
|
||||||
event.dataTransfer.setData('openmct/snapshot/id', snapshot.id);
|
event.dataTransfer.setData('openmct/snapshot/id', snapshot.embedObject.id);
|
||||||
},
|
|
||||||
updateSnapshot(snapshot) {
|
|
||||||
this.snapshotContainer.updateSnapshot(snapshot);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
72
src/plugins/notebook/monkeyPatchObjectAPIForNotebooks.js
Normal file
72
src/plugins/notebook/monkeyPatchObjectAPIForNotebooks.js
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import {NOTEBOOK_TYPE} from './notebook-constants';
|
||||||
|
|
||||||
|
export default function (openmct) {
|
||||||
|
const apiSave = openmct.objects.save.bind(openmct.objects);
|
||||||
|
|
||||||
|
openmct.objects.save = async (domainObject) => {
|
||||||
|
if (domainObject.type !== NOTEBOOK_TYPE) {
|
||||||
|
return apiSave(domainObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
const localMutable = openmct.objects._toMutable(domainObject);
|
||||||
|
let result;
|
||||||
|
|
||||||
|
try {
|
||||||
|
result = await apiSave(localMutable);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof openmct.objects.errors.Conflict) {
|
||||||
|
result = resolveConflicts(localMutable, openmct);
|
||||||
|
} else {
|
||||||
|
result = Promise.reject(error);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
openmct.objects.destroyMutable(localMutable);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveConflicts(localMutable, openmct) {
|
||||||
|
return openmct.objects.getMutable(localMutable.identifier).then((remoteMutable) => {
|
||||||
|
const localEntries = localMutable.configuration.entries;
|
||||||
|
remoteMutable.$refresh(remoteMutable);
|
||||||
|
applyLocalEntries(remoteMutable, localEntries);
|
||||||
|
|
||||||
|
openmct.objects.destroyMutable(remoteMutable);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyLocalEntries(mutable, entries) {
|
||||||
|
Object.entries(entries).forEach(([sectionKey, pagesInSection]) => {
|
||||||
|
Object.entries(pagesInSection).forEach(([pageKey, localEntries]) => {
|
||||||
|
const remoteEntries = mutable.configuration.entries[sectionKey][pageKey];
|
||||||
|
const mergedEntries = [].concat(remoteEntries);
|
||||||
|
let shouldMutate = false;
|
||||||
|
|
||||||
|
const locallyAddedEntries = _.differenceBy(localEntries, remoteEntries, 'id');
|
||||||
|
const locallyModifiedEntries = _.differenceWith(localEntries, remoteEntries, (localEntry, remoteEntry) => {
|
||||||
|
return localEntry.id === remoteEntry.id && localEntry.text === remoteEntry.text;
|
||||||
|
});
|
||||||
|
|
||||||
|
locallyAddedEntries.forEach((localEntry) => {
|
||||||
|
mergedEntries.push(localEntry);
|
||||||
|
shouldMutate = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
locallyModifiedEntries.forEach((locallyModifiedEntry) => {
|
||||||
|
let mergedEntry = mergedEntries.find(entry => entry.id === locallyModifiedEntry.id);
|
||||||
|
if (mergedEntry !== undefined) {
|
||||||
|
mergedEntry.text = locallyModifiedEntry.text;
|
||||||
|
shouldMutate = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (shouldMutate) {
|
||||||
|
mutable.$set(`configuration.entries.${sectionKey}.${pageKey}`, mergedEntries);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -2,6 +2,7 @@ import CopyToNotebookAction from './actions/CopyToNotebookAction';
|
|||||||
import Notebook from './components/Notebook.vue';
|
import Notebook from './components/Notebook.vue';
|
||||||
import NotebookSnapshotIndicator from './components/NotebookSnapshotIndicator.vue';
|
import NotebookSnapshotIndicator from './components/NotebookSnapshotIndicator.vue';
|
||||||
import SnapshotContainer from './snapshot-container';
|
import SnapshotContainer from './snapshot-container';
|
||||||
|
import monkeyPatchObjectAPIForNotebooks from './monkeyPatchObjectAPIForNotebooks.js';
|
||||||
|
|
||||||
import { notebookImageMigration } from '../notebook/utils/notebook-migration';
|
import { notebookImageMigration } from '../notebook/utils/notebook-migration';
|
||||||
import { NOTEBOOK_TYPE } from './notebook-constants';
|
import { NOTEBOOK_TYPE } from './notebook-constants';
|
||||||
@ -165,5 +166,7 @@ export default function NotebookPlugin() {
|
|||||||
return domainObject;
|
return domainObject;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
monkeyPatchObjectAPIForNotebooks(openmct);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -154,6 +154,8 @@ describe("Notebook plugin:", () => {
|
|||||||
testObjectProvider.get.and.returnValue(Promise.resolve(notebookViewObject));
|
testObjectProvider.get.and.returnValue(Promise.resolve(notebookViewObject));
|
||||||
openmct.objects.addProvider('test-namespace', testObjectProvider);
|
openmct.objects.addProvider('test-namespace', testObjectProvider);
|
||||||
testObjectProvider.observe.and.returnValue(() => {});
|
testObjectProvider.observe.and.returnValue(() => {});
|
||||||
|
testObjectProvider.create.and.returnValue(Promise.resolve(true));
|
||||||
|
testObjectProvider.update.and.returnValue(Promise.resolve(true));
|
||||||
|
|
||||||
return openmct.objects.getMutable(notebookViewObject.identifier).then((mutableObject) => {
|
return openmct.objects.getMutable(notebookViewObject.identifier).then((mutableObject) => {
|
||||||
mutableNotebookObject = mutableObject;
|
mutableNotebookObject = mutableObject;
|
||||||
|
@ -18,13 +18,18 @@ export default class SnapshotContainer extends EventEmitter {
|
|||||||
return SnapshotContainer.instance;
|
return SnapshotContainer.instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
addSnapshot(embedObject) {
|
addSnapshot(notebookImageDomainObject, embedObject) {
|
||||||
const snapshots = this.getSnapshots();
|
const snapshots = this.getSnapshots();
|
||||||
if (snapshots.length >= NOTEBOOK_SNAPSHOT_MAX_COUNT) {
|
if (snapshots.length >= NOTEBOOK_SNAPSHOT_MAX_COUNT) {
|
||||||
snapshots.pop();
|
snapshots.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
snapshots.unshift(embedObject);
|
const snapshotObject = {
|
||||||
|
notebookImageDomainObject,
|
||||||
|
embedObject
|
||||||
|
};
|
||||||
|
|
||||||
|
snapshots.unshift(snapshotObject);
|
||||||
|
|
||||||
return this.saveSnapshots(snapshots);
|
return this.saveSnapshots(snapshots);
|
||||||
}
|
}
|
||||||
@ -32,7 +37,7 @@ export default class SnapshotContainer extends EventEmitter {
|
|||||||
getSnapshot(id) {
|
getSnapshot(id) {
|
||||||
const snapshots = this.getSnapshots();
|
const snapshots = this.getSnapshots();
|
||||||
|
|
||||||
return snapshots.find(s => s.id === id);
|
return snapshots.find(s => s.embedObject.id === id);
|
||||||
}
|
}
|
||||||
|
|
||||||
getSnapshots() {
|
getSnapshots() {
|
||||||
@ -47,7 +52,7 @@ export default class SnapshotContainer extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const snapshots = this.getSnapshots();
|
const snapshots = this.getSnapshots();
|
||||||
const filteredsnapshots = snapshots.filter(snapshot => snapshot.id !== id);
|
const filteredsnapshots = snapshots.filter(snapshot => snapshot.embedObject.id !== id);
|
||||||
|
|
||||||
return this.saveSnapshots(filteredsnapshots);
|
return this.saveSnapshots(filteredsnapshots);
|
||||||
}
|
}
|
||||||
@ -73,7 +78,7 @@ export default class SnapshotContainer extends EventEmitter {
|
|||||||
updateSnapshot(snapshot) {
|
updateSnapshot(snapshot) {
|
||||||
const snapshots = this.getSnapshots();
|
const snapshots = this.getSnapshots();
|
||||||
const updatedSnapshots = snapshots.map(s => {
|
const updatedSnapshots = snapshots.map(s => {
|
||||||
return s.id === snapshot.id
|
return s.embedObject.id === snapshot.embedObject.id
|
||||||
? snapshot
|
? snapshot
|
||||||
: s;
|
: s;
|
||||||
});
|
});
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { addNotebookEntry, createNewEmbed } from './utils/notebook-entries';
|
import { addNotebookEntry, createNewEmbed } from './utils/notebook-entries';
|
||||||
import { getDefaultNotebook, getNotebookSectionAndPage, getDefaultNotebookLink, setDefaultNotebook } from './utils/notebook-storage';
|
import { getDefaultNotebook, getNotebookSectionAndPage, getDefaultNotebookLink, setDefaultNotebook } from './utils/notebook-storage';
|
||||||
import { NOTEBOOK_DEFAULT } from '@/plugins/notebook/notebook-constants';
|
import { NOTEBOOK_DEFAULT } from '@/plugins/notebook/notebook-constants';
|
||||||
import { createNotebookImageDomainObject, DEFAULT_SIZE } from './utils/notebook-image';
|
import { createNotebookImageDomainObject, saveNotebookImageDomainObject, updateNamespaceOfDomainObject, DEFAULT_SIZE } from './utils/notebook-image';
|
||||||
|
|
||||||
import SnapshotContainer from './snapshot-container';
|
import SnapshotContainer from './snapshot-container';
|
||||||
import ImageExporter from '../../exporters/ImageExporter';
|
import ImageExporter from '../../exporters/ImageExporter';
|
||||||
@ -35,29 +35,28 @@ export default class Snapshot {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_saveSnapShot(notebookType, fullSizeImageURL, thumbnailImageURL, snapshotMeta) {
|
_saveSnapShot(notebookType, fullSizeImageURL, thumbnailImageURL, snapshotMeta) {
|
||||||
createNotebookImageDomainObject(this.openmct, fullSizeImageURL)
|
const object = createNotebookImageDomainObject(fullSizeImageURL);
|
||||||
.then(object => {
|
const thumbnailImage = { src: thumbnailImageURL || '' };
|
||||||
const thumbnailImage = { src: thumbnailImageURL || '' };
|
const snapshot = {
|
||||||
const snapshot = {
|
fullSizeImageObjectIdentifier: object.identifier,
|
||||||
fullSizeImageObjectIdentifier: object.identifier,
|
thumbnailImage
|
||||||
thumbnailImage
|
};
|
||||||
};
|
const embed = createNewEmbed(snapshotMeta, snapshot);
|
||||||
const embed = createNewEmbed(snapshotMeta, snapshot);
|
if (notebookType === NOTEBOOK_DEFAULT) {
|
||||||
if (notebookType === NOTEBOOK_DEFAULT) {
|
const notebookStorage = getDefaultNotebook();
|
||||||
this._saveToDefaultNoteBook(embed);
|
|
||||||
|
|
||||||
return;
|
this._saveToDefaultNoteBook(notebookStorage, embed);
|
||||||
}
|
const notebookImageDomainObject = updateNamespaceOfDomainObject(object, notebookStorage.identifier.namespace);
|
||||||
|
saveNotebookImageDomainObject(this.openmct, notebookImageDomainObject);
|
||||||
this._saveToNotebookSnapshots(embed);
|
} else {
|
||||||
});
|
this._saveToNotebookSnapshots(object, embed);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_saveToDefaultNoteBook(embed) {
|
_saveToDefaultNoteBook(notebookStorage, embed) {
|
||||||
const notebookStorage = getDefaultNotebook();
|
|
||||||
this.openmct.objects.get(notebookStorage.identifier)
|
this.openmct.objects.get(notebookStorage.identifier)
|
||||||
.then(async (domainObject) => {
|
.then(async (domainObject) => {
|
||||||
addNotebookEntry(this.openmct, domainObject, notebookStorage, embed);
|
addNotebookEntry(this.openmct, domainObject, notebookStorage, embed);
|
||||||
@ -85,19 +84,22 @@ export default class Snapshot {
|
|||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_saveToNotebookSnapshots(embed) {
|
_saveToNotebookSnapshots(notebookImageDomainObject, embed) {
|
||||||
this.snapshotContainer.addSnapshot(embed);
|
this.snapshotContainer.addSnapshot(notebookImageDomainObject, embed);
|
||||||
}
|
}
|
||||||
|
|
||||||
_showNotification(msg, url) {
|
_showNotification(msg, url) {
|
||||||
const options = {
|
const options = {
|
||||||
autoDismissTimeout: 30000,
|
autoDismissTimeout: 30000
|
||||||
link: {
|
};
|
||||||
|
|
||||||
|
if (!this.openmct.editor.isEditing()) {
|
||||||
|
options.link = {
|
||||||
cssClass: '',
|
cssClass: '',
|
||||||
text: 'click to view',
|
text: 'click to view',
|
||||||
onClick: this._navigateToNotebook(url)
|
onClick: this._navigateToNotebook(url)
|
||||||
}
|
};
|
||||||
};
|
}
|
||||||
|
|
||||||
this.openmct.notifications.info(msg, options);
|
this.openmct.notifications.info(msg, options);
|
||||||
}
|
}
|
||||||
@ -108,7 +110,8 @@ export default class Snapshot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.location.href = window.location.origin + url;
|
const path = window.location.href.split('#');
|
||||||
|
window.location.href = path[0] + url;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -125,7 +125,7 @@ export function addNotebookEntry(openmct, domainObject, notebookStorage, embed =
|
|||||||
const newEntries = addEntryIntoPage(notebookStorage, entries, entry);
|
const newEntries = addEntryIntoPage(notebookStorage, entries, entry);
|
||||||
|
|
||||||
addDefaultClass(domainObject, openmct);
|
addDefaultClass(domainObject, openmct);
|
||||||
openmct.objects.mutate(domainObject, 'configuration.entries', newEntries);
|
domainObject.configuration.entries = newEntries;
|
||||||
|
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
@ -5,14 +5,14 @@ export const DEFAULT_SIZE = {
|
|||||||
height: 30
|
height: 30
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createNotebookImageDomainObject(openmct, fullSizeImageURL) {
|
export function createNotebookImageDomainObject(fullSizeImageURL) {
|
||||||
const identifier = {
|
const identifier = {
|
||||||
key: uuid(),
|
key: uuid(),
|
||||||
namespace: ''
|
namespace: ''
|
||||||
};
|
};
|
||||||
const viewType = 'notebookSnapshotImage';
|
const viewType = 'notebookSnapshotImage';
|
||||||
|
|
||||||
const object = {
|
return {
|
||||||
name: 'Notebook Snapshot Image',
|
name: 'Notebook Snapshot Image',
|
||||||
type: viewType,
|
type: viewType,
|
||||||
identifier,
|
identifier,
|
||||||
@ -20,21 +20,6 @@ export function createNotebookImageDomainObject(openmct, fullSizeImageURL) {
|
|||||||
fullSizeImageURL
|
fullSizeImageURL
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
openmct.objects.save(object)
|
|
||||||
.then(result => {
|
|
||||||
if (result) {
|
|
||||||
resolve(object);
|
|
||||||
}
|
|
||||||
|
|
||||||
reject();
|
|
||||||
})
|
|
||||||
.catch(e => {
|
|
||||||
console.error(e);
|
|
||||||
reject();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getThumbnailURLFromCanvas(canvas, size = DEFAULT_SIZE) {
|
export function getThumbnailURLFromCanvas(canvas, size = DEFAULT_SIZE) {
|
||||||
@ -67,6 +52,23 @@ export function getThumbnailURLFromimageUrl(imageUrl, size = DEFAULT_SIZE) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function saveNotebookImageDomainObject(openmct, object) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
openmct.objects.save(object)
|
||||||
|
.then(result => {
|
||||||
|
if (result) {
|
||||||
|
resolve(object);
|
||||||
|
} else {
|
||||||
|
reject();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
console.error(e);
|
||||||
|
reject();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function updateNotebookImageDomainObject(openmct, identifier, fullSizeImage) {
|
export function updateNotebookImageDomainObject(openmct, identifier, fullSizeImage) {
|
||||||
openmct.objects.get(identifier)
|
openmct.objects.get(identifier)
|
||||||
.then(domainObject => {
|
.then(domainObject => {
|
||||||
@ -76,3 +78,9 @@ export function updateNotebookImageDomainObject(openmct, identifier, fullSizeIma
|
|||||||
openmct.objects.mutate(domainObject, 'configuration', configuration);
|
openmct.objects.mutate(domainObject, 'configuration', configuration);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function updateNamespaceOfDomainObject(object, namespace) {
|
||||||
|
object.identifier.namespace = namespace;
|
||||||
|
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
@ -1,16 +1,18 @@
|
|||||||
import { createNotebookImageDomainObject, getThumbnailURLFromimageUrl } from './notebook-image';
|
import { createNotebookImageDomainObject, getThumbnailURLFromimageUrl, saveNotebookImageDomainObject, updateNamespaceOfDomainObject } from './notebook-image';
|
||||||
import { mutateObject } from './notebook-entries';
|
import { mutateObject } from './notebook-entries';
|
||||||
|
|
||||||
|
const IMAGE_MIGRATION_VER = "v1";
|
||||||
|
|
||||||
export function notebookImageMigration(openmct, domainObject) {
|
export function notebookImageMigration(openmct, domainObject) {
|
||||||
const configuration = domainObject.configuration;
|
const configuration = domainObject.configuration;
|
||||||
const notebookEntries = configuration.entries;
|
const notebookEntries = configuration.entries;
|
||||||
|
|
||||||
const imageMigrationVer = configuration.imageMigrationVer;
|
const imageMigrationVer = configuration.imageMigrationVer;
|
||||||
if (imageMigrationVer && imageMigrationVer === 'v1') {
|
if (imageMigrationVer && imageMigrationVer === IMAGE_MIGRATION_VER) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
configuration.imageMigrationVer = 'v1';
|
configuration.imageMigrationVer = IMAGE_MIGRATION_VER;
|
||||||
|
|
||||||
// to avoid muliple notebookImageMigration calls updating images.
|
// to avoid muliple notebookImageMigration calls updating images.
|
||||||
mutateObject(openmct, domainObject, 'configuration', configuration);
|
mutateObject(openmct, domainObject, 'configuration', configuration);
|
||||||
@ -27,14 +29,16 @@ export function notebookImageMigration(openmct, domainObject) {
|
|||||||
const fullSizeImageURL = snapshot.src;
|
const fullSizeImageURL = snapshot.src;
|
||||||
if (fullSizeImageURL) {
|
if (fullSizeImageURL) {
|
||||||
const thumbnailImageURL = await getThumbnailURLFromimageUrl(fullSizeImageURL);
|
const thumbnailImageURL = await getThumbnailURLFromimageUrl(fullSizeImageURL);
|
||||||
const notebookImageDomainObject = await createNotebookImageDomainObject(openmct, fullSizeImageURL);
|
const object = createNotebookImageDomainObject(fullSizeImageURL);
|
||||||
|
const notebookImageDomainObject = updateNamespaceOfDomainObject(object, domainObject.identifier.namespace);
|
||||||
embed.snapshot = {
|
embed.snapshot = {
|
||||||
fullSizeImageObjectIdentifier: notebookImageDomainObject.identifier,
|
fullSizeImageObjectIdentifier: notebookImageDomainObject.identifier,
|
||||||
thumbnailImage: { src: thumbnailImageURL || '' }
|
thumbnailImage: { src: thumbnailImageURL || '' }
|
||||||
};
|
};
|
||||||
|
|
||||||
mutateObject(openmct, domainObject, 'configuration.entries', notebookEntries);
|
mutateObject(openmct, domainObject, 'configuration.entries', notebookEntries);
|
||||||
|
|
||||||
|
saveNotebookImageDomainObject(openmct, notebookImageDomainObject);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
31
src/plugins/notebook/utils/notebook-snapshot-menu.js
Normal file
31
src/plugins/notebook/utils/notebook-snapshot-menu.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { getDefaultNotebook, getNotebookSectionAndPage } from './notebook-storage';
|
||||||
|
|
||||||
|
export async function getMenuItems(openmct, menuItemOptions) {
|
||||||
|
const notebookTypes = [];
|
||||||
|
|
||||||
|
const defaultNotebook = getDefaultNotebook();
|
||||||
|
const defaultNotebookObject = defaultNotebook && await openmct.objects.get(defaultNotebook.identifier);
|
||||||
|
if (defaultNotebookObject) {
|
||||||
|
const { section, page } = getNotebookSectionAndPage(defaultNotebookObject, defaultNotebook.defaultSectionId, defaultNotebook.defaultPageId);
|
||||||
|
if (section && page) {
|
||||||
|
const name = defaultNotebookObject.name;
|
||||||
|
const sectionName = section.name;
|
||||||
|
const pageName = page.name;
|
||||||
|
const defaultPath = `${name} - ${sectionName} - ${pageName}`;
|
||||||
|
|
||||||
|
notebookTypes.push({
|
||||||
|
cssClass: menuItemOptions.default.cssClass,
|
||||||
|
name: `${menuItemOptions.default.name} ${defaultPath}`,
|
||||||
|
onItemClicked: menuItemOptions.default.onItemClicked
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
notebookTypes.push({
|
||||||
|
cssClass: menuItemOptions.snapshot.cssClass,
|
||||||
|
name: menuItemOptions.snapshot.name,
|
||||||
|
onItemClicked: menuItemOptions.snapshot.onItemClicked
|
||||||
|
});
|
||||||
|
|
||||||
|
return notebookTypes;
|
||||||
|
}
|
@ -71,11 +71,7 @@ export async function getDefaultNotebookLink(openmct, domainObject = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const path = await openmct.objects.getOriginalPath(domainObject.identifier)
|
const path = await openmct.objects.getOriginalPath(domainObject.identifier)
|
||||||
.then(objectPath => objectPath
|
.then(openmct.objects.getRelativePath);
|
||||||
.map(o => o && openmct.objects.makeKeyString(o.identifier))
|
|
||||||
.reverse()
|
|
||||||
.join('/')
|
|
||||||
);
|
|
||||||
const { defaultPageId, defaultSectionId } = getDefaultNotebook();
|
const { defaultPageId, defaultSectionId } = getDefaultNotebook();
|
||||||
|
|
||||||
return `#/browse/${path}?sectionId=${defaultSectionId}&pageId=${defaultPageId}`;
|
return `#/browse/${path}?sectionId=${defaultSectionId}&pageId=${defaultPageId}`;
|
||||||
|
@ -15,12 +15,16 @@
|
|||||||
|
|
||||||
port.onmessage = async function (event) {
|
port.onmessage = async function (event) {
|
||||||
if (event.data.request === 'close') {
|
if (event.data.request === 'close') {
|
||||||
|
console.log('Closing connection');
|
||||||
connections.splice(event.data.connectionId - 1, 1);
|
connections.splice(event.data.connectionId - 1, 1);
|
||||||
if (connections.length <= 0) {
|
if (connections.length <= 0) {
|
||||||
// abort any outstanding requests if there's nobody listening to it.
|
// abort any outstanding requests if there's nobody listening to it.
|
||||||
controller.abort();
|
controller.abort();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('Closed.');
|
||||||
|
connected = false;
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -29,68 +33,9 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
connected = true;
|
do {
|
||||||
|
await self.listenForChanges(event.data.url, event.data.body, port);
|
||||||
let url = event.data.url;
|
} while (connected);
|
||||||
let body = event.data.body;
|
|
||||||
let error = false;
|
|
||||||
// feed=continuous maintains an indefinitely open connection with a keep-alive of HEARTBEAT milliseconds until this client closes the connection
|
|
||||||
// style=main_only returns only the current winning revision of the document
|
|
||||||
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
"Content-Type": 'application/json'
|
|
||||||
},
|
|
||||||
signal,
|
|
||||||
body
|
|
||||||
});
|
|
||||||
|
|
||||||
let reader;
|
|
||||||
|
|
||||||
if (response.body === undefined) {
|
|
||||||
error = true;
|
|
||||||
} else {
|
|
||||||
reader = response.body.getReader();
|
|
||||||
}
|
|
||||||
|
|
||||||
while (!error) {
|
|
||||||
const {done, value} = await reader.read();
|
|
||||||
//done is true when we lose connection with the provider
|
|
||||||
if (done) {
|
|
||||||
error = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value) {
|
|
||||||
let chunk = new Uint8Array(value.length);
|
|
||||||
chunk.set(value, 0);
|
|
||||||
const decodedChunk = new TextDecoder("utf-8").decode(chunk).split('\n');
|
|
||||||
if (decodedChunk.length && decodedChunk[decodedChunk.length - 1] === '') {
|
|
||||||
decodedChunk.forEach((doc, index) => {
|
|
||||||
try {
|
|
||||||
if (doc) {
|
|
||||||
const objectChanges = JSON.parse(doc);
|
|
||||||
connections.forEach(function (connection) {
|
|
||||||
connection.postMessage({
|
|
||||||
objectChanges
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (decodeError) {
|
|
||||||
//do nothing;
|
|
||||||
console.log(decodeError);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
port.postMessage({
|
|
||||||
error
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -103,4 +48,64 @@
|
|||||||
console.log('Error on feed');
|
console.log('Error on feed');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
self.listenForChanges = async function (url, body, port) {
|
||||||
|
connected = true;
|
||||||
|
let error = false;
|
||||||
|
// feed=continuous maintains an indefinitely open connection with a keep-alive of HEARTBEAT milliseconds until this client closes the connection
|
||||||
|
// style=main_only returns only the current winning revision of the document
|
||||||
|
|
||||||
|
console.log('Opening changes feed connection.');
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
"Content-Type": 'application/json'
|
||||||
|
},
|
||||||
|
signal,
|
||||||
|
body
|
||||||
|
});
|
||||||
|
|
||||||
|
let reader;
|
||||||
|
|
||||||
|
if (response.body === undefined) {
|
||||||
|
error = true;
|
||||||
|
} else {
|
||||||
|
reader = response.body.getReader();
|
||||||
|
}
|
||||||
|
|
||||||
|
while (!error) {
|
||||||
|
const {done, value} = await reader.read();
|
||||||
|
//done is true when we lose connection with the provider
|
||||||
|
if (done) {
|
||||||
|
error = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
let chunk = new Uint8Array(value.length);
|
||||||
|
chunk.set(value, 0);
|
||||||
|
const decodedChunk = new TextDecoder("utf-8").decode(chunk).split('\n');
|
||||||
|
console.log('Received chunk');
|
||||||
|
if (decodedChunk.length && decodedChunk[decodedChunk.length - 1] === '') {
|
||||||
|
decodedChunk.forEach((doc, index) => {
|
||||||
|
try {
|
||||||
|
if (doc) {
|
||||||
|
const objectChanges = JSON.parse(doc);
|
||||||
|
connections.forEach(function (connection) {
|
||||||
|
connection.postMessage({
|
||||||
|
objectChanges
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (decodeError) {
|
||||||
|
//do nothing;
|
||||||
|
console.log(decodeError);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Done reading changes feed');
|
||||||
|
};
|
||||||
|
|
||||||
}());
|
}());
|
||||||
|
@ -29,7 +29,7 @@ const ID = "_id";
|
|||||||
const HEARTBEAT = 50000;
|
const HEARTBEAT = 50000;
|
||||||
const ALL_DOCS = "_all_docs?include_docs=true";
|
const ALL_DOCS = "_all_docs?include_docs=true";
|
||||||
|
|
||||||
export default class CouchObjectProvider {
|
class CouchObjectProvider {
|
||||||
constructor(openmct, options, namespace) {
|
constructor(openmct, options, namespace) {
|
||||||
options = this._normalize(options);
|
options = this._normalize(options);
|
||||||
this.openmct = openmct;
|
this.openmct = openmct;
|
||||||
@ -74,13 +74,6 @@ export default class CouchObjectProvider {
|
|||||||
if (event.data.type === 'connection') {
|
if (event.data.type === 'connection') {
|
||||||
this.changesFeedSharedWorkerConnectionId = event.data.connectionId;
|
this.changesFeedSharedWorkerConnectionId = event.data.connectionId;
|
||||||
} else {
|
} else {
|
||||||
const error = event.data.error;
|
|
||||||
if (error && Object.keys(this.observers).length > 0) {
|
|
||||||
this.observeObjectChanges();
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let objectChanges = event.data.objectChanges;
|
let objectChanges = event.data.objectChanges;
|
||||||
objectChanges.identifier = {
|
objectChanges.identifier = {
|
||||||
namespace: this.namespace,
|
namespace: this.namespace,
|
||||||
@ -126,11 +119,12 @@ export default class CouchObjectProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return fetch(this.url + '/' + subPath, fetchOptions)
|
return fetch(this.url + '/' + subPath, fetchOptions)
|
||||||
.then(response => response.json())
|
.then((response) => {
|
||||||
.then(function (response) {
|
if (response.status === CouchObjectProvider.HTTP_CONFLICT) {
|
||||||
return response;
|
throw new this.openmct.objects.errors.Conflict(`Conflict persisting ${fetchOptions.body.name}`);
|
||||||
}, function () {
|
}
|
||||||
return undefined;
|
|
||||||
|
return response.json();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -561,12 +555,18 @@ export default class CouchObjectProvider {
|
|||||||
let intermediateResponse = this.getIntermediateResponse();
|
let intermediateResponse = this.getIntermediateResponse();
|
||||||
const key = model.identifier.key;
|
const key = model.identifier.key;
|
||||||
this.enqueueObject(key, model, intermediateResponse);
|
this.enqueueObject(key, model, intermediateResponse);
|
||||||
this.objectQueue[key].pending = true;
|
if (!this.objectQueue[key].pending) {
|
||||||
const queued = this.objectQueue[key].dequeue();
|
this.objectQueue[key].pending = true;
|
||||||
let document = new CouchDocument(key, queued.model);
|
const queued = this.objectQueue[key].dequeue();
|
||||||
this.request(key, "PUT", document).then((response) => {
|
let document = new CouchDocument(key, queued.model);
|
||||||
this.checkResponse(response, queued.intermediateResponse, key);
|
this.request(key, "PUT", document).then((response) => {
|
||||||
});
|
console.log('create check response', key);
|
||||||
|
this.checkResponse(response, queued.intermediateResponse, key);
|
||||||
|
}).catch(error => {
|
||||||
|
queued.intermediateResponse.reject(error);
|
||||||
|
this.objectQueue[key].pending = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return intermediateResponse.promise;
|
return intermediateResponse.promise;
|
||||||
}
|
}
|
||||||
@ -581,6 +581,9 @@ export default class CouchObjectProvider {
|
|||||||
let document = new CouchDocument(key, queued.model, this.objectQueue[key].rev);
|
let document = new CouchDocument(key, queued.model, this.objectQueue[key].rev);
|
||||||
this.request(key, "PUT", document).then((response) => {
|
this.request(key, "PUT", document).then((response) => {
|
||||||
this.checkResponse(response, queued.intermediateResponse, key);
|
this.checkResponse(response, queued.intermediateResponse, key);
|
||||||
|
}).catch((error) => {
|
||||||
|
queued.intermediateResponse.reject(error);
|
||||||
|
this.objectQueue[key].pending = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -594,3 +597,7 @@ export default class CouchObjectProvider {
|
|||||||
return intermediateResponse.promise;
|
return intermediateResponse.promise;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CouchObjectProvider.HTTP_CONFLICT = 409;
|
||||||
|
|
||||||
|
export default CouchObjectProvider;
|
||||||
|
@ -67,7 +67,7 @@ export default {
|
|||||||
TimelineAxis,
|
TimelineAxis,
|
||||||
SwimLane
|
SwimLane
|
||||||
},
|
},
|
||||||
inject: ['openmct', 'domainObject'],
|
inject: ['openmct', 'domainObject', 'path'],
|
||||||
props: {
|
props: {
|
||||||
options: {
|
options: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@ -99,21 +99,37 @@ export default {
|
|||||||
this.canvasContext = this.canvas.getContext('2d');
|
this.canvasContext = this.canvas.getContext('2d');
|
||||||
|
|
||||||
this.setDimensions();
|
this.setDimensions();
|
||||||
this.updateViewBounds();
|
this.setTimeContext();
|
||||||
this.openmct.time.on("timeSystem", this.setScaleAndPlotActivities);
|
|
||||||
this.openmct.time.on("bounds", this.updateViewBounds);
|
|
||||||
this.resizeTimer = setInterval(this.resize, RESIZE_POLL_INTERVAL);
|
this.resizeTimer = setInterval(this.resize, RESIZE_POLL_INTERVAL);
|
||||||
this.unlisten = this.openmct.objects.observe(this.domainObject, '*', this.observeForChanges);
|
this.unlisten = this.openmct.objects.observe(this.domainObject, '*', this.observeForChanges);
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
clearInterval(this.resizeTimer);
|
clearInterval(this.resizeTimer);
|
||||||
this.openmct.time.off("timeSystem", this.setScaleAndPlotActivities);
|
this.stopFollowingTimeContext();
|
||||||
this.openmct.time.off("bounds", this.updateViewBounds);
|
|
||||||
if (this.unlisten) {
|
if (this.unlisten) {
|
||||||
this.unlisten();
|
this.unlisten();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
setTimeContext() {
|
||||||
|
this.stopFollowingTimeContext();
|
||||||
|
this.timeContext = this.openmct.time.getContextForView(this.path);
|
||||||
|
this.timeContext.on("timeContext", this.setTimeContext);
|
||||||
|
this.followTimeContext();
|
||||||
|
},
|
||||||
|
followTimeContext() {
|
||||||
|
this.updateViewBounds(this.timeContext.bounds());
|
||||||
|
|
||||||
|
this.timeContext.on("timeSystem", this.setScaleAndPlotActivities);
|
||||||
|
this.timeContext.on("bounds", this.updateViewBounds);
|
||||||
|
},
|
||||||
|
stopFollowingTimeContext() {
|
||||||
|
if (this.timeContext) {
|
||||||
|
this.timeContext.off("timeSystem", this.setScaleAndPlotActivities);
|
||||||
|
this.timeContext.off("bounds", this.updateViewBounds);
|
||||||
|
this.timeContext.off("timeContext", this.setTimeContext);
|
||||||
|
}
|
||||||
|
},
|
||||||
observeForChanges(mutatedObject) {
|
observeForChanges(mutatedObject) {
|
||||||
this.getPlanData(mutatedObject);
|
this.getPlanData(mutatedObject);
|
||||||
this.setScaleAndPlotActivities();
|
this.setScaleAndPlotActivities();
|
||||||
@ -141,12 +157,10 @@ export default {
|
|||||||
getPlanData(domainObject) {
|
getPlanData(domainObject) {
|
||||||
this.planData = getValidatedPlan(domainObject);
|
this.planData = getValidatedPlan(domainObject);
|
||||||
},
|
},
|
||||||
updateViewBounds() {
|
updateViewBounds(bounds) {
|
||||||
this.viewBounds = this.openmct.time.bounds();
|
if (bounds) {
|
||||||
//Add a 50% padding to the end bounds to look ahead
|
this.viewBounds = Object.create(bounds);
|
||||||
let timespan = (this.viewBounds.end - this.viewBounds.start);
|
}
|
||||||
let padding = timespan / 2;
|
|
||||||
this.viewBounds.end = this.viewBounds.end + padding;
|
|
||||||
|
|
||||||
if (this.timeSystem === undefined) {
|
if (this.timeSystem === undefined) {
|
||||||
this.timeSystem = this.openmct.time.timeSystem();
|
this.timeSystem = this.openmct.time.timeSystem();
|
||||||
|
@ -54,7 +54,8 @@ export default function PlanViewProvider(openmct) {
|
|||||||
},
|
},
|
||||||
provide: {
|
provide: {
|
||||||
openmct,
|
openmct,
|
||||||
domainObject
|
domainObject,
|
||||||
|
path: objectPath
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
154
src/plugins/plot/ColorSwatch.vue
Normal file
154
src/plugins/plot/ColorSwatch.vue
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
<!--
|
||||||
|
Open MCT, Copyright (c) 2014-2020, United States Government
|
||||||
|
as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
Administration. All rights reserved.
|
||||||
|
|
||||||
|
Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
"License"); you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
License for the specific language governing permissions and limitations
|
||||||
|
under the License.
|
||||||
|
|
||||||
|
Open MCT includes source code licensed under additional open source
|
||||||
|
licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
this source code distribution or the Licensing information page available
|
||||||
|
at runtime from the About dialog for additional information.
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<div class="u-contents">
|
||||||
|
<div v-if="canEdit"
|
||||||
|
class="grid-row"
|
||||||
|
>
|
||||||
|
<div class="grid-cell label"
|
||||||
|
:title="editTitle"
|
||||||
|
>{{ shortLabel }}</div>
|
||||||
|
<div class="grid-cell value">
|
||||||
|
<div class="c-click-swatch c-click-swatch--menu"
|
||||||
|
@click="toggleSwatch()"
|
||||||
|
>
|
||||||
|
<span class="c-color-swatch"
|
||||||
|
:style="{ background: currentColor }"
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="c-palette c-palette--color">
|
||||||
|
<div v-show="swatchActive"
|
||||||
|
class="c-palette__items"
|
||||||
|
>
|
||||||
|
<div v-for="group in colorPaletteGroups"
|
||||||
|
:key="group.id"
|
||||||
|
class="u-contents"
|
||||||
|
>
|
||||||
|
<div v-for="color in group"
|
||||||
|
:key="color.id"
|
||||||
|
class="c-palette__item"
|
||||||
|
:class="{ 'selected': currentColor === color.hexString }"
|
||||||
|
:style="{ background: color.hexString }"
|
||||||
|
@click="setColor(color)"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else
|
||||||
|
class="grid-row"
|
||||||
|
>
|
||||||
|
<div class="grid-cell label"
|
||||||
|
:title="viewTitle"
|
||||||
|
>{{ shortLabel }}</div>
|
||||||
|
<div class="grid-cell value">
|
||||||
|
<span class="c-color-swatch"
|
||||||
|
:style="{
|
||||||
|
'background': currentColor
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ColorPalette from './lib/ColorPalette';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
inject: ['openmct', 'domainObject'],
|
||||||
|
props: {
|
||||||
|
currentColor: {
|
||||||
|
type: String,
|
||||||
|
default() {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
editTitle: {
|
||||||
|
type: String,
|
||||||
|
default() {
|
||||||
|
return 'Set the color.';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
viewTitle: {
|
||||||
|
type: String,
|
||||||
|
default() {
|
||||||
|
return 'The current color.';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
shortLabel: {
|
||||||
|
type: String,
|
||||||
|
default() {
|
||||||
|
return 'Color';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
swatchActive: false,
|
||||||
|
colorPaletteGroups: [],
|
||||||
|
isEditing: this.openmct.editor.isEditing()
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
canEdit() {
|
||||||
|
return this.isEditing && !this.domainObject.locked;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.colorPalette = new ColorPalette();
|
||||||
|
this.openmct.editor.on('isEditing', this.setEditState);
|
||||||
|
this.initialize();
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.openmct.editor.off('isEditing', this.setEditState);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
initialize() {
|
||||||
|
const colorPaletteGroups = this.colorPalette.groups();
|
||||||
|
colorPaletteGroups.forEach((group, index) => {
|
||||||
|
let groupId = [];
|
||||||
|
group.forEach(color => {
|
||||||
|
color.hexString = color.asHexString();
|
||||||
|
color.id = `${color.hexString}-${index}`;
|
||||||
|
groupId.push(color.id);
|
||||||
|
});
|
||||||
|
group.id = groupId.join('-');
|
||||||
|
});
|
||||||
|
this.colorPaletteGroups = colorPaletteGroups;
|
||||||
|
},
|
||||||
|
setEditState(isEditing) {
|
||||||
|
this.isEditing = isEditing;
|
||||||
|
},
|
||||||
|
setColor(chosenColor) {
|
||||||
|
this.$emit('colorSet', chosenColor);
|
||||||
|
},
|
||||||
|
toggleSwatch() {
|
||||||
|
this.swatchActive = !this.swatchActive;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
@ -156,7 +156,7 @@
|
|||||||
import eventHelpers from './lib/eventHelpers';
|
import eventHelpers from './lib/eventHelpers';
|
||||||
import LinearScale from "./LinearScale";
|
import LinearScale from "./LinearScale";
|
||||||
import PlotConfigurationModel from './configuration/PlotConfigurationModel';
|
import PlotConfigurationModel from './configuration/PlotConfigurationModel';
|
||||||
import configStore from './configuration/configStore';
|
import configStore from './configuration/ConfigStore';
|
||||||
|
|
||||||
import PlotLegend from "./legend/PlotLegend.vue";
|
import PlotLegend from "./legend/PlotLegend.vue";
|
||||||
import MctTicks from "./MctTicks.vue";
|
import MctTicks from "./MctTicks.vue";
|
||||||
@ -173,7 +173,7 @@ export default {
|
|||||||
MctTicks,
|
MctTicks,
|
||||||
MctChart
|
MctChart
|
||||||
},
|
},
|
||||||
inject: ['openmct', 'domainObject'],
|
inject: ['openmct', 'domainObject', 'path'],
|
||||||
props: {
|
props: {
|
||||||
options: {
|
options: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@ -244,6 +244,9 @@ export default {
|
|||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
eventHelpers.extend(this);
|
eventHelpers.extend(this);
|
||||||
|
this.updateRealTime = this.updateRealTime.bind(this);
|
||||||
|
this.updateDisplayBounds = this.updateDisplayBounds.bind(this);
|
||||||
|
this.setTimeContext = this.setTimeContext.bind(this);
|
||||||
|
|
||||||
this.config = this.getConfig();
|
this.config = this.getConfig();
|
||||||
this.legend = this.config.legend;
|
this.legend = this.config.legend;
|
||||||
@ -261,7 +264,7 @@ export default {
|
|||||||
this.removeStatusListener = this.openmct.status.observe(this.domainObject.identifier, this.updateStatus);
|
this.removeStatusListener = this.openmct.status.observe(this.domainObject.identifier, this.updateStatus);
|
||||||
|
|
||||||
this.openmct.objectViews.on('clearData', this.clearData);
|
this.openmct.objectViews.on('clearData', this.clearData);
|
||||||
this.followTimeConductor();
|
this.setTimeContext();
|
||||||
|
|
||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
|
|
||||||
@ -274,11 +277,27 @@ export default {
|
|||||||
this.destroy();
|
this.destroy();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
followTimeConductor() {
|
setTimeContext() {
|
||||||
this.openmct.time.on('clock', this.updateRealTime);
|
this.stopFollowingTimeContext();
|
||||||
this.openmct.time.on('bounds', this.updateDisplayBounds);
|
|
||||||
|
this.timeContext = this.openmct.time.getContextForView(this.path);
|
||||||
|
this.timeContext.on('timeContext', this.setTimeContext);
|
||||||
|
this.followTimeContext();
|
||||||
|
|
||||||
|
},
|
||||||
|
followTimeContext() {
|
||||||
|
this.updateDisplayBounds(this.timeContext.bounds());
|
||||||
|
this.timeContext.on('clock', this.updateRealTime);
|
||||||
|
this.timeContext.on('bounds', this.updateDisplayBounds);
|
||||||
this.synchronized(true);
|
this.synchronized(true);
|
||||||
},
|
},
|
||||||
|
stopFollowingTimeContext() {
|
||||||
|
if (this.timeContext) {
|
||||||
|
this.timeContext.off("clock", this.updateRealTime);
|
||||||
|
this.timeContext.off("bounds", this.updateDisplayBounds);
|
||||||
|
this.timeContext.off("timeContext", this.setTimeContext);
|
||||||
|
}
|
||||||
|
},
|
||||||
getConfig() {
|
getConfig() {
|
||||||
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||||
let config = configStore.get(configId);
|
let config = configStore.get(configId);
|
||||||
@ -393,12 +412,31 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
clearData() {
|
clearSeries() {
|
||||||
this.config.series.forEach(function (series) {
|
this.config.series.forEach(function (series) {
|
||||||
series.reset();
|
series.reset();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
compositionPathContainsId(domainObjectToClear) {
|
||||||
|
return domainObjectToClear.composition.some((compositionIdentifier) => {
|
||||||
|
return this.openmct.objects.areIdsEqual(compositionIdentifier, this.domainObject.identifier);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
clearData(domainObjectToClear) {
|
||||||
|
// If we don't have an object to clear (global), or the IDs are equal, just clear the data.
|
||||||
|
// If we have an object to clear, but the IDs don't match, we need to check the composition
|
||||||
|
// of the object we've been asked to clear to see if it contains the id we're looking for.
|
||||||
|
// This happens with stacked plots for example.
|
||||||
|
// If we find the ID, clear the plot.
|
||||||
|
if (!domainObjectToClear
|
||||||
|
|| this.openmct.objects.areIdsEqual(domainObjectToClear.identifier, this.domainObject.identifier)
|
||||||
|
|| this.compositionPathContainsId(domainObjectToClear)) {
|
||||||
|
this.clearSeries();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
setDisplayRange(series, xKey) {
|
setDisplayRange(series, xKey) {
|
||||||
if (this.config.series.length !== 1) {
|
if (this.config.series.length !== 1) {
|
||||||
return;
|
return;
|
||||||
@ -466,7 +504,7 @@ export default {
|
|||||||
* displays can update accordingly.
|
* displays can update accordingly.
|
||||||
*/
|
*/
|
||||||
synchronized(value) {
|
synchronized(value) {
|
||||||
const isLocalClock = this.openmct.time.clock();
|
const isLocalClock = this.timeContext.clock();
|
||||||
|
|
||||||
if (typeof value !== 'undefined') {
|
if (typeof value !== 'undefined') {
|
||||||
this._synchronized = value;
|
this._synchronized = value;
|
||||||
@ -939,7 +977,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
showSynchronizeDialog() {
|
showSynchronizeDialog() {
|
||||||
const isLocalClock = this.openmct.time.clock();
|
const isLocalClock = this.timeContext.clock();
|
||||||
if (isLocalClock !== undefined) {
|
if (isLocalClock !== undefined) {
|
||||||
const message = `
|
const message = `
|
||||||
This action will change the Time Conductor to Fixed Timespan mode with this plot view's current time bounds.
|
This action will change the Time Conductor to Fixed Timespan mode with this plot view's current time bounds.
|
||||||
@ -974,9 +1012,9 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
synchronizeTimeConductor() {
|
synchronizeTimeConductor() {
|
||||||
this.openmct.time.stopClock();
|
this.timeContext.stopClock();
|
||||||
const range = this.config.xAxis.get('displayRange');
|
const range = this.config.xAxis.get('displayRange');
|
||||||
this.openmct.time.bounds({
|
this.timeContext.bounds({
|
||||||
start: range.min,
|
start: range.min,
|
||||||
end: range.max
|
end: range.max
|
||||||
});
|
});
|
||||||
@ -987,6 +1025,7 @@ export default {
|
|||||||
configStore.deleteStore(this.config.id);
|
configStore.deleteStore(this.config.id);
|
||||||
|
|
||||||
this.stopListening();
|
this.stopListening();
|
||||||
|
|
||||||
if (this.checkForSize) {
|
if (this.checkForSize) {
|
||||||
clearInterval(this.checkForSize);
|
clearInterval(this.checkForSize);
|
||||||
delete this.checkForSize;
|
delete this.checkForSize;
|
||||||
@ -1002,15 +1041,15 @@ export default {
|
|||||||
|
|
||||||
this.plotContainerResizeObserver.disconnect();
|
this.plotContainerResizeObserver.disconnect();
|
||||||
|
|
||||||
this.openmct.time.off('clock', this.updateRealTime);
|
this.stopFollowingTimeContext();
|
||||||
this.openmct.time.off('bounds', this.updateDisplayBounds);
|
|
||||||
this.openmct.objectViews.off('clearData', this.clearData);
|
this.openmct.objectViews.off('clearData', this.clearData);
|
||||||
},
|
},
|
||||||
updateStatus(status) {
|
updateStatus(status) {
|
||||||
this.$emit('statusUpdated', status);
|
this.$emit('statusUpdated', status);
|
||||||
},
|
},
|
||||||
handleWindowResize() {
|
handleWindowResize() {
|
||||||
if (this.offsetWidth !== this.$parent.$refs.plotWrapper.offsetWidth) {
|
if (this.$parent.$refs.plotWrapper
|
||||||
|
&& (this.offsetWidth !== this.$parent.$refs.plotWrapper.offsetWidth)) {
|
||||||
this.offsetWidth = this.$parent.$refs.plotWrapper.offsetWidth;
|
this.offsetWidth = this.$parent.$refs.plotWrapper.offsetWidth;
|
||||||
this.config.series.models.forEach(this.loadSeriesData, this);
|
this.config.series.models.forEach(this.loadSeriesData, this);
|
||||||
}
|
}
|
||||||
|
@ -77,7 +77,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import eventHelpers from "./lib/eventHelpers";
|
import eventHelpers from "./lib/eventHelpers";
|
||||||
import { ticks, getFormattedTicks } from "./tickUtils";
|
import { ticks, getFormattedTicks } from "./tickUtils";
|
||||||
import configStore from "./configuration/configStore";
|
import configStore from "./configuration/ConfigStore";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
inject: ['openmct', 'domainObject'],
|
inject: ['openmct', 'domainObject'],
|
||||||
|
@ -80,7 +80,7 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
MctPlot
|
MctPlot
|
||||||
},
|
},
|
||||||
inject: ['openmct', 'domainObject'],
|
inject: ['openmct', 'domainObject', 'path'],
|
||||||
props: {
|
props: {
|
||||||
options: {
|
options: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
@ -68,7 +68,8 @@ export default function PlotViewProvider(openmct) {
|
|||||||
},
|
},
|
||||||
provide: {
|
provide: {
|
||||||
openmct,
|
openmct,
|
||||||
domainObject
|
domainObject,
|
||||||
|
path: objectPath
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -54,7 +54,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import MctTicks from "../MctTicks.vue";
|
import MctTicks from "../MctTicks.vue";
|
||||||
import eventHelpers from '../lib/eventHelpers';
|
import eventHelpers from '../lib/eventHelpers';
|
||||||
import configStore from "../configuration/configStore";
|
import configStore from "../configuration/ConfigStore";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
@ -57,7 +57,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import MctTicks from "../MctTicks.vue";
|
import MctTicks from "../MctTicks.vue";
|
||||||
import configStore from "../configuration/configStore";
|
import configStore from "../configuration/ConfigStore";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
57
src/plugins/plot/barGraph/BarGraphCompositionPolicy.js
Normal file
57
src/plugins/plot/barGraph/BarGraphCompositionPolicy.js
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* Open MCT includes source code licensed under additional open source
|
||||||
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
* this source code distribution or the Licensing information page available
|
||||||
|
* at runtime from the About dialog for additional information.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
import { BAR_GRAPH_KEY } from './BarGraphConstants';
|
||||||
|
|
||||||
|
export default function BarGraphCompositionPolicy(openmct) {
|
||||||
|
function hasAggregateDomainAndRange(metadata) {
|
||||||
|
const rangeValues = metadata.valuesForHints(['range']);
|
||||||
|
|
||||||
|
return rangeValues.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasBarGraphTelemetry(domainObject) {
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(domainObject, 'telemetry')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let metadata = openmct.telemetry.getMetadata(domainObject);
|
||||||
|
|
||||||
|
return metadata.values().length > 0 && hasAggregateDomainAndRange(metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasNoChildren(parentObject) {
|
||||||
|
return parentObject.composition && parentObject.composition.length < 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
allow: function (parent, child) {
|
||||||
|
if ((parent.type === BAR_GRAPH_KEY)
|
||||||
|
&& ((child.type !== 'telemetry.plot.overlay') && (hasBarGraphTelemetry(child) === false))
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
346
src/plugins/plot/barGraph/BarGraphCompositionPolicySpec.js
Normal file
346
src/plugins/plot/barGraph/BarGraphCompositionPolicySpec.js
Normal file
@ -0,0 +1,346 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* Open MCT includes source code licensed under additional open source
|
||||||
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
* this source code distribution or the Licensing information page available
|
||||||
|
* at runtime from the About dialog for additional information.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
import BarGraphCompositionPolicy from "./BarGraphCompositionPolicy";
|
||||||
|
import { createOpenMct } from "utils/testing";
|
||||||
|
|
||||||
|
describe("The bar graph composition policy", () => {
|
||||||
|
let openmct;
|
||||||
|
const mockMetaDataWithNoRangeHints = {
|
||||||
|
"period": 10,
|
||||||
|
"amplitude": 1,
|
||||||
|
"offset": 0,
|
||||||
|
"dataRateInHz": 1,
|
||||||
|
"phase": 0,
|
||||||
|
"randomness": 0,
|
||||||
|
valuesForHints: () => {
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
"key": "name",
|
||||||
|
"name": "Name",
|
||||||
|
"format": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "utc",
|
||||||
|
"name": "Time",
|
||||||
|
"format": "utc",
|
||||||
|
"hints": {
|
||||||
|
"domain": 1,
|
||||||
|
"priority": 1
|
||||||
|
},
|
||||||
|
"source": "utc"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
const mockMetaDataWithRangeHints = {
|
||||||
|
"period": 10,
|
||||||
|
"amplitude": 1,
|
||||||
|
"offset": 0,
|
||||||
|
"dataRateInHz": 1,
|
||||||
|
"phase": 0,
|
||||||
|
"randomness": 0,
|
||||||
|
"wavelength": 0,
|
||||||
|
valuesForHints: () => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"key": "sin",
|
||||||
|
"name": "Sine",
|
||||||
|
"unit": "Hz",
|
||||||
|
"formatString": "%0.2f",
|
||||||
|
"hints": {
|
||||||
|
"range": 1,
|
||||||
|
"priority": 4
|
||||||
|
},
|
||||||
|
"source": "sin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "cos",
|
||||||
|
"name": "Cosine",
|
||||||
|
"unit": "deg",
|
||||||
|
"formatString": "%0.2f",
|
||||||
|
"hints": {
|
||||||
|
"range": 2,
|
||||||
|
"priority": 5
|
||||||
|
},
|
||||||
|
"source": "cos"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
},
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
"key": "name",
|
||||||
|
"name": "Name",
|
||||||
|
"format": "string",
|
||||||
|
"source": "name",
|
||||||
|
"hints": {
|
||||||
|
"priority": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "utc",
|
||||||
|
"name": "Time",
|
||||||
|
"format": "utc",
|
||||||
|
"hints": {
|
||||||
|
"domain": 1,
|
||||||
|
"priority": 1
|
||||||
|
},
|
||||||
|
"source": "utc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "yesterday",
|
||||||
|
"name": "Yesterday",
|
||||||
|
"format": "utc",
|
||||||
|
"hints": {
|
||||||
|
"domain": 2,
|
||||||
|
"priority": 2
|
||||||
|
},
|
||||||
|
"source": "yesterday"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "sin",
|
||||||
|
"name": "Sine",
|
||||||
|
"unit": "Hz",
|
||||||
|
"formatString": "%0.2f",
|
||||||
|
"hints": {
|
||||||
|
"range": 1,
|
||||||
|
"spectralAttribute": true
|
||||||
|
},
|
||||||
|
"source": "sin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "cos",
|
||||||
|
"name": "Cosine",
|
||||||
|
"unit": "deg",
|
||||||
|
"formatString": "%0.2f",
|
||||||
|
"hints": {
|
||||||
|
"range": 2,
|
||||||
|
"priority": 5
|
||||||
|
},
|
||||||
|
"source": "cos"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
openmct = createOpenMct();
|
||||||
|
const mockTypeDef = {
|
||||||
|
telemetry: mockMetaDataWithRangeHints
|
||||||
|
};
|
||||||
|
const mockTypeService = {
|
||||||
|
getType: () => {
|
||||||
|
return {
|
||||||
|
typeDef: mockTypeDef
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
openmct.$injector = {
|
||||||
|
get: () => {
|
||||||
|
return mockTypeService;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
openmct.telemetry.isTelemetryObject = function (domainObject) {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it("exists", () => {
|
||||||
|
expect(BarGraphCompositionPolicy(openmct).allow).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
xit("allow composition for telemetry that provides/supports bar graph meta data", () => {
|
||||||
|
const parent = {
|
||||||
|
"composition": [],
|
||||||
|
"configuration": {},
|
||||||
|
"name": "Some Bar Graph",
|
||||||
|
"type": "telemetry.plot.bar-graph",
|
||||||
|
"location": "mine",
|
||||||
|
"modified": 1631005183584,
|
||||||
|
"persisted": 1631005183502,
|
||||||
|
"identifier": {
|
||||||
|
"namespace": "",
|
||||||
|
"key": "b78e7e23-f2b8-4776-b1f0-3ff778f5c8a9"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const child = {
|
||||||
|
"telemetry": {
|
||||||
|
"period": 10,
|
||||||
|
"amplitude": 1,
|
||||||
|
"offset": 0,
|
||||||
|
"dataRateInHz": 1,
|
||||||
|
"phase": 0,
|
||||||
|
"randomness": 0
|
||||||
|
},
|
||||||
|
"name": "Unnamed Sine Wave Generator",
|
||||||
|
"type": "generator",
|
||||||
|
"location": "mine",
|
||||||
|
"modified": 1630399715531,
|
||||||
|
"persisted": 1630399715531,
|
||||||
|
"identifier": {
|
||||||
|
"namespace": "",
|
||||||
|
"key": "21d61f2d-6d2d-4bea-8b0a-7f59fd504c6c"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expect(BarGraphCompositionPolicy(openmct).allow(parent, child)).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows composition for telemetry that contain at least one range", () => {
|
||||||
|
const mockTypeDef = {
|
||||||
|
telemetry: mockMetaDataWithRangeHints
|
||||||
|
};
|
||||||
|
const mockTypeService = {
|
||||||
|
getType: () => {
|
||||||
|
return {
|
||||||
|
typeDef: mockTypeDef
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
openmct.$injector = {
|
||||||
|
get: () => {
|
||||||
|
return mockTypeService;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const parent = {
|
||||||
|
"composition": [],
|
||||||
|
"configuration": {},
|
||||||
|
"name": "Some Bar Graph",
|
||||||
|
"type": "telemetry.plot.bar-graph",
|
||||||
|
"location": "mine",
|
||||||
|
"modified": 1631005183584,
|
||||||
|
"persisted": 1631005183502,
|
||||||
|
"identifier": {
|
||||||
|
"namespace": "",
|
||||||
|
"key": "b78e7e23-f2b8-4776-b1f0-3ff778f5c8a9"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const child = {
|
||||||
|
"telemetry": {
|
||||||
|
"period": 10,
|
||||||
|
"amplitude": 1,
|
||||||
|
"offset": 0,
|
||||||
|
"dataRateInHz": 1,
|
||||||
|
"phase": 0,
|
||||||
|
"randomness": 0
|
||||||
|
},
|
||||||
|
"name": "Unnamed Sine Wave Generator",
|
||||||
|
"type": "generator",
|
||||||
|
"location": "mine",
|
||||||
|
"modified": 1630399715531,
|
||||||
|
"persisted": 1630399715531,
|
||||||
|
"identifier": {
|
||||||
|
"namespace": "",
|
||||||
|
"key": "21d61f2d-6d2d-4bea-8b0a-7f59fd504c6c"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expect(BarGraphCompositionPolicy(openmct).allow(parent, child)).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("disallows composition for telemetry that don't contain any range hints", () => {
|
||||||
|
const mockTypeDef = {
|
||||||
|
telemetry: mockMetaDataWithNoRangeHints
|
||||||
|
};
|
||||||
|
const mockTypeService = {
|
||||||
|
getType: () => {
|
||||||
|
return {
|
||||||
|
typeDef: mockTypeDef
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
openmct.$injector = {
|
||||||
|
get: () => {
|
||||||
|
return mockTypeService;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const parent = {
|
||||||
|
"composition": [],
|
||||||
|
"configuration": {},
|
||||||
|
"name": "Some Bar Graph",
|
||||||
|
"type": "telemetry.plot.bar-graph",
|
||||||
|
"location": "mine",
|
||||||
|
"modified": 1631005183584,
|
||||||
|
"persisted": 1631005183502,
|
||||||
|
"identifier": {
|
||||||
|
"namespace": "",
|
||||||
|
"key": "b78e7e23-f2b8-4776-b1f0-3ff778f5c8a9"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const child = {
|
||||||
|
"telemetry": {
|
||||||
|
"period": 10,
|
||||||
|
"amplitude": 1,
|
||||||
|
"offset": 0,
|
||||||
|
"dataRateInHz": 1,
|
||||||
|
"phase": 0,
|
||||||
|
"randomness": 0
|
||||||
|
},
|
||||||
|
"name": "Unnamed Sine Wave Generator",
|
||||||
|
"type": "generator",
|
||||||
|
"location": "mine",
|
||||||
|
"modified": 1630399715531,
|
||||||
|
"persisted": 1630399715531,
|
||||||
|
"identifier": {
|
||||||
|
"namespace": "",
|
||||||
|
"key": "21d61f2d-6d2d-4bea-8b0a-7f59fd504c6c"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expect(BarGraphCompositionPolicy(openmct).allow(parent, child)).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("passthrough for composition for non bar graph plots", () => {
|
||||||
|
const parent = {
|
||||||
|
"composition": [],
|
||||||
|
"configuration": {},
|
||||||
|
"name": "Some Stacked Plot",
|
||||||
|
"type": "telemetry.plot.stacked",
|
||||||
|
"location": "mine",
|
||||||
|
"modified": 1631005183584,
|
||||||
|
"persisted": 1631005183502,
|
||||||
|
"identifier": {
|
||||||
|
"namespace": "",
|
||||||
|
"key": "b78e7e23-f2b8-4776-b1f0-3ff778f5c8a9"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const child = {
|
||||||
|
"telemetry": {
|
||||||
|
"period": 10,
|
||||||
|
"amplitude": 1,
|
||||||
|
"offset": 0,
|
||||||
|
"dataRateInHz": 1,
|
||||||
|
"phase": 0,
|
||||||
|
"randomness": 0
|
||||||
|
},
|
||||||
|
"name": "Unnamed Sine Wave Generator",
|
||||||
|
"type": "generator",
|
||||||
|
"location": "mine",
|
||||||
|
"modified": 1630399715531,
|
||||||
|
"persisted": 1630399715531,
|
||||||
|
"identifier": {
|
||||||
|
"namespace": "",
|
||||||
|
"key": "21d61f2d-6d2d-4bea-8b0a-7f59fd504c6c"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expect(BarGraphCompositionPolicy(openmct).allow(parent, child)).toEqual(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
5
src/plugins/plot/barGraph/BarGraphConstants.js
Normal file
5
src/plugins/plot/barGraph/BarGraphConstants.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export const BAR_GRAPH_VIEW = 'bar-graph.view';
|
||||||
|
export const BAR_GRAPH_KEY = 'telemetry.plot.bar-graph';
|
||||||
|
export const BAR_GRAPH_INSPECTOR_KEY = 'telemetry.plot.bar-graph.inspector';
|
||||||
|
export const SUBSCRIBE = 'subscribe';
|
||||||
|
export const UNSUBSCRIBE = 'unsubscribe';
|
293
src/plugins/plot/barGraph/BarGraphPlot.vue
Normal file
293
src/plugins/plot/barGraph/BarGraphPlot.vue
Normal file
@ -0,0 +1,293 @@
|
|||||||
|
<template>
|
||||||
|
<div ref="plotWrapper"
|
||||||
|
class="has-local-controls"
|
||||||
|
:class="{ 's-unsynced' : isZoomed }"
|
||||||
|
>
|
||||||
|
<div v-if="isZoomed"
|
||||||
|
class="l-state-indicators"
|
||||||
|
>
|
||||||
|
<span class="l-state-indicators__alert-no-lad t-object-alert t-alert-unsynced icon-alert-triangle"
|
||||||
|
title="This plot is not currently displaying the latest data. Reset pan/zoom to view latest data."
|
||||||
|
></span>
|
||||||
|
</div>
|
||||||
|
<div ref="plot"
|
||||||
|
class="c-bar-chart"
|
||||||
|
></div>
|
||||||
|
<div v-if="false"
|
||||||
|
ref="localControl"
|
||||||
|
class="gl-plot__local-controls h-local-controls h-local-controls--overlay-content c-local-controls--show-on-hover"
|
||||||
|
>
|
||||||
|
<button v-if="data.length"
|
||||||
|
class="c-button icon-reset"
|
||||||
|
:disabled="!isZoomed"
|
||||||
|
title="Reset pan/zoom"
|
||||||
|
@click="reset()"
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import Plotly from 'plotly.js-basic-dist';
|
||||||
|
import { SUBSCRIBE, UNSUBSCRIBE } from './BarGraphConstants';
|
||||||
|
|
||||||
|
const MULTI_AXES_X_PADDING_PERCENT = {
|
||||||
|
LEFT: 8,
|
||||||
|
RIGHT: 94
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
inject: ['openmct', 'domainObject'],
|
||||||
|
props: {
|
||||||
|
data: {
|
||||||
|
type: Array,
|
||||||
|
default() {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plotAxisTitle: {
|
||||||
|
type: Object,
|
||||||
|
default() {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isZoomed: false,
|
||||||
|
primaryYAxisRange: {
|
||||||
|
min: '',
|
||||||
|
max: ''
|
||||||
|
},
|
||||||
|
xAxisRange: {
|
||||||
|
min: '',
|
||||||
|
max: ''
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
data: {
|
||||||
|
immediate: false,
|
||||||
|
handler: 'updateData'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
Plotly.newPlot(this.$refs.plot, Array.from(this.data), this.getLayout(), {
|
||||||
|
responsive: true,
|
||||||
|
displayModeBar: false
|
||||||
|
});
|
||||||
|
this.registerListeners();
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.$refs.plot.removeAllListeners();
|
||||||
|
|
||||||
|
if (this.plotResizeObserver) {
|
||||||
|
this.plotResizeObserver.unobserve(this.$refs.plotWrapper);
|
||||||
|
clearTimeout(this.resizeTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.removeBarColorListener) {
|
||||||
|
this.removeBarColorListener();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getAxisMinMax(axis) {
|
||||||
|
const min = axis.autoSize
|
||||||
|
? ''
|
||||||
|
: axis.min;
|
||||||
|
const max = axis.autoSize
|
||||||
|
? ''
|
||||||
|
: axis.max;
|
||||||
|
|
||||||
|
return {
|
||||||
|
min,
|
||||||
|
max
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getLayout() {
|
||||||
|
const yAxesMeta = this.getYAxisMeta();
|
||||||
|
const primaryYaxis = this.getYaxisLayout(yAxesMeta['1']);
|
||||||
|
const xAxisDomain = this.getXAxisDomain(yAxesMeta);
|
||||||
|
|
||||||
|
return {
|
||||||
|
autosize: true,
|
||||||
|
showlegend: false,
|
||||||
|
textposition: 'auto',
|
||||||
|
font: {
|
||||||
|
family: 'Helvetica Neue, Helvetica, Arial, sans-serif',
|
||||||
|
size: '12px',
|
||||||
|
color: '#666'
|
||||||
|
},
|
||||||
|
xaxis: {
|
||||||
|
domain: xAxisDomain,
|
||||||
|
range: [this.xAxisRange.min, this.xAxisRange.max],
|
||||||
|
title: this.plotAxisTitle.xAxisTitle,
|
||||||
|
automargin: true,
|
||||||
|
fixedrange: true
|
||||||
|
},
|
||||||
|
yaxis: primaryYaxis,
|
||||||
|
margin: {
|
||||||
|
l: 5,
|
||||||
|
r: 5,
|
||||||
|
t: 5,
|
||||||
|
b: 0
|
||||||
|
},
|
||||||
|
paper_bgcolor: 'transparent',
|
||||||
|
plot_bgcolor: 'transparent'
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getYAxisMeta() {
|
||||||
|
const yAxisMeta = {};
|
||||||
|
|
||||||
|
this.data.forEach(d => {
|
||||||
|
const yAxisMetadata = d.yAxisMetadata;
|
||||||
|
const range = '1';
|
||||||
|
const side = 'left';
|
||||||
|
const name = '';
|
||||||
|
const unit = yAxisMetadata.units;
|
||||||
|
|
||||||
|
yAxisMeta[range] = {
|
||||||
|
range,
|
||||||
|
side,
|
||||||
|
name,
|
||||||
|
unit
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return yAxisMeta;
|
||||||
|
},
|
||||||
|
getXAxisDomain(yAxisMeta) {
|
||||||
|
let leftPaddingPerc = 0;
|
||||||
|
let rightPaddingPerc = 100;
|
||||||
|
let rightSide = yAxisMeta && Object.values(yAxisMeta).filter((axisMeta => axisMeta.side === 'right'));
|
||||||
|
let leftSide = yAxisMeta && Object.values(yAxisMeta).filter((axisMeta => axisMeta.side === 'left'));
|
||||||
|
if (yAxisMeta && rightSide.length > 1) {
|
||||||
|
rightPaddingPerc = MULTI_AXES_X_PADDING_PERCENT.RIGHT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (yAxisMeta && leftSide.length > 1) {
|
||||||
|
leftPaddingPerc = MULTI_AXES_X_PADDING_PERCENT.LEFT;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [leftPaddingPerc / 100, rightPaddingPerc / 100];
|
||||||
|
},
|
||||||
|
getYaxisLayout(yAxisMeta) {
|
||||||
|
if (!yAxisMeta) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const { name, range, side = 'left', unit } = yAxisMeta;
|
||||||
|
const title = `${name} ${unit ? '(' + unit + ')' : ''}`;
|
||||||
|
const yaxis = {
|
||||||
|
automargin: true,
|
||||||
|
fixedrange: true,
|
||||||
|
title
|
||||||
|
};
|
||||||
|
|
||||||
|
let yAxistype = this.primaryYAxisRange;
|
||||||
|
if (range === '1') {
|
||||||
|
yaxis.range = [yAxistype.min, yAxistype.max];
|
||||||
|
|
||||||
|
return yaxis;
|
||||||
|
}
|
||||||
|
|
||||||
|
yaxis.range = [yAxistype.min, yAxistype.max];
|
||||||
|
yaxis.anchor = side.toLowerCase() === 'left'
|
||||||
|
? 'free'
|
||||||
|
: 'x';
|
||||||
|
yaxis.showline = side.toLowerCase() === 'left';
|
||||||
|
yaxis.side = side.toLowerCase();
|
||||||
|
yaxis.overlaying = 'y';
|
||||||
|
yaxis.position = 0.01;
|
||||||
|
|
||||||
|
return yaxis;
|
||||||
|
},
|
||||||
|
registerListeners() {
|
||||||
|
this.$refs.plot.on('plotly_relayout', this.zoom);
|
||||||
|
|
||||||
|
this.removeBarColorListener = this.openmct.objects.observe(
|
||||||
|
this.domainObject,
|
||||||
|
'configuration.barStyles',
|
||||||
|
this.barColorChanged
|
||||||
|
);
|
||||||
|
this.resizeTimer = false;
|
||||||
|
if (window.ResizeObserver) {
|
||||||
|
this.plotResizeObserver = new ResizeObserver(() => {
|
||||||
|
// debounce and trigger window resize so that plotly can resize the plot
|
||||||
|
clearTimeout(this.resizeTimer);
|
||||||
|
this.resizeTimer = setTimeout(() => {
|
||||||
|
window.dispatchEvent(new Event('resize'));
|
||||||
|
}, 250);
|
||||||
|
});
|
||||||
|
this.plotResizeObserver.observe(this.$refs.plotWrapper);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
reset() {
|
||||||
|
this.updatePlot();
|
||||||
|
|
||||||
|
this.isZoomed = false;
|
||||||
|
this.$emit(SUBSCRIBE);
|
||||||
|
},
|
||||||
|
barColorChanged() {
|
||||||
|
const colors = [];
|
||||||
|
const indices = [];
|
||||||
|
this.data.forEach((item, index) => {
|
||||||
|
const key = item.key;
|
||||||
|
const color = this.domainObject.configuration.barStyles[key] && this.domainObject.configuration.barStyles[key].color;
|
||||||
|
indices.push(index);
|
||||||
|
if (color) {
|
||||||
|
colors.push();
|
||||||
|
} else {
|
||||||
|
colors.push(item.marker.color);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const plotUpdate = {
|
||||||
|
'marker.color': colors
|
||||||
|
};
|
||||||
|
Plotly.restyle(this.$refs.plot, plotUpdate, indices);
|
||||||
|
},
|
||||||
|
updateData() {
|
||||||
|
this.updatePlot();
|
||||||
|
},
|
||||||
|
updateLocalControlPosition() {
|
||||||
|
const localControl = this.$refs.localControl;
|
||||||
|
localControl.style.display = 'none';
|
||||||
|
|
||||||
|
const plot = this.$refs.plot;
|
||||||
|
const bgLayer = this.$el.querySelector('.bglayer');
|
||||||
|
|
||||||
|
const plotBoundingRect = plot.getBoundingClientRect();
|
||||||
|
const bgLayerBoundingRect = bgLayer.getBoundingClientRect();
|
||||||
|
|
||||||
|
const top = bgLayerBoundingRect.top - plotBoundingRect.top + 5;
|
||||||
|
const left = bgLayerBoundingRect.left - plotBoundingRect.left + 5;
|
||||||
|
|
||||||
|
localControl.style.top = `${top}px`;
|
||||||
|
localControl.style.left = `${left}px`;
|
||||||
|
localControl.style.display = 'block';
|
||||||
|
},
|
||||||
|
updatePlot() {
|
||||||
|
if (!this.$refs || !this.$refs.plot) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Plotly.react(this.$refs.plot, Array.from(this.data), this.getLayout());
|
||||||
|
},
|
||||||
|
zoom(eventData) {
|
||||||
|
const autorange = eventData['xaxis.autorange'];
|
||||||
|
const { autosize } = eventData;
|
||||||
|
|
||||||
|
if (autosize || autorange) {
|
||||||
|
this.isZoomed = false;
|
||||||
|
this.reset();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isZoomed = true;
|
||||||
|
this.$emit(UNSUBSCRIBE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
293
src/plugins/plot/barGraph/BarGraphView.vue
Normal file
293
src/plugins/plot/barGraph/BarGraphView.vue
Normal file
@ -0,0 +1,293 @@
|
|||||||
|
<!--
|
||||||
|
Open MCT, Copyright (c) 2014-2021, United States Government
|
||||||
|
as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
Administration. All rights reserved.
|
||||||
|
|
||||||
|
Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
"License"); you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
License for the specific language governing permissions and limitations
|
||||||
|
under the License.
|
||||||
|
|
||||||
|
Open MCT includes source code licensed under additional open source
|
||||||
|
licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
this source code distribution or the Licensing information page available
|
||||||
|
at runtime from the About dialog for additional information.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<BarGraph ref="barGraph"
|
||||||
|
class="c-plot c-bar-chart-view"
|
||||||
|
:data="trace"
|
||||||
|
:plot-axis-title="plotAxisTitle"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import * as SPECTRAL_AGGREGATE from './BarGraphConstants';
|
||||||
|
import ColorPalette from '../lib/ColorPalette';
|
||||||
|
import BarGraph from './BarGraphPlot.vue';
|
||||||
|
import Color from "@/plugins/plot/lib/Color";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
BarGraph
|
||||||
|
},
|
||||||
|
inject: ['openmct', 'domainObject'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
composition: {},
|
||||||
|
currentDomainObject: this.domainObject,
|
||||||
|
subscriptions: [],
|
||||||
|
telemetryObjects: {},
|
||||||
|
trace: []
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
activeClock() {
|
||||||
|
return this.openmct.time.activeClock;
|
||||||
|
},
|
||||||
|
plotAxisTitle() {
|
||||||
|
const { xAxisMetadata = {}, yAxisMetadata = {} } = this.trace[0] || {};
|
||||||
|
const xAxisUnit = xAxisMetadata.units ? `(${xAxisMetadata.units})` : '';
|
||||||
|
const yAxisUnit = yAxisMetadata.units ? `(${yAxisMetadata.units})` : '';
|
||||||
|
|
||||||
|
return {
|
||||||
|
xAxisTitle: `${xAxisMetadata.name || ''} ${xAxisUnit}`,
|
||||||
|
yAxisTitle: `${yAxisMetadata.name || ''} ${yAxisUnit}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.colorPalette = new ColorPalette();
|
||||||
|
this.loadComposition();
|
||||||
|
|
||||||
|
this.openmct.time.on('bounds', this.refreshData);
|
||||||
|
this.openmct.time.on('clock', this.clockChanged);
|
||||||
|
|
||||||
|
this.$refs.barGraph.$on(SPECTRAL_AGGREGATE.SUBSCRIBE, this.subscribeToAll);
|
||||||
|
this.$refs.barGraph.$on(SPECTRAL_AGGREGATE.UNSUBSCRIBE, this.removeAllSubscriptions);
|
||||||
|
|
||||||
|
this.unobserve = this.openmct.objects.observe(this.currentDomainObject, '*', this.updateDomainObject);
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.$refs.barGraph.$off();
|
||||||
|
this.openmct.time.off('bounds', this.refreshData);
|
||||||
|
this.openmct.time.off('clock', this.clockChanged);
|
||||||
|
|
||||||
|
this.removeAllSubscriptions();
|
||||||
|
this.unobserve();
|
||||||
|
|
||||||
|
if (!this.composition) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.composition.off('add', this.addTelemetryObject);
|
||||||
|
this.composition.off('remove', this.removeTelemetryObject);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
addTelemetryObject(telemetryObject) {
|
||||||
|
const key = this.openmct.objects.makeKeyString(telemetryObject.identifier);
|
||||||
|
|
||||||
|
if (!this.domainObject.configuration.barStyles) {
|
||||||
|
this.domainObject.configuration.barStyles = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// check to see if we've set a bar color
|
||||||
|
if (!this.domainObject.configuration.barStyles[key] || !this.domainObject.configuration.barStyles[key].color) {
|
||||||
|
const color = this.colorPalette.getNextColor().asHexString();
|
||||||
|
this.domainObject.configuration.barStyles[key] = {
|
||||||
|
name: telemetryObject.name,
|
||||||
|
color
|
||||||
|
};
|
||||||
|
this.openmct.objects.mutate(
|
||||||
|
this.domainObject,
|
||||||
|
`configuration.barStyles[${key}]`,
|
||||||
|
this.domainObject.configuration.barStyles[key]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
let color = this.domainObject.configuration.barStyles[key].color;
|
||||||
|
if (!(color instanceof Color)) {
|
||||||
|
color = Color.fromHexString(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.colorPalette.remove(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.telemetryObjects[key] = telemetryObject;
|
||||||
|
|
||||||
|
this.requestDataFor(telemetryObject);
|
||||||
|
this.subscribeToObject(telemetryObject);
|
||||||
|
},
|
||||||
|
addTrace(trace, key) {
|
||||||
|
if (!this.trace.length) {
|
||||||
|
this.trace = this.trace.concat([trace]);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let isInTrace = false;
|
||||||
|
const newTrace = this.trace.map((currentTrace, index) => {
|
||||||
|
if (currentTrace.key !== key) {
|
||||||
|
return currentTrace;
|
||||||
|
}
|
||||||
|
|
||||||
|
isInTrace = true;
|
||||||
|
|
||||||
|
return trace;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.trace = isInTrace ? newTrace : newTrace.concat([trace]);
|
||||||
|
},
|
||||||
|
clockChanged() {
|
||||||
|
this.removeAllSubscriptions();
|
||||||
|
this.subscribeToAll();
|
||||||
|
},
|
||||||
|
getAxisMetadata(telemetryObject) {
|
||||||
|
const metadata = this.openmct.telemetry.getMetadata(telemetryObject);
|
||||||
|
if (!metadata) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const yAxisMetadata = metadata.valuesForHints(['range'])[0];
|
||||||
|
//Exclude 'name' and 'time' based metadata specifically, from the x-Axis values by using range hints only
|
||||||
|
const xAxisMetadata = metadata.valuesForHints(['range']);
|
||||||
|
|
||||||
|
return {
|
||||||
|
xAxisMetadata,
|
||||||
|
yAxisMetadata
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getOptions(telemetryObject) {
|
||||||
|
const { start, end } = this.openmct.time.bounds();
|
||||||
|
|
||||||
|
return {
|
||||||
|
end,
|
||||||
|
start,
|
||||||
|
startTime: null,
|
||||||
|
spectra: true
|
||||||
|
};
|
||||||
|
},
|
||||||
|
loadComposition() {
|
||||||
|
this.composition = this.openmct.composition.get(this.currentDomainObject);
|
||||||
|
|
||||||
|
if (!this.composition) {
|
||||||
|
this.addTelemetryObject(this.currentDomainObject);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.composition.on('add', this.addTelemetryObject);
|
||||||
|
this.composition.on('remove', this.removeTelemetryObject);
|
||||||
|
this.composition.load();
|
||||||
|
},
|
||||||
|
refreshData(bounds, isTick) {
|
||||||
|
if (!isTick) {
|
||||||
|
const telemetryObjects = Object.values(this.telemetryObjects);
|
||||||
|
telemetryObjects.forEach(this.requestDataFor);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removeAllSubscriptions() {
|
||||||
|
this.subscriptions.forEach(subscription => subscription.unsubscribe());
|
||||||
|
this.subscriptions = [];
|
||||||
|
},
|
||||||
|
removeSubscription(key) {
|
||||||
|
const found = this.subscriptions.findIndex(subscription => subscription.key === key);
|
||||||
|
if (found > -1) {
|
||||||
|
this.subscriptions[found].unsubscribe();
|
||||||
|
this.subscriptions.splice(found, 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removeTelemetryObject(identifier) {
|
||||||
|
const key = this.openmct.objects.makeKeyString(identifier);
|
||||||
|
delete this.telemetryObjects[key];
|
||||||
|
if (this.domainObject.configuration.barStyles[key]) {
|
||||||
|
delete this.domainObject.configuration.barStyles[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.removeSubscription(key);
|
||||||
|
|
||||||
|
this.trace = this.trace.filter(t => t.key !== key);
|
||||||
|
},
|
||||||
|
processData(telemetryObject, data, axisMetadata) {
|
||||||
|
const key = this.openmct.objects.makeKeyString(telemetryObject.identifier);
|
||||||
|
|
||||||
|
if (data.message) {
|
||||||
|
this.openmct.notifications.alert(data.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
let xValues = [];
|
||||||
|
let yValues = [];
|
||||||
|
|
||||||
|
//populate X and Y values for plotly
|
||||||
|
axisMetadata.xAxisMetadata.forEach((metadata) => {
|
||||||
|
xValues.push(metadata.name);
|
||||||
|
if (data[metadata.key]) {
|
||||||
|
//TODO: Format the data?
|
||||||
|
yValues.push(data[metadata.key]);
|
||||||
|
} else {
|
||||||
|
yValues.push('');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const trace = {
|
||||||
|
key,
|
||||||
|
name: telemetryObject.name,
|
||||||
|
x: xValues,
|
||||||
|
y: yValues,
|
||||||
|
text: yValues.map(String),
|
||||||
|
xAxisMetadata: axisMetadata.xAxisMetadata,
|
||||||
|
yAxisMetadata: axisMetadata.yAxisMetadata,
|
||||||
|
type: 'bar',
|
||||||
|
marker: {
|
||||||
|
color: this.domainObject.configuration.barStyles[key].color
|
||||||
|
},
|
||||||
|
hoverinfo: 'skip'
|
||||||
|
};
|
||||||
|
|
||||||
|
this.addTrace(trace, key);
|
||||||
|
},
|
||||||
|
requestDataFor(telemetryObject) {
|
||||||
|
const axisMetadata = this.getAxisMetadata(telemetryObject);
|
||||||
|
this.openmct.telemetry.request(telemetryObject, this.getOptions(telemetryObject))
|
||||||
|
.then(data => {
|
||||||
|
data.forEach((datum) => {
|
||||||
|
this.processData(telemetryObject, datum, axisMetadata);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.warn(`Error fetching data`, error);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
subscribeToObject(telemetryObject) {
|
||||||
|
const key = this.openmct.objects.makeKeyString(telemetryObject.identifier);
|
||||||
|
|
||||||
|
this.removeSubscription(key);
|
||||||
|
|
||||||
|
const options = this.getOptions(telemetryObject);
|
||||||
|
const axisMetadata = this.getAxisMetadata(telemetryObject);
|
||||||
|
const unsubscribe = this.openmct.telemetry.subscribe(telemetryObject,
|
||||||
|
data => this.processData(telemetryObject, data, axisMetadata)
|
||||||
|
, options);
|
||||||
|
|
||||||
|
this.subscriptions.push({
|
||||||
|
key,
|
||||||
|
unsubscribe
|
||||||
|
});
|
||||||
|
},
|
||||||
|
subscribeToAll() {
|
||||||
|
const telemetryObjects = Object.values(this.telemetryObjects);
|
||||||
|
telemetryObjects.forEach(this.subscribeToObject);
|
||||||
|
},
|
||||||
|
updateDomainObject(newDomainObject) {
|
||||||
|
this.currentDomainObject = newDomainObject;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
76
src/plugins/plot/barGraph/BarGraphViewProvider.js
Normal file
76
src/plugins/plot/barGraph/BarGraphViewProvider.js
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* Open MCT includes source code licensed under additional open source
|
||||||
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
* this source code distribution or the Licensing information page available
|
||||||
|
* at runtime from the About dialog for additional information.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
import BarGraphView from './BarGraphView.vue';
|
||||||
|
import { BAR_GRAPH_KEY, BAR_GRAPH_VIEW } from './BarGraphConstants';
|
||||||
|
import Vue from 'vue';
|
||||||
|
|
||||||
|
export default function BarGraphViewProvider(openmct) {
|
||||||
|
function isCompactView(objectPath) {
|
||||||
|
return objectPath.find(object => object.type === 'time-strip');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: BAR_GRAPH_VIEW,
|
||||||
|
name: 'Spectral Aggregate Plot',
|
||||||
|
cssClass: 'icon-telemetry',
|
||||||
|
canView(domainObject, objectPath) {
|
||||||
|
return domainObject && domainObject.type === BAR_GRAPH_KEY;
|
||||||
|
},
|
||||||
|
|
||||||
|
canEdit(domainObject, objectPath) {
|
||||||
|
return domainObject && domainObject.type === BAR_GRAPH_KEY;
|
||||||
|
},
|
||||||
|
|
||||||
|
view: function (domainObject, objectPath) {
|
||||||
|
let component;
|
||||||
|
|
||||||
|
return {
|
||||||
|
show: function (element) {
|
||||||
|
let isCompact = isCompactView(objectPath);
|
||||||
|
component = new Vue({
|
||||||
|
el: element,
|
||||||
|
components: {
|
||||||
|
BarGraphView
|
||||||
|
},
|
||||||
|
provide: {
|
||||||
|
openmct,
|
||||||
|
domainObject
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
options: {
|
||||||
|
compact: isCompact
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
template: '<bar-graph-view :options="options"></bar-graph-view>'
|
||||||
|
});
|
||||||
|
},
|
||||||
|
destroy: function () {
|
||||||
|
component.$destroy();
|
||||||
|
component = undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
import { BAR_GRAPH_INSPECTOR_KEY, BAR_GRAPH_KEY } from '../BarGraphConstants';
|
||||||
|
import Vue from 'vue';
|
||||||
|
import Options from "./Options.vue";
|
||||||
|
|
||||||
|
export default function BarGraphInspectorViewProvider(openmct) {
|
||||||
|
return {
|
||||||
|
key: BAR_GRAPH_INSPECTOR_KEY,
|
||||||
|
name: 'Bar Graph Inspector View',
|
||||||
|
canView: function (selection) {
|
||||||
|
if (selection.length === 0 || selection[0].length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let object = selection[0][0].context.item;
|
||||||
|
|
||||||
|
return object
|
||||||
|
&& object.type === BAR_GRAPH_KEY;
|
||||||
|
},
|
||||||
|
view: function (selection) {
|
||||||
|
let component;
|
||||||
|
|
||||||
|
return {
|
||||||
|
show: function (element) {
|
||||||
|
component = new Vue({
|
||||||
|
el: element,
|
||||||
|
components: {
|
||||||
|
Options
|
||||||
|
},
|
||||||
|
provide: {
|
||||||
|
openmct,
|
||||||
|
domainObject: selection[0][0].context.item
|
||||||
|
},
|
||||||
|
template: '<options></options>'
|
||||||
|
});
|
||||||
|
},
|
||||||
|
destroy: function () {
|
||||||
|
if (component) {
|
||||||
|
component.$destroy();
|
||||||
|
component = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
priority: function () {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
107
src/plugins/plot/barGraph/inspector/BarGraphOptions.vue
Normal file
107
src/plugins/plot/barGraph/inspector/BarGraphOptions.vue
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
<!--
|
||||||
|
Open MCT, Copyright (c) 2014-2020, United States Government
|
||||||
|
as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
Administration. All rights reserved.
|
||||||
|
|
||||||
|
Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
"License"); you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
License for the specific language governing permissions and limitations
|
||||||
|
under the License.
|
||||||
|
|
||||||
|
Open MCT includes source code licensed under additional open source
|
||||||
|
licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
this source code distribution or the Licensing information page available
|
||||||
|
at runtime from the About dialog for additional information.
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<ul>
|
||||||
|
<li class="c-tree__item menus-to-left">
|
||||||
|
<span class="c-disclosure-triangle is-enabled flex-elem"
|
||||||
|
:class="expandedCssClass"
|
||||||
|
@click="expanded = !expanded"
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<div class="c-object-label__name">{{ name }}</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<ColorSwatch v-if="expanded"
|
||||||
|
:current-color="currentColor"
|
||||||
|
title="Manually set the color for this bar graph series."
|
||||||
|
edit-title="Manually set the color for this bar graph series"
|
||||||
|
view-title="The color for this bar graph series."
|
||||||
|
short-label="Color"
|
||||||
|
class="grid-properties"
|
||||||
|
@colorSet="setColor"
|
||||||
|
/>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ColorSwatch from '../../ColorSwatch.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
ColorSwatch
|
||||||
|
},
|
||||||
|
inject: ['openmct', 'domainObject'],
|
||||||
|
props: {
|
||||||
|
item: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
currentColor: undefined,
|
||||||
|
name: '',
|
||||||
|
expanded: false
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
expandedCssClass() {
|
||||||
|
return this.expanded ? 'c-disclosure-triangle--expanded' : '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
item: {
|
||||||
|
handler() {
|
||||||
|
this.initColor();
|
||||||
|
},
|
||||||
|
deep: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.key = this.openmct.objects.makeKeyString(this.item);
|
||||||
|
this.initColor();
|
||||||
|
this.unObserve = this.openmct.objects.observe(this.domainObject, `this.domainObject.configuration.barStyles[${this.key}]`, this.initColor);
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
if (this.unObserve) {
|
||||||
|
this.unObserve();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
initColor() {
|
||||||
|
if (this.domainObject.configuration.barStyles && this.domainObject.configuration.barStyles[this.key]) {
|
||||||
|
this.currentColor = this.domainObject.configuration.barStyles[this.key].color;
|
||||||
|
this.name = this.domainObject.configuration.barStyles[this.key].name;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setColor(chosenColor) {
|
||||||
|
this.currentColor = chosenColor.asHexString();
|
||||||
|
this.openmct.objects.mutate(
|
||||||
|
this.domainObject,
|
||||||
|
`configuration.barStyles[${this.key}].color`,
|
||||||
|
this.currentColor
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
61
src/plugins/plot/barGraph/inspector/Options.vue
Normal file
61
src/plugins/plot/barGraph/inspector/Options.vue
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
<!--
|
||||||
|
Open MCT, Copyright (c) 2014-2020, United States Government
|
||||||
|
as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
Administration. All rights reserved.
|
||||||
|
|
||||||
|
Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
"License"); you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
License for the specific language governing permissions and limitations
|
||||||
|
under the License.
|
||||||
|
|
||||||
|
Open MCT includes source code licensed under additional open source
|
||||||
|
licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
this source code distribution or the Licensing information page available
|
||||||
|
at runtime from the About dialog for additional information.
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<ul class="c-tree">
|
||||||
|
<h2 title="Display properties for this object">Bar Graph Series</h2>
|
||||||
|
<bar-graph-options v-for="series in domainObject.composition"
|
||||||
|
:key="series.key"
|
||||||
|
:item="series"
|
||||||
|
/>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import BarGraphOptions from "./BarGraphOptions.vue";
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
BarGraphOptions
|
||||||
|
},
|
||||||
|
inject: ['openmct', 'domainObject'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isEditing: this.openmct.editor.isEditing()
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
canEdit() {
|
||||||
|
return this.isEditing && !this.domainObject.locked;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.openmct.editor.on('isEditing', this.setEditState);
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.openmct.editor.off('isEditing', this.setEditState);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setEditState(isEditing) {
|
||||||
|
this.isEditing = isEditing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
@ -38,7 +38,7 @@ import MCTChartLineStepAfter from './MCTChartLineStepAfter';
|
|||||||
import MCTChartPointSet from './MCTChartPointSet';
|
import MCTChartPointSet from './MCTChartPointSet';
|
||||||
import MCTChartAlarmPointSet from './MCTChartAlarmPointSet';
|
import MCTChartAlarmPointSet from './MCTChartAlarmPointSet';
|
||||||
import MCTChartAlarmLineSet from "./MCTChartAlarmLineSet";
|
import MCTChartAlarmLineSet from "./MCTChartAlarmLineSet";
|
||||||
import configStore from "../configuration/configStore";
|
import configStore from "../configuration/ConfigStore";
|
||||||
import PlotConfigurationModel from "../configuration/PlotConfigurationModel";
|
import PlotConfigurationModel from "../configuration/PlotConfigurationModel";
|
||||||
import LimitLine from "./LimitLine.vue";
|
import LimitLine from "./LimitLine.vue";
|
||||||
import LimitLabel from "./LimitLabel.vue";
|
import LimitLabel from "./LimitLabel.vue";
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import Model from "./Model";
|
import Model from "./Model";
|
||||||
import { MARKER_SHAPES } from '../draw/MarkerShapes';
|
import { MARKER_SHAPES } from '../draw/MarkerShapes';
|
||||||
import configStore from "../configuration/configStore";
|
import configStore from "../configuration/ConfigStore";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Plot series handle interpreting telemetry metadata for a single telemetry
|
* Plot series handle interpreting telemetry metadata for a single telemetry
|
||||||
@ -82,12 +82,17 @@ export default class PlotSeries extends Model {
|
|||||||
.openmct
|
.openmct
|
||||||
.telemetry
|
.telemetry
|
||||||
.getMetadata(options.domainObject);
|
.getMetadata(options.domainObject);
|
||||||
|
|
||||||
this.formats = options
|
this.formats = options
|
||||||
.openmct
|
.openmct
|
||||||
.telemetry
|
.telemetry
|
||||||
.getFormatMap(this.metadata);
|
.getFormatMap(this.metadata);
|
||||||
|
|
||||||
const range = this.metadata.valuesForHints(['range'])[0];
|
//if the object is missing or doesn't have metadata for some reason
|
||||||
|
let range = {};
|
||||||
|
if (this.metadata) {
|
||||||
|
range = this.metadata.valuesForHints(['range'])[0];
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: options.domainObject.name,
|
name: options.domainObject.name,
|
||||||
@ -191,7 +196,10 @@ export default class PlotSeries extends Model {
|
|||||||
.uniq(true, point => [this.getXVal(point), this.getYVal(point)].join())
|
.uniq(true, point => [this.getXVal(point), this.getYVal(point)].join())
|
||||||
.value();
|
.value();
|
||||||
this.reset(newPoints);
|
this.reset(newPoints);
|
||||||
}.bind(this));
|
}.bind(this))
|
||||||
|
.catch((error) => {
|
||||||
|
console.warn('Error fetching data', error);
|
||||||
|
});
|
||||||
/* eslint-enable you-dont-need-lodash-underscore/concat */
|
/* eslint-enable you-dont-need-lodash-underscore/concat */
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
@ -199,7 +207,9 @@ export default class PlotSeries extends Model {
|
|||||||
*/
|
*/
|
||||||
onXKeyChange(xKey) {
|
onXKeyChange(xKey) {
|
||||||
const format = this.formats[xKey];
|
const format = this.formats[xKey];
|
||||||
this.getXVal = format.parse.bind(format);
|
if (format) {
|
||||||
|
this.getXVal = format.parse.bind(format);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Update y formatter on change, default to stepAfter interpolation if
|
* Update y formatter on change, default to stepAfter interpolation if
|
||||||
|
@ -184,7 +184,7 @@ export default class YAxisModel extends Model {
|
|||||||
this.set('values', yMetadata.values);
|
this.set('values', yMetadata.values);
|
||||||
if (!label) {
|
if (!label) {
|
||||||
const labelName = series.map(function (s) {
|
const labelName = series.map(function (s) {
|
||||||
return s.metadata.value(s.get('yKey')).name;
|
return s.metadata ? s.metadata.value(s.get('yKey')).name : '';
|
||||||
}).reduce(function (a, b) {
|
}).reduce(function (a, b) {
|
||||||
if (a === undefined) {
|
if (a === undefined) {
|
||||||
return b;
|
return b;
|
||||||
@ -204,7 +204,7 @@ export default class YAxisModel extends Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const labelUnits = series.map(function (s) {
|
const labelUnits = series.map(function (s) {
|
||||||
return s.metadata.value(s.get('yKey')).units;
|
return s.metadata ? s.metadata.value(s.get('yKey')).units : '';
|
||||||
}).reduce(function (a, b) {
|
}).reduce(function (a, b) {
|
||||||
if (a === undefined) {
|
if (a === undefined) {
|
||||||
return b;
|
return b;
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user