Compare commits

..

22 Commits

Author SHA1 Message Date
Andrew Henry
b062615c25 Merge branch 'master' into unload-openmct 2021-09-30 14:25:43 -07:00
David Tsay
4ba8f893a6 authors should think about backwards compatibility (#4223)
* authors should think about backwards compatibility

* Add reviewer check for breaking changes
2021-09-23 10:45:07 -05:00
Shefali Joshi
c4b9be18f1 Support for Bar Graphs (#4221)
* Adds new types for Bar Graphs (#4168)
* Adds new spectral test data

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

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

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2021-09-17 19:50:53 -05:00
Andrew Henry
e784242379 Fix leaking test specs 2021-09-10 17:21:09 -07:00
Andrew Henry
da8dbf25bf Make sure app holder gets destroyed 2021-09-10 17:20:54 -07:00
Andrew Henry
a5571eec05 Clean up router on destroy 2021-09-10 16:31:14 -07:00
Andrew Henry
19802ce2cc Fix merge error and restore missing bind statement 2021-09-10 16:30:49 -07:00
Andrew Henry
e83735a927 Clean up conductor resize handler on destroy 2021-09-10 16:24:35 -07:00
Andrew Henry
c9a47a411a Clean up notebook 2021-09-10 16:23:55 -07:00
Andrew Henry
a6e55fd493 Removed console logS 2021-09-10 16:17:38 -07:00
Andrew Henry
76f35a0bcd Merge recent changes to MCT.js from master 2021-09-10 16:17:12 -07:00
Andrew Henry
b6930ef7cd Merge branch 'master' into unload-openmct 2021-09-10 15:58:54 -07:00
Andrew Henry
57b9cbd42f Remove console log in Selection.js 2021-09-10 15:58:41 -07:00
Andrew Henry
17901147ef Fix broken tests 2021-09-10 15:50:44 -07:00
Andrew Henry
58b8f90682 Clean up MCT class 2021-09-10 15:50:36 -07:00
Andrew Henry
d06cbc8975 Destroy API listeners 2021-09-10 11:34:52 -07:00
Andrew Henry
553b18587c Clean up workers on destroy 2021-09-03 15:03:47 -07:00
Andrew Henry
590dc74e45 Fix memory leak in legacy ticker service 2021-09-03 14:46:23 -07:00
Andrew Henry
3557ed4b4c Fix memory leaks related to navigation 2021-09-03 13:36:51 -07:00
Andrew Henry
41cc52d50a Remove legacy routes support 2021-09-03 12:15:32 -07:00
102 changed files with 5102 additions and 1323 deletions

View File

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

View File

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

View File

@@ -41,11 +41,6 @@ define([
"$scope"
]
}
],
"routes": [
{
"templateUrl": "templates/exampleForm.html"
}
]
}
}

View File

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

View File

@@ -96,5 +96,9 @@ define([
return this.workerInterface.subscribe(workerRequest, callback);
};
GeneratorProvider.prototype.destroy = function () {
this.workerInterface.destroy();
};
return GeneratorProvider;
});

View File

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

View File

@@ -0,0 +1,102 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
'./WorkerInterface'
], function (
WorkerInterface
) {
var REQUEST_DEFAULTS = {
amplitude: 1,
wavelength: 1,
period: 10,
offset: 0,
dataRateInHz: 1,
randomness: 0,
phase: 0
};
function SpectralGeneratorProvider() {
this.workerInterface = new WorkerInterface();
}
SpectralGeneratorProvider.prototype.canProvideTelemetry = function (domainObject) {
return domainObject.type === 'example.spectral-generator';
};
SpectralGeneratorProvider.prototype.supportsRequest =
SpectralGeneratorProvider.prototype.supportsSubscribe =
SpectralGeneratorProvider.prototype.canProvideTelemetry;
SpectralGeneratorProvider.prototype.makeWorkerRequest = function (domainObject, request = {}) {
var props = [
'amplitude',
'wavelength',
'period',
'offset',
'dataRateInHz',
'phase',
'randomness'
];
var workerRequest = {};
props.forEach(function (prop) {
if (domainObject.telemetry && Object.prototype.hasOwnProperty.call(domainObject.telemetry, prop)) {
workerRequest[prop] = domainObject.telemetry[prop];
}
if (request && Object.prototype.hasOwnProperty.call(request, prop)) {
workerRequest[prop] = request[prop];
}
if (!Object.prototype.hasOwnProperty.call(workerRequest, prop)) {
workerRequest[prop] = REQUEST_DEFAULTS[prop];
}
workerRequest[prop] = Number(workerRequest[prop]);
});
workerRequest.name = domainObject.name;
return workerRequest;
};
SpectralGeneratorProvider.prototype.request = function (domainObject, request) {
var workerRequest = this.makeWorkerRequest(domainObject, request);
workerRequest.start = request.start;
workerRequest.end = request.end;
workerRequest.spectra = true;
return this.workerInterface.request(workerRequest);
};
SpectralGeneratorProvider.prototype.subscribe = function (domainObject, callback) {
var workerRequest = this.makeWorkerRequest(domainObject, {});
workerRequest.spectra = true;
return this.workerInterface.subscribe(workerRequest, callback);
};
return SpectralGeneratorProvider;
});

View File

@@ -40,6 +40,11 @@ define([
this.callbacks = {};
}
WorkerInterface.prototype.destroy = function () {
delete this.worker.onmessage;
this.worker.terminate();
};
WorkerInterface.prototype.onMessage = function (message) {
message = message.data;
var callback = this.callbacks[message.id];

View File

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

View File

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

View File

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

View File

@@ -164,16 +164,6 @@ define([
"license": "license-apache",
"link": "http://logging.apache.org/log4net/license.html"
}
],
"routes": [
{
"when": "/licenses",
"template": licensesTemplate
},
{
"when": "/licenses-md",
"template": licensesExportMdTemplate
}
]
}
}

View File

@@ -50,8 +50,6 @@ define([
name: "platform/commonUI/browse",
definition: {
"extensions": {
"routes": [
],
"constants": [
{
"key": "DEFAULT_PATH",

View File

@@ -39,9 +39,6 @@ define(
this.callbacks = [];
this.checks = [];
this.$window = $window;
this.oldUnload = $window.onbeforeunload;
$window.onbeforeunload = this.onBeforeUnload.bind(this);
}
/**

View File

@@ -71,7 +71,8 @@ define([
"implementation": TickerService,
"depends": [
"$timeout",
"now"
"now",
"$rootScope"
]
},
{

View File

@@ -32,8 +32,13 @@ define(
* @param $timeout Angular's $timeout
* @param {Function} now function to provide the current time in ms
*/
function TickerService($timeout, now) {
function TickerService($timeout, now, $rootScope) {
var self = this;
var timeoutId;
$rootScope.$on('$destroy', function () {
$timeout.cancel(timeoutId);
});
function tick() {
var timestamp = now(),
@@ -48,7 +53,7 @@ define(
}
// Try to update at exactly the next second
$timeout(tick, 1000 - millis, true);
timeoutId = $timeout(tick, 1000 - millis, true);
}
tick();

View File

@@ -30,16 +30,18 @@ define(
var mockTimeout,
mockNow,
mockCallback,
tickerService;
tickerService,
mockRootScope;
beforeEach(function () {
mockTimeout = jasmine.createSpy('$timeout');
mockNow = jasmine.createSpy('now');
mockCallback = jasmine.createSpy('callback');
mockRootScope = jasmine.createSpyObj('rootScope', ['$on']);
mockNow.and.returnValue(TEST_TIMESTAMP);
tickerService = new TickerService(mockTimeout, mockNow);
tickerService = new TickerService(mockTimeout, mockNow, mockRootScope);
});
it("notifies listeners of clock ticks", function () {

View File

@@ -58,7 +58,7 @@ define([
) {
var $http = this.$http,
$log = this.$log,
app = angular.module(Constants.MODULE_NAME, ["ngRoute"]),
app = angular.module(Constants.MODULE_NAME, []),
loader = new BundleLoader($http, $log, openmct.legacyRegistry),
resolver = new BundleResolver(
new ExtensionResolver(

View File

@@ -28,8 +28,7 @@
define(
[
'./FrameworkLayer',
'angular',
'angular-route'
'angular'
],
function (
FrameworkLayer,

View File

@@ -138,34 +138,6 @@ define(
}
}
// Custom registration function for extensions of category "route"
function registerRoute(extension) {
var app = this.app,
$log = this.$log,
route = Object.create(extension);
// Adjust path for bundle
if (route.templateUrl) {
route.templateUrl = [
route.bundle.path,
route.bundle.resources,
route.templateUrl
].join(Constants.SEPARATOR);
}
// Log the registration
$log.info("Registering route: " + (route.key || route.when));
// Register the route with Angular
app.config(['$routeProvider', function ($routeProvider) {
if (route.when) {
$routeProvider.when(route.when, route);
} else {
$routeProvider.otherwise(route);
}
}]);
}
// Handle service compositing
function registerComponents(components) {
var app = this.app,
@@ -194,13 +166,6 @@ define(
CustomRegistrars.prototype.constants =
mapUpon(registerConstant);
/**
* Register Angular routes.
* @param {Array} extensions the resolved extensions
*/
CustomRegistrars.prototype.routes =
mapUpon(registerRoute);
/**
* Register Angular directives.
* @param {Array} extensions the resolved extensions

View File

@@ -57,7 +57,6 @@ define(
expect(customRegistrars.directives).toBeTruthy();
expect(customRegistrars.controllers).toBeTruthy();
expect(customRegistrars.services).toBeTruthy();
expect(customRegistrars.routes).toBeTruthy();
expect(customRegistrars.constants).toBeTruthy();
expect(customRegistrars.runs).toBeTruthy();
});
@@ -139,47 +138,6 @@ define(
expect(mockLog.warn.calls.count()).toEqual(0);
});
it("allows routes to be registered", function () {
var mockRouteProvider = jasmine.createSpyObj(
"$routeProvider",
["when", "otherwise"]
),
bundle = {
path: "test/bundle",
resources: "res"
},
routes = [
{
when: "foo",
templateUrl: "templates/test.html",
bundle: bundle
},
{
templateUrl: "templates/default.html",
bundle: bundle
}
];
customRegistrars.routes(routes);
// Give it the route provider based on its config call
mockApp.config.calls.all().forEach(function (call) {
// Invoke the provided callback
call.args[0][1](mockRouteProvider);
});
// The "when" clause should have been mapped to the when method...
expect(mockRouteProvider.when).toHaveBeenCalled();
expect(mockRouteProvider.when.calls.mostRecent().args[0]).toEqual("foo");
expect(mockRouteProvider.when.calls.mostRecent().args[1].templateUrl)
.toEqual("test/bundle/res/templates/test.html");
// ...while the other should have been treated as a default route
expect(mockRouteProvider.otherwise).toHaveBeenCalled();
expect(mockRouteProvider.otherwise.calls.mostRecent().args[0].templateUrl)
.toEqual("test/bundle/res/templates/default.html");
});
it("accepts components for service compositing", function () {
// Most relevant code will be exercised in service compositor spec
expect(customRegistrars.components).toBeTruthy();

View File

@@ -110,8 +110,15 @@ define([
worker = workerService.run('bareBonesSearchWorker');
}
worker.addEventListener('message', function (messageEvent) {
function handleWorkerMessage(messageEvent) {
provider.onWorkerMessage(messageEvent);
}
worker.addEventListener('message', handleWorkerMessage);
this.openmct.once('destroy', () => {
worker.removeEventListener('message', handleWorkerMessage);
worker.terminate();
});
return worker;

View File

@@ -31,7 +31,6 @@ define([
'objectUtils',
'./plugins/plugins',
'./adapter/indicators/legacy-indicators-plugin',
'./plugins/buildInfo/plugin',
'./ui/registries/ViewRegistry',
'./plugins/imagery/plugin',
'./ui/registries/InspectorViewRegistry',
@@ -40,6 +39,7 @@ define([
'./ui/router/Browse',
'../platform/framework/src/Main',
'./ui/layout/Layout.vue',
'./ui/inspector/styles/StylesManager',
'../platform/core/src/objects/DomainObjectImpl',
'../platform/core/src/capabilities/ContextualDomainObject',
'./ui/preview/plugin',
@@ -60,7 +60,6 @@ define([
objectUtils,
plugins,
LegacyIndicatorsPlugin,
buildInfoPlugin,
ViewRegistry,
ImageryPlugin,
InspectorViewRegistry,
@@ -69,6 +68,7 @@ define([
Browse,
Main,
Layout,
stylesManager,
DomainObjectImpl,
ContextualDomainObject,
PreviewPlugin,
@@ -123,6 +123,7 @@ define([
};
this.destroy = this.destroy.bind(this);
/**
* Tracks current selection state of the application.
* @private
@@ -136,7 +137,7 @@ define([
* @memberof module:openmct.MCT#
* @name conductor
*/
this.time = new api.TimeAPI();
this.time = new api.TimeAPI(this);
/**
* An interface for interacting with the composition of domain objects.
@@ -376,6 +377,7 @@ define([
* MCT; if undefined, MCT will be run in the body of the document
*/
MCT.prototype.start = function (domElement = document.body, isHeadlessMode = false) {
if (this.types.get('layout') === undefined) {
this.install(this.plugins.DisplayLayout({
showAsView: ['summary-widget']
@@ -434,6 +436,9 @@ define([
domElement.appendChild(appLayout.$mount().$el);
this.layout = appLayout.$refs.layout;
this.once('destroy', () => {
appLayout.$destroy();
});
Browse(this);
}
@@ -462,9 +467,40 @@ define([
};
MCT.prototype.destroy = function () {
if (this._destroyed === true) {
return;
}
window.removeEventListener('beforeunload', this.destroy);
this.emit('destroy');
this.router.destroy();
this.removeAllListeners();
if (this.$injector) {
this.$injector.get('$rootScope').$destroy();
this.$injector = null;
}
if (this.$angular) {
this.$angular.element(this.element).off().removeData();
this.$angular.element(this.element).empty();
this.$angular = null;
}
this.overlays.destroy();
if (this.element) {
this.element.remove();
}
stylesManager.default.removeAllListeners();
window.angular = null;
window.openmct = null;
Object.keys(require.cache).forEach(key => delete require.cache[key]);
this._destroyed = true;
};
MCT.prototype.plugins = plugins;

View File

@@ -32,6 +32,10 @@ define([
// cannot be injected.
function AlternateCompositionInitializer(openmct) {
AlternateCompositionCapability.appliesTo = function (model, id) {
openmct.once('destroy', () => {
delete AlternateCompositionCapability.appliesTo;
});
model = objectUtils.toNewFormat(model, id || '');
return Boolean(openmct.composition.get(model));

View File

@@ -28,6 +28,10 @@ export default class Editor extends EventEmitter {
super();
this.editing = false;
this.openmct = openmct;
openmct.once('destroy', () => {
this.removeAllListeners();
});
}
/**

View File

@@ -44,7 +44,15 @@ describe('The ActionCollection', () => {
}
});
openmct.$injector.get.and.returnValue(mockIdentifierService);
openmct.$injector.get.and.callFake((key) => {
return {
'identifierService': mockIdentifierService,
'$rootScope': {
'$destroy': () => {}
}
}[key];
});
mockObjectPath = [
{
name: 'mock folder',

View File

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

View File

@@ -17,11 +17,7 @@ class OverlayAPI {
this.dismissLastOverlay = this.dismissLastOverlay.bind(this);
document.addEventListener('keyup', (event) => {
if (event.key === 'Escape') {
this.dismissLastOverlay();
}
});
document.addEventListener('keyup', this.dismissLastOverlay);
}
@@ -52,10 +48,12 @@ class OverlayAPI {
/**
* private
*/
dismissLastOverlay() {
let lastOverlay = this.activeOverlays[this.activeOverlays.length - 1];
if (lastOverlay && lastOverlay.dismissable) {
lastOverlay.dismiss();
dismissLastOverlay(event) {
if (event.key === 'Escape') {
let lastOverlay = this.activeOverlays[this.activeOverlays.length - 1];
if (lastOverlay && lastOverlay.dismissable) {
lastOverlay.dismiss();
}
}
}
@@ -63,7 +61,7 @@ class OverlayAPI {
* A description of option properties that can be passed into the overlay
* @typedef options
* @property {object} element DOMElement that is to be inserted/shown on the overlay
* @property {string} size preferred size of the overlay (large, small, fit)
* @property {string} size prefered size of the overlay (large, small, fit)
* @property {array} buttons optional button objects with label and callback properties
* @property {function} onDestroy callback to be called when overlay is destroyed
* @property {boolean} dismissable allow user to dismiss overlay by using esc, and clicking away
@@ -132,6 +130,10 @@ class OverlayAPI {
return progressDialog;
}
destroy() {
document.removeEventListener('keyup', this.dismissLastOverlay);
}
}
export default OverlayAPI;

View File

@@ -32,6 +32,10 @@ export default class StatusAPI extends EventEmitter {
this.get = this.get.bind(this);
this.set = this.set.bind(this);
this.observe = this.observe.bind(this);
openmct.once('destroy', () => {
this.removeAllListeners();
});
}
get(identifier) {

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

@@ -40,7 +40,6 @@ import PreviewAction from "@/ui/preview/PreviewAction";
import _ from "lodash";
const PADDING = 1;
const RESIZE_POLL_INTERVAL = 200;
const ROW_HEIGHT = 100;
const IMAGE_WIDTH_THRESHOLD = 40;

View File

@@ -32,19 +32,19 @@ const TEN_MINUTES = ONE_MINUTE * 10;
const MAIN_IMAGE_CLASS = '.js-imageryView-image';
const NEW_IMAGE_CLASS = '.c-imagery__age.c-imagery--new';
const REFRESH_CSS_MS = 500;
const TOLERANCE = 0.50;
// const TOLERANCE = 0.50;
function comparisonFunction(valueOne, valueTwo) {
let larger = valueOne;
let smaller = valueTwo;
if (larger < smaller) {
larger = valueTwo;
smaller = valueOne;
}
return (larger - smaller) < TOLERANCE;
}
// function comparisonFunction(valueOne, valueTwo) {
// let larger = valueOne;
// let smaller = valueTwo;
//
// if (larger < smaller) {
// larger = valueTwo;
// smaller = valueOne;
// }
//
// return (larger - smaller) < TOLERANCE;
// }
function getImageInfo(doc) {
let imageElement = doc.querySelectorAll(MAIN_IMAGE_CLASS)[0];

View File

@@ -75,7 +75,16 @@ describe("the plugin", () => {
mockDialogService.getUserInput.and.returnValue(mockPromise);
spyOn(openmct.$injector, 'get').and.returnValue(mockDialogService);
spyOn(openmct.$injector, 'get');
openmct.$injector.get.and.callFake((key) => {
return {
'dialogService': mockDialogService,
'$rootScope': {
'$destroy': () => {}
}
}[key];
});
spyOn(compositionAPI, 'get').and.returnValue(mockComposition);
spyOn(openmct.objects, 'save').and.returnValue(Promise.resolve(true));

View File

@@ -117,6 +117,10 @@ export default function NotebookPlugin() {
key: 'notebook-snapshot-indicator'
};
openmct.once('destroy', () => {
snapshotContainer.destroy();
});
openmct.indicators.add(indicator);
openmct.objectViews.addProvider({

View File

@@ -85,4 +85,8 @@ export default class SnapshotContainer extends EventEmitter {
return this.saveSnapshots(updatedSnapshots);
}
destroy() {
delete SnapshotContainer.instance;
}
}

View File

@@ -22,95 +22,109 @@
import * as NotebookEntries from './notebook-entries';
import { createOpenMct, resetApplicationState } from 'utils/testing';
const notebookStorage = {
name: 'notebook',
identifier: {
namespace: '',
key: 'test-notebook'
},
defaultSectionId: '03a79b6a-971c-4e56-9892-ec536332c3f0',
defaultPageId: '8b548fd9-2b8a-4b02-93a9-4138e22eba00'
};
const notebookEntries = {
'03a79b6a-971c-4e56-9892-ec536332c3f0': {
'8b548fd9-2b8a-4b02-93a9-4138e22eba00': []
}
};
const notebookDomainObject = {
identifier: {
key: 'notebook',
namespace: ''
},
type: 'notebook',
configuration: {
defaultSort: 'oldest',
entries: notebookEntries,
pageTitle: 'Page',
sections: [],
sectionTitle: 'Section',
type: 'General'
}
};
const selectedSection = {
id: '03a79b6a-971c-4e56-9892-ec536332c3f0',
isDefault: false,
isSelected: true,
name: 'Day 1',
pages: [
{
id: '54deb3d5-8267-4be4-95e9-3579ed8c082d',
isDefault: false,
isSelected: false,
name: 'Shift 1',
pageTitle: 'Page'
},
{
id: '2ea41c78-8e60-4657-a350-53f1a1fa3021',
isDefault: false,
isSelected: false,
name: 'Shift 2',
pageTitle: 'Page'
},
{
id: '8b548fd9-2b8a-4b02-93a9-4138e22eba00',
isDefault: false,
isSelected: true,
name: 'Unnamed Page',
pageTitle: 'Page'
}
],
sectionTitle: 'Section'
};
const selectedPage = {
id: '8b548fd9-2b8a-4b02-93a9-4138e22eba00',
isDefault: false,
isSelected: true,
name: 'Unnamed Page',
pageTitle: 'Page'
};
let notebookStorage;
let notebookEntries;
let notebookDomainObject;
let selectedSection;
let selectedPage;
let openmct;
let mockIdentifierService;
describe('Notebook Entries:', () => {
beforeEach(() => {
notebookStorage = {
name: 'notebook',
identifier: {
namespace: '',
key: 'test-notebook'
},
defaultSectionId: '03a79b6a-971c-4e56-9892-ec536332c3f0',
defaultPageId: '8b548fd9-2b8a-4b02-93a9-4138e22eba00'
};
notebookEntries = {
'03a79b6a-971c-4e56-9892-ec536332c3f0': {
'8b548fd9-2b8a-4b02-93a9-4138e22eba00': []
}
};
notebookDomainObject = {
identifier: {
key: 'notebook',
namespace: ''
},
type: 'notebook',
configuration: {
defaultSort: 'oldest',
entries: notebookEntries,
pageTitle: 'Page',
sections: [],
sectionTitle: 'Section',
type: 'General'
}
};
selectedSection = {
id: '03a79b6a-971c-4e56-9892-ec536332c3f0',
isDefault: false,
isSelected: true,
name: 'Day 1',
pages: [
{
id: '54deb3d5-8267-4be4-95e9-3579ed8c082d',
isDefault: false,
isSelected: false,
name: 'Shift 1',
pageTitle: 'Page'
},
{
id: '2ea41c78-8e60-4657-a350-53f1a1fa3021',
isDefault: false,
isSelected: false,
name: 'Shift 2',
pageTitle: 'Page'
},
{
id: '8b548fd9-2b8a-4b02-93a9-4138e22eba00',
isDefault: false,
isSelected: true,
name: 'Unnamed Page',
pageTitle: 'Page'
}
],
sectionTitle: 'Section'
};
selectedPage = {
id: '8b548fd9-2b8a-4b02-93a9-4138e22eba00',
isDefault: false,
isSelected: true,
name: 'Unnamed Page',
pageTitle: 'Page'
};
openmct = createOpenMct();
openmct.$injector = jasmine.createSpyObj('$injector', ['get']);
mockIdentifierService = jasmine.createSpyObj(
'identifierService',
['parse']
);
openmct.$injector.get.and.callFake((key) => {
return {
'identifierService': mockIdentifierService,
'$rootScope': {
'$destroy': () => {}
}
}[key];
});
mockIdentifierService.parse.and.returnValue({
getSpace: () => {
return '';
}
});
openmct.$injector.get.and.returnValue(mockIdentifierService);
openmct.types.addType('notebook', {
creatable: true
});

View File

@@ -23,51 +23,55 @@
import * as NotebookStorage from './notebook-storage';
import { createOpenMct, resetApplicationState } from 'utils/testing';
const notebookSection = {
id: 'temp-section',
isDefault: false,
isSelected: true,
name: 'section',
pages: [
{
id: 'temp-page',
isDefault: false,
isSelected: true,
name: 'page',
pageTitle: 'Page'
}
],
sectionTitle: 'Section'
};
const domainObject = {
name: 'notebook',
identifier: {
namespace: '',
key: 'test-notebook'
},
configuration: {
sections: [
notebookSection
]
}
};
const notebookStorage = {
name: 'notebook',
identifier: {
namespace: '',
key: 'test-notebook'
},
defaultSectionId: 'temp-section',
defaultPageId: 'temp-page'
};
let notebookSection;
let domainObject;
let notebookStorage;
let openmct;
let mockIdentifierService;
describe('Notebook Storage:', () => {
beforeEach(() => {
notebookSection = {
id: 'temp-section',
isDefault: false,
isSelected: true,
name: 'section',
pages: [
{
id: 'temp-page',
isDefault: false,
isSelected: true,
name: 'page',
pageTitle: 'Page'
}
],
sectionTitle: 'Section'
};
domainObject = {
name: 'notebook',
identifier: {
namespace: '',
key: 'test-notebook'
},
configuration: {
sections: [
notebookSection
]
}
};
notebookStorage = {
name: 'notebook',
identifier: {
namespace: '',
key: 'test-notebook'
},
defaultSectionId: 'temp-section',
defaultPageId: 'temp-page'
};
openmct = createOpenMct();
openmct.$injector = jasmine.createSpyObj('$injector', ['get']);
mockIdentifierService = jasmine.createSpyObj(
@@ -80,7 +84,15 @@ describe('Notebook Storage:', () => {
}
});
openmct.$injector.get.and.returnValue(mockIdentifierService);
openmct.$injector.get.and.callFake((key) => {
return {
'identifierService': mockIdentifierService,
'$rootScope': {
'$destroy': () => {}
}
}[key];
});
window.localStorage.setItem('notebook-storage', null);
openmct.objects.addProvider('', jasmine.createSpyObj('mockNotebookProvider', [
'create',

View File

@@ -67,7 +67,7 @@ export default {
TimelineAxis,
SwimLane
},
inject: ['openmct', 'domainObject'],
inject: ['openmct', 'domainObject', 'path'],
props: {
options: {
type: Object,
@@ -99,21 +99,37 @@ export default {
this.canvasContext = this.canvas.getContext('2d');
this.setDimensions();
this.updateViewBounds();
this.openmct.time.on("timeSystem", this.setScaleAndPlotActivities);
this.openmct.time.on("bounds", this.updateViewBounds);
this.setTimeContext();
this.resizeTimer = setInterval(this.resize, RESIZE_POLL_INTERVAL);
this.unlisten = this.openmct.objects.observe(this.domainObject, '*', this.observeForChanges);
},
beforeDestroy() {
clearInterval(this.resizeTimer);
this.openmct.time.off("timeSystem", this.setScaleAndPlotActivities);
this.openmct.time.off("bounds", this.updateViewBounds);
this.stopFollowingTimeContext();
if (this.unlisten) {
this.unlisten();
}
},
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) {
this.getPlanData(mutatedObject);
this.setScaleAndPlotActivities();
@@ -141,13 +157,9 @@ export default {
getPlanData(domainObject) {
this.planData = getValidatedPlan(domainObject);
},
updateViewBounds() {
this.viewBounds = this.openmct.time.bounds();
if (!this.options.compact) {
//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;
updateViewBounds(bounds) {
if (bounds) {
this.viewBounds = Object.create(bounds);
}
if (this.timeSystem === undefined) {

View File

@@ -54,7 +54,8 @@ export default function PlanViewProvider(openmct) {
},
provide: {
openmct,
domainObject
domainObject,
path: objectPath
},
data() {
return {

View File

@@ -29,13 +29,8 @@ describe('the plugin', function () {
let element;
let child;
let openmct;
let appHolder;
beforeEach((done) => {
appHolder = document.createElement('div');
appHolder.style.width = '640px';
appHolder.style.height = '480px';
openmct = createOpenMct();
openmct.install(new PlanPlugin());
@@ -50,7 +45,7 @@ describe('the plugin', function () {
element.appendChild(child);
openmct.on('start', done);
openmct.start(appHolder);
openmct.start(element);
});
afterEach(() => {
@@ -99,6 +94,7 @@ describe('the plugin', function () {
}
];
let planView;
let view;
beforeEach(() => {
openmct.time.timeSystem('utc', {
@@ -139,12 +135,16 @@ describe('the plugin', function () {
const applicableViews = openmct.objectViews.get(planDomainObject, []);
planView = applicableViews.find((viewProvider) => viewProvider.key === 'plan.view');
let view = planView.view(planDomainObject, mockObjectPath);
view = planView.view(planDomainObject, mockObjectPath);
view.show(child, true);
return Vue.nextTick();
});
afterEach(() => {
view.destroy();
});
it('loads activities into the view', () => {
const svgEls = element.querySelectorAll('.c-plan__contents svg');
expect(svgEls.length).toEqual(1);

View File

@@ -0,0 +1,170 @@
<!--
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">
<ul v-if="canEdit"
class="l-inspector-part"
>
<h2 v-if="heading"
:title="heading"
>{{ heading }}</h2>
<li 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>
</li>
</ul>
<ul v-else
class="l-inspector-part"
>
<h2 v-if="heading"
:title="heading"
>{{ heading }}</h2>
<li 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>
</li>
</ul>
</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';
}
},
heading: {
type: String,
default() {
return '';
}
}
},
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>

View File

@@ -156,7 +156,7 @@
import eventHelpers from './lib/eventHelpers';
import LinearScale from "./LinearScale";
import PlotConfigurationModel from './configuration/PlotConfigurationModel';
import configStore from './configuration/configStore';
import configStore from './configuration/ConfigStore';
import PlotLegend from "./legend/PlotLegend.vue";
import MctTicks from "./MctTicks.vue";
@@ -173,7 +173,7 @@ export default {
MctTicks,
MctChart
},
inject: ['openmct', 'domainObject'],
inject: ['openmct', 'domainObject', 'path'],
props: {
options: {
type: Object,
@@ -244,6 +244,9 @@ export default {
},
mounted() {
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.legend = this.config.legend;
@@ -261,7 +264,7 @@ export default {
this.removeStatusListener = this.openmct.status.observe(this.domainObject.identifier, this.updateStatus);
this.openmct.objectViews.on('clearData', this.clearData);
this.followTimeConductor();
this.setTimeContext();
this.loaded = true;
@@ -274,11 +277,27 @@ export default {
this.destroy();
},
methods: {
followTimeConductor() {
this.openmct.time.on('clock', this.updateRealTime);
this.openmct.time.on('bounds', this.updateDisplayBounds);
setTimeContext() {
this.stopFollowingTimeContext();
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);
},
stopFollowingTimeContext() {
if (this.timeContext) {
this.timeContext.off("clock", this.updateRealTime);
this.timeContext.off("bounds", this.updateDisplayBounds);
this.timeContext.off("timeContext", this.setTimeContext);
}
},
getConfig() {
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
let config = configStore.get(configId);
@@ -485,7 +504,7 @@ export default {
* displays can update accordingly.
*/
synchronized(value) {
const isLocalClock = this.openmct.time.clock();
const isLocalClock = this.timeContext.clock();
if (typeof value !== 'undefined') {
this._synchronized = value;
@@ -958,7 +977,7 @@ export default {
},
showSynchronizeDialog() {
const isLocalClock = this.openmct.time.clock();
const isLocalClock = this.timeContext.clock();
if (isLocalClock !== undefined) {
const message = `
This action will change the Time Conductor to Fixed Timespan mode with this plot view's current time bounds.
@@ -993,9 +1012,9 @@ export default {
},
synchronizeTimeConductor() {
this.openmct.time.stopClock();
this.timeContext.stopClock();
const range = this.config.xAxis.get('displayRange');
this.openmct.time.bounds({
this.timeContext.bounds({
start: range.min,
end: range.max
});
@@ -1006,6 +1025,7 @@ export default {
configStore.deleteStore(this.config.id);
this.stopListening();
if (this.checkForSize) {
clearInterval(this.checkForSize);
delete this.checkForSize;
@@ -1021,8 +1041,7 @@ export default {
this.plotContainerResizeObserver.disconnect();
this.openmct.time.off('clock', this.updateRealTime);
this.openmct.time.off('bounds', this.updateDisplayBounds);
this.stopFollowingTimeContext();
this.openmct.objectViews.off('clearData', this.clearData);
},
updateStatus(status) {

View File

@@ -77,7 +77,7 @@
<script>
import eventHelpers from "./lib/eventHelpers";
import { ticks, getFormattedTicks } from "./tickUtils";
import configStore from "./configuration/configStore";
import configStore from "./configuration/ConfigStore";
export default {
inject: ['openmct', 'domainObject'],

View File

@@ -80,7 +80,7 @@ export default {
components: {
MctPlot
},
inject: ['openmct', 'domainObject'],
inject: ['openmct', 'domainObject', 'path'],
props: {
options: {
type: Object,

View File

@@ -68,7 +68,8 @@ export default function PlotViewProvider(openmct) {
},
provide: {
openmct,
domainObject
domainObject,
path: objectPath
},
data() {
return {

View File

@@ -54,7 +54,7 @@
<script>
import MctTicks from "../MctTicks.vue";
import eventHelpers from '../lib/eventHelpers';
import configStore from "../configuration/configStore";
import configStore from "../configuration/ConfigStore";
export default {
components: {

View File

@@ -57,7 +57,7 @@
<script>
import MctTicks from "../MctTicks.vue";
import configStore from "../configuration/configStore";
import configStore from "../configuration/ConfigStore";
export default {
components: {

View File

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

View 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);
});
});

View 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';

View 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>

View File

@@ -0,0 +1,286 @@
<!--
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[${this.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);
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);
});
});
},
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>

View 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;
}
};
}
};
}

View File

@@ -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;
}
};
}

View File

@@ -0,0 +1,107 @@
<!--
Open MCT, Copyright (c) 2014-2020, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT is licensed under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
Open MCT includes source code licensed under additional open source
licenses. See the Open Source Licenses file (LICENSES.md) included with
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<template>
<ul>
<li class="c-tree__item menus-to-left">
<span class="c-disclosure-triangle is-enabled flex-elem"
:class="expandedCssClass"
@click="expanded = !expanded"
>
</span>
<div>
<div class="c-object-label__name">{{ name }}</div>
</div>
</li>
<ColorSwatch v-if="expanded"
:current-color="currentColor"
title="Manually set the color for this bar graph."
edit-title="Manually set the color for this bar graph"
view-title="The color for this bar graph."
short-label="Color"
class="grid-properties"
@colorSet="setColor"
/>
</ul>
</template>
<script>
import ColorSwatch from '../../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>

View File

@@ -0,0 +1,63 @@
<!--
Open MCT, Copyright (c) 2014-2020, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT is licensed under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
Open MCT includes source code licensed under additional open source
licenses. See the Open Source Licenses file (LICENSES.md) included with
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<template>
<div>
<ul class="c-tree">
<li v-for="series in domainObject.composition"
:key="series.key"
>
<bar-graph-options :item="series" />
</li>
</ul>
</div>
</template>
<script>
import BarGraphOptions from "./BarGraphOptions.vue";
export default {
components: {
BarGraphOptions
},
inject: ['openmct', 'domainObject'],
data() {
return {
isEditing: this.openmct.editor.isEditing()
};
},
computed: {
canEdit() {
return this.isEditing && !this.domainObject.locked;
}
},
mounted() {
this.openmct.editor.on('isEditing', this.setEditState);
},
beforeDestroy() {
this.openmct.editor.off('isEditing', this.setEditState);
},
methods: {
setEditState(isEditing) {
this.isEditing = isEditing;
}
}
};
</script>

View File

@@ -38,7 +38,7 @@ import MCTChartLineStepAfter from './MCTChartLineStepAfter';
import MCTChartPointSet from './MCTChartPointSet';
import MCTChartAlarmPointSet from './MCTChartAlarmPointSet';
import MCTChartAlarmLineSet from "./MCTChartAlarmLineSet";
import configStore from "../configuration/configStore";
import configStore from "../configuration/ConfigStore";
import PlotConfigurationModel from "../configuration/PlotConfigurationModel";
import LimitLine from "./LimitLine.vue";
import LimitLabel from "./LimitLabel.vue";

View File

@@ -22,7 +22,7 @@
import _ from 'lodash';
import Model from "./Model";
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

View File

@@ -115,7 +115,7 @@
<script>
import PlotOptionsItem from "./PlotOptionsItem.vue";
import configStore from "../configuration/configStore";
import configStore from "../configuration/ConfigStore";
import eventHelpers from "../lib/eventHelpers";
export default {

View File

@@ -49,7 +49,7 @@ import SeriesForm from "./forms/SeriesForm.vue";
import YAxisForm from "./forms/YAxisForm.vue";
import LegendForm from "./forms/LegendForm.vue";
import eventHelpers from "../lib/eventHelpers";
import configStore from "../configuration/configStore";
import configStore from "../configuration/ConfigStore";
export default {
components: {

View File

@@ -68,26 +68,23 @@
{{ limitLines ? "Enabled" : "Disabled" }}
</div>
</li>
<li class="grid-row">
<div class="grid-cell label"
title="The plot line and marker color for this series."
>Color</div>
<div class="grid-cell value">
<span class="c-color-swatch"
:style="{
'background': seriesHexColor
}"
>
</span>
</div>
</li>
<ColorSwatch :current-color="seriesHexColor"
edit-title="Manually set the plot line and marker color for this series."
view-title="The plot line and marker color for this series."
short-label="Color"
/>
</ul>
</li>
</ul>
</template>
<script>
import ColorSwatch from "@/plugins/plot/ColorSwatch.vue";
export default {
components: {
ColorSwatch
},
inject: ['openmct', 'domainObject', 'path'],
props: {
series: {

View File

@@ -117,49 +117,27 @@
<li v-show="interpolate !== 'none' || markers"
class="grid-row"
>
<div class="grid-cell label"
title="Manually set the plot line and marker color for this series."
>Color</div>
<div class="grid-cell value">
<div class="c-click-swatch c-click-swatch--menu"
@click="toggleSwatch()"
>
<span class="c-color-swatch"
:style="{ background: seriesColorAsHex }"
>
</span>
</div>
<div class="c-palette c-palette--color">
<div v-show="swatchActive"
class="c-palette__items"
>
<div v-for="(group, index) in colorPalette"
:key="index"
class="u-contents"
>
<div v-for="(color, colorIndex) in group"
:key="colorIndex"
class="c-palette__item"
:class="{ 'selected': series.get('color').equalTo(color) }"
:style="{ background: color.asHexString() }"
@click="setColor(color)"
>
</div>
</div>
</div>
</div>
</div>
<ColorSwatch :current-color="currentColor"
edit-title="Manually set the plot line and marker color for this series."
view-title="The plot line and marker color for this series."
short-label="Color"
@colorSet="setColor"
/>
</li>
</ul>
</ul>
</template>
<script>
import ColorSwatch from '../../ColorSwatch.vue';
import { MARKER_SHAPES } from "../../draw/MarkerShapes";
import { objectPath, validate, coerce } from "./formUtil";
import _ from 'lodash';
export default {
components: {
ColorSwatch
},
inject: ['openmct', 'domainObject', 'path'],
props: {
series: {
@@ -209,7 +187,7 @@ export default {
expandedCssClass() {
return this.expanded ? 'c-disclosure-triangle--expanded' : '';
},
seriesColorAsHex() {
currentColor() {
return this.series.get('color').asHexString();
}
},

View File

@@ -89,13 +89,4 @@ ColorPalette.prototype.getNextColor = function () {
return this.availableColors.shift();
};
/**
* @param {number} index the index of the color to return. An index
* value larger than the size of the index will wrap around.
* @returns {Color}
*/
ColorPalette.prototype.getColor = function (index) {
return this.colors[index % this.colors.length];
};
export default ColorPalette;

View File

@@ -53,7 +53,8 @@ export default function OverlayPlotViewProvider(openmct) {
},
provide: {
openmct,
domainObject
domainObject,
path: objectPath
},
data() {
return {

View File

@@ -19,13 +19,18 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { BAR_GRAPH_KEY } from './barGraph/BarGraphConstants';
import PlotViewProvider from './PlotViewProvider';
import SpectralPlotViewProvider from './spectralPlot/SpectralPlotViewProvider';
import BarGraphViewProvider from './barGraph/BarGraphViewProvider';
import OverlayPlotViewProvider from './overlayPlot/OverlayPlotViewProvider';
import StackedPlotViewProvider from './stackedPlot/StackedPlotViewProvider';
import PlotsInspectorViewProvider from './inspector/PlotsInspectorViewProvider';
import BarGraphInspectorViewProvider from './barGraph/inspector/BarGraphInspectorViewProvider';
import OverlayPlotCompositionPolicy from './overlayPlot/OverlayPlotCompositionPolicy';
import StackedPlotCompositionPolicy from './stackedPlot/StackedPlotCompositionPolicy';
import SpectralPlotCompositionPolicy from './spectralPlot/SpectralPlotCompositionPolicy';
import BarGraphCompositionPolicy from './barGraph/BarGraphCompositionPolicy';
export default function () {
return function install(openmct) {
@@ -59,13 +64,48 @@ export default function () {
},
priority: 890
});
openmct.types.addType('telemetry.plot.spectral', {
key: "telemetry.plot.spectral",
name: "Spectral Plot",
cssClass: "icon-plot-stacked",
description: "View Spectra on Y Axes with non-time domain on the X axis. Can be added to Display Layouts.",
//Temporarily disabling spectral plots
creatable: false,
initialize: function (domainObject) {
domainObject.composition = [];
domainObject.configuration = {};
},
priority: 890
});
openmct.types.addType(BAR_GRAPH_KEY, {
key: BAR_GRAPH_KEY,
name: "Bar Graph",
cssClass: "icon-bar-chart",
description: "View data as a bar graph. Can be added to Display Layouts.",
creatable: true,
initialize: function (domainObject) {
domainObject.composition = [];
domainObject.configuration = {
plotType: 'bar'
};
},
priority: 891
});
openmct.objectViews.addProvider(new StackedPlotViewProvider(openmct));
openmct.objectViews.addProvider(new OverlayPlotViewProvider(openmct));
openmct.objectViews.addProvider(new PlotViewProvider(openmct));
openmct.objectViews.addProvider(new SpectralPlotViewProvider(openmct));
openmct.objectViews.addProvider(new BarGraphViewProvider(openmct));
openmct.inspectorViews.addProvider(new PlotsInspectorViewProvider(openmct));
openmct.inspectorViews.addProvider(new BarGraphInspectorViewProvider(openmct));
openmct.composition.addPolicy(new OverlayPlotCompositionPolicy(openmct).allow);
openmct.composition.addPolicy(new StackedPlotCompositionPolicy(openmct).allow);
openmct.composition.addPolicy(new SpectralPlotCompositionPolicy(openmct).allow);
openmct.composition.addPolicy(new BarGraphCompositionPolicy(openmct).allow);
};
}

View File

@@ -24,10 +24,12 @@ import {createMouseEvent, createOpenMct, resetApplicationState, spyOnBuiltins} f
import PlotVuePlugin from "./plugin";
import Vue from "vue";
import StackedPlot from "./stackedPlot/StackedPlot.vue";
import configStore from "./configuration/configStore";
// import SpectralPlot from "./spectralPlot/SpectralPlot.vue";
import configStore from "./configuration/ConfigStore";
import EventEmitter from "EventEmitter";
import PlotOptions from "./inspector/PlotOptions.vue";
import PlotConfigurationModel from "./configuration/PlotConfigurationModel";
import { BAR_GRAPH_VIEW, BAR_GRAPH_KEY } from './barGraph/BarGraphConstants';
describe("the plugin", function () {
let element;
@@ -312,6 +314,38 @@ describe("the plugin", function () {
let plotView = applicableViews.find((viewProvider) => viewProvider.key === "plot-stacked");
expect(plotView).toBeDefined();
});
it("provides a spectral plot view for objects with telemetry", () => {
const testTelemetryObject = {
id: "test-object",
type: "telemetry.plot.spectral",
telemetry: {
values: [{
key: "a-very-fine-key"
}]
}
};
const applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath);
let plotView = applicableViews.find((viewProvider) => viewProvider.key === "plot-spectral");
expect(plotView).toBeDefined();
});
it("provides a spectral aggregate plot view for objects with telemetry", () => {
const testTelemetryObject = {
id: "test-object",
type: BAR_GRAPH_KEY,
telemetry: {
values: [{
key: "lots-of-aggregate-telemetry"
}]
}
};
const applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath);
let plotView = applicableViews.find((viewProvider) => viewProvider.key === BAR_GRAPH_VIEW);
expect(plotView).toBeDefined();
});
});
describe("The single plot view", () => {
@@ -462,6 +496,146 @@ describe("the plugin", function () {
});
});
/*
* disabling this until we develop the plot view
describe("The spectral plot view", () => {
let testTelemetryObject;
// eslint-disable-next-line no-unused-vars
let testTelemetryObject2;
// eslint-disable-next-line no-unused-vars
let config;
let spectralPlotObject;
let component;
let mockComposition;
// eslint-disable-next-line no-unused-vars
let plotViewComponentObject;
beforeEach(() => {
const getFunc = openmct.$injector.get;
spyOn(openmct.$injector, "get")
.withArgs("exportImageService").and.returnValue({
exportPNG: () => {},
exportJPG: () => {}
})
.and.callFake(getFunc);
spectralPlotObject = {
identifier: {
namespace: "",
key: "test-spectral-plot"
},
type: "telemetry.plot.spectral",
name: "Test Spectral Plot"
};
testTelemetryObject = {
identifier: {
namespace: "",
key: "test-object"
},
type: "test-object",
name: "Test Object",
telemetry: {
values: [{
key: "utc",
format: "utc",
name: "Time",
hints: {
domain: 1
}
}, {
key: "some-key",
name: "Some attribute",
hints: {
range: 1
}
}, {
key: "some-other-key",
name: "Another attribute",
hints: {
range: 2
}
}]
}
};
testTelemetryObject2 = {
identifier: {
namespace: "",
key: "test-object2"
},
type: "test-object",
name: "Test Object2",
telemetry: {
values: [{
key: "utc",
format: "utc",
name: "Time",
hints: {
domain: 1
}
}, {
key: "wavelength",
name: "Wavelength",
hints: {
range: 1
}
}, {
key: "some-other-key2",
name: "Another attribute2",
hints: {
range: 2
}
}]
}
};
mockComposition = new EventEmitter();
mockComposition.load = () => {
mockComposition.emit('add', testTelemetryObject);
return [testTelemetryObject];
};
spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
let viewContainer = document.createElement("div");
child.append(viewContainer);
component = new Vue({
el: viewContainer,
components: {
SpectralPlot
},
provide: {
openmct: openmct,
domainObject: spectralPlotObject,
composition: openmct.composition.get(spectralPlotObject)
},
template: "<spectral-plot></spectral-plot>"
});
cleanupFirst.push(() => {
component.$destroy();
component = undefined;
});
return telemetryPromise
.then(Vue.nextTick())
.then(() => {
plotViewComponentObject = component.$root.$children[0];
const configId = openmct.objects.makeKeyString(testTelemetryObject.identifier);
config = configStore.get(configId);
});
});
it("Renders a collapsed legend for every telemetry", () => {
let legend = element.querySelectorAll(".plot-wrapper-collapsed-legend .plot-series-name");
expect(legend.length).toBe(1);
expect(legend[0].innerHTML).toEqual("Test Object");
});
}); */
describe("The stacked plot view", () => {
let testTelemetryObject;
let testTelemetryObject2;
@@ -570,7 +744,8 @@ describe("the plugin", function () {
provide: {
openmct: openmct,
domainObject: stackedPlotObject,
composition: openmct.composition.get(stackedPlotObject)
composition: openmct.composition.get(stackedPlotObject),
path: [stackedPlotObject]
},
template: "<stacked-plot></stacked-plot>"
});
@@ -962,7 +1137,7 @@ describe("the plugin", function () {
expandControl.dispatchEvent(clickEvent);
const plotOptionsProperties = editOptionsEl.querySelectorAll(".js-plot-options-edit-properties .grid-row");
expect(plotOptionsProperties.length).toEqual(7);
expect(plotOptionsProperties.length).toEqual(8);
});
it('shows yKeyOptions', () => {
@@ -990,4 +1165,39 @@ describe("the plugin", function () {
});
});
describe("the spectral plot", () => {
const mockObject = {
name: 'A Very Nice Spectral Plot',
key: 'telemetry.plot.spectral',
creatable: true
};
it('defines a spectral plot object type with the correct key', () => {
const objectDef = openmct.types.get('telemetry.plot.spectral').definition;
expect(objectDef.key).toEqual(mockObject.key);
});
xit('is creatable', () => {
const objectDef = openmct.types.get('telemetry.plot.spectral').definition;
expect(objectDef.creatable).toEqual(mockObject.creatable);
});
});
describe("the aggregate spectral plot", () => {
const mockObject = {
name: 'An Even Nicer Aggregate Spectral Plot',
key: BAR_GRAPH_KEY,
creatable: true
};
it('defines a spectral plot object type with the correct key', () => {
const objectDef = openmct.types.get(BAR_GRAPH_KEY).definition;
expect(objectDef.key).toEqual(mockObject.key);
});
it('is creatable', () => {
const objectDef = openmct.types.get(BAR_GRAPH_KEY).definition;
expect(objectDef.creatable).toEqual(mockObject.creatable);
});
});
});

View File

@@ -0,0 +1,36 @@
export default function SpectralPlotCompositionPolicy(openmct) {
function hasSpectralDomainAndRange(metadata) {
const rangeValues = metadata.valuesForHints(['range']);
const domainValues = metadata.valuesForHints(['domain']);
const containsSomeSpectralData = domainValues.some(value => {
return ((value.key === 'wavelength') || (value.key === 'frequency'));
});
return rangeValues.length > 0
&& domainValues.length > 0
&& containsSomeSpectralData;
}
function hasSpectralTelemetry(domainObject) {
if (!Object.prototype.hasOwnProperty.call(domainObject, 'telemetry')) {
return false;
}
let metadata = openmct.telemetry.getMetadata(domainObject);
return metadata.values().length > 0 && hasSpectralDomainAndRange(metadata);
}
return {
allow: function (parent, child) {
if ((parent.type === 'telemetry.plot.spectral')
&& ((child.type !== 'telemetry.plot.overlay') && (hasSpectralTelemetry(child) === false))
) {
return false;
}
return true;
}
};
}

View File

@@ -0,0 +1,75 @@
/*****************************************************************************
* 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 SpectralView from './SpectralView.vue';
import Vue from 'vue';
export default function SpectralPlotViewProvider(openmct) {
function isCompactView(objectPath) {
return objectPath.find(object => object.type === 'time-strip');
}
return {
key: 'plot-spectral',
name: 'Spectral Plot',
cssClass: 'icon-telemetry',
canView(domainObject, objectPath) {
return domainObject && domainObject.type === 'telemetry.plot.spectral';
},
canEdit(domainObject, objectPath) {
return domainObject && domainObject.type === 'telemetry.plot.spectral';
},
view: function (domainObject, objectPath) {
let component;
return {
show: function (element) {
let isCompact = isCompactView(objectPath);
component = new Vue({
el: element,
components: {
SpectralView
},
provide: {
openmct,
domainObject
},
data() {
return {
options: {
compact: isCompact
}
};
},
template: '<spectral-view :options="options"></spectral-view>'
});
},
destroy: function () {
component.$destroy();
component = undefined;
}
};
}
};
}

View File

@@ -0,0 +1,13 @@
<template>
<div>
</div>
</template>
<script>
export default {
inject: ['openmct', 'domainObject']
};
</script>

View File

@@ -75,7 +75,7 @@ export default {
components: {
StackedPlotItem
},
inject: ['openmct', 'domainObject', 'composition'],
inject: ['openmct', 'domainObject', 'composition', 'path'],
props: {
options: {
type: Object,

View File

@@ -28,7 +28,7 @@ import MctPlot from '../MctPlot.vue';
import Vue from "vue";
export default {
inject: ['openmct', 'domainObject'],
inject: ['openmct', 'domainObject', 'path'],
props: {
object: {
type: Object,
@@ -94,6 +94,7 @@ export default {
const openmct = this.openmct;
const object = this.object;
const path = this.path;
const getProps = this.getProps;
let viewContainer = document.createElement('div');
@@ -106,7 +107,8 @@ export default {
},
provide: {
openmct,
domainObject: object
domainObject: object,
path
},
data() {
return {

View File

@@ -55,7 +55,8 @@ export default function StackedPlotViewProvider(openmct) {
provide: {
openmct,
domainObject,
composition: openmct.composition.get(domainObject)
composition: openmct.composition.get(domainObject),
path: objectPath
},
data() {
return {

View File

@@ -29,144 +29,36 @@
isFixed ? 'is-fixed-mode' : 'is-realtime-mode'
]"
>
<form
ref="conductorForm"
class="u-contents"
@submit.prevent="updateTimeFromConductor"
>
<div class="c-conductor__time-bounds">
<button
ref="submitButton"
class="c-input--submit"
type="submit"
></button>
<ConductorModeIcon class="c-conductor__mode-icon" />
<div
v-if="isFixed"
class="c-ctrl-wrapper c-conductor-input c-conductor__start-fixed"
>
<!-- Fixed start -->
<div class="c-conductor__start-fixed__label">
Start
</div>
<input
ref="startDate"
v-model="formattedBounds.start"
class="c-input--datetime"
type="text"
autocorrect="off"
spellcheck="false"
@change="validateAllBounds('startDate'); submitForm()"
>
<date-picker
v-if="isFixed && isUTCBased"
:default-date-time="formattedBounds.start"
:formatter="timeFormatter"
@date-selected="startDateSelected"
/>
</div>
<div
v-if="!isFixed"
class="c-ctrl-wrapper c-conductor-input c-conductor__start-delta"
>
<!-- RT start -->
<div class="c-direction-indicator icon-minus"></div>
<time-popup
v-if="showTCInputStart"
class="pr-tc-input-menu--start"
:type="'start'"
:offset="offsets.start"
@focus.native="$event.target.select()"
@hide="hideAllTimePopups"
@update="timePopUpdate"
/>
<button
ref="startOffset"
class="c-button c-conductor__delta-button"
@click.prevent="showTimePopupStart"
>
{{ offsets.start }}
</button>
</div>
<div class="c-ctrl-wrapper c-conductor-input c-conductor__end-fixed">
<!-- Fixed end and RT 'last update' display -->
<div class="c-conductor__end-fixed__label">
{{ isFixed ? 'End' : 'Updated' }}
</div>
<input
ref="endDate"
v-model="formattedBounds.end"
class="c-input--datetime"
type="text"
autocorrect="off"
spellcheck="false"
:disabled="!isFixed"
@change="validateAllBounds('endDate'); submitForm()"
>
<date-picker
v-if="isFixed && isUTCBased"
class="c-ctrl-wrapper--menus-left"
:default-date-time="formattedBounds.end"
:formatter="timeFormatter"
@date-selected="endDateSelected"
/>
</div>
<div
v-if="!isFixed"
class="c-ctrl-wrapper c-conductor-input c-conductor__end-delta"
>
<!-- RT end -->
<div class="c-direction-indicator icon-plus"></div>
<time-popup
v-if="showTCInputEnd"
class="pr-tc-input-menu--end"
:type="'end'"
:offset="offsets.end"
@focus.native="$event.target.select()"
@hide="hideAllTimePopups"
@update="timePopUpdate"
/>
<button
ref="endOffset"
class="c-button c-conductor__delta-button"
@click.prevent="showTimePopupEnd"
>
{{ offsets.end }}
</button>
</div>
<conductor-axis
class="c-conductor__ticks"
:view-bounds="viewBounds"
:is-fixed="isFixed"
:alt-pressed="altPressed"
@endPan="endPan"
@endZoom="endZoom"
@panAxis="pan"
@zoomAxis="zoom"
/>
</div>
<div class="c-conductor__controls">
<ConductorMode class="c-conductor__mode-select" />
<ConductorTimeSystem class="c-conductor__time-system-select" />
<ConductorHistory
class="c-conductor__history-select"
:offsets="openmct.time.clockOffsets()"
:bounds="bounds"
:time-system="timeSystem"
:mode="timeMode"
/>
</div>
<input
type="submit"
class="invisible"
>
</form>
<div class="c-conductor__time-bounds">
<conductor-inputs-fixed v-if="isFixed"
@updated="saveFixedOffsets"
/>
<conductor-inputs-realtime v-else
@updated="saveClockOffsets"
/>
<ConductorModeIcon class="c-conductor__mode-icon" />
<conductor-axis
class="c-conductor__ticks"
:view-bounds="viewBounds"
:is-fixed="isFixed"
:alt-pressed="altPressed"
@endPan="endPan"
@endZoom="endZoom"
@panAxis="pan"
@zoomAxis="zoom"
/>
</div>
<div class="c-conductor__controls">
<ConductorMode class="c-conductor__mode-select" />
<ConductorTimeSystem class="c-conductor__time-system-select" />
<ConductorHistory
class="c-conductor__history-select"
:offsets="openmct.time.clockOffsets()"
:bounds="bounds"
:time-system="timeSystem"
:mode="timeMode"
/>
</div>
</div>
</template>
@@ -174,23 +66,23 @@
import _ from 'lodash';
import ConductorMode from './ConductorMode.vue';
import ConductorTimeSystem from './ConductorTimeSystem.vue';
import DatePicker from './DatePicker.vue';
import ConductorAxis from './ConductorAxis.vue';
import ConductorModeIcon from './ConductorModeIcon.vue';
import ConductorHistory from './ConductorHistory.vue';
import TimePopup from './timePopup.vue';
import ConductorInputsFixed from "./ConductorInputsFixed.vue";
import ConductorInputsRealtime from "./ConductorInputsRealtime.vue";
const DEFAULT_DURATION_FORMATTER = 'duration';
export default {
components: {
ConductorInputsRealtime,
ConductorInputsFixed,
ConductorMode,
ConductorTimeSystem,
DatePicker,
ConductorAxis,
ConductorModeIcon,
ConductorHistory,
TimePopup
ConductorHistory
},
inject: ['openmct', 'configuration'],
data() {
@@ -242,7 +134,6 @@ export default {
this.openmct.time.on('bounds', _.throttle(this.handleNewBounds, 300));
this.openmct.time.on('timeSystem', this.setTimeSystem);
this.openmct.time.on('clock', this.setViewFromClock);
this.openmct.time.on('clockOffsets', this.setViewFromOffsets);
},
beforeDestroy() {
document.removeEventListener('keydown', this.handleKeyDown);
@@ -297,42 +188,8 @@ export default {
timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
this.isUTCBased = timeSystem.isUTCBased;
},
setOffsetsFromView($event) {
if (this.$refs.conductorForm.checkValidity()) {
let startOffset = 0 - this.durationFormatter.parse(this.offsets.start);
let endOffset = this.durationFormatter.parse(this.offsets.end);
this.openmct.time.clockOffsets({
start: startOffset,
end: endOffset
});
}
if ($event) {
$event.preventDefault();
return false;
}
},
setBoundsFromView($event) {
if (this.$refs.conductorForm.checkValidity()) {
let start = this.timeFormatter.parse(this.formattedBounds.start);
let end = this.timeFormatter.parse(this.formattedBounds.end);
this.openmct.time.bounds({
start: start,
end: end
});
}
if ($event) {
$event.preventDefault();
return false;
}
},
setViewFromClock(clock) {
this.clearAllValidation();
// this.clearAllValidation();
this.isFixed = clock === undefined;
},
setViewFromBounds(bounds) {
@@ -341,158 +198,16 @@ export default {
this.viewBounds.start = bounds.start;
this.viewBounds.end = bounds.end;
},
setViewFromOffsets(offsets) {
this.offsets.start = this.durationFormatter.format(Math.abs(offsets.start));
this.offsets.end = this.durationFormatter.format(Math.abs(offsets.end));
},
updateTimeFromConductor() {
if (this.isFixed) {
this.setBoundsFromView();
} else {
this.setOffsetsFromView();
}
},
getBoundsLimit() {
const configuration = this.configuration.menuOptions
.filter(option => option.timeSystem === this.timeSystem.key)
.find(option => option.limit);
const limit = configuration ? configuration.limit : undefined;
return limit;
},
clearAllValidation() {
if (this.isFixed) {
[this.$refs.startDate, this.$refs.endDate].forEach(this.clearValidationForInput);
} else {
[this.$refs.startOffset, this.$refs.endOffset].forEach(this.clearValidationForInput);
}
},
clearValidationForInput(input) {
input.setCustomValidity('');
input.title = '';
},
validateAllBounds(ref) {
if (!this.areBoundsFormatsValid()) {
return false;
}
let validationResult = true;
const currentInput = this.$refs[ref];
return [this.$refs.startDate, this.$refs.endDate].every((input) => {
let boundsValues = {
start: this.timeFormatter.parse(this.formattedBounds.start),
end: this.timeFormatter.parse(this.formattedBounds.end)
};
const limit = this.getBoundsLimit();
if (
this.timeSystem.isUTCBased
&& limit
&& boundsValues.end - boundsValues.start > limit
) {
if (input === currentInput) {
validationResult = "Start and end difference exceeds allowable limit";
}
} else {
if (input === currentInput) {
validationResult = this.openmct.time.validateBounds(boundsValues);
}
}
return this.handleValidationResults(input, validationResult);
});
},
areBoundsFormatsValid() {
let validationResult = true;
return [this.$refs.startDate, this.$refs.endDate].every((input) => {
const formattedDate = input === this.$refs.startDate
? this.formattedBounds.start
: this.formattedBounds.end
;
if (!this.timeFormatter.validate(formattedDate)) {
validationResult = 'Invalid date';
}
return this.handleValidationResults(input, validationResult);
});
},
validateAllOffsets(event) {
return [this.$refs.startOffset, this.$refs.endOffset].every((input) => {
let validationResult = true;
let formattedOffset;
if (input === this.$refs.startOffset) {
formattedOffset = this.offsets.start;
} else {
formattedOffset = this.offsets.end;
}
if (!this.durationFormatter.validate(formattedOffset)) {
validationResult = 'Offsets must be in the format hh:mm:ss and less than 24 hours in duration';
} else {
let offsetValues = {
start: 0 - this.durationFormatter.parse(this.offsets.start),
end: this.durationFormatter.parse(this.offsets.end)
};
validationResult = this.openmct.time.validateOffsets(offsetValues);
}
return this.handleValidationResults(input, validationResult);
});
},
handleValidationResults(input, validationResult) {
if (validationResult !== true) {
input.setCustomValidity(validationResult);
input.title = validationResult;
return false;
} else {
input.setCustomValidity('');
input.title = '';
return true;
}
},
submitForm() {
// Allow Vue model to catch up to user input.
// Submitting form will cause validation messages to display (but only if triggered by button click)
this.$nextTick(() => this.$refs.submitButton.click());
},
getFormatter(key) {
return this.openmct.telemetry.getValueFormatter({
format: key
}).formatter;
},
startDateSelected(date) {
this.formattedBounds.start = this.timeFormatter.format(date);
this.validateAllBounds('startDate');
this.submitForm();
saveClockOffsets(offsets) {
this.openmct.time.clockOffsets(offsets);
},
endDateSelected(date) {
this.formattedBounds.end = this.timeFormatter.format(date);
this.validateAllBounds('endDate');
this.submitForm();
},
hideAllTimePopups() {
this.showTCInputStart = false;
this.showTCInputEnd = false;
},
showTimePopupStart() {
this.hideAllTimePopups();
this.showTCInputStart = !this.showTCInputStart;
},
showTimePopupEnd() {
this.hideAllTimePopups();
this.showTCInputEnd = !this.showTCInputEnd;
},
timePopUpdate({ type, hours, minutes, seconds }) {
this.offsets[type] = [hours, minutes, seconds].join(':');
this.setOffsetsFromView();
this.hideAllTimePopups();
saveFixedOffsets(bounds) {
this.openmct.time.bounds(bounds);
}
}
};

View File

@@ -98,7 +98,10 @@ export default {
//Respond to changes in conductor
this.openmct.time.on("timeSystem", this.setViewFromTimeSystem);
setInterval(this.resize, RESIZE_POLL_INTERVAL);
this.resizeTimer = setInterval(this.resize, RESIZE_POLL_INTERVAL);
},
destroyed() {
clearInterval(this.resizeTimer);
},
methods: {
setAxisDimensions() {

View File

@@ -0,0 +1,280 @@
<template>
<form ref="fixedDeltaInput"
class="c-conductor__inputs"
@submit.prevent="updateTimeFromConductor"
>
<button
ref="submitButton"
class="c-input--submit"
type="submit"
></button>
<div
class="c-ctrl-wrapper c-conductor-input c-conductor__start-fixed"
>
<!-- Fixed start -->
<div class="c-conductor__start-fixed__label">
Start
</div>
<input
ref="startDate"
v-model="formattedBounds.start"
class="c-input--datetime"
type="text"
autocorrect="off"
spellcheck="false"
@change="validateAllBounds('startDate'); submitForm()"
>
<date-picker
v-if="isUTCBased"
class="c-ctrl-wrapper--menus-left"
:bottom="keyString !== undefined"
:default-date-time="formattedBounds.start"
:formatter="timeFormatter"
@date-selected="startDateSelected"
/>
</div>
<div class="c-ctrl-wrapper c-conductor-input c-conductor__end-fixed">
<!-- Fixed end and RT 'last update' display -->
<div class="c-conductor__end-fixed__label">
End
</div>
<input
ref="endDate"
v-model="formattedBounds.end"
class="c-input--datetime"
type="text"
autocorrect="off"
spellcheck="false"
@change="validateAllBounds('endDate'); submitForm()"
>
<date-picker
v-if="isUTCBased"
class="c-ctrl-wrapper--menus-left"
:bottom="keyString !== undefined"
:default-date-time="formattedBounds.end"
:formatter="timeFormatter"
@date-selected="endDateSelected"
/>
</div>
<input
type="submit"
class="invisible"
>
</form>
</template>
<script>
import DatePicker from "./DatePicker.vue";
import _ from "lodash";
const DEFAULT_DURATION_FORMATTER = 'duration';
export default {
components: {
DatePicker
},
inject: ['openmct'],
props: {
keyString: {
type: String,
default() {
return undefined;
}
}
},
data() {
let timeSystem = this.openmct.time.timeSystem();
let durationFormatter = this.getFormatter(timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
let timeFormatter = this.getFormatter(timeSystem.timeFormat);
let bounds = this.bounds || this.openmct.time.bounds();
return {
showTCInputStart: true,
showTCInputEnd: true,
durationFormatter,
timeFormatter,
bounds: {
start: bounds.start,
end: bounds.end
},
formattedBounds: {
start: timeFormatter.format(bounds.start),
end: timeFormatter.format(bounds.end)
},
isUTCBased: timeSystem.isUTCBased
};
},
mounted() {
this.handleNewBounds = _.throttle(this.handleNewBounds, 300);
this.setTimeSystem(JSON.parse(JSON.stringify(this.openmct.time.timeSystem())));
this.openmct.time.on('timeSystem', this.setTimeSystem);
this.setTimeContext();
},
beforeDestroy() {
this.clearAllValidation();
this.openmct.time.off('timeSystem', this.setTimeSystem);
this.stopFollowingTimeContext();
},
methods: {
setTimeContext() {
this.stopFollowingTimeContext();
this.timeContext = this.openmct.time.getContextForView(this.keyString ? [{identifier: this.keyString}] : []);
this.timeContext.on('timeContext', this.setTimeContext);
this.handleNewBounds(this.timeContext.bounds());
this.timeContext.on('bounds', this.handleNewBounds);
this.timeContext.on('clock', this.clearAllValidation);
},
stopFollowingTimeContext() {
if (this.timeContext) {
this.timeContext.off('bounds', this.handleNewBounds);
this.timeContext.off('clock', this.clearAllValidation);
this.timeContext.off('timeContext', this.setTimeContext);
}
},
handleNewBounds(bounds) {
this.setBounds(bounds);
this.setViewFromBounds(bounds);
},
clearAllValidation() {
[this.$refs.startDate, this.$refs.endDate].forEach(this.clearValidationForInput);
},
clearValidationForInput(input) {
input.setCustomValidity('');
input.title = '';
},
setBounds(bounds) {
this.bounds = bounds;
},
setViewFromBounds(bounds) {
this.formattedBounds.start = this.timeFormatter.format(bounds.start);
this.formattedBounds.end = this.timeFormatter.format(bounds.end);
},
setTimeSystem(timeSystem) {
this.timeSystem = timeSystem;
this.timeFormatter = this.getFormatter(timeSystem.timeFormat);
this.durationFormatter = this.getFormatter(
timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
this.isUTCBased = timeSystem.isUTCBased;
},
getFormatter(key) {
return this.openmct.telemetry.getValueFormatter({
format: key
}).formatter;
},
setBoundsFromView($event) {
if (this.$refs.fixedDeltaInput.checkValidity()) {
let start = this.timeFormatter.parse(this.formattedBounds.start);
let end = this.timeFormatter.parse(this.formattedBounds.end);
this.$emit('updated', {
start: start,
end: end
});
}
if ($event) {
$event.preventDefault();
return false;
}
},
submitForm() {
// Allow Vue model to catch up to user input.
// Submitting form will cause validation messages to display (but only if triggered by button click)
this.$nextTick(() => this.$refs.submitButton.click());
},
updateTimeFromConductor() {
this.setBoundsFromView();
},
validateAllBounds(ref) {
if (!this.areBoundsFormatsValid()) {
return false;
}
let validationResult = {
valid: true
};
const currentInput = this.$refs[ref];
return [this.$refs.startDate, this.$refs.endDate].every((input) => {
let boundsValues = {
start: this.timeFormatter.parse(this.formattedBounds.start),
end: this.timeFormatter.parse(this.formattedBounds.end)
};
//TODO: Do we need limits here? We have conductor limits disabled right now
// const limit = this.getBoundsLimit();
const limit = false;
if (this.timeSystem.isUTCBased && limit
&& boundsValues.end - boundsValues.start > limit) {
if (input === currentInput) {
validationResult = {
valid: false,
message: "Start and end difference exceeds allowable limit"
};
}
} else {
if (input === currentInput) {
validationResult = this.openmct.time.validateBounds(boundsValues);
}
}
return this.handleValidationResults(input, validationResult);
});
},
areBoundsFormatsValid() {
let validationResult = {
valid: true
};
return [this.$refs.startDate, this.$refs.endDate].every((input) => {
const formattedDate = input === this.$refs.startDate
? this.formattedBounds.start
: this.formattedBounds.end
;
if (!this.timeFormatter.validate(formattedDate)) {
validationResult = {
valid: false,
message: 'Invalid date'
};
}
return this.handleValidationResults(input, validationResult);
});
},
getBoundsLimit() {
const configuration = this.configuration.menuOptions
.filter(option => option.timeSystem === this.timeSystem.key)
.find(option => option.limit);
const limit = configuration ? configuration.limit : undefined;
return limit;
},
handleValidationResults(input, validationResult) {
if (validationResult.valid !== true) {
input.setCustomValidity(validationResult.message);
input.title = validationResult.message;
} else {
input.setCustomValidity('');
input.title = '';
}
return validationResult.valid;
},
startDateSelected(date) {
this.formattedBounds.start = this.timeFormatter.format(date);
this.validateAllBounds('startDate');
this.submitForm();
},
endDateSelected(date) {
this.formattedBounds.end = this.timeFormatter.format(date);
this.validateAllBounds('endDate');
this.submitForm();
}
}
};
</script>

View File

@@ -0,0 +1,269 @@
<template>
<form ref="deltaInput"
class="c-conductor__inputs"
>
<div
class="c-ctrl-wrapper c-conductor-input c-conductor__start-delta"
>
<!-- RT start -->
<div class="c-direction-indicator icon-minus"></div>
<time-popup
v-if="showTCInputStart"
class="pr-tc-input-menu--start"
:bottom="keyString !== undefined"
:type="'start'"
:offset="offsets.start"
@focus.native="$event.target.select()"
@hide="hideAllTimePopups"
@update="timePopUpdate"
/>
<button
ref="startOffset"
class="c-button c-conductor__delta-button"
title="Set the time offset after now"
@click.prevent="showTimePopupStart"
>
{{ offsets.start }}
</button>
</div>
<div class="c-ctrl-wrapper c-conductor-input c-conductor__end-fixed">
<!-- RT 'last update' display -->
<div class="c-conductor__end-fixed__label">
Current
</div>
<input
ref="endDate"
v-model="formattedBounds.end"
class="c-input--datetime"
type="text"
autocorrect="off"
spellcheck="false"
:disabled="true"
>
</div>
<div
class="c-ctrl-wrapper c-conductor-input c-conductor__end-delta"
>
<!-- RT end -->
<div class="c-direction-indicator icon-plus"></div>
<time-popup
v-if="showTCInputEnd"
class="pr-tc-input-menu--end"
:bottom="keyString !== undefined"
:type="'end'"
:offset="offsets.end"
@focus.native="$event.target.select()"
@hide="hideAllTimePopups"
@update="timePopUpdate"
/>
<button
ref="endOffset"
class="c-button c-conductor__delta-button"
title="Set the time offset preceding now"
@click.prevent="showTimePopupEnd"
>
{{ offsets.end }}
</button>
</div>
</form>
</template>
<script>
import timePopup from "./timePopup.vue";
import _ from "lodash";
const DEFAULT_DURATION_FORMATTER = 'duration';
export default {
components: {
timePopup
},
inject: ['openmct'],
props: {
keyString: {
type: String,
default() {
return undefined;
}
}
},
data() {
let timeSystem = this.openmct.time.timeSystem();
let durationFormatter = this.getFormatter(timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
let timeFormatter = this.getFormatter(timeSystem.timeFormat);
let bounds = this.bounds || this.openmct.time.bounds();
let offsets = this.openmct.time.clockOffsets();
return {
showTCInputStart: false,
showTCInputEnd: false,
durationFormatter,
timeFormatter,
bounds: {
start: bounds.start,
end: bounds.end
},
offsets: {
start: offsets && durationFormatter.format(Math.abs(offsets.start)),
end: offsets && durationFormatter.format(Math.abs(offsets.end))
},
formattedBounds: {
start: timeFormatter.format(bounds.start),
end: timeFormatter.format(bounds.end)
},
isUTCBased: timeSystem.isUTCBased
};
},
mounted() {
this.handleNewBounds = _.throttle(this.handleNewBounds, 300);
this.setTimeSystem(JSON.parse(JSON.stringify(this.openmct.time.timeSystem())));
this.openmct.time.on('timeSystem', this.setTimeSystem);
this.setTimeContext();
},
beforeDestroy() {
this.openmct.time.off('timeSystem', this.setTimeSystem);
this.stopFollowingTime();
},
methods: {
followTime() {
this.handleNewBounds(this.timeContext.bounds());
this.setViewFromOffsets(this.timeContext.clockOffsets());
this.timeContext.on('bounds', this.handleNewBounds);
this.timeContext.on('clock', this.clearAllValidation);
this.timeContext.on('clockOffsets', this.setViewFromOffsets);
},
stopFollowingTime() {
if (this.timeContext) {
this.timeContext.off('bounds', this.handleNewBounds);
this.timeContext.off('clock', this.clearAllValidation);
this.timeContext.off('clockOffsets', this.setViewFromOffsets);
this.timeContext.off('timeContext', this.setTimeContext);
}
},
setTimeContext() {
this.stopFollowingTime();
this.timeContext = this.openmct.time.getContextForView(this.keyString ? [{identifier: this.keyString}] : []);
this.timeContext.on('timeContext', this.setTimeContext);
this.followTime();
},
handleNewBounds(bounds) {
this.setBounds(bounds);
this.setViewFromBounds(bounds);
},
clearAllValidation() {
[this.$refs.startOffset, this.$refs.endOffset].forEach(this.clearValidationForInput);
},
clearValidationForInput(input) {
input.setCustomValidity('');
input.title = '';
},
setViewFromOffsets(offsets) {
if (offsets) {
this.offsets.start = this.durationFormatter.format(Math.abs(offsets.start));
this.offsets.end = this.durationFormatter.format(Math.abs(offsets.end));
}
},
setBounds(bounds) {
this.bounds = bounds;
},
setViewFromBounds(bounds) {
this.formattedBounds.start = this.timeFormatter.format(bounds.start);
this.formattedBounds.end = this.timeFormatter.format(bounds.end);
},
setTimeSystem(timeSystem) {
this.timeSystem = timeSystem;
this.timeFormatter = this.getFormatter(timeSystem.timeFormat);
this.durationFormatter = this.getFormatter(
timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
this.isUTCBased = timeSystem.isUTCBased;
},
getFormatter(key) {
return this.openmct.telemetry.getValueFormatter({
format: key
}).formatter;
},
hideAllTimePopups() {
this.showTCInputStart = false;
this.showTCInputEnd = false;
},
showTimePopupStart() {
this.hideAllTimePopups();
this.showTCInputStart = !this.showTCInputStart;
},
showTimePopupEnd() {
this.hideAllTimePopups();
this.showTCInputEnd = !this.showTCInputEnd;
},
timePopUpdate({ type, hours, minutes, seconds }) {
this.offsets[type] = [hours, minutes, seconds].join(':');
this.setOffsetsFromView();
this.hideAllTimePopups();
},
setOffsetsFromView($event) {
if (this.$refs.deltaInput.checkValidity()) {
let startOffset = 0 - this.durationFormatter.parse(this.offsets.start);
let endOffset = this.durationFormatter.parse(this.offsets.end);
this.$emit('updated', {
start: startOffset,
end: endOffset
});
}
if ($event) {
$event.preventDefault();
return false;
}
},
validateAllBounds(ref) {
if (!this.areBoundsFormatsValid()) {
return false;
}
let validationResult = {
valid: true
};
const currentInput = this.$refs[ref];
return [this.$refs.startDate, this.$refs.endDate].every((input) => {
let boundsValues = {
start: this.timeFormatter.parse(this.formattedBounds.start),
end: this.timeFormatter.parse(this.formattedBounds.end)
};
//TODO: Do we need limits here? We have conductor limits disabled right now
// const limit = this.getBoundsLimit();
const limit = false;
if (this.timeSystem.isUTCBased && limit
&& boundsValues.end - boundsValues.start > limit) {
if (input === currentInput) {
validationResult = {
valid: false,
message: "Start and end difference exceeds allowable limit"
};
}
} else {
if (input === currentInput) {
validationResult = this.openmct.time.validateBounds(boundsValues);
}
}
return this.handleValidationResults(input, validationResult);
});
},
handleValidationResults(input, validationResult) {
if (validationResult.valid !== true) {
input.setCustomValidity(validationResult.message);
input.title = validationResult.message;
} else {
input.setCustomValidity('');
input.title = '';
}
return validationResult.valid;
}
}
};
</script>

View File

@@ -22,7 +22,8 @@
<template>
<div
ref="calendarHolder"
class="c-ctrl-wrapper c-ctrl-wrapper--menus-up c-datetime-picker__wrapper"
class="c-ctrl-wrapper c-datetime-picker__wrapper"
:class="{'c-ctrl-wrapper--menus-up': bottom !== true, 'c-ctrl-wrapper--menus-down': bottom === true}"
>
<a
class="c-icon-button icon-calendar"
@@ -118,6 +119,12 @@ export default {
formatter: {
type: Object,
required: true
},
bottom: {
type: Boolean,
default() {
return false;
}
}
},
data: function () {

View File

@@ -8,6 +8,10 @@
/*********************************************** CONDUCTOR LAYOUT */
.c-conductor {
&__inputs {
display: contents;
}
&__time-bounds {
display: grid;
grid-column-gap: $interiorMargin;
@@ -50,13 +54,6 @@
}
}
[class*='-delta'] {
&:before {
content: $glyph-icon-clock;
font-family: symbolsfont;
}
}
&.is-fixed-mode {
.c-conductor-axis {
&__zoom-indicator {
@@ -181,6 +178,27 @@
}
}
.c-conductor-holder--compact {
min-height: 22px;
.c-conductor {
&__inputs,
&__time-bounds {
display: flex;
}
&__inputs {
> * + * {
margin-left: $interiorMarginSm;
}
}
}
.is-realtime-mode .c-conductor__end-fixed {
display: none !important;
}
}
.c-conductor-input {
color: $colorInputFg;
display: flex;
@@ -250,18 +268,22 @@
box-shadow: $shdwMenu;
padding: $interiorMargin;
position: absolute;
left: 8px;
bottom: 24px;
z-index: 99;
&[class*='--start'] {
left: -25px;
}
&[class*='--end'] {
right: 0;
&[class*='--bottom'] {
bottom: auto;
top: 24px;
}
}
.l-shell__time-conductor .pr-tc-input-menu--end {
left: auto;
right: 0;
}
[class^='pr-time'] {
&[class*='label'] {
font-size: 0.8em;

View File

@@ -0,0 +1,231 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web 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 Web 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-conductor"
:class="[
isFixed ? 'is-fixed-mode' : independentTCEnabled ? 'is-realtime-mode' : 'is-fixed-mode'
]"
>
<div class="c-conductor__time-bounds">
<toggle-switch
id="independentTCToggle"
:checked="independentTCEnabled"
:title="`${independentTCEnabled ? 'Disable' : 'Enable'} independent Time Conductor`"
@change="toggleIndependentTC"
/>
<ConductorModeIcon />
<div v-if="timeOptions && independentTCEnabled"
class="c-conductor__controls"
>
<Mode v-if="mode"
class="c-conductor__mode-select"
:key-string="domainObject.identifier.key"
:mode="timeOptions.mode"
:enabled="independentTCEnabled"
@modeChanged="saveMode"
/>
<conductor-inputs-fixed v-if="isFixed"
:key-string="domainObject.identifier.key"
@updated="saveFixedOffsets"
/>
<conductor-inputs-realtime v-else
:key-string="domainObject.identifier.key"
@updated="saveClockOffsets"
/>
</div>
</div>
</div>
</template>
<script>
import ConductorInputsFixed from "../ConductorInputsFixed.vue";
import ConductorInputsRealtime from "../ConductorInputsRealtime.vue";
import ConductorModeIcon from "@/plugins/timeConductor/ConductorModeIcon.vue";
import ToggleSwitch from '../../../ui/components/ToggleSwitch.vue';
import Mode from "./Mode.vue";
export default {
components: {
Mode,
ConductorModeIcon,
ConductorInputsRealtime,
ConductorInputsFixed,
ToggleSwitch
},
inject: ['openmct'],
props: {
domainObject: {
type: Object,
required: true
}
},
data() {
return {
timeOptions: this.domainObject.configuration.timeOptions || {
clockOffsets: this.openmct.time.clockOffsets(),
fixedOffsets: this.openmct.time.bounds()
},
mode: undefined,
independentTCEnabled: this.domainObject.configuration.useIndependentTime === true
};
},
computed: {
isFixed() {
if (!this.mode || !this.mode.key) {
return this.openmct.time.clock() === undefined;
} else {
return this.mode.key === 'fixed';
}
}
},
watch: {
domainObject: {
handler(domainObject) {
if (!domainObject.configuration.timeOptions || !this.independentTCEnabled) {
return;
}
if (this.timeOptions.start !== domainObject.configuration.timeOptions.start
|| this.timeOptions.end !== domainObject.configuration.timeOptions.end) {
this.timeOptions = domainObject.configuration.timeOptions;
this.destroyIndependentTime();
this.registerIndependentTimeOffsets();
}
},
deep: true
}
},
mounted() {
this.setTimeContext();
if (this.timeOptions.mode) {
this.mode = this.timeOptions.mode;
} else {
this.timeOptions.mode = this.mode = this.timeContext.clock() === undefined ? { key: 'fixed' } : { key: Object.create(this.timeContext.clock()).key};
}
if (this.independentTCEnabled) {
this.registerIndependentTimeOffsets();
}
},
beforeDestroy() {
this.stopFollowingTimeContext();
this.destroyIndependentTime();
},
methods: {
toggleIndependentTC() {
this.independentTCEnabled = !this.independentTCEnabled;
if (this.independentTCEnabled) {
this.registerIndependentTimeOffsets();
} else {
this.destroyIndependentTime();
}
this.$emit('stateChanged', this.independentTCEnabled);
},
setTimeContext() {
this.stopFollowingTimeContext();
this.timeContext = this.openmct.time.getContextForView([this.domainObject]);
this.timeContext.on('timeContext', this.setTimeContext);
this.timeContext.on('clock', this.setViewFromClock);
},
stopFollowingTimeContext() {
if (this.timeContext) {
this.timeContext.off('timeContext', this.setTimeContext);
this.timeContext.off('clock', this.setViewFromClock);
}
},
setViewFromClock(clock) {
if (!this.timeOptions.mode) {
this.setTimeOptions(clock);
}
},
setTimeOptions() {
if (!this.timeOptions || !this.timeOptions.mode) {
this.mode = this.timeContext.clock() === undefined ? { key: 'fixed' } : { key: Object.create(this.timeContext.clock()).key};
this.timeOptions = {
clockOffsets: this.timeContext.clockOffsets(),
fixedOffsets: this.timeContext.bounds()
};
}
this.registerIndependentTimeOffsets();
},
saveFixedOffsets(offsets) {
const newOptions = Object.assign({}, this.timeOptions, {
fixedOffsets: offsets
});
this.updateTimeOptions(newOptions);
},
saveClockOffsets(offsets) {
const newOptions = Object.assign({}, this.timeOptions, {
clockOffsets: offsets
});
this.updateTimeOptions(newOptions);
},
saveMode(mode) {
this.mode = mode;
const newOptions = Object.assign({}, this.timeOptions, {
mode: this.mode
});
this.updateTimeOptions(newOptions);
},
updateTimeOptions(options) {
this.timeOptions = options;
if (!this.timeOptions.mode) {
this.timeOptions.mode = this.mode;
}
this.registerIndependentTimeOffsets();
this.$emit('updated', this.timeOptions);
},
registerIndependentTimeOffsets() {
if (!this.timeOptions.mode) {
return;
}
let offsets;
if (this.isFixed) {
offsets = this.timeOptions.fixedOffsets;
} else {
offsets = this.timeOptions.clockOffsets;
}
const key = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.unregisterIndependentTime = this.openmct.time.addIndependentContext(key, offsets, this.isFixed ? undefined : this.mode.key);
},
destroyIndependentTime() {
if (this.unregisterIndependentTime) {
this.unregisterIndependentTime();
}
}
}
};
</script>

View File

@@ -0,0 +1,222 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web 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 Web 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="modeMenuButton"
class="c-ctrl-wrapper c-ctrl-wrapper--menus-up"
>
<div class="c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left">
<button v-if="selectedMode"
class="c-button--menu c-mode-button"
@click.prevent.stop="showModesMenu"
>
<span class="c-button__label">{{ selectedMode.name }}</span>
</button>
</div>
</div>
</template>
<script>
import toggleMixin from '../../../ui/mixins/toggle-mixin';
export default {
mixins: [toggleMixin],
inject: ['openmct'],
props: {
mode: {
type: Object,
default() {
return undefined;
}
},
enabled: {
type: Boolean,
default() {
return false;
}
}
},
data: function () {
let clock;
if (this.mode && this.mode.key === 'fixed') {
clock = undefined;
} else {
//We want the clock from the global time context here
clock = this.openmct.time.clock();
}
if (clock !== undefined) {
//Create copy of active clock so the time API does not get reactified.
clock = Object.create(clock);
}
return {
selectedMode: this.getModeOptionForClock(clock),
modes: []
};
},
watch: {
mode: {
deep: true,
handler(newMode) {
if (newMode) {
this.setViewFromClock(newMode.key === 'fixed' ? undefined : newMode);
}
}
},
enabled(newValue, oldValue) {
if (newValue !== undefined && (newValue !== oldValue) && (newValue === true)) {
this.setViewFromClock(this.mode.key === 'fixed' ? undefined : this.mode);
}
}
},
mounted: function () {
if (this.mode) {
this.setViewFromClock(this.mode.key === 'fixed' ? undefined : this.mode);
}
this.followTimeConductor();
},
destroyed: function () {
this.stopFollowTimeConductor();
},
methods: {
followTimeConductor() {
this.openmct.time.on('clock', this.setViewFromClock);
},
stopFollowTimeConductor() {
this.openmct.time.off('clock', this.setViewFromClock);
},
showModesMenu() {
const elementBoundingClientRect = this.$refs.modeMenuButton.getBoundingClientRect();
const x = elementBoundingClientRect.x;
const y = elementBoundingClientRect.y;
const menuOptions = {
menuClass: 'c-conductor__mode-menu',
placement: this.openmct.menus.menuPlacement.TOP_RIGHT
};
this.openmct.menus.showSuperMenu(x, y, this.modes, menuOptions);
},
getMenuOptions() {
let clocks = [{
name: 'Fixed Timespan',
timeSystem: 'utc'
}];
let currentGlobalClock = this.openmct.time.clock();
if (currentGlobalClock !== undefined) {
//Create copy of active clock so the time API does not get reactified.
currentGlobalClock = Object.assign({}, {
name: currentGlobalClock.name,
clock: currentGlobalClock.key,
timeSystem: this.openmct.time.timeSystem().key
});
clocks.push(currentGlobalClock);
}
return clocks;
},
loadClocks() {
let clocks = this.getMenuOptions()
.map(menuOption => menuOption.clock)
.filter(isDefinedAndUnique)
.map(this.getClock);
/*
* Populate the modes menu with metadata from the available clocks
* "Fixed Mode" is always first, and has no defined clock
*/
this.modes = [undefined]
.concat(clocks)
.map(this.getModeOptionForClock);
function isDefinedAndUnique(key, index, array) {
return key !== undefined && array.indexOf(key) === index;
}
},
getModeOptionForClock(clock) {
if (clock === undefined) {
const key = 'fixed';
return {
key,
name: 'Fixed Timespan',
description: 'Query and explore data that falls between two fixed datetimes.',
cssClass: 'icon-tabular',
onItemClicked: () => this.setOption(key)
};
} else {
const key = clock.key;
return {
key,
name: clock.name,
description: "Monitor streaming data in real-time. The Time "
+ "Conductor and displays will automatically advance themselves based on this clock. " + clock.description,
cssClass: clock.cssClass || 'icon-clock',
onItemClicked: () => this.setOption(key)
};
}
},
getClock(key) {
return this.openmct.time.getAllClocks().filter(function (clock) {
return clock.key === key;
})[0];
},
setOption(clockKey) {
let key = clockKey;
if (clockKey === 'fixed') {
key = undefined;
}
const matchingOptions = this.getMenuOptions().filter(option => option.clock === key);
const clock = matchingOptions.length && matchingOptions[0].clock ? Object.assign({}, matchingOptions[0], { key: matchingOptions[0].clock }) : undefined;
this.selectedMode = this.getModeOptionForClock(clock);
if (this.mode) {
this.$emit('modeChanged', { key: clockKey });
}
},
setViewFromClock(clock) {
this.loadClocks();
//retain the mode chosen by the user
if (this.mode) {
let found = this.modes.find(mode => mode.key === this.selectedMode.key);
if (!found) {
found = this.modes.find(mode => mode.key === clock && clock.key);
this.setOption(found ? this.getModeOptionForClock(clock).key : this.getModeOptionForClock().key);
} else if (this.mode.key !== this.selectedMode.key) {
this.setOption(this.selectedMode.key);
}
} else {
this.setOption(this.getModeOptionForClock(clock).key);
}
}
}
};
</script>

View File

@@ -0,0 +1,128 @@
/*****************************************************************************
* 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 {createMouseEvent, createOpenMct, resetApplicationState} from "utils/testing";
import ConductorPlugin from "./plugin";
import Vue from 'vue';
const THIRTY_SECONDS = 30 * 1000;
const ONE_MINUTE = THIRTY_SECONDS * 2;
const FIVE_MINUTES = ONE_MINUTE * 5;
const FIFTEEN_MINUTES = FIVE_MINUTES * 3;
const THIRTY_MINUTES = FIFTEEN_MINUTES * 2;
const date = new Date(Date.UTC(78, 0, 20, 0, 0, 0)).getTime();
describe('time conductor', () => {
let element;
let child;
let appHolder;
let openmct;
let config = {
menuOptions: [
{
name: "FixedTimeRange",
timeSystem: 'utc',
bounds: {
start: date - THIRTY_MINUTES,
end: date
},
presets: [],
records: 2
},
{
name: "LocalClock",
timeSystem: 'utc',
clock: 'local',
clockOffsets: {
start: -THIRTY_MINUTES,
end: THIRTY_SECONDS
},
presets: []
}
]
};
beforeEach((done) => {
openmct = createOpenMct();
openmct.install(new ConductorPlugin(config));
element = document.createElement('div');
element.style.width = '640px';
element.style.height = '480px';
child = document.createElement('div');
child.style.width = '640px';
child.style.height = '480px';
element.appendChild(child);
openmct.on('start', () => {
openmct.time.bounds({
start: config.menuOptions[0].bounds.start,
end: config.menuOptions[0].bounds.end
});
Vue.nextTick(() => {
done();
});
});
appHolder = document.createElement("div");
openmct.start(appHolder);
});
afterEach(() => {
appHolder = undefined;
openmct = undefined;
return resetApplicationState(openmct);
});
it('shows delta inputs in fixed mode', () => {
const fixedModeEl = appHolder.querySelector('.is-fixed-mode');
const dateTimeInputs = fixedModeEl.querySelectorAll('.c-input--datetime');
expect(dateTimeInputs[0].value).toEqual('1978-01-19 23:30:00.000Z');
expect(dateTimeInputs[1].value).toEqual('1978-01-20 00:00:00.000Z');
expect(fixedModeEl.querySelector('.c-mode-button .c-button__label').innerHTML).toEqual('Fixed Timespan');
});
describe('shows delta inputs in realtime mode', () => {
beforeEach((done) => {
const switcher = appHolder.querySelector('.c-mode-button');
const clickEvent = createMouseEvent("click");
switcher.dispatchEvent(clickEvent);
Vue.nextTick(() => {
const clockItem = document.querySelectorAll('.c-conductor__mode-menu li')[1];
clockItem.dispatchEvent(clickEvent);
Vue.nextTick(() => {
done();
});
});
});
it('shows clock options', () => {
const realtimeModeEl = appHolder.querySelector('.is-realtime-mode');
const dateTimeInputs = realtimeModeEl.querySelectorAll('.c-conductor__delta-button');
expect(dateTimeInputs[0].innerHTML.replace(/[^(\d|:)]/g, '')).toEqual('00:30:00');
expect(dateTimeInputs[1].innerHTML.replace(/[^(\d|:)]/g, '')).toEqual('00:00:30');
expect(realtimeModeEl.querySelector('.c-mode-button .c-button__label').innerHTML).toEqual('Local Clock');
});
});
});

View File

@@ -1,6 +1,7 @@
<template>
<div
class="pr-tc-input-menu"
:class="{'pr-tc-input-menu--bottom' : bottom === true}"
@keydown.enter.prevent
@keyup.enter.prevent="submit"
@keydown.esc.prevent
@@ -88,6 +89,12 @@ export default {
offset: {
type: String,
required: true
},
bottom: {
type: Boolean,
default() {
return false;
}
}
},
data() {

View File

@@ -86,15 +86,16 @@ export default {
return {
items: [],
timeSystems: [],
height: 0
height: 0,
useIndependentTime: this.domainObject.configuration.useIndependentTime === true,
timeOptions: this.domainObject.configuration.timeOptions
};
},
beforeDestroy() {
this.composition.off('add', this.addItem);
this.composition.off('remove', this.removeItem);
this.composition.off('reorder', this.reorder);
this.openmct.time.off("bounds", this.updateViewBounds);
this.stopFollowingTimeContext();
},
mounted() {
if (this.composition) {
@@ -104,8 +105,8 @@ export default {
this.composition.load();
}
this.setTimeContext();
this.getTimeSystems();
this.openmct.time.on("bounds", this.updateViewBounds);
},
methods: {
addItem(domainObject) {
@@ -132,8 +133,8 @@ export default {
},
removeItem(identifier) {
let index = this.items.findIndex(item => this.openmct.objects.areIdsEqual(identifier, item.domainObject.identifier));
this.removeSelectable(this.items[index]);
this.items.splice(index, 1);
this.updateContentHeight();
},
reorder(reorderPlan) {
let oldItems = this.items.slice();
@@ -154,7 +155,7 @@ export default {
});
},
getBoundsForTimeSystem(timeSystem) {
const currentBounds = this.openmct.time.bounds();
const currentBounds = this.timeContext.bounds();
//TODO: Some kind of translation via an offset? of current bounds to target timeSystem
return currentBounds;
@@ -164,6 +165,20 @@ export default {
if (currentTimeSystem) {
currentTimeSystem.bounds = bounds;
}
},
setTimeContext() {
this.stopFollowingTimeContext();
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
this.timeContext.on('timeContext', this.setTimeContext);
this.updateViewBounds(this.timeContext.bounds());
this.timeContext.on('bounds', this.updateViewBounds);
},
stopFollowingTimeContext() {
if (this.timeContext) {
this.timeContext.off('bounds', this.updateViewBounds);
this.timeContext.off('timeContext', this.setTimeContext);
}
}
}
};

View File

@@ -20,7 +20,8 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import TimelineViewProvider from '../timeline/TimelineViewProvider';
import TimelineViewProvider from './TimelineViewProvider';
import timelineInterceptor from "./timelineInterceptor";
export default function () {
return function install(openmct) {
@@ -32,8 +33,12 @@ export default function () {
cssClass: 'icon-timeline',
initialize: function (domainObject) {
domainObject.composition = [];
domainObject.configuration = {
useIndependentTime: false
};
}
});
timelineInterceptor(openmct);
openmct.objectViews.addProvider(new TimelineViewProvider(openmct));
};
}

View File

@@ -96,10 +96,15 @@ describe('the plugin', function () {
describe('the view', () => {
let timelineView;
let testViewObject;
beforeEach(() => {
const testViewObject = {
testViewObject = {
id: "test-object",
identifier: {
key: "test-object",
namespace: ''
},
type: "time-strip"
};
@@ -119,6 +124,106 @@ describe('the plugin', function () {
const el = element.querySelector('.c-timesystem-axis');
expect(el).toBeDefined();
});
it('does not show the independent time conductor based on configuration', () => {
const independentTimeConductorEl = element.querySelector('.c-timeline-holder > .c-conductor__controls');
expect(independentTimeConductorEl).toBeNull();
});
});
describe('the independent time conductor', () => {
let timelineView;
let testViewObject = {
id: "test-object",
identifier: {
key: "test-object",
namespace: ''
},
type: "time-strip",
configuration: {
useIndependentTime: true,
timeOptions: {
mode: {
key: 'local'
},
fixedOffsets: {
start: 10,
end: 11
},
clockOffsets: {
start: -(30 * 60 * 1000),
end: (30 * 60 * 1000)
}
}
}
};
beforeEach(done => {
const applicableViews = openmct.objectViews.get(testViewObject, mockObjectPath);
timelineView = applicableViews.find((viewProvider) => viewProvider.key === 'time-strip.view');
let view = timelineView.view(testViewObject, element);
view.show(child, true);
Vue.nextTick(done);
});
it('displays an independent time conductor with saved options - local clock', () => {
return Vue.nextTick(() => {
const independentTimeConductorEl = element.querySelector('.c-timeline-holder > .c-conductor__controls');
expect(independentTimeConductorEl).toBeDefined();
const independentTimeContext = openmct.time.getIndependentContext(testViewObject.identifier.key);
expect(independentTimeContext.clockOffsets()).toEqual(testViewObject.configuration.timeOptions.clockOffsets);
});
});
});
describe('the independent time conductor', () => {
let timelineView;
let testViewObject2 = {
id: "test-object2",
identifier: {
key: "test-object2",
namespace: ''
},
type: "time-strip",
configuration: {
useIndependentTime: true,
timeOptions: {
mode: {
key: 'fixed'
},
fixedOffsets: {
start: 10,
end: 11
},
clockOffsets: {
start: -(30 * 60 * 1000),
end: (30 * 60 * 1000)
}
}
}
};
beforeEach((done) => {
const applicableViews = openmct.objectViews.get(testViewObject2, mockObjectPath);
timelineView = applicableViews.find((viewProvider) => viewProvider.key === 'time-strip.view');
let view = timelineView.view(testViewObject2, element);
view.show(child, true);
Vue.nextTick(done);
});
it('displays an independent time conductor with saved options - fixed timespan', () => {
return Vue.nextTick(() => {
const independentTimeConductorEl = element.querySelector('.c-timeline-holder > .c-conductor__controls');
expect(independentTimeConductorEl).toBeDefined();
const independentTimeContext = openmct.time.getIndependentContext(testViewObject2.identifier.key);
expect(independentTimeContext.bounds()).toEqual(testViewObject2.configuration.timeOptions.fixedOffsets);
});
});
});
});

View File

@@ -1,4 +1,10 @@
.c-timeline-holder {
@include abs();
display: flex;
flex-direction: column;
overflow-x: hidden;
}
> * + * {
margin-top: $interiorMargin;
}
}

View File

@@ -0,0 +1,40 @@
/*****************************************************************************
* 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.
*****************************************************************************/
export default function timelineInterceptor(openmct) {
openmct.objects.addGetInterceptor({
appliesTo: (identifier, domainObject) => {
return domainObject && domainObject.type === 'time-strip';
},
invoke: (identifier, object) => {
if (object && object.configuration === undefined) {
object.configuration = {
useIndependentTime: true
};
}
return object;
}
});
}

View File

@@ -38,6 +38,10 @@ define(
this.openmct = openmct;
this.selected = [];
this.openmct.once('destroy', () => {
this.removeAllListeners();
});
}
Selection.prototype = Object.create(EventEmitter.prototype);

View File

@@ -729,6 +729,12 @@ mct-plot {
}
/********************************************************************* BAR CHARTS */
.c-bar-chart {
flex: 1 1 auto;
overflow: hidden;
}
/***************** CURSOR GUIDES */
[class*='c-cursor-guide'] {
box-shadow: $shdwCursorGuide;

View File

@@ -582,6 +582,12 @@
}
}
&[class*='--menus-bottom'] {
.c-menu {
top: auto; bottom: 100%;
}
}
&[class*='--menus-left'],
&[class*='menus-to-left'] {
.c-menu {

View File

@@ -40,6 +40,11 @@
}
}
.zerolinelayer {
// Hide unneeded plotly-styled horizontal line
display: none;
}
path.xy2-y {
stroke: $colorPlotHash !important; // Using this instead of $colorPlotAreaBorder because that is an rgba
opacity: $opacityPlotHash !important;

View File

@@ -1,13 +1,29 @@
<template>
<div></div>
<div>
<div v-if="domainObject && domainObject.type === 'time-strip'"
class="c-conductor-holder--compact l-shell__main-independent-time-conductor"
>
<independent-time-conductor :domain-object="domainObject"
@stateChanged="updateIndependentTimeState"
@updated="saveTimeOptions"
/>
</div>
<div ref="objectViewWrapper"
:class="objectViewStyle"
></div>
</div>
</template>
<script>
import _ from "lodash";
import StyleRuleManager from "@/plugins/condition/StyleRuleManager";
import {STYLE_CONSTANTS} from "@/plugins/condition/utils/constants";
import IndependentTimeConductor from '@/plugins/timeConductor/independent/IndependentTimeConductor.vue';
export default {
components: {
IndependentTimeConductor
},
inject: ["openmct"],
props: {
showEditView: Boolean,
@@ -48,6 +64,13 @@ export default {
},
font() {
return this.objectFontStyle ? this.objectFontStyle.font : this.layoutFont;
},
objectViewStyle() {
if (this.domainObject && this.domainObject.type === 'time-strip') {
return 'l-shell__main-object-view';
} else {
return 'u-contents';
}
}
},
destroyed() {
@@ -79,13 +102,13 @@ export default {
},
mounted() {
this.updateView();
this.$el.addEventListener('dragover', this.onDragOver, {
this.$refs.objectViewWrapper.addEventListener('dragover', this.onDragOver, {
capture: true
});
this.$el.addEventListener('drop', this.editIfEditable, {
this.$refs.objectViewWrapper.addEventListener('drop', this.editIfEditable, {
capture: true
});
this.$el.addEventListener('drop', this.addObjectToParent);
this.$refs.objectViewWrapper.addEventListener('drop', this.addObjectToParent);
if (this.domainObject) {
//This is to apply styles to subobjects in a layout
this.initObjectStyles();
@@ -95,7 +118,9 @@ export default {
clear() {
if (this.currentView) {
this.currentView.destroy();
this.$el.innerHTML = '';
if (this.$refs.objectViewWrapper) {
this.$refs.objectViewWrapper.innerHTML = '';
}
if (this.releaseEditModeHandler) {
this.releaseEditModeHandler();
@@ -118,8 +143,8 @@ export default {
this.openmct.objectViews.off('clearData', this.clearData);
},
getStyleReceiver() {
let styleReceiver = this.$el.querySelector('.js-style-receiver')
|| this.$el.querySelector(':first-child');
let styleReceiver = this.$refs.objectViewWrapper.querySelector('.js-style-receiver')
|| this.$refs.objectViewWrapper.querySelector(':first-child');
if (styleReceiver === null) {
styleReceiver = undefined;
@@ -183,7 +208,7 @@ export default {
this.viewContainer = document.createElement('div');
this.viewContainer.classList.add('l-angular-ov-wrapper');
this.$el.append(this.viewContainer);
this.$refs.objectViewWrapper.append(this.viewContainer);
let provider = this.getViewProvider();
if (!provider) {
return;
@@ -213,7 +238,7 @@ export default {
if (immediatelySelect) {
this.removeSelectable = this.openmct.selection.selectable(
this.$el, this.getSelectionContext(), true);
this.$refs.objectViewWrapper, this.getSelectionContext(), true);
}
this.openmct.objectViews.on('clearData', this.clearData);
@@ -388,6 +413,13 @@ export default {
if (elemToStyle !== undefined) {
elemToStyle.dataset.font = newFont;
}
},
//Should the domainObject be updated in the Independent Time conductor component itself?
updateIndependentTimeState(useIndependentTime) {
this.openmct.objects.mutate(this.domainObject, 'configuration.useIndependentTime', useIndependentTime);
},
saveTimeOptions(options) {
this.openmct.objects.mutate(this.domainObject, 'configuration.timeOptions', options);
}
}
};

View File

@@ -54,6 +54,9 @@ describe("the inspector", () => {
});
afterEach(() => {
stylesViewComponent.$destroy();
savedStylesViewComponent.$destroy();
return resetApplicationState(openmct);
});
@@ -77,7 +80,7 @@ describe("the inspector", () => {
expect(savedStylesViewComponent.$children[0].$children.length).toBe(0);
stylesViewComponent.$children[0].saveStyle(mockStyle);
stylesViewComponent.$nextTick().then(() => {
return stylesViewComponent.$nextTick().then(() => {
expect(savedStylesViewComponent.$children[0].$children.length).toBe(1);
});
});
@@ -91,7 +94,7 @@ describe("the inspector", () => {
stylesViewComponent.$children[0].saveStyle(mockStyle);
stylesViewComponent.$nextTick().then(() => {
return stylesViewComponent.$nextTick().then(() => {
const styleSelectorComponent = savedStylesViewComponent.$children[0].$children[0];
styleSelectorComponent.selectStyle();
@@ -147,7 +150,7 @@ describe("the inspector", () => {
stylesViewComponent = createViewComponent(StylesView, selection, openmct);
savedStylesViewComponent = createViewComponent(SavedStylesView, selection, openmct);
stylesViewComponent.$nextTick().then(() => {
return stylesViewComponent.$nextTick().then(() => {
const styleEditorComponentIndex = stylesViewComponent.$children[0].$children.length - 1;
const styleEditorComponent = stylesViewComponent.$children[0].$children[styleEditorComponentIndex];
const saveStyleButtonIndex = styleEditorComponent.$children.length - 1;
@@ -168,7 +171,7 @@ describe("the inspector", () => {
stylesViewComponent = createViewComponent(StylesView, selection, openmct);
savedStylesViewComponent = createViewComponent(SavedStylesView, selection, openmct);
stylesViewComponent.$nextTick().then(() => {
return stylesViewComponent.$nextTick().then(() => {
const styleEditorComponentIndex = stylesViewComponent.$children[0].$children.length - 1;
const styleEditorComponent = stylesViewComponent.$children[0].$children[styleEditorComponentIndex];
const saveStyleButtonIndex = styleEditorComponent.$children.length - 1;
@@ -185,7 +188,7 @@ describe("the inspector", () => {
stylesViewComponent = createViewComponent(StylesView, selection, openmct);
savedStylesViewComponent = createViewComponent(SavedStylesView, selection, openmct);
stylesViewComponent.$nextTick().then(() => {
return stylesViewComponent.$nextTick().then(() => {
const styleEditorComponentIndex = stylesViewComponent.$children[0].$children.length - 1;
const styleEditorComponent = stylesViewComponent.$children[0].$children[styleEditorComponentIndex];
const saveStyleButtonIndex = styleEditorComponent.$children.length - 1;

View File

@@ -134,7 +134,7 @@ export default {
actionCollection: {
type: Object,
default: () => {
return {};
return undefined;
}
}
},
@@ -233,6 +233,7 @@ export default {
},
mounted: function () {
document.addEventListener('click', this.closeViewAndSaveMenu);
this.promptUserbeforeNavigatingAway = this.promptUserbeforeNavigatingAway.bind(this);
window.addEventListener('beforeunload', this.promptUserbeforeNavigatingAway);

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