Compare commits
55 Commits
progress-i
...
plots-dup-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53d0f8c504 | ||
|
|
001f755842 | ||
|
|
252d7d9bc4 | ||
|
|
364a97d8b7 | ||
|
|
de5da9445b | ||
|
|
9abd60d9bb | ||
|
|
53d63d69fe | ||
|
|
c5350659e2 | ||
|
|
76830b24ac | ||
|
|
fcb17f87eb | ||
|
|
99e90c7488 | ||
|
|
2c4fb4cb9d | ||
|
|
c91b9f7825 | ||
|
|
166211a8be | ||
|
|
f371d5479d | ||
|
|
c4e41d784a | ||
|
|
35eceea793 | ||
|
|
903a44fd80 | ||
|
|
a9b80753d7 | ||
|
|
3c546a0a1f | ||
|
|
48a237e25d | ||
|
|
b6786b2be3 | ||
|
|
4e13b3ff43 | ||
|
|
741b575a48 | ||
|
|
c20369d9bf | ||
|
|
f620433bce | ||
|
|
f58cd4b9ce | ||
|
|
476128ced8 | ||
|
|
22c812d67d | ||
|
|
648f3532c1 | ||
|
|
4fe44a5619 | ||
|
|
d221610df9 | ||
|
|
2e9e95644d | ||
|
|
d9df31a8c4 | ||
|
|
8243cf5d7b | ||
|
|
135edab448 | ||
|
|
c4c1fea17f | ||
|
|
5e920e90ce | ||
|
|
4a7ff3f797 | ||
|
|
886db23eb6 | ||
|
|
0ccb546a2e | ||
|
|
a6e8f76b55 | ||
|
|
271f8ed38f | ||
|
|
650f84e95c | ||
|
|
da1fa0b095 | ||
|
|
b70af5a1bb | ||
|
|
cc1a5ef1dd | ||
|
|
0af21632db | ||
|
|
e2f1ff5442 | ||
|
|
c4b9be18f1 | ||
|
|
eabdf6cd04 | ||
|
|
e56c673005 | ||
|
|
dad9f12a5c | ||
|
|
aa5edb0b83 | ||
|
|
b315803180 |
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
15
package.json
15
package.json
@@ -2,8 +2,8 @@
|
||||
"name": "openmct",
|
||||
"version": "1.7.8-SNAPSHOT",
|
||||
"description": "The Open MCT core platform",
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@braintree/sanitize-url": "^5.0.2",
|
||||
"angular": ">=1.8.0",
|
||||
"angular-route": "1.4.14",
|
||||
"babel-eslint": "10.0.3",
|
||||
@@ -12,16 +12,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 +34,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 +55,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",
|
||||
|
||||
@@ -44,9 +44,11 @@ define(
|
||||
setText(result.name);
|
||||
scope.ngModel[scope.field] = result;
|
||||
control.$setValidity("file-input", true);
|
||||
scope.$digest();
|
||||
}, function () {
|
||||
setText('Select File');
|
||||
control.$setValidity("file-input", false);
|
||||
scope.$digest();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -136,7 +136,7 @@ define([
|
||||
* @memberof module:openmct.MCT#
|
||||
* @name conductor
|
||||
*/
|
||||
this.time = new api.TimeAPI();
|
||||
this.time = new api.TimeAPI(this);
|
||||
|
||||
/**
|
||||
* An interface for interacting with the composition of domain objects.
|
||||
@@ -263,6 +263,7 @@ define([
|
||||
// Plugins that are installed by default
|
||||
|
||||
this.install(this.plugins.Plot());
|
||||
this.install(this.plugins.Chart());
|
||||
this.install(this.plugins.TelemetryTable.default());
|
||||
this.install(PreviewPlugin.default());
|
||||
this.install(LegacyIndicatorsPlugin());
|
||||
|
||||
@@ -81,14 +81,8 @@ define([
|
||||
return models;
|
||||
}
|
||||
|
||||
return this.apiFetch(missingIds)
|
||||
.then(function (apiResults) {
|
||||
Object.keys(apiResults).forEach(function (k) {
|
||||
models[k] = apiResults[k];
|
||||
});
|
||||
|
||||
return models;
|
||||
});
|
||||
//Temporary fix for missing models - don't retry using this.apiFetch
|
||||
return models;
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
|
||||
@@ -110,7 +110,7 @@ class ActionsAPI extends EventEmitter {
|
||||
return actionsObject;
|
||||
}
|
||||
|
||||
_groupAndSortActions(actionsArray) {
|
||||
_groupAndSortActions(actionsArray = []) {
|
||||
if (!Array.isArray(actionsArray) && typeof actionsArray === 'object') {
|
||||
actionsArray = Object.keys(actionsArray).map(key => actionsArray[key]);
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ define([
|
||||
StatusAPI
|
||||
) {
|
||||
return {
|
||||
TimeAPI: TimeAPI,
|
||||
TimeAPI: TimeAPI.default,
|
||||
ObjectAPI: ObjectAPI,
|
||||
CompositionAPI: CompositionAPI,
|
||||
TypeRegistry: TypeRegistry,
|
||||
|
||||
2
src/api/objects/ConflictError.js
Normal file
2
src/api/objects/ConflictError.js
Normal file
@@ -0,0 +1,2 @@
|
||||
export default class ConflictError extends Error {
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import RootRegistry from './RootRegistry';
|
||||
import RootObjectProvider from './RootObjectProvider';
|
||||
import EventEmitter from 'EventEmitter';
|
||||
import InterceptorRegistry from './InterceptorRegistry';
|
||||
import ConflictError from './ConflictError';
|
||||
|
||||
/**
|
||||
* Utilities for loading, saving, and manipulating domain objects.
|
||||
@@ -34,6 +35,7 @@ import InterceptorRegistry from './InterceptorRegistry';
|
||||
*/
|
||||
|
||||
function ObjectAPI(typeRegistry, openmct) {
|
||||
this.openmct = openmct;
|
||||
this.typeRegistry = typeRegistry;
|
||||
this.eventEmitter = new EventEmitter();
|
||||
this.providers = {};
|
||||
@@ -47,6 +49,10 @@ function ObjectAPI(typeRegistry, openmct) {
|
||||
this.interceptorRegistry = new InterceptorRegistry();
|
||||
|
||||
this.SYNCHRONIZED_OBJECT_TYPES = ['notebook', 'plan'];
|
||||
|
||||
this.errors = {
|
||||
Conflict: ConflictError
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -181,8 +187,17 @@ ObjectAPI.prototype.get = function (identifier, abortSignal) {
|
||||
|
||||
let objectPromise = provider.get(identifier, abortSignal).then(result => {
|
||||
delete this.cache[keystring];
|
||||
|
||||
result = this.applyGetInterceptors(identifier, result);
|
||||
|
||||
return result;
|
||||
}).catch((result) => {
|
||||
console.warn(`Failed to retrieve ${keystring}:`, result);
|
||||
|
||||
delete this.cache[keystring];
|
||||
|
||||
result = this.applyGetInterceptors(identifier);
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
@@ -285,6 +300,7 @@ ObjectAPI.prototype.isPersistable = function (idOrKeyString) {
|
||||
ObjectAPI.prototype.save = function (domainObject) {
|
||||
let provider = this.getProvider(domainObject.identifier);
|
||||
let savedResolve;
|
||||
let savedReject;
|
||||
let result;
|
||||
|
||||
if (!this.isPersistable(domainObject.identifier)) {
|
||||
@@ -294,14 +310,18 @@ ObjectAPI.prototype.save = function (domainObject) {
|
||||
} else {
|
||||
const persistedTime = Date.now();
|
||||
if (domainObject.persisted === undefined) {
|
||||
result = new Promise((resolve) => {
|
||||
result = new Promise((resolve, reject) => {
|
||||
savedResolve = resolve;
|
||||
savedReject = reject;
|
||||
});
|
||||
domainObject.persisted = persistedTime;
|
||||
provider.create(domainObject).then((response) => {
|
||||
this.mutate(domainObject, 'persisted', persistedTime);
|
||||
savedResolve(response);
|
||||
});
|
||||
provider.create(domainObject)
|
||||
.then((response) => {
|
||||
this.mutate(domainObject, 'persisted', persistedTime);
|
||||
savedResolve(response);
|
||||
}).catch((error) => {
|
||||
savedReject(error);
|
||||
});
|
||||
} else {
|
||||
domainObject.persisted = persistedTime;
|
||||
this.mutate(domainObject, 'persisted', persistedTime);
|
||||
|
||||
@@ -483,6 +483,10 @@ define([
|
||||
* @returns {Object<String, {TelemetryValueFormatter}>}
|
||||
*/
|
||||
TelemetryAPI.prototype.getFormatMap = function (metadata) {
|
||||
if (!metadata) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (!this.formatMapCache.has(metadata)) {
|
||||
const formatMap = metadata.values().reduce(function (map, valueMetadata) {
|
||||
map[valueMetadata.key] = this.getValueFormatter(valueMetadata);
|
||||
|
||||
@@ -49,7 +49,6 @@ export class TelemetryCollection extends EventEmitter {
|
||||
this.parseTime = undefined;
|
||||
this.metadata = this.openmct.telemetry.getMetadata(domainObject);
|
||||
this.unsubscribe = undefined;
|
||||
this.historicalProvider = undefined;
|
||||
this.options = options;
|
||||
this.pageState = undefined;
|
||||
this.lastBounds = undefined;
|
||||
@@ -65,13 +64,13 @@ export class TelemetryCollection extends EventEmitter {
|
||||
this._error(ERRORS.LOADED);
|
||||
}
|
||||
|
||||
this._timeSystem(this.openmct.time.timeSystem());
|
||||
this._setTimeSystem(this.openmct.time.timeSystem());
|
||||
this.lastBounds = this.openmct.time.bounds();
|
||||
|
||||
this._watchBounds();
|
||||
this._watchTimeSystem();
|
||||
|
||||
this._initiateHistoricalRequests();
|
||||
this._requestHistoricalTelemetry();
|
||||
this._initiateSubscriptionTelemetry();
|
||||
|
||||
this.loaded = true;
|
||||
@@ -103,43 +102,49 @@ export class TelemetryCollection extends EventEmitter {
|
||||
return this.boundedTelemetry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the telemetry collection for historical requests,
|
||||
* this uses the "standardizeRequestOptions" from Telemetry API
|
||||
* @private
|
||||
*/
|
||||
_initiateHistoricalRequests() {
|
||||
this.openmct.telemetry.standardizeRequestOptions(this.options);
|
||||
this.historicalProvider = this.openmct.telemetry.
|
||||
findRequestProvider(this.domainObject, this.options);
|
||||
|
||||
this._requestHistoricalTelemetry();
|
||||
}
|
||||
/**
|
||||
* If a historical provider exists, then historical requests will be made
|
||||
* @private
|
||||
*/
|
||||
async _requestHistoricalTelemetry() {
|
||||
if (!this.historicalProvider) {
|
||||
let options = { ...this.options };
|
||||
let historicalProvider;
|
||||
|
||||
this.openmct.telemetry.standardizeRequestOptions(options);
|
||||
historicalProvider = this.openmct.telemetry.
|
||||
findRequestProvider(this.domainObject, options);
|
||||
|
||||
if (!historicalProvider) {
|
||||
return;
|
||||
}
|
||||
|
||||
let historicalData;
|
||||
|
||||
options.onPartialResponse = this._processNewTelemetry.bind(this);
|
||||
|
||||
try {
|
||||
if (this.requestAbort) {
|
||||
this.requestAbort.abort();
|
||||
}
|
||||
|
||||
this.requestAbort = new AbortController();
|
||||
this.options.signal = this.requestAbort.signal;
|
||||
historicalData = await this.historicalProvider.request(this.domainObject, this.options);
|
||||
this.requestAbort = undefined;
|
||||
options.signal = this.requestAbort.signal;
|
||||
this.emit('requestStarted');
|
||||
historicalData = await historicalProvider.request(this.domainObject, options);
|
||||
} catch (error) {
|
||||
console.error('Error requesting telemetry data...');
|
||||
this.requestAbort = undefined;
|
||||
this._error(error);
|
||||
if (error.name !== 'AbortError') {
|
||||
console.error('Error requesting telemetry data...');
|
||||
this._error(error);
|
||||
}
|
||||
}
|
||||
|
||||
this.emit('requestEnded');
|
||||
this.requestAbort = undefined;
|
||||
|
||||
this._processNewTelemetry(historicalData);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* This uses the built in subscription function from Telemetry API
|
||||
* @private
|
||||
@@ -167,6 +172,10 @@ export class TelemetryCollection extends EventEmitter {
|
||||
* @private
|
||||
*/
|
||||
_processNewTelemetry(telemetryData) {
|
||||
if (telemetryData === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
let data = Array.isArray(telemetryData) ? telemetryData : [telemetryData];
|
||||
let parsedValue;
|
||||
let beforeStartOfBounds;
|
||||
@@ -193,9 +202,10 @@ export class TelemetryCollection extends EventEmitter {
|
||||
|
||||
if (endIndex > startIndex) {
|
||||
let potentialDupes = this.boundedTelemetry.slice(startIndex, endIndex);
|
||||
|
||||
isDuplicate = potentialDupes.some(_.isEqual.bind(undefined, datum));
|
||||
}
|
||||
} else if (startIndex === this.boundedTelemetry.length) {
|
||||
isDuplicate = _.isEqual(datum, this.boundedTelemetry[this.boundedTelemetry.length - 1]);
|
||||
}
|
||||
|
||||
if (!isDuplicate) {
|
||||
@@ -311,7 +321,7 @@ export class TelemetryCollection extends EventEmitter {
|
||||
* Time System
|
||||
* @private
|
||||
*/
|
||||
_timeSystem(timeSystem) {
|
||||
_setTimeSystem(timeSystem) {
|
||||
let domains = this.metadata.valuesForHints(['domain']);
|
||||
let domain = domains.find((d) => d.key === timeSystem.key);
|
||||
|
||||
@@ -327,7 +337,10 @@ export class TelemetryCollection extends EventEmitter {
|
||||
this.parseTime = (datum) => {
|
||||
return valueFormatter.parse(datum);
|
||||
};
|
||||
}
|
||||
|
||||
_setTimeSystemAndFetchData(timeSystem) {
|
||||
this._setTimeSystem(timeSystem);
|
||||
this._reset();
|
||||
}
|
||||
|
||||
@@ -364,19 +377,19 @@ export class TelemetryCollection extends EventEmitter {
|
||||
}
|
||||
|
||||
/**
|
||||
* adds the _timeSystem callback to the 'timeSystem' timeAPI listener
|
||||
* adds the _setTimeSystemAndFetchData callback to the 'timeSystem' timeAPI listener
|
||||
* @private
|
||||
*/
|
||||
_watchTimeSystem() {
|
||||
this.openmct.time.on('timeSystem', this._timeSystem, this);
|
||||
this.openmct.time.on('timeSystem', this._setTimeSystemAndFetchData, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* removes the _timeSystem callback from the 'timeSystem' timeAPI listener
|
||||
* removes the _setTimeSystemAndFetchData callback from the 'timeSystem' timeAPI listener
|
||||
* @private
|
||||
*/
|
||||
_unwatchTimeSystem() {
|
||||
this.openmct.time.off('timeSystem', this._timeSystem, this);
|
||||
this.openmct.time.off('timeSystem', this._setTimeSystemAndFetchData, this);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
106
src/api/time/GlobalTimeContext.js
Normal file
106
src/api/time/GlobalTimeContext.js
Normal file
@@ -0,0 +1,106 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import TimeContext from "./TimeContext";
|
||||
|
||||
/**
|
||||
* The GlobalContext handles getting and setting time of the openmct application in general.
|
||||
* Views will use this context unless they specify an alternate/independent time context
|
||||
*/
|
||||
class GlobalTimeContext extends TimeContext {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
//The Time Of Interest
|
||||
this.toi = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or set the start and end time of the time conductor. Basic validation
|
||||
* of bounds is performed.
|
||||
*
|
||||
* @param {module:openmct.TimeAPI~TimeConductorBounds} newBounds
|
||||
* @throws {Error} Validation error
|
||||
* @fires module:openmct.TimeAPI~bounds
|
||||
* @returns {module:openmct.TimeAPI~TimeConductorBounds}
|
||||
* @memberof module:openmct.TimeAPI#
|
||||
* @method bounds
|
||||
*/
|
||||
bounds(newBounds) {
|
||||
if (arguments.length > 0) {
|
||||
super.bounds.call(this, ...arguments);
|
||||
// If a bounds change results in a TOI outside of the current
|
||||
// bounds, unset it
|
||||
if (this.toi < newBounds.start || this.toi > newBounds.end) {
|
||||
this.timeOfInterest(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
//Return a copy to prevent direct mutation of time conductor bounds.
|
||||
return JSON.parse(JSON.stringify(this.boundsVal));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update bounds based on provided time and current offsets
|
||||
* @private
|
||||
* @param {number} timestamp A time from which bounds will be calculated
|
||||
* using current offsets.
|
||||
*/
|
||||
tick(timestamp) {
|
||||
super.tick.call(this, ...arguments);
|
||||
|
||||
// If a bounds change results in a TOI outside of the current
|
||||
// bounds, unset it
|
||||
if (this.toi < this.boundsVal.start || this.toi > this.boundsVal.end) {
|
||||
this.timeOfInterest(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or set the Time of Interest. The Time of Interest is a single point
|
||||
* in time, and constitutes the temporal focus of application views. It can
|
||||
* be manipulated by the user from the time conductor or from other views.
|
||||
* The time of interest can effectively be unset by assigning a value of
|
||||
* 'undefined'.
|
||||
* @fires module:openmct.TimeAPI~timeOfInterest
|
||||
* @param newTOI
|
||||
* @returns {number} the current time of interest
|
||||
* @memberof module:openmct.TimeAPI#
|
||||
* @method timeOfInterest
|
||||
*/
|
||||
timeOfInterest(newTOI) {
|
||||
if (arguments.length > 0) {
|
||||
this.toi = newTOI;
|
||||
/**
|
||||
* The Time of Interest has moved.
|
||||
* @event timeOfInterest
|
||||
* @memberof module:openmct.TimeAPI~
|
||||
* @property {number} Current time of interest
|
||||
*/
|
||||
this.emit('timeOfInterest', this.toi);
|
||||
}
|
||||
|
||||
return this.toi;
|
||||
}
|
||||
}
|
||||
|
||||
export default GlobalTimeContext;
|
||||
94
src/api/time/IndependentTimeContext.js
Normal file
94
src/api/time/IndependentTimeContext.js
Normal file
@@ -0,0 +1,94 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import TimeContext from "./TimeContext";
|
||||
|
||||
/**
|
||||
* The IndependentTimeContext handles getting and setting time of the openmct application in general.
|
||||
* Views will use the GlobalTimeContext unless they specify an alternate/independent time context here.
|
||||
*/
|
||||
class IndependentTimeContext extends TimeContext {
|
||||
constructor(globalTimeContext, key) {
|
||||
super();
|
||||
this.key = key;
|
||||
|
||||
this.globalTimeContext = globalTimeContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the active clock. Tick source will be immediately subscribed to
|
||||
* and ticking will begin. Offsets from 'now' must also be provided. A clock
|
||||
* can be unset by calling {@link stopClock}.
|
||||
*
|
||||
* @param {Clock || string} keyOrClock The clock to activate, or its key
|
||||
* @param {ClockOffsets} offsets on each tick these will be used to calculate
|
||||
* the start and end bounds. This maintains a sliding time window of a fixed
|
||||
* width that automatically updates.
|
||||
* @fires module:openmct.TimeAPI~clock
|
||||
* @return {Clock} the currently active clock;
|
||||
*/
|
||||
clock(keyOrClock, offsets) {
|
||||
if (arguments.length === 2) {
|
||||
let clock;
|
||||
|
||||
if (typeof keyOrClock === 'string') {
|
||||
clock = this.globalTimeContext.clocks.get(keyOrClock);
|
||||
if (clock === undefined) {
|
||||
throw "Unknown clock '" + keyOrClock + "'. Has it been registered with 'addClock'?";
|
||||
}
|
||||
} else if (typeof keyOrClock === 'object') {
|
||||
clock = keyOrClock;
|
||||
if (!this.globalTimeContext.clocks.has(clock.key)) {
|
||||
throw "Unknown clock '" + keyOrClock.key + "'. Has it been registered with 'addClock'?";
|
||||
}
|
||||
}
|
||||
|
||||
const previousClock = this.activeClock;
|
||||
if (previousClock !== undefined) {
|
||||
previousClock.off("tick", this.tick);
|
||||
}
|
||||
|
||||
this.activeClock = clock;
|
||||
|
||||
/**
|
||||
* The active clock has changed. Clock can be unset by calling {@link stopClock}
|
||||
* @event clock
|
||||
* @memberof module:openmct.TimeAPI~
|
||||
* @property {Clock} clock The newly activated clock, or undefined
|
||||
* if the system is no longer following a clock source
|
||||
*/
|
||||
this.emit("clock", this.activeClock);
|
||||
|
||||
if (this.activeClock !== undefined) {
|
||||
this.clockOffsets(offsets);
|
||||
this.activeClock.on("tick", this.tick);
|
||||
}
|
||||
|
||||
} else if (arguments.length === 1) {
|
||||
throw "When setting the clock, clock offsets must also be provided";
|
||||
}
|
||||
|
||||
return this.activeClock;
|
||||
}
|
||||
}
|
||||
|
||||
export default IndependentTimeContext;
|
||||
@@ -20,51 +20,35 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
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;
|
||||
|
||||
@@ -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
360
src/api/time/TimeContext.js
Normal file
@@ -0,0 +1,360 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import EventEmitter from 'EventEmitter';
|
||||
|
||||
class TimeContext extends EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
//The Time System
|
||||
this.timeSystems = new Map();
|
||||
|
||||
this.system = undefined;
|
||||
|
||||
this.clocks = new Map();
|
||||
|
||||
this.boundsVal = {
|
||||
start: undefined,
|
||||
end: undefined
|
||||
};
|
||||
|
||||
this.activeClock = undefined;
|
||||
this.offsets = undefined;
|
||||
|
||||
this.tick = this.tick.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or set the time system of the TimeAPI.
|
||||
* @param {TimeSystem | string} timeSystem
|
||||
* @param {module:openmct.TimeAPI~TimeConductorBounds} bounds
|
||||
* @fires module:openmct.TimeAPI~timeSystem
|
||||
* @returns {TimeSystem} The currently applied time system
|
||||
* @memberof module:openmct.TimeAPI#
|
||||
* @method timeSystem
|
||||
*/
|
||||
timeSystem(timeSystemOrKey, bounds) {
|
||||
if (arguments.length >= 1) {
|
||||
if (arguments.length === 1 && !this.activeClock) {
|
||||
throw new Error(
|
||||
"Must specify bounds when changing time system without "
|
||||
+ "an active clock."
|
||||
);
|
||||
}
|
||||
|
||||
let timeSystem;
|
||||
|
||||
if (timeSystemOrKey === undefined) {
|
||||
throw "Please provide a time system";
|
||||
}
|
||||
|
||||
if (typeof timeSystemOrKey === 'string') {
|
||||
timeSystem = this.timeSystems.get(timeSystemOrKey);
|
||||
|
||||
if (timeSystem === undefined) {
|
||||
throw "Unknown time system " + timeSystemOrKey + ". Has it been registered with 'addTimeSystem'?";
|
||||
}
|
||||
} else if (typeof timeSystemOrKey === 'object') {
|
||||
timeSystem = timeSystemOrKey;
|
||||
|
||||
if (!this.timeSystems.has(timeSystem.key)) {
|
||||
throw "Unknown time system " + timeSystem.key + ". Has it been registered with 'addTimeSystem'?";
|
||||
}
|
||||
} else {
|
||||
throw "Attempt to set invalid time system in Time API. Please provide a previously registered time system object or key";
|
||||
}
|
||||
|
||||
this.system = timeSystem;
|
||||
|
||||
/**
|
||||
* The time system used by the time
|
||||
* conductor has changed. A change in Time System will always be
|
||||
* followed by a bounds event specifying new query bounds.
|
||||
*
|
||||
* @event module:openmct.TimeAPI~timeSystem
|
||||
* @property {TimeSystem} The value of the currently applied
|
||||
* Time System
|
||||
* */
|
||||
this.emit('timeSystem', this.system);
|
||||
if (bounds) {
|
||||
this.bounds(bounds);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return this.system;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clock offsets are used to calculate temporal bounds when the system is
|
||||
* ticking on a clock source.
|
||||
*
|
||||
* @typedef {object} ValidationResult
|
||||
* @property {boolean} valid Result of the validation - true or false.
|
||||
* @property {string} message An error message if valid is false.
|
||||
*/
|
||||
/**
|
||||
* Validate the given bounds. This can be used for pre-validation of bounds,
|
||||
* for example by views validating user inputs.
|
||||
* @param {TimeBounds} bounds The start and end time of the conductor.
|
||||
* @returns {ValidationResult} A validation error, or true if valid
|
||||
* @memberof module:openmct.TimeAPI#
|
||||
* @method validateBounds
|
||||
*/
|
||||
validateBounds(bounds) {
|
||||
if ((bounds.start === undefined)
|
||||
|| (bounds.end === undefined)
|
||||
|| isNaN(bounds.start)
|
||||
|| isNaN(bounds.end)
|
||||
) {
|
||||
return {
|
||||
valid: false,
|
||||
message: "Start and end must be specified as integer values"
|
||||
};
|
||||
} else if (bounds.start > bounds.end) {
|
||||
return {
|
||||
valid: false,
|
||||
message: "Specified start date exceeds end bound"
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
message: ''
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or set the start and end time of the time conductor. Basic validation
|
||||
* of bounds is performed.
|
||||
*
|
||||
* @param {module:openmct.TimeAPI~TimeConductorBounds} newBounds
|
||||
* @throws {Error} Validation error
|
||||
* @fires module:openmct.TimeAPI~bounds
|
||||
* @returns {module:openmct.TimeAPI~TimeConductorBounds}
|
||||
* @memberof module:openmct.TimeAPI#
|
||||
* @method bounds
|
||||
*/
|
||||
bounds(newBounds) {
|
||||
if (arguments.length > 0) {
|
||||
const validationResult = this.validateBounds(newBounds);
|
||||
if (validationResult.valid !== true) {
|
||||
throw new Error(validationResult.message);
|
||||
}
|
||||
|
||||
//Create a copy to avoid direct mutation of conductor bounds
|
||||
this.boundsVal = JSON.parse(JSON.stringify(newBounds));
|
||||
/**
|
||||
* The start time, end time, or both have been updated.
|
||||
* @event bounds
|
||||
* @memberof module:openmct.TimeAPI~
|
||||
* @property {TimeConductorBounds} bounds The newly updated bounds
|
||||
* @property {boolean} [tick] `true` if the bounds update was due to
|
||||
* a "tick" event (ie. was an automatic update), false otherwise.
|
||||
*/
|
||||
this.emit('bounds', this.boundsVal, false);
|
||||
}
|
||||
|
||||
//Return a copy to prevent direct mutation of time conductor bounds.
|
||||
return JSON.parse(JSON.stringify(this.boundsVal));
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the given offsets. This can be used for pre-validation of
|
||||
* offsets, for example by views validating user inputs.
|
||||
* @param {ClockOffsets} offsets The start and end offsets from a 'now' value.
|
||||
* @returns { ValidationResult } A validation error, and true/false if valid or not
|
||||
* @memberof module:openmct.TimeAPI#
|
||||
* @method validateOffsets
|
||||
*/
|
||||
validateOffsets(offsets) {
|
||||
if ((offsets.start === undefined)
|
||||
|| (offsets.end === undefined)
|
||||
|| isNaN(offsets.start)
|
||||
|| isNaN(offsets.end)
|
||||
) {
|
||||
return {
|
||||
valid: false,
|
||||
message: "Start and end offsets must be specified as integer values"
|
||||
};
|
||||
} else if (offsets.start >= offsets.end) {
|
||||
return {
|
||||
valid: false,
|
||||
message: "Specified start offset must be < end offset"
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
message: ''
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} TimeBounds
|
||||
* @property {number} start The start time displayed by the time conductor
|
||||
* in ms since epoch. Epoch determined by currently active time system
|
||||
* @property {number} end The end time displayed by the time conductor in ms
|
||||
* since epoch.
|
||||
* @memberof module:openmct.TimeAPI~
|
||||
*/
|
||||
|
||||
/**
|
||||
* Clock offsets are used to calculate temporal bounds when the system is
|
||||
* ticking on a clock source.
|
||||
*
|
||||
* @typedef {object} ClockOffsets
|
||||
* @property {number} start A time span relative to the current value of the
|
||||
* ticking clock, from which start bounds will be calculated. This value must
|
||||
* be < 0. When a clock is active, bounds will be calculated automatically
|
||||
* based on the value provided by the clock, and the defined clock offsets.
|
||||
* @property {number} end A time span relative to the current value of the
|
||||
* ticking clock, from which end bounds will be calculated. This value must
|
||||
* be >= 0.
|
||||
*/
|
||||
/**
|
||||
* Get or set the currently applied clock offsets. If no parameter is provided,
|
||||
* the current value will be returned. If provided, the new value will be
|
||||
* used as the new clock offsets.
|
||||
* @param {ClockOffsets} offsets
|
||||
* @returns {ClockOffsets}
|
||||
*/
|
||||
clockOffsets(offsets) {
|
||||
if (arguments.length > 0) {
|
||||
|
||||
const validationResult = this.validateOffsets(offsets);
|
||||
if (validationResult.valid !== true) {
|
||||
throw new Error(validationResult.message);
|
||||
}
|
||||
|
||||
this.offsets = offsets;
|
||||
|
||||
const currentValue = this.activeClock.currentValue();
|
||||
const newBounds = {
|
||||
start: currentValue + offsets.start,
|
||||
end: currentValue + offsets.end
|
||||
};
|
||||
|
||||
this.bounds(newBounds);
|
||||
|
||||
/**
|
||||
* Event that is triggered when clock offsets change.
|
||||
* @event clockOffsets
|
||||
* @memberof module:openmct.TimeAPI~
|
||||
* @property {ClockOffsets} clockOffsets The newly activated clock
|
||||
* offsets.
|
||||
*/
|
||||
this.emit("clockOffsets", offsets);
|
||||
}
|
||||
|
||||
return this.offsets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the currently active clock from ticking, and unset it. This will
|
||||
* revert all views to showing a static time frame defined by the current
|
||||
* bounds.
|
||||
*/
|
||||
stopClock() {
|
||||
if (this.activeClock) {
|
||||
this.clock(undefined, undefined);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the active clock. Tick source will be immediately subscribed to
|
||||
* and ticking will begin. Offsets from 'now' must also be provided. A clock
|
||||
* can be unset by calling {@link stopClock}.
|
||||
*
|
||||
* @param {Clock || string} keyOrClock The clock to activate, or its key
|
||||
* @param {ClockOffsets} offsets on each tick these will be used to calculate
|
||||
* the start and end bounds. This maintains a sliding time window of a fixed
|
||||
* width that automatically updates.
|
||||
* @fires module:openmct.TimeAPI~clock
|
||||
* @return {Clock} the currently active clock;
|
||||
*/
|
||||
clock(keyOrClock, offsets) {
|
||||
if (arguments.length === 2) {
|
||||
let clock;
|
||||
|
||||
if (typeof keyOrClock === 'string') {
|
||||
clock = this.clocks.get(keyOrClock);
|
||||
if (clock === undefined) {
|
||||
throw "Unknown clock '" + keyOrClock + "'. Has it been registered with 'addClock'?";
|
||||
}
|
||||
} else if (typeof keyOrClock === 'object') {
|
||||
clock = keyOrClock;
|
||||
if (!this.clocks.has(clock.key)) {
|
||||
throw "Unknown clock '" + keyOrClock.key + "'. Has it been registered with 'addClock'?";
|
||||
}
|
||||
}
|
||||
|
||||
const previousClock = this.activeClock;
|
||||
if (previousClock !== undefined) {
|
||||
previousClock.off("tick", this.tick);
|
||||
}
|
||||
|
||||
this.activeClock = clock;
|
||||
|
||||
/**
|
||||
* The active clock has changed. Clock can be unset by calling {@link stopClock}
|
||||
* @event clock
|
||||
* @memberof module:openmct.TimeAPI~
|
||||
* @property {Clock} clock The newly activated clock, or undefined
|
||||
* if the system is no longer following a clock source
|
||||
*/
|
||||
this.emit("clock", this.activeClock);
|
||||
|
||||
if (this.activeClock !== undefined) {
|
||||
this.clockOffsets(offsets);
|
||||
this.activeClock.on("tick", this.tick);
|
||||
}
|
||||
|
||||
} else if (arguments.length === 1) {
|
||||
throw "When setting the clock, clock offsets must also be provided";
|
||||
}
|
||||
|
||||
return this.activeClock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update bounds based on provided time and current offsets
|
||||
* @param {number} timestamp A time from which bounds will be calculated
|
||||
* using current offsets.
|
||||
*/
|
||||
tick(timestamp) {
|
||||
if (!this.activeClock) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newBounds = {
|
||||
start: timestamp + this.offsets.start,
|
||||
end: timestamp + this.offsets.end
|
||||
};
|
||||
|
||||
this.boundsVal = newBounds;
|
||||
this.emit('bounds', this.boundsVal, true);
|
||||
}
|
||||
}
|
||||
|
||||
export default TimeContext;
|
||||
155
src/api/time/independentTimeAPISpec.js
Normal file
155
src/api/time/independentTimeAPISpec.js
Normal file
@@ -0,0 +1,155 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import TimeAPI from "./TimeAPI";
|
||||
import {createOpenMct} from "utils/testing";
|
||||
describe("The Independent Time API", function () {
|
||||
let api;
|
||||
let domainObjectKey;
|
||||
let clockKey;
|
||||
let clock;
|
||||
let bounds;
|
||||
let independentBounds;
|
||||
let eventListener;
|
||||
let openmct;
|
||||
|
||||
beforeEach(function () {
|
||||
openmct = createOpenMct();
|
||||
api = new TimeAPI(openmct);
|
||||
clockKey = "someClockKey";
|
||||
clock = jasmine.createSpyObj("clock", [
|
||||
"on",
|
||||
"off",
|
||||
"currentValue"
|
||||
]);
|
||||
clock.currentValue.and.returnValue(100);
|
||||
clock.key = clockKey;
|
||||
api.addClock(clock);
|
||||
domainObjectKey = 'test-key';
|
||||
bounds = {
|
||||
start: 0,
|
||||
end: 1
|
||||
};
|
||||
api.bounds(bounds);
|
||||
independentBounds = {
|
||||
start: 10,
|
||||
end: 11
|
||||
};
|
||||
eventListener = jasmine.createSpy("eventListener");
|
||||
});
|
||||
|
||||
it("Creates an independent time context", () => {
|
||||
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
|
||||
let timeContext = api.getIndependentContext(domainObjectKey);
|
||||
expect(timeContext.bounds()).toEqual(independentBounds);
|
||||
destroyTimeContext();
|
||||
});
|
||||
|
||||
it("Gets an independent time context given the objectPath", () => {
|
||||
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
|
||||
let timeContext = api.getContextForView([{
|
||||
identifier: {
|
||||
namespace: '',
|
||||
key: 'blah'
|
||||
}
|
||||
}, { identifier: domainObjectKey }]);
|
||||
expect(timeContext.bounds()).toEqual(independentBounds);
|
||||
destroyTimeContext();
|
||||
});
|
||||
|
||||
it("defaults to the global time context given the objectPath", () => {
|
||||
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
|
||||
let timeContext = api.getContextForView([{
|
||||
identifier: {
|
||||
namespace: '',
|
||||
key: 'blah'
|
||||
}
|
||||
}]);
|
||||
expect(timeContext.bounds()).toEqual(bounds);
|
||||
destroyTimeContext();
|
||||
});
|
||||
|
||||
it("Allows setting of valid bounds", function () {
|
||||
bounds = {
|
||||
start: 0,
|
||||
end: 1
|
||||
};
|
||||
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
|
||||
let timeContext = api.getContextForView([{identifier: domainObjectKey}]);
|
||||
expect(timeContext.bounds()).not.toEqual(bounds);
|
||||
timeContext.bounds(bounds);
|
||||
expect(timeContext.bounds()).toEqual(bounds);
|
||||
destroyTimeContext();
|
||||
});
|
||||
|
||||
it("Disallows setting of invalid bounds", function () {
|
||||
bounds = {
|
||||
start: 1,
|
||||
end: 0
|
||||
};
|
||||
|
||||
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
|
||||
let timeContext = api.getContextForView([{identifier: domainObjectKey}]);
|
||||
expect(timeContext.bounds()).not.toBe(bounds);
|
||||
|
||||
expect(timeContext.bounds.bind(timeContext, bounds)).toThrow();
|
||||
expect(timeContext.bounds()).not.toEqual(bounds);
|
||||
|
||||
bounds = {start: 1};
|
||||
expect(timeContext.bounds()).not.toEqual(bounds);
|
||||
expect(timeContext.bounds.bind(timeContext, bounds)).toThrow();
|
||||
expect(timeContext.bounds()).not.toEqual(bounds);
|
||||
destroyTimeContext();
|
||||
});
|
||||
|
||||
it("Emits an event when bounds change", function () {
|
||||
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
|
||||
let timeContext = api.getContextForView([{identifier: domainObjectKey}]);
|
||||
expect(eventListener).not.toHaveBeenCalled();
|
||||
timeContext.on('bounds', eventListener);
|
||||
timeContext.bounds(bounds);
|
||||
expect(eventListener).toHaveBeenCalledWith(bounds, false);
|
||||
destroyTimeContext();
|
||||
});
|
||||
|
||||
describe(" when using real time clock", function () {
|
||||
const mockOffsets = {
|
||||
start: 10,
|
||||
end: 11
|
||||
};
|
||||
|
||||
it("Emits an event when bounds change based on current value", function () {
|
||||
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
|
||||
let timeContext = api.getContextForView([{identifier: domainObjectKey}]);
|
||||
expect(eventListener).not.toHaveBeenCalled();
|
||||
timeContext.clock('someClockKey', mockOffsets);
|
||||
timeContext.on('bounds', eventListener);
|
||||
timeContext.tick(10);
|
||||
expect(eventListener).toHaveBeenCalledWith({
|
||||
start: 20,
|
||||
end: 21
|
||||
}, true);
|
||||
destroyTimeContext();
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
@@ -41,7 +41,6 @@ const DEFAULTS = [
|
||||
'platform/forms',
|
||||
'platform/identity',
|
||||
'platform/persistence/aggregator',
|
||||
'platform/persistence/queue',
|
||||
'platform/policy',
|
||||
'platform/entanglement',
|
||||
'platform/search',
|
||||
|
||||
@@ -32,7 +32,7 @@ describe('the plugin', function () {
|
||||
let openmct;
|
||||
let composition;
|
||||
|
||||
beforeEach((done) => {
|
||||
beforeEach(() => {
|
||||
|
||||
openmct = createOpenMct();
|
||||
|
||||
@@ -47,11 +47,6 @@ describe('the plugin', function () {
|
||||
}
|
||||
}));
|
||||
|
||||
openmct.on('start', done);
|
||||
openmct.startHeadless();
|
||||
|
||||
composition = openmct.composition.get({identifier});
|
||||
|
||||
spyOn(couchPlugin.couchProvider, 'getObjectsByFilter').and.returnValue(Promise.resolve([
|
||||
{
|
||||
identifier: {
|
||||
@@ -66,6 +61,19 @@ describe('the plugin', function () {
|
||||
}
|
||||
}
|
||||
]));
|
||||
|
||||
spyOn(couchPlugin.couchProvider, "get").and.callFake((id) => {
|
||||
return Promise.resolve({
|
||||
identifier: id
|
||||
});
|
||||
});
|
||||
|
||||
return new Promise((resolve) => {
|
||||
openmct.once('start', resolve);
|
||||
openmct.startHeadless();
|
||||
}).then(() => {
|
||||
composition = openmct.composition.get({identifier});
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@@ -96,11 +96,11 @@ export default {
|
||||
|
||||
this.timestampKey = this.openmct.time.timeSystem().key;
|
||||
|
||||
this.valueMetadata = this
|
||||
this.valueMetadata = this.metadata ? this
|
||||
.metadata
|
||||
.valuesForHints(['range'])[0];
|
||||
.valuesForHints(['range'])[0] : undefined;
|
||||
|
||||
this.valueKey = this.valueMetadata.key;
|
||||
this.valueKey = this.valueMetadata ? this.valueMetadata.key : undefined;
|
||||
|
||||
this.unsubscribe = this.openmct
|
||||
.telemetry
|
||||
@@ -151,7 +151,10 @@ export default {
|
||||
size: 1,
|
||||
strategy: 'latest'
|
||||
})
|
||||
.then((array) => this.updateValues(array[array.length - 1]));
|
||||
.then((array) => this.updateValues(array[array.length - 1]))
|
||||
.catch((error) => {
|
||||
console.warn('Error fetching data', error);
|
||||
});
|
||||
},
|
||||
updateBounds(bounds, isTick) {
|
||||
this.bounds = bounds;
|
||||
|
||||
@@ -73,8 +73,9 @@ export default {
|
||||
hasUnits() {
|
||||
let itemsWithUnits = this.items.filter((item) => {
|
||||
let metadata = this.openmct.telemetry.getMetadata(item.domainObject);
|
||||
const valueMetadatas = metadata ? metadata.valueMetadatas : [];
|
||||
|
||||
return this.metadataHasUnits(metadata.valueMetadatas);
|
||||
return this.metadataHasUnits(valueMetadatas);
|
||||
|
||||
});
|
||||
|
||||
|
||||
53
src/plugins/charts/BarGraphCompositionPolicy.js
Normal file
53
src/plugins/charts/BarGraphCompositionPolicy.js
Normal file
@@ -0,0 +1,53 @@
|
||||
/*****************************************************************************
|
||||
* 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 hasRange(metadata) {
|
||||
const rangeValues = metadata.valuesForHints(['range']);
|
||||
|
||||
return rangeValues.length > 0;
|
||||
}
|
||||
|
||||
function hasBarGraphTelemetry(domainObject) {
|
||||
if (!openmct.telemetry.isTelemetryObject(domainObject)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let metadata = openmct.telemetry.getMetadata(domainObject);
|
||||
|
||||
return metadata.values().length > 0 && hasRange(metadata);
|
||||
}
|
||||
|
||||
return {
|
||||
allow: function (parent, child) {
|
||||
if ((parent.type === BAR_GRAPH_KEY)
|
||||
&& (hasBarGraphTelemetry(child) === false)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
3
src/plugins/charts/BarGraphConstants.js
Normal file
3
src/plugins/charts/BarGraphConstants.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export const BAR_GRAPH_VIEW = 'bar-graph.view';
|
||||
export const BAR_GRAPH_KEY = 'telemetry.plot.bar-graph';
|
||||
export const BAR_GRAPH_INSPECTOR_KEY = 'telemetry.plot.bar-graph.inspector';
|
||||
289
src/plugins/charts/BarGraphPlot.vue
Normal file
289
src/plugins/charts/BarGraphPlot.vue
Normal file
@@ -0,0 +1,289 @@
|
||||
<template>
|
||||
<div ref="plotWrapper"
|
||||
class="has-local-controls"
|
||||
:class="{ 's-unsynced' : isZoomed }"
|
||||
>
|
||||
<div v-if="isZoomed"
|
||||
class="l-state-indicators"
|
||||
>
|
||||
<span class="l-state-indicators__alert-no-lad t-object-alert t-alert-unsynced icon-alert-triangle"
|
||||
title="This plot is not currently displaying the latest data. Reset pan/zoom to view latest data."
|
||||
></span>
|
||||
</div>
|
||||
<div ref="plot"
|
||||
class="c-bar-chart"
|
||||
@plotly_relayout="zoom"
|
||||
></div>
|
||||
<div v-if="false"
|
||||
ref="localControl"
|
||||
class="gl-plot__local-controls h-local-controls h-local-controls--overlay-content c-local-controls--show-on-hover"
|
||||
>
|
||||
<button v-if="data.length"
|
||||
class="c-button icon-reset"
|
||||
:disabled="!isZoomed"
|
||||
title="Reset pan/zoom"
|
||||
@click="reset()"
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Plotly from 'plotly-basic';
|
||||
|
||||
const MULTI_AXES_X_PADDING_PERCENT = {
|
||||
LEFT: 8,
|
||||
RIGHT: 94
|
||||
};
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'domainObject'],
|
||||
props: {
|
||||
data: {
|
||||
type: Array,
|
||||
default() {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
plotAxisTitle: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isZoomed: false,
|
||||
primaryYAxisRange: {
|
||||
min: '',
|
||||
max: ''
|
||||
},
|
||||
xAxisRange: {
|
||||
min: '',
|
||||
max: ''
|
||||
}
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
data: {
|
||||
immediate: false,
|
||||
handler: 'updateData'
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
Plotly.newPlot(this.$refs.plot, Array.from(this.data), this.getLayout(), {
|
||||
responsive: true,
|
||||
displayModeBar: false
|
||||
});
|
||||
this.registerListeners();
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.plotResizeObserver) {
|
||||
this.plotResizeObserver.unobserve(this.$refs.plotWrapper);
|
||||
clearTimeout(this.resizeTimer);
|
||||
}
|
||||
|
||||
if (this.removeBarColorListener) {
|
||||
this.removeBarColorListener();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getAxisMinMax(axis) {
|
||||
const min = axis.autoSize
|
||||
? ''
|
||||
: axis.min;
|
||||
const max = axis.autoSize
|
||||
? ''
|
||||
: axis.max;
|
||||
|
||||
return {
|
||||
min,
|
||||
max
|
||||
};
|
||||
},
|
||||
getLayout() {
|
||||
const yAxesMeta = this.getYAxisMeta();
|
||||
const primaryYaxis = this.getYaxisLayout(yAxesMeta['1']);
|
||||
const xAxisDomain = this.getXAxisDomain(yAxesMeta);
|
||||
|
||||
return {
|
||||
autosize: true,
|
||||
showlegend: false,
|
||||
textposition: 'auto',
|
||||
font: {
|
||||
family: 'Helvetica Neue, Helvetica, Arial, sans-serif',
|
||||
size: '12px',
|
||||
color: '#666'
|
||||
},
|
||||
xaxis: {
|
||||
domain: xAxisDomain,
|
||||
range: [this.xAxisRange.min, this.xAxisRange.max],
|
||||
title: this.plotAxisTitle.xAxisTitle,
|
||||
automargin: true,
|
||||
fixedrange: true
|
||||
},
|
||||
yaxis: primaryYaxis,
|
||||
margin: {
|
||||
l: 5,
|
||||
r: 5,
|
||||
t: 5,
|
||||
b: 0
|
||||
},
|
||||
paper_bgcolor: 'transparent',
|
||||
plot_bgcolor: 'transparent'
|
||||
};
|
||||
},
|
||||
getYAxisMeta() {
|
||||
const yAxisMeta = {};
|
||||
|
||||
this.data.forEach(datum => {
|
||||
const yAxisMetadata = datum.yAxisMetadata;
|
||||
const range = '1';
|
||||
const side = 'left';
|
||||
const name = '';
|
||||
const unit = yAxisMetadata.units;
|
||||
|
||||
yAxisMeta[range] = {
|
||||
range,
|
||||
side,
|
||||
name,
|
||||
unit
|
||||
};
|
||||
});
|
||||
|
||||
return yAxisMeta;
|
||||
},
|
||||
getXAxisDomain(yAxisMeta) {
|
||||
let leftPaddingPerc = 0;
|
||||
let rightPaddingPerc = 100;
|
||||
let rightSide = yAxisMeta && Object.values(yAxisMeta).filter((axisMeta => axisMeta.side === 'right'));
|
||||
let leftSide = yAxisMeta && Object.values(yAxisMeta).filter((axisMeta => axisMeta.side === 'left'));
|
||||
if (yAxisMeta && rightSide.length > 1) {
|
||||
rightPaddingPerc = MULTI_AXES_X_PADDING_PERCENT.RIGHT;
|
||||
}
|
||||
|
||||
if (yAxisMeta && leftSide.length > 1) {
|
||||
leftPaddingPerc = MULTI_AXES_X_PADDING_PERCENT.LEFT;
|
||||
}
|
||||
|
||||
return [leftPaddingPerc / 100, rightPaddingPerc / 100];
|
||||
},
|
||||
getYaxisLayout(yAxisMeta) {
|
||||
if (!yAxisMeta) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const { name, range, side = 'left', unit } = yAxisMeta;
|
||||
const title = `${name} ${unit ? '(' + unit + ')' : ''}`;
|
||||
const yaxis = {
|
||||
automargin: true,
|
||||
fixedrange: true,
|
||||
title
|
||||
};
|
||||
|
||||
let yAxistype = this.primaryYAxisRange;
|
||||
if (range === '1') {
|
||||
yaxis.range = [yAxistype.min, yAxistype.max];
|
||||
|
||||
return yaxis;
|
||||
}
|
||||
|
||||
yaxis.range = [yAxistype.min, yAxistype.max];
|
||||
yaxis.anchor = side.toLowerCase() === 'left'
|
||||
? 'free'
|
||||
: 'x';
|
||||
yaxis.showline = side.toLowerCase() === 'left';
|
||||
yaxis.side = side.toLowerCase();
|
||||
yaxis.overlaying = 'y';
|
||||
yaxis.position = 0.01;
|
||||
|
||||
return yaxis;
|
||||
},
|
||||
registerListeners() {
|
||||
this.removeBarColorListener = this.openmct.objects.observe(
|
||||
this.domainObject,
|
||||
'configuration.barStyles',
|
||||
this.barColorChanged
|
||||
);
|
||||
this.resizeTimer = false;
|
||||
if (window.ResizeObserver) {
|
||||
this.plotResizeObserver = new ResizeObserver(() => {
|
||||
// debounce and trigger window resize so that plotly can resize the plot
|
||||
clearTimeout(this.resizeTimer);
|
||||
this.resizeTimer = setTimeout(() => {
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
}, 250);
|
||||
});
|
||||
this.plotResizeObserver.observe(this.$refs.plotWrapper);
|
||||
}
|
||||
},
|
||||
reset() {
|
||||
this.updatePlot();
|
||||
|
||||
this.isZoomed = false;
|
||||
this.$emit('subscribe');
|
||||
},
|
||||
barColorChanged() {
|
||||
const colors = [];
|
||||
const indices = [];
|
||||
this.data.forEach((item, index) => {
|
||||
const key = item.key;
|
||||
const colorExists = this.domainObject.configuration.barStyles.series[key] && this.domainObject.configuration.barStyles.series[key].color;
|
||||
indices.push(index);
|
||||
if (colorExists) {
|
||||
colors.push(this.domainObject.configuration.barStyles.series[key].color);
|
||||
} 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>
|
||||
|
||||
304
src/plugins/charts/BarGraphView.vue
Normal file
304
src/plugins/charts/BarGraphView.vue
Normal file
@@ -0,0 +1,304 @@
|
||||
<!--
|
||||
Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
as represented by the Administrator of the National Aeronautics and Space
|
||||
Administration. All rights reserved.
|
||||
|
||||
Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0.
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
License for the specific language governing permissions and limitations
|
||||
under the License.
|
||||
|
||||
Open MCT includes source code licensed under additional open source
|
||||
licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
this source code distribution or the Licensing information page available
|
||||
at runtime from the About dialog for additional information.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<BarGraph ref="barGraph"
|
||||
class="c-plot c-bar-chart-view"
|
||||
:data="trace"
|
||||
:plot-axis-title="plotAxisTitle"
|
||||
@subscribe="subscribeToAll"
|
||||
@unsubscribe="removeAllSubscriptions"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BarGraph from './BarGraphPlot.vue';
|
||||
import _ from 'lodash';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BarGraph
|
||||
},
|
||||
inject: ['openmct', 'domainObject', 'path'],
|
||||
data() {
|
||||
this.telemetryObjects = {};
|
||||
this.telemetryObjectFormats = {};
|
||||
this.subscriptions = [];
|
||||
this.composition = {};
|
||||
|
||||
return {
|
||||
trace: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
plotAxisTitle() {
|
||||
const { xAxisMetadata = {}, yAxisMetadata = {} } = this.trace[0] || {};
|
||||
const xAxisUnit = xAxisMetadata.units ? `(${xAxisMetadata.units})` : '';
|
||||
const yAxisUnit = yAxisMetadata.units ? `(${yAxisMetadata.units})` : '';
|
||||
|
||||
return {
|
||||
xAxisTitle: `${xAxisMetadata.name || ''} ${xAxisUnit}`,
|
||||
yAxisTitle: `${yAxisMetadata.name || ''} ${yAxisUnit}`
|
||||
};
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadComposition();
|
||||
|
||||
this.openmct.time.on('bounds', this.refreshData);
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.openmct.time.off('bounds', this.refreshData);
|
||||
|
||||
this.removeAllSubscriptions();
|
||||
|
||||
if (!this.composition) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.composition.off('add', this.addTelemetryObject);
|
||||
this.composition.off('remove', this.removeTelemetryObject);
|
||||
},
|
||||
methods: {
|
||||
addTelemetryObject(telemetryObject) {
|
||||
// grab information we need from the added telmetry object
|
||||
const key = this.openmct.objects.makeKeyString(telemetryObject.identifier);
|
||||
this.telemetryObjects[key] = telemetryObject;
|
||||
const metadata = this.openmct.telemetry.getMetadata(telemetryObject);
|
||||
this.telemetryObjectFormats[key] = this.openmct.telemetry.getFormatMap(metadata);
|
||||
const telemetryObjectPath = [telemetryObject, ...this.path];
|
||||
const telemetryIsAlias = this.openmct.objects.isObjectPathToALink(telemetryObject, telemetryObjectPath);
|
||||
|
||||
// make an update object that's a clone of the existing styles object so we preserve existing choices
|
||||
let stylesUpdate = {};
|
||||
if (this.domainObject.configuration.barStyles.series[key]) {
|
||||
stylesUpdate = _.clone(this.domainObject.configuration.barStyles.series[key]);
|
||||
}
|
||||
|
||||
stylesUpdate.name = telemetryObject.name;
|
||||
stylesUpdate.type = telemetryObject.type;
|
||||
stylesUpdate.isAlias = telemetryIsAlias;
|
||||
|
||||
// if something has changed, mutate and notify listeners
|
||||
if (!_.isEqual(stylesUpdate, this.domainObject.configuration.barStyles.series[key])) {
|
||||
this.openmct.objects.mutate(
|
||||
this.domainObject,
|
||||
`configuration.barStyles.series["${key}"]`,
|
||||
stylesUpdate
|
||||
);
|
||||
}
|
||||
|
||||
// ask for the current telemetry data, then subcribe for changes
|
||||
this.requestDataFor(telemetryObject);
|
||||
this.subscribeToObject(telemetryObject);
|
||||
},
|
||||
addTrace(trace, key) {
|
||||
if (!this.trace.length) {
|
||||
this.trace = this.trace.concat([trace]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let isInTrace = false;
|
||||
const newTrace = this.trace.map((currentTrace, index) => {
|
||||
if (currentTrace.key !== key) {
|
||||
return currentTrace;
|
||||
}
|
||||
|
||||
isInTrace = true;
|
||||
|
||||
return trace;
|
||||
});
|
||||
|
||||
this.trace = isInTrace ? newTrace : newTrace.concat([trace]);
|
||||
},
|
||||
getAxisMetadata(telemetryObject) {
|
||||
const metadata = this.openmct.telemetry.getMetadata(telemetryObject);
|
||||
if (!metadata) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const yAxisMetadata = metadata.valuesForHints(['range'])[0];
|
||||
//Exclude 'name' and 'time' based metadata specifically, from the x-Axis values by using range hints only
|
||||
const xAxisMetadata = metadata.valuesForHints(['range']);
|
||||
|
||||
return {
|
||||
xAxisMetadata,
|
||||
yAxisMetadata
|
||||
};
|
||||
},
|
||||
getOptions() {
|
||||
const { start, end } = this.openmct.time.bounds();
|
||||
|
||||
return {
|
||||
end,
|
||||
start
|
||||
};
|
||||
},
|
||||
loadComposition() {
|
||||
this.composition = this.openmct.composition.get(this.domainObject);
|
||||
|
||||
if (!this.composition) {
|
||||
this.addTelemetryObject(this.domainObject);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.composition.on('add', this.addTelemetryObject);
|
||||
this.composition.on('remove', this.removeTelemetryObject);
|
||||
this.composition.load();
|
||||
},
|
||||
refreshData(bounds, isTick) {
|
||||
if (!isTick) {
|
||||
const telemetryObjects = Object.values(this.telemetryObjects);
|
||||
telemetryObjects.forEach(this.requestDataFor);
|
||||
}
|
||||
},
|
||||
removeAllSubscriptions() {
|
||||
this.subscriptions.forEach(subscription => subscription.unsubscribe());
|
||||
this.subscriptions = [];
|
||||
},
|
||||
removeSubscription(key) {
|
||||
const found = this.subscriptions.findIndex(subscription => subscription.key === key);
|
||||
if (found > -1) {
|
||||
this.subscriptions[found].unsubscribe();
|
||||
this.subscriptions.splice(found, 1);
|
||||
}
|
||||
},
|
||||
removeTelemetryObject(identifier) {
|
||||
const key = this.openmct.objects.makeKeyString(identifier);
|
||||
delete this.telemetryObjects[key];
|
||||
if (this.telemetryObjectFormats && this.telemetryObjectFormats[key]) {
|
||||
delete this.telemetryObjectFormats[key];
|
||||
}
|
||||
|
||||
if (this.domainObject.configuration.barStyles.series[key]) {
|
||||
delete this.domainObject.configuration.barStyles.series[key];
|
||||
this.openmct.objects.mutate(
|
||||
this.domainObject,
|
||||
`configuration.barStyles.series["${key}"]`,
|
||||
undefined
|
||||
);
|
||||
}
|
||||
|
||||
this.removeSubscription(key);
|
||||
|
||||
this.trace = this.trace.filter(t => t.key !== key);
|
||||
},
|
||||
addDataToGraph(telemetryObject, data, axisMetadata) {
|
||||
const key = this.openmct.objects.makeKeyString(telemetryObject.identifier);
|
||||
|
||||
if (data.message) {
|
||||
this.openmct.notifications.alert(data.message);
|
||||
}
|
||||
|
||||
if (!this.isDataInTimeRange(data, key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let xValues = [];
|
||||
let yValues = [];
|
||||
|
||||
//populate X and Y values for plotly
|
||||
axisMetadata.xAxisMetadata.forEach((metadata) => {
|
||||
xValues.push(metadata.name);
|
||||
if (data[metadata.key]) {
|
||||
const formattedValue = this.format(key, metadata.key, data);
|
||||
yValues.push(formattedValue);
|
||||
} else {
|
||||
yValues.push(null);
|
||||
}
|
||||
});
|
||||
|
||||
const trace = {
|
||||
key,
|
||||
name: telemetryObject.name,
|
||||
x: xValues,
|
||||
y: yValues,
|
||||
text: yValues.map(String),
|
||||
xAxisMetadata: axisMetadata.xAxisMetadata,
|
||||
yAxisMetadata: axisMetadata.yAxisMetadata,
|
||||
type: 'bar',
|
||||
marker: {
|
||||
color: this.domainObject.configuration.barStyles.series[key].color
|
||||
},
|
||||
hoverinfo: 'skip'
|
||||
};
|
||||
|
||||
this.addTrace(trace, key);
|
||||
},
|
||||
isDataInTimeRange(datum, key) {
|
||||
const timeSystemKey = this.openmct.time.timeSystem().key;
|
||||
let currentTimestamp = this.parse(key, timeSystemKey, datum);
|
||||
|
||||
return currentTimestamp && this.openmct.time.bounds().end >= currentTimestamp;
|
||||
},
|
||||
format(telemetryObjectKey, metadataKey, data) {
|
||||
const formats = this.telemetryObjectFormats[telemetryObjectKey];
|
||||
|
||||
return formats[metadataKey].format(data);
|
||||
},
|
||||
parse(telemetryObjectKey, metadataKey, datum) {
|
||||
if (!datum) {
|
||||
return;
|
||||
}
|
||||
|
||||
const formats = this.telemetryObjectFormats[telemetryObjectKey];
|
||||
|
||||
return formats[metadataKey].parse(datum);
|
||||
},
|
||||
requestDataFor(telemetryObject) {
|
||||
const axisMetadata = this.getAxisMetadata(telemetryObject);
|
||||
this.openmct.telemetry.request(telemetryObject)
|
||||
.then(data => {
|
||||
data.forEach((datum) => {
|
||||
this.addDataToGraph(telemetryObject, datum, axisMetadata);
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn(`Error fetching data`, error);
|
||||
});
|
||||
},
|
||||
subscribeToObject(telemetryObject) {
|
||||
const key = this.openmct.objects.makeKeyString(telemetryObject.identifier);
|
||||
|
||||
this.removeSubscription(key);
|
||||
|
||||
const options = this.getOptions();
|
||||
const axisMetadata = this.getAxisMetadata(telemetryObject);
|
||||
const unsubscribe = this.openmct.telemetry.subscribe(telemetryObject,
|
||||
data => this.addDataToGraph(telemetryObject, data, axisMetadata)
|
||||
, options);
|
||||
|
||||
this.subscriptions.push({
|
||||
key,
|
||||
unsubscribe
|
||||
});
|
||||
},
|
||||
subscribeToAll() {
|
||||
const telemetryObjects = Object.values(this.telemetryObjects);
|
||||
telemetryObjects.forEach(this.subscribeToObject);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
79
src/plugins/charts/BarGraphViewProvider.js
Normal file
79
src/plugins/charts/BarGraphViewProvider.js
Normal file
@@ -0,0 +1,79 @@
|
||||
/*****************************************************************************
|
||||
* 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) {
|
||||
let isChildOfTimeStrip = objectPath.find(object => object.type === 'time-strip');
|
||||
|
||||
return isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath);
|
||||
}
|
||||
|
||||
return {
|
||||
key: BAR_GRAPH_VIEW,
|
||||
name: 'Bar Graph',
|
||||
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,
|
||||
path: objectPath
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
options: {
|
||||
compact: isCompact
|
||||
}
|
||||
};
|
||||
},
|
||||
template: '<bar-graph-view :options="options"></bar-graph-view>'
|
||||
});
|
||||
},
|
||||
destroy: function () {
|
||||
component.$destroy();
|
||||
component = undefined;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { BAR_GRAPH_INSPECTOR_KEY, BAR_GRAPH_KEY } from '../BarGraphConstants';
|
||||
import Vue from 'vue';
|
||||
import BarGraphOptions from "./BarGraphOptions.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: {
|
||||
BarGraphOptions
|
||||
},
|
||||
provide: {
|
||||
openmct,
|
||||
domainObject: selection[0][0].context.item
|
||||
},
|
||||
template: '<bar-graph-options></bar-graph-options>'
|
||||
});
|
||||
},
|
||||
destroy: function () {
|
||||
if (component) {
|
||||
component.$destroy();
|
||||
component = undefined;
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
priority: function () {
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
}
|
||||
70
src/plugins/charts/inspector/BarGraphOptions.vue
Normal file
70
src/plugins/charts/inspector/BarGraphOptions.vue
Normal file
@@ -0,0 +1,70 @@
|
||||
<!--
|
||||
Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
as represented by the Administrator of the National Aeronautics and Space
|
||||
Administration. All rights reserved.
|
||||
|
||||
Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0.
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
License for the specific language governing permissions and limitations
|
||||
under the License.
|
||||
|
||||
Open MCT includes source code licensed under additional open source
|
||||
licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
this source code distribution or the Licensing information page available
|
||||
at runtime from the About dialog for additional information.
|
||||
-->
|
||||
<template>
|
||||
<ul class="c-tree c-bar-graph-options">
|
||||
<h2 title="Display properties for this object">Bar Graph Series</h2>
|
||||
<li v-for="series in domainObject.composition"
|
||||
:key="series.key"
|
||||
>
|
||||
<series-options :item="series"
|
||||
:color-palette="colorPalette"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SeriesOptions from "./SeriesOptions.vue";
|
||||
import ColorPalette from '@/ui/color/ColorPalette';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SeriesOptions
|
||||
},
|
||||
inject: ['openmct', 'domainObject'],
|
||||
data() {
|
||||
return {
|
||||
isEditing: this.openmct.editor.isEditing(),
|
||||
colorPalette: this.colorPalette
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
canEdit() {
|
||||
return this.isEditing && !this.domainObject.locked;
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
this.colorPalette = new ColorPalette();
|
||||
},
|
||||
mounted() {
|
||||
this.openmct.editor.on('isEditing', this.setEditState);
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.openmct.editor.off('isEditing', this.setEditState);
|
||||
},
|
||||
methods: {
|
||||
setEditState(isEditing) {
|
||||
this.isEditing = isEditing;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
153
src/plugins/charts/inspector/SeriesOptions.vue
Normal file
153
src/plugins/charts/inspector/SeriesOptions.vue
Normal file
@@ -0,0 +1,153 @@
|
||||
<!--
|
||||
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"
|
||||
:class="aliasCss"
|
||||
>
|
||||
<span class="c-disclosure-triangle is-enabled flex-elem"
|
||||
:class="expandedCssClass"
|
||||
@click="expanded = !expanded"
|
||||
>
|
||||
</span>
|
||||
|
||||
<div class="c-object-label">
|
||||
<div :class="[seriesCss]">
|
||||
</div>
|
||||
<div class="c-object-label__name">{{ name }}</div>
|
||||
</div>
|
||||
</li>
|
||||
<ColorSwatch v-if="expanded"
|
||||
:current-color="currentColor"
|
||||
title="Manually set the color for this bar graph series."
|
||||
edit-title="Manually set the color for this bar graph series"
|
||||
view-title="The color for this bar graph series."
|
||||
short-label="Color"
|
||||
class="grid-properties"
|
||||
@colorSet="setColor"
|
||||
/>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ColorSwatch from '@/ui/color/ColorSwatch.vue';
|
||||
import Color from "@/ui/color/Color";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ColorSwatch
|
||||
},
|
||||
inject: ['openmct', 'domainObject'],
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
colorPalette: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentColor: undefined,
|
||||
name: '',
|
||||
type: '',
|
||||
isAlias: false,
|
||||
expanded: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
expandedCssClass() {
|
||||
return this.expanded ? 'c-disclosure-triangle--expanded' : '';
|
||||
},
|
||||
seriesCss() {
|
||||
const type = this.openmct.types.get(this.type);
|
||||
if (type && type.definition && type.definition.cssClass) {
|
||||
return `c-object-label__type-icon ${type.definition.cssClass}`;
|
||||
}
|
||||
|
||||
return 'c-object-label__type-icon';
|
||||
},
|
||||
aliasCss() {
|
||||
let cssClass = '';
|
||||
if (this.isAlias) {
|
||||
cssClass = 'is-alias';
|
||||
}
|
||||
|
||||
return cssClass;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
item: {
|
||||
handler() {
|
||||
this.initColorAndName();
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.key = this.openmct.objects.makeKeyString(this.item);
|
||||
this.initColorAndName();
|
||||
this.removeBarStylesListener = this.openmct.objects.observe(this.domainObject, `configuration.barStyles.series["${this.key}"]`, this.initColorAndName);
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.removeBarStylesListener) {
|
||||
this.removeBarStylesListener();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
initColorAndName() {
|
||||
// this is called before the plot is initialized
|
||||
if (!this.domainObject.configuration.barStyles.series[this.key]) {
|
||||
const color = this.colorPalette.getNextColor().asHexString();
|
||||
this.domainObject.configuration.barStyles.series[this.key] = {
|
||||
color,
|
||||
type: '',
|
||||
name: '',
|
||||
isAlias: false
|
||||
};
|
||||
} else if (!this.domainObject.configuration.barStyles.series[this.key].color) {
|
||||
this.domainObject.configuration.barStyles.series[this.key].color = this.colorPalette.getNextColor().asHexString();
|
||||
}
|
||||
|
||||
this.currentColor = this.domainObject.configuration.barStyles.series[this.key].color;
|
||||
this.name = this.domainObject.configuration.barStyles.series[this.key].name;
|
||||
this.type = this.domainObject.configuration.barStyles.series[this.key].type;
|
||||
this.isAlias = this.domainObject.configuration.barStyles.series[this.key].isAlias;
|
||||
|
||||
let colorHexString = this.currentColor;
|
||||
const colorObject = Color.fromHexString(colorHexString);
|
||||
|
||||
this.colorPalette.remove(colorObject);
|
||||
},
|
||||
setColor(chosenColor) {
|
||||
this.currentColor = chosenColor.asHexString();
|
||||
this.openmct.objects.mutate(
|
||||
this.domainObject,
|
||||
`configuration.barStyles.series["${this.key}"].color`,
|
||||
this.currentColor
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
51
src/plugins/charts/plugin.js
Normal file
51
src/plugins/charts/plugin.js
Normal file
@@ -0,0 +1,51 @@
|
||||
/*****************************************************************************
|
||||
* 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';
|
||||
import BarGraphViewProvider from './BarGraphViewProvider';
|
||||
import BarGraphInspectorViewProvider from './inspector/BarGraphInspectorViewProvider';
|
||||
import BarGraphCompositionPolicy from './BarGraphCompositionPolicy';
|
||||
|
||||
export default function () {
|
||||
return function install(openmct) {
|
||||
openmct.types.addType(BAR_GRAPH_KEY, {
|
||||
key: BAR_GRAPH_KEY,
|
||||
name: "Bar Graph",
|
||||
cssClass: "icon-bar-chart",
|
||||
description: "View data as a bar graph. Can be added to Display Layouts.",
|
||||
creatable: true,
|
||||
initialize: function (domainObject) {
|
||||
domainObject.composition = [];
|
||||
domainObject.configuration = {
|
||||
barStyles: { series: {} }
|
||||
};
|
||||
},
|
||||
priority: 891
|
||||
});
|
||||
|
||||
openmct.objectViews.addProvider(new BarGraphViewProvider(openmct));
|
||||
|
||||
openmct.inspectorViews.addProvider(new BarGraphInspectorViewProvider(openmct));
|
||||
|
||||
openmct.composition.addPolicy(new BarGraphCompositionPolicy(openmct).allow);
|
||||
};
|
||||
}
|
||||
|
||||
486
src/plugins/charts/pluginSpec.js
Normal file
486
src/plugins/charts/pluginSpec.js
Normal file
@@ -0,0 +1,486 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import {createOpenMct, resetApplicationState, createMouseEvent} from "utils/testing";
|
||||
import Vue from "vue";
|
||||
import BarGraphPlugin from "./plugin";
|
||||
import BarGraph from './BarGraphPlot.vue';
|
||||
import EventEmitter from "EventEmitter";
|
||||
import { BAR_GRAPH_VIEW, BAR_GRAPH_KEY, BAR_GRAPH_INSPECTOR_KEY } from './BarGraphConstants';
|
||||
import BarGraphOptions from "./inspector/BarGraphOptions.vue";
|
||||
|
||||
describe("the plugin", function () {
|
||||
let element;
|
||||
let child;
|
||||
let openmct;
|
||||
let telemetryPromise;
|
||||
let telemetryPromiseResolve;
|
||||
let mockObjectPath;
|
||||
|
||||
beforeEach((done) => {
|
||||
mockObjectPath = [
|
||||
{
|
||||
name: 'mock folder',
|
||||
type: 'fake-folder',
|
||||
identifier: {
|
||||
key: 'mock-folder',
|
||||
namespace: ''
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'mock parent folder',
|
||||
type: 'time-strip',
|
||||
identifier: {
|
||||
key: 'mock-parent-folder',
|
||||
namespace: ''
|
||||
}
|
||||
}
|
||||
];
|
||||
const testTelemetry = [
|
||||
{
|
||||
'utc': 1,
|
||||
'some-key': 'some-value 1',
|
||||
'some-other-key': 'some-other-value 1'
|
||||
},
|
||||
{
|
||||
'utc': 2,
|
||||
'some-key': 'some-value 2',
|
||||
'some-other-key': 'some-other-value 2'
|
||||
},
|
||||
{
|
||||
'utc': 3,
|
||||
'some-key': 'some-value 3',
|
||||
'some-other-key': 'some-other-value 3'
|
||||
}
|
||||
];
|
||||
|
||||
openmct = createOpenMct();
|
||||
|
||||
telemetryPromise = new Promise((resolve) => {
|
||||
telemetryPromiseResolve = resolve;
|
||||
});
|
||||
|
||||
spyOn(openmct.telemetry, 'request').and.callFake(() => {
|
||||
telemetryPromiseResolve(testTelemetry);
|
||||
|
||||
return telemetryPromise;
|
||||
});
|
||||
|
||||
openmct.install(new BarGraphPlugin());
|
||||
|
||||
element = document.createElement("div");
|
||||
element.style.width = "640px";
|
||||
element.style.height = "480px";
|
||||
child = document.createElement("div");
|
||||
child.style.width = "640px";
|
||||
child.style.height = "480px";
|
||||
element.appendChild(child);
|
||||
document.body.appendChild(element);
|
||||
|
||||
spyOn(window, 'ResizeObserver').and.returnValue({
|
||||
observe() {},
|
||||
unobserve() {},
|
||||
disconnect() {}
|
||||
});
|
||||
|
||||
openmct.time.timeSystem("utc", {
|
||||
start: 0,
|
||||
end: 4
|
||||
});
|
||||
|
||||
openmct.types.addType("test-object", {
|
||||
creatable: true
|
||||
});
|
||||
|
||||
openmct.on("start", done);
|
||||
openmct.startHeadless();
|
||||
});
|
||||
|
||||
afterEach((done) => {
|
||||
openmct.time.timeSystem('utc', {
|
||||
start: 0,
|
||||
end: 1
|
||||
});
|
||||
resetApplicationState(openmct).then(done).catch(done);
|
||||
});
|
||||
|
||||
describe("The bar graph view", () => {
|
||||
let testDomainObject;
|
||||
let barGraphObject;
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
let component;
|
||||
let mockComposition;
|
||||
|
||||
beforeEach(async () => {
|
||||
const getFunc = openmct.$injector.get;
|
||||
spyOn(openmct.$injector, "get")
|
||||
.withArgs("exportImageService").and.returnValue({
|
||||
exportPNG: () => {},
|
||||
exportJPG: () => {}
|
||||
})
|
||||
.and.callFake(getFunc);
|
||||
|
||||
barGraphObject = {
|
||||
identifier: {
|
||||
namespace: "",
|
||||
key: "test-plot"
|
||||
},
|
||||
type: "telemetry.plot.bar-graph",
|
||||
name: "Test Bar Graph"
|
||||
};
|
||||
|
||||
testDomainObject = {
|
||||
identifier: {
|
||||
namespace: "",
|
||||
key: "test-object"
|
||||
},
|
||||
configuration: {
|
||||
barStyles: {
|
||||
series: {}
|
||||
}
|
||||
},
|
||||
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
|
||||
}
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
mockComposition = new EventEmitter();
|
||||
mockComposition.load = () => {
|
||||
mockComposition.emit('add', testDomainObject);
|
||||
|
||||
return [testDomainObject];
|
||||
};
|
||||
|
||||
spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
|
||||
|
||||
let viewContainer = document.createElement("div");
|
||||
child.append(viewContainer);
|
||||
component = new Vue({
|
||||
el: viewContainer,
|
||||
components: {
|
||||
BarGraph
|
||||
},
|
||||
provide: {
|
||||
openmct: openmct,
|
||||
domainObject: barGraphObject,
|
||||
composition: openmct.composition.get(barGraphObject)
|
||||
},
|
||||
template: "<BarGraph></BarGraph>"
|
||||
});
|
||||
|
||||
await Vue.nextTick();
|
||||
});
|
||||
|
||||
it("provides a bar graph view", () => {
|
||||
const applicableViews = openmct.objectViews.get(barGraphObject, mockObjectPath);
|
||||
const plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === BAR_GRAPH_VIEW);
|
||||
expect(plotViewProvider).toBeDefined();
|
||||
});
|
||||
|
||||
it("Renders plotly bar graph", () => {
|
||||
let barChartElement = element.querySelectorAll(".plotly");
|
||||
expect(barChartElement.length).toBe(1);
|
||||
});
|
||||
|
||||
it("Handles dots in telemetry id", () => {
|
||||
const dotFullTelemetryObject = {
|
||||
identifier: {
|
||||
namespace: "someNamespace",
|
||||
key: "~OpenMCT~outer.test-object.foo.bar"
|
||||
},
|
||||
type: "test-dotful-object",
|
||||
name: "A Dotful Object",
|
||||
telemetry: {
|
||||
values: [{
|
||||
key: "utc",
|
||||
format: "utc",
|
||||
name: "Time",
|
||||
hints: {
|
||||
domain: 1
|
||||
}
|
||||
}, {
|
||||
key: "some-key.foo.name.45",
|
||||
name: "Some dotful attribute",
|
||||
hints: {
|
||||
range: 1
|
||||
}
|
||||
}, {
|
||||
key: "some-other-key.bar.344.rad",
|
||||
name: "Another dotful attribute",
|
||||
hints: {
|
||||
range: 2
|
||||
}
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
const applicableViews = openmct.objectViews.get(barGraphObject, mockObjectPath);
|
||||
const plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === BAR_GRAPH_VIEW);
|
||||
const barGraphView = plotViewProvider.view(testDomainObject, [testDomainObject]);
|
||||
barGraphView.show(child, true);
|
||||
expect(testDomainObject.configuration.barStyles.series["test-object"].name).toEqual("Test Object");
|
||||
mockComposition.emit('add', dotFullTelemetryObject);
|
||||
expect(testDomainObject.configuration.barStyles.series["someNamespace:~OpenMCT~outer.test-object.foo.bar"].name).toEqual("A Dotful Object");
|
||||
barGraphView.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("the bar graph objects", () => {
|
||||
const mockObject = {
|
||||
name: 'A very nice bar graph',
|
||||
key: BAR_GRAPH_KEY,
|
||||
creatable: true
|
||||
};
|
||||
|
||||
it('defines a bar graph 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);
|
||||
});
|
||||
});
|
||||
|
||||
describe("The bar graph composition policy", () => {
|
||||
|
||||
it("allows composition for telemetry that contain at least one range", () => {
|
||||
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 testTelemetryObject = {
|
||||
identifier: {
|
||||
namespace: "",
|
||||
key: "test-object"
|
||||
},
|
||||
type: "test-object",
|
||||
name: "Test Object",
|
||||
telemetry: {
|
||||
values: [{
|
||||
key: "some-key",
|
||||
name: "Some attribute",
|
||||
hints: {
|
||||
domain: 1
|
||||
}
|
||||
}, {
|
||||
key: "some-other-key",
|
||||
name: "Another attribute",
|
||||
hints: {
|
||||
range: 1
|
||||
}
|
||||
}]
|
||||
}
|
||||
};
|
||||
const composition = openmct.composition.get(parent);
|
||||
expect(() => {
|
||||
composition.add(testTelemetryObject);
|
||||
}).not.toThrow();
|
||||
expect(parent.composition.length).toBe(1);
|
||||
});
|
||||
|
||||
it("disallows composition for telemetry that don't contain any range hints", () => {
|
||||
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 testTelemetryObject = {
|
||||
identifier: {
|
||||
namespace: "",
|
||||
key: "test-object"
|
||||
},
|
||||
type: "test-object",
|
||||
name: "Test Object",
|
||||
telemetry: {
|
||||
values: [{
|
||||
key: "some-key",
|
||||
name: "Some attribute"
|
||||
}, {
|
||||
key: "some-other-key",
|
||||
name: "Another attribute"
|
||||
}]
|
||||
}
|
||||
};
|
||||
const composition = openmct.composition.get(parent);
|
||||
expect(() => {
|
||||
composition.add(testTelemetryObject);
|
||||
}).toThrow();
|
||||
expect(parent.composition.length).toBe(0);
|
||||
});
|
||||
});
|
||||
describe('the inspector view', () => {
|
||||
let mockComposition;
|
||||
let testDomainObject;
|
||||
let selection;
|
||||
let plotInspectorView;
|
||||
let viewContainer;
|
||||
let optionsElement;
|
||||
beforeEach(async () => {
|
||||
testDomainObject = {
|
||||
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
|
||||
}
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
selection = [
|
||||
[
|
||||
{
|
||||
context: {
|
||||
item: {
|
||||
id: "test-object",
|
||||
identifier: {
|
||||
key: "test-object",
|
||||
namespace: ''
|
||||
},
|
||||
type: "telemetry.plot.bar-graph",
|
||||
configuration: {
|
||||
barStyles: {
|
||||
series: {
|
||||
'~Some~foo.bar': {
|
||||
name: 'A telemetry object',
|
||||
type: 'some-type',
|
||||
isAlias: true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
composition: [
|
||||
{
|
||||
key: '~Some~foo.bar'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
context: {
|
||||
item: {
|
||||
type: 'time-strip',
|
||||
identifier: {
|
||||
key: 'some-other-key',
|
||||
namespace: ''
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
];
|
||||
|
||||
mockComposition = new EventEmitter();
|
||||
mockComposition.load = () => {
|
||||
mockComposition.emit('add', testDomainObject);
|
||||
|
||||
return [testDomainObject];
|
||||
};
|
||||
|
||||
spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
|
||||
|
||||
viewContainer = document.createElement('div');
|
||||
child.append(viewContainer);
|
||||
|
||||
const applicableViews = openmct.inspectorViews.get(selection);
|
||||
plotInspectorView = applicableViews[0];
|
||||
plotInspectorView.show(viewContainer);
|
||||
|
||||
await Vue.nextTick();
|
||||
optionsElement = element.querySelector('.c-bar-graph-options');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
plotInspectorView.destroy();
|
||||
});
|
||||
|
||||
it('it renders the options', () => {
|
||||
expect(optionsElement).toBeDefined();
|
||||
});
|
||||
|
||||
it('shows the name', () => {
|
||||
const seriesEl = optionsElement.querySelector('.c-object-label__name');
|
||||
expect(seriesEl.innerHTML).toEqual('A telemetry object');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -65,7 +65,7 @@ export default class Condition extends EventEmitter {
|
||||
}
|
||||
|
||||
this.trigger = conditionConfiguration.configuration.trigger;
|
||||
this.description = '';
|
||||
this.summary = '';
|
||||
}
|
||||
|
||||
updateResult(datum) {
|
||||
@@ -134,7 +134,6 @@ export default class Condition extends EventEmitter {
|
||||
criterionConfigurations.forEach((criterionConfiguration) => {
|
||||
this.addCriterion(criterionConfiguration);
|
||||
});
|
||||
this.updateDescription();
|
||||
}
|
||||
|
||||
updateCriteria(criterionConfigurations) {
|
||||
@@ -146,7 +145,6 @@ export default class Condition extends EventEmitter {
|
||||
this.criteria.forEach((criterion) => {
|
||||
criterion.updateTelemetryObjects(this.conditionManager.telemetryObjects);
|
||||
});
|
||||
this.updateDescription();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -200,7 +198,6 @@ export default class Condition extends EventEmitter {
|
||||
criterion.off('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
|
||||
criterion.off('telemetryIsStale', (obj) => this.handleStaleCriterion(obj));
|
||||
this.criteria.splice(found.index, 1, newCriterion);
|
||||
this.updateDescription();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,7 +213,6 @@ export default class Condition extends EventEmitter {
|
||||
});
|
||||
criterion.destroy();
|
||||
this.criteria.splice(found.index, 1);
|
||||
this.updateDescription();
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -228,7 +224,6 @@ export default class Condition extends EventEmitter {
|
||||
let found = this.findCriterion(criterion.id);
|
||||
if (found) {
|
||||
this.criteria[found.index] = criterion.data;
|
||||
this.updateDescription();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,8 +249,7 @@ export default class Condition extends EventEmitter {
|
||||
|
||||
description = `${description} ${criterion.getDescription()} ${(index < this.criteria.length - 1) ? triggerDescription.conjunction : ''}`;
|
||||
});
|
||||
this.description = description;
|
||||
this.conditionManager.updateConditionDescription(this);
|
||||
this.summary = description;
|
||||
}
|
||||
|
||||
getTriggerDescription() {
|
||||
|
||||
@@ -105,7 +105,14 @@ export default class ConditionManager extends EventEmitter {
|
||||
}
|
||||
|
||||
updateConditionTelemetryObjects() {
|
||||
this.conditions.forEach((condition) => condition.updateTelemetryObjects());
|
||||
this.conditions.forEach((condition) => {
|
||||
condition.updateTelemetryObjects();
|
||||
let index = this.conditionSetDomainObject.configuration.conditionCollection.findIndex(item => item.id === condition.id);
|
||||
if (index > -1) {
|
||||
//Only assign the summary, don't mutate the domain object
|
||||
this.conditionSetDomainObject.configuration.conditionCollection[index].summary = this.updateConditionDescription(condition);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
removeConditionTelemetryObjects() {
|
||||
@@ -139,10 +146,17 @@ export default class ConditionManager extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
updateConditionDescription(condition) {
|
||||
condition.updateDescription();
|
||||
|
||||
return condition.summary;
|
||||
}
|
||||
|
||||
updateCondition(conditionConfiguration) {
|
||||
let condition = this.findConditionById(conditionConfiguration.id);
|
||||
if (condition) {
|
||||
condition.update(conditionConfiguration);
|
||||
conditionConfiguration.summary = this.updateConditionDescription(condition);
|
||||
}
|
||||
|
||||
let index = this.conditionSetDomainObject.configuration.conditionCollection.findIndex(item => item.id === conditionConfiguration.id);
|
||||
@@ -152,16 +166,10 @@ export default class ConditionManager extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
updateConditionDescription(condition) {
|
||||
const found = this.conditionSetDomainObject.configuration.conditionCollection.find(conditionConfiguration => (conditionConfiguration.id === condition.id));
|
||||
if (found.summary !== condition.description) {
|
||||
found.summary = condition.description;
|
||||
this.persistConditions();
|
||||
}
|
||||
}
|
||||
|
||||
initCondition(conditionConfiguration, index) {
|
||||
let condition = new Condition(conditionConfiguration, this.openmct, this);
|
||||
conditionConfiguration.summary = this.updateConditionDescription(condition);
|
||||
|
||||
if (index !== undefined) {
|
||||
this.conditions.splice(index + 1, 0, condition);
|
||||
} else {
|
||||
|
||||
@@ -33,8 +33,10 @@ export default class ConditionSetViewProvider {
|
||||
this.cssClass = 'icon-conditional';
|
||||
}
|
||||
|
||||
canView(domainObject) {
|
||||
return domainObject.type === 'conditionSet';
|
||||
canView(domainObject, objectPath) {
|
||||
const isConditionSet = domainObject.type === 'conditionSet';
|
||||
|
||||
return isConditionSet && this.openmct.router.isNavigatedObject(objectPath);
|
||||
}
|
||||
|
||||
canEdit(domainObject) {
|
||||
|
||||
@@ -244,7 +244,7 @@ export default {
|
||||
this.telemetryMetadataOptions = [];
|
||||
telemetryObjects.forEach(telemetryObject => {
|
||||
let telemetryMetadata = this.openmct.telemetry.getMetadata(telemetryObject);
|
||||
this.addMetaDataOptions(telemetryMetadata.values());
|
||||
this.addMetaDataOptions(telemetryMetadata ? telemetryMetadata.values() : []);
|
||||
});
|
||||
this.updateOperations();
|
||||
}
|
||||
|
||||
@@ -192,7 +192,11 @@ export default {
|
||||
this.telemetry.forEach((telemetryObject) => {
|
||||
const id = this.openmct.objects.makeKeyString(telemetryObject.identifier);
|
||||
let telemetryMetadata = this.openmct.telemetry.getMetadata(telemetryObject);
|
||||
this.telemetryMetadataOptions[id] = telemetryMetadata.values().slice();
|
||||
if (telemetryMetadata) {
|
||||
this.telemetryMetadataOptions[id] = telemetryMetadata.values().slice();
|
||||
} else {
|
||||
this.telemetryMetadataOptions[id] = [];
|
||||
}
|
||||
});
|
||||
},
|
||||
addTestInput(testInput) {
|
||||
|
||||
@@ -177,7 +177,7 @@ export default class AllTelemetryCriterion extends TelemetryCriterion {
|
||||
const timeSystem = this.openmct.time.timeSystem();
|
||||
|
||||
telemetryRequestsResults.forEach((results, index) => {
|
||||
const latestDatum = results.length ? results[results.length - 1] : {};
|
||||
const latestDatum = (Array.isArray(results) && results.length) ? results[results.length - 1] : {};
|
||||
const datumId = keys[index];
|
||||
const normalizedDatum = this.createNormalizedDatum(latestDatum, telemetryObjects[datumId]);
|
||||
|
||||
|
||||
@@ -167,6 +167,11 @@ export default class TelemetryCriterion extends EventEmitter {
|
||||
id: this.id,
|
||||
data: this.formatData(normalizedDatum)
|
||||
};
|
||||
}).catch((error) => {
|
||||
return {
|
||||
id: this.id,
|
||||
data: this.formatData()
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import StylesView from "./components/inspector/StylesView.vue";
|
||||
import Vue from 'vue';
|
||||
import {getApplicableStylesForItem} from "./utils/styleUtils";
|
||||
import ConditionManager from "@/plugins/condition/ConditionManager";
|
||||
import StyleRuleManager from "./StyleRuleManager";
|
||||
|
||||
describe('the plugin', function () {
|
||||
let conditionSetDefinition;
|
||||
@@ -96,8 +97,12 @@ describe('the plugin', function () {
|
||||
|
||||
mockListener = jasmine.createSpy('mockListener');
|
||||
|
||||
openmct.router.isNavigatedObject = jasmine.createSpy().and.returnValue(true);
|
||||
|
||||
conditionSetDefinition.initialize(mockConditionSetDomainObject);
|
||||
|
||||
spyOn(openmct.objects, "save").and.returnValue(Promise.resolve(true));
|
||||
|
||||
openmct.on('start', done);
|
||||
openmct.startHeadless();
|
||||
});
|
||||
@@ -126,21 +131,6 @@ describe('the plugin', function () {
|
||||
expect(mockConditionSetDomainObject.composition instanceof Array).toBeTrue();
|
||||
expect(mockConditionSetDomainObject.composition.length).toEqual(0);
|
||||
});
|
||||
|
||||
it('provides a view', () => {
|
||||
const testViewObject = {
|
||||
id: "test-object",
|
||||
type: "conditionSet",
|
||||
configuration: {
|
||||
conditionCollection: []
|
||||
}
|
||||
};
|
||||
|
||||
const applicableViews = openmct.objectViews.get(testViewObject, []);
|
||||
let conditionSetView = applicableViews.find((viewProvider) => viewProvider.key === 'conditionSet.view');
|
||||
expect(conditionSetView).toBeDefined();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('the condition set usage for multiple display layout items', () => {
|
||||
@@ -722,4 +712,123 @@ describe('the plugin', function () {
|
||||
expect(result[2]).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('canView of ConditionSetViewProvider', () => {
|
||||
let conditionSetView;
|
||||
const testViewObject = {
|
||||
id: "test-object",
|
||||
type: "conditionSet",
|
||||
configuration: {
|
||||
conditionCollection: []
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
const applicableViews = openmct.objectViews.get(testViewObject, []);
|
||||
conditionSetView = applicableViews.find((viewProvider) => viewProvider.key === 'conditionSet.view');
|
||||
});
|
||||
|
||||
it('provides a view', () => {
|
||||
expect(conditionSetView).toBeDefined();
|
||||
});
|
||||
|
||||
it('returns true for type `conditionSet` and is a navigated Object', () => {
|
||||
openmct.router.isNavigatedObject = jasmine.createSpy().and.returnValue(true);
|
||||
|
||||
const isCanView = conditionSetView.canView(testViewObject, []);
|
||||
|
||||
expect(isCanView).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for type `conditionSet` and is not a navigated Object', () => {
|
||||
openmct.router.isNavigatedObject = jasmine.createSpy().and.returnValue(false);
|
||||
|
||||
const isCanView = conditionSetView.canView(testViewObject, []);
|
||||
|
||||
expect(isCanView).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for type `notConditionSet` and is a navigated Object', () => {
|
||||
openmct.router.isNavigatedObject = jasmine.createSpy().and.returnValue(true);
|
||||
testViewObject.type = 'notConditionSet';
|
||||
const isCanView = conditionSetView.canView(testViewObject, []);
|
||||
|
||||
expect(isCanView).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('The Style Rule Manager', () => {
|
||||
it('should subscribe to the conditionSet after the editor saves', async () => {
|
||||
const stylesObject = {
|
||||
"styles": [
|
||||
{
|
||||
"conditionId": "a8bf7d1a-c1bb-4fc7-936a-62056a51b5cd",
|
||||
"style": {
|
||||
"backgroundColor": "#38761d",
|
||||
"border": "",
|
||||
"color": "#073763",
|
||||
"isStyleInvisible": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"conditionId": "0558fa77-9bdc-4142-9f9a-7a28fe95182e",
|
||||
"style": {
|
||||
"backgroundColor": "#980000",
|
||||
"border": "",
|
||||
"color": "#ff9900",
|
||||
"isStyleInvisible": ""
|
||||
}
|
||||
}
|
||||
],
|
||||
"staticStyle": {
|
||||
"style": {
|
||||
"backgroundColor": "",
|
||||
"border": "",
|
||||
"color": ""
|
||||
}
|
||||
},
|
||||
"selectedConditionId": "0558fa77-9bdc-4142-9f9a-7a28fe95182e",
|
||||
"defaultConditionId": "0558fa77-9bdc-4142-9f9a-7a28fe95182e",
|
||||
"conditionSetIdentifier": {
|
||||
"namespace": "",
|
||||
"key": "035c589c-d98f-429e-8b89-d76bd8d22b29"
|
||||
}
|
||||
};
|
||||
openmct.$injector = jasmine.createSpyObj('$injector', ['get']);
|
||||
const mockTransactionService = jasmine.createSpyObj(
|
||||
'transactionService',
|
||||
['commit']
|
||||
);
|
||||
openmct.telemetry = jasmine.createSpyObj('telemetry', ['isTelemetryObject', "subscribe", "getMetadata", "getValueFormatter", "request"]);
|
||||
openmct.telemetry.isTelemetryObject.and.returnValue(true);
|
||||
openmct.telemetry.subscribe.and.returnValue(function () {});
|
||||
openmct.telemetry.getValueFormatter.and.returnValue({
|
||||
parse: function (value) {
|
||||
return value;
|
||||
}
|
||||
});
|
||||
openmct.telemetry.getMetadata.and.returnValue(testTelemetryObject.telemetry);
|
||||
openmct.telemetry.request.and.returnValue(Promise.resolve([]));
|
||||
|
||||
mockTransactionService.commit = async () => {};
|
||||
const mockIdentifierService = jasmine.createSpyObj(
|
||||
'identifierService',
|
||||
['parse']
|
||||
);
|
||||
mockIdentifierService.parse.and.returnValue({
|
||||
getSpace: () => {
|
||||
return '';
|
||||
}
|
||||
});
|
||||
|
||||
openmct.$injector = jasmine.createSpyObj('$injector', ['get']);
|
||||
openmct.$injector.get.withArgs('identifierService').and.returnValue(mockIdentifierService)
|
||||
.withArgs('transactionService').and.returnValue(mockTransactionService);
|
||||
|
||||
const styleRuleManger = new StyleRuleManager(stylesObject, openmct, null, true);
|
||||
spyOn(styleRuleManger, 'subscribeToConditionSet');
|
||||
await openmct.editor.save();
|
||||
expect(styleRuleManger.subscribeToConditionSet).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<template>
|
||||
<component :is="urlDefined ? 'a' : 'span'"
|
||||
class="c-condition-widget u-style-receiver js-style-receiver"
|
||||
:href="urlDefined ? internalDomainObject.url : null"
|
||||
:href="url"
|
||||
>
|
||||
<div class="c-condition-widget__label">
|
||||
{{ internalDomainObject.label }}
|
||||
@@ -32,6 +32,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const sanitizeUrl = require("@braintree/sanitize-url").sanitizeUrl;
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'domainObject'],
|
||||
data: function () {
|
||||
@@ -42,6 +44,9 @@ export default {
|
||||
computed: {
|
||||
urlDefined() {
|
||||
return this.internalDomainObject.url && this.internalDomainObject.url.length > 0;
|
||||
},
|
||||
url() {
|
||||
return this.urlDefined ? sanitizeUrl(this.internalDomainObject.url) : null;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
||||
@@ -101,7 +101,7 @@ export default {
|
||||
addChildren(domainObject) {
|
||||
let keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
|
||||
let metadata = this.openmct.telemetry.getMetadata(domainObject);
|
||||
let metadataWithFilters = metadata.valueMetadatas.filter(value => value.filters);
|
||||
let metadataWithFilters = metadata ? metadata.valueMetadatas.filter(value => value.filters) : [];
|
||||
let hasFiltersWithKeyString = this.persistedFilters[keyString] !== undefined;
|
||||
let mutateFilters = false;
|
||||
let childObject = {
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
'c-hyperlink--button' : isButton
|
||||
}"
|
||||
:target="domainObject.linkTarget"
|
||||
:href="domainObject.url"
|
||||
:href="url"
|
||||
>
|
||||
<span class="c-hyperlink__label">{{ domainObject.displayText }}</span>
|
||||
</a>
|
||||
@@ -35,6 +35,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const sanitizeUrl = require("@braintree/sanitize-url").sanitizeUrl;
|
||||
|
||||
export default {
|
||||
inject: ['domainObject'],
|
||||
@@ -45,6 +46,9 @@ export default {
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
url() {
|
||||
return sanitizeUrl(this.domainObject.url);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
77
src/plugins/imagery/ImageryTimestripViewProvider.js
Normal file
77
src/plugins/imagery/ImageryTimestripViewProvider.js
Normal file
@@ -0,0 +1,77 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import ImageryTimeView from './components/ImageryTimeView.vue';
|
||||
import Vue from "vue";
|
||||
|
||||
export default function ImageryTimestripViewProvider(openmct) {
|
||||
const type = 'example.imagery.time-strip.view';
|
||||
|
||||
function hasImageTelemetry(domainObject) {
|
||||
const metadata = openmct.telemetry.getMetadata(domainObject);
|
||||
if (!metadata) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return metadata.valuesForHints(['image']).length > 0;
|
||||
}
|
||||
|
||||
return {
|
||||
key: type,
|
||||
name: 'Imagery Timestrip View',
|
||||
cssClass: 'icon-image',
|
||||
canView: function (domainObject, objectPath) {
|
||||
let isChildOfTimeStrip = objectPath.find(object => object.type === 'time-strip');
|
||||
|
||||
return hasImageTelemetry(domainObject) && isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath);
|
||||
},
|
||||
view: function (domainObject, objectPath) {
|
||||
let component;
|
||||
|
||||
return {
|
||||
show: function (element) {
|
||||
component = new Vue({
|
||||
el: element,
|
||||
components: {
|
||||
ImageryTimeView
|
||||
},
|
||||
provide: {
|
||||
openmct: openmct,
|
||||
domainObject: domainObject,
|
||||
objectPath: objectPath
|
||||
},
|
||||
template: '<imagery-time-view></imagery-time-view>'
|
||||
|
||||
});
|
||||
},
|
||||
|
||||
destroy: function () {
|
||||
component.$destroy();
|
||||
component = undefined;
|
||||
},
|
||||
|
||||
getComponent() {
|
||||
return component;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import ImageryViewLayout from './components/ImageryViewLayout.vue';
|
||||
import ImageryViewComponent from './components/ImageryView.vue';
|
||||
|
||||
import Vue from 'vue';
|
||||
|
||||
@@ -10,19 +10,32 @@ export default class ImageryView {
|
||||
this.component = undefined;
|
||||
}
|
||||
|
||||
show(element) {
|
||||
show(element, isEditing, viewOptions) {
|
||||
let alternateObjectPath;
|
||||
let indexForFocusedImage;
|
||||
if (viewOptions) {
|
||||
indexForFocusedImage = viewOptions.indexForFocusedImage;
|
||||
alternateObjectPath = viewOptions.objectPath;
|
||||
}
|
||||
|
||||
this.component = new Vue({
|
||||
el: element,
|
||||
components: {
|
||||
ImageryViewLayout
|
||||
'imagery-view': ImageryViewComponent
|
||||
},
|
||||
provide: {
|
||||
openmct: this.openmct,
|
||||
domainObject: this.domainObject,
|
||||
objectPath: this.objectPath,
|
||||
objectPath: alternateObjectPath || this.objectPath,
|
||||
currentView: this
|
||||
},
|
||||
template: '<imagery-view-layout ref="ImageryLayout"></imagery-view-layout>'
|
||||
data() {
|
||||
return {
|
||||
indexForFocusedImage
|
||||
};
|
||||
},
|
||||
template: '<imagery-view :index-for-focused-image="indexForFocusedImage" ref="ImageryContainer"></imagery-view>'
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -37,8 +37,10 @@ export default function ImageryViewProvider(openmct) {
|
||||
key: type,
|
||||
name: 'Imagery Layout',
|
||||
cssClass: 'icon-image',
|
||||
canView: function (domainObject) {
|
||||
return hasImageTelemetry(domainObject);
|
||||
canView: function (domainObject, objectPath) {
|
||||
let isChildOfTimeStrip = objectPath.find(object => object.type === 'time-strip');
|
||||
|
||||
return hasImageTelemetry(domainObject) && (!isChildOfTimeStrip || openmct.router.isNavigatedObject(objectPath));
|
||||
},
|
||||
view: function (domainObject, objectPath) {
|
||||
return new ImageryView(openmct, domainObject, objectPath);
|
||||
|
||||
411
src/plugins/imagery/components/ImageryTimeView.vue
Normal file
411
src/plugins/imagery/components/ImageryTimeView.vue
Normal file
@@ -0,0 +1,411 @@
|
||||
<!--
|
||||
Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
as represented by the Administrator of the National Aeronautics and Space
|
||||
Administration. All rights reserved.
|
||||
|
||||
Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0.
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
License for the specific language governing permissions and limitations
|
||||
under the License.
|
||||
|
||||
Open MCT includes source code licensed under additional open source
|
||||
licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
this source code distribution or the Licensing information page available
|
||||
at runtime from the About dialog for additional information.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div ref="imagery"
|
||||
class="c-imagery-tsv c-timeline-holder"
|
||||
>
|
||||
<div ref="imageryHolder"
|
||||
class="c-imagery-tsv__contents u-contents"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as d3Scale from 'd3-scale';
|
||||
import SwimLane from "@/ui/components/swim-lane/SwimLane.vue";
|
||||
import Vue from "vue";
|
||||
import imageryData from "../../imagery/mixins/imageryData";
|
||||
import PreviewAction from "@/ui/preview/PreviewAction";
|
||||
import _ from "lodash";
|
||||
|
||||
const PADDING = 1;
|
||||
const ROW_HEIGHT = 100;
|
||||
const IMAGE_SIZE = 85;
|
||||
const IMAGE_WIDTH_THRESHOLD = 25;
|
||||
const CONTAINER_CLASS = 'c-imagery-tsv-container';
|
||||
const NO_ITEMS_CLASS = 'c-imagery-tsv__no-items';
|
||||
const IMAGE_WRAPPER_CLASS = 'c-imagery-tsv__image-wrapper';
|
||||
const ID_PREFIX = 'wrapper-';
|
||||
const IMAGE_ID_PREFIX = 'image-';
|
||||
|
||||
export default {
|
||||
mixins: [imageryData],
|
||||
inject: ['openmct', 'domainObject', 'objectPath'],
|
||||
data() {
|
||||
let timeSystem = this.openmct.time.timeSystem();
|
||||
this.metadata = {};
|
||||
this.requestCount = 0;
|
||||
|
||||
return {
|
||||
viewBounds: undefined,
|
||||
height: 0,
|
||||
durationFormatter: undefined,
|
||||
imageHistory: [],
|
||||
timeSystem: timeSystem,
|
||||
keyString: undefined
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
imageHistorySize() {
|
||||
return this.imageHistory.length;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
imageHistorySize(newSize, oldSize) {
|
||||
this.updatePlotImagery();
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.previewAction = new PreviewAction(this.openmct);
|
||||
|
||||
this.canvas = this.$refs.imagery.appendChild(document.createElement('canvas'));
|
||||
this.canvas.height = 0;
|
||||
this.canvasContext = this.canvas.getContext('2d');
|
||||
this.setDimensions();
|
||||
|
||||
this.setScaleAndPlotImagery = this.setScaleAndPlotImagery.bind(this);
|
||||
this.updateViewBounds = this.updateViewBounds.bind(this);
|
||||
this.setTimeContext = this.setTimeContext.bind(this);
|
||||
this.setTimeContext();
|
||||
|
||||
this.updateViewBounds();
|
||||
|
||||
this.resize = _.debounce(this.resize, 400);
|
||||
this.imageryStripResizeObserver = new ResizeObserver(this.resize);
|
||||
this.imageryStripResizeObserver.observe(this.$refs.imagery);
|
||||
|
||||
this.unlisten = this.openmct.objects.observe(this.domainObject, '*', this.observeForChanges);
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.imageryStripResizeObserver) {
|
||||
this.imageryStripResizeObserver.disconnect();
|
||||
}
|
||||
|
||||
this.stopFollowingTimeContext();
|
||||
if (this.unlisten) {
|
||||
this.unlisten();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setTimeContext() {
|
||||
this.stopFollowingTimeContext();
|
||||
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
|
||||
this.timeContext.on("timeSystem", this.setScaleAndPlotImagery);
|
||||
this.timeContext.on("bounds", this.updateViewBounds);
|
||||
this.timeContext.on("timeContext", this.setTimeContext);
|
||||
},
|
||||
stopFollowingTimeContext() {
|
||||
if (this.timeContext) {
|
||||
this.timeContext.off("timeSystem", this.setScaleAndPlotImagery);
|
||||
this.timeContext.off("bounds", this.updateViewBounds);
|
||||
this.timeContext.off("timeContext", this.setTimeContext);
|
||||
}
|
||||
},
|
||||
expand(index) {
|
||||
const path = this.objectPath[0];
|
||||
this.previewAction.invoke([path], {
|
||||
indexForFocusedImage: index,
|
||||
objectPath: this.objectPath
|
||||
});
|
||||
},
|
||||
observeForChanges(mutatedObject) {
|
||||
this.updateViewBounds();
|
||||
},
|
||||
resize() {
|
||||
let clientWidth = this.getClientWidth();
|
||||
if (clientWidth !== this.width) {
|
||||
this.setDimensions();
|
||||
this.updateViewBounds();
|
||||
}
|
||||
},
|
||||
getClientWidth() {
|
||||
let clientWidth = this.$refs.imagery.clientWidth;
|
||||
|
||||
if (!clientWidth) {
|
||||
//this is a hack - need a better way to find the parent of this component
|
||||
let parent = this.openmct.layout.$refs.browseObject.$el;
|
||||
if (parent) {
|
||||
clientWidth = parent.getBoundingClientRect().width;
|
||||
}
|
||||
}
|
||||
|
||||
return clientWidth;
|
||||
},
|
||||
updateViewBounds(bounds, isTick) {
|
||||
this.viewBounds = this.timeContext.bounds();
|
||||
|
||||
if (this.timeSystem === undefined) {
|
||||
this.timeSystem = this.timeContext.timeSystem();
|
||||
}
|
||||
|
||||
this.setScaleAndPlotImagery(this.timeSystem, !isTick);
|
||||
|
||||
},
|
||||
setScaleAndPlotImagery(timeSystem, clearAllImagery) {
|
||||
if (timeSystem !== undefined) {
|
||||
this.timeSystem = timeSystem;
|
||||
this.timeFormatter = this.getFormatter(this.timeSystem.key);
|
||||
}
|
||||
|
||||
this.setScale(this.timeSystem);
|
||||
this.updatePlotImagery(clearAllImagery);
|
||||
},
|
||||
getFormatter(key) {
|
||||
const metadata = this.openmct.telemetry.getMetadata(this.domainObject);
|
||||
|
||||
let metadataValue = metadata.value(key) || { format: key };
|
||||
let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);
|
||||
|
||||
return valueFormatter;
|
||||
},
|
||||
updatePlotImagery(clearAllImagery) {
|
||||
this.clearPreviousImagery(clearAllImagery);
|
||||
if (this.xScale) {
|
||||
this.drawImagery();
|
||||
}
|
||||
},
|
||||
clearPreviousImagery(clearAllImagery) {
|
||||
//TODO: Only clear items that are out of bounds
|
||||
let noItemsEl = this.$el.querySelectorAll(`.${NO_ITEMS_CLASS}`);
|
||||
noItemsEl.forEach(item => {
|
||||
item.remove();
|
||||
});
|
||||
let imagery = this.$el.querySelectorAll(`.${IMAGE_WRAPPER_CLASS}`);
|
||||
imagery.forEach(item => {
|
||||
if (clearAllImagery) {
|
||||
item.remove();
|
||||
} else {
|
||||
const id = item.getAttributeNS(null, 'id');
|
||||
if (id) {
|
||||
const timestamp = id.replace(ID_PREFIX, '');
|
||||
if (!this.isImageryInBounds({
|
||||
time: timestamp
|
||||
})) {
|
||||
item.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
setDimensions() {
|
||||
const imageryHolder = this.$refs.imagery;
|
||||
this.width = this.getClientWidth();
|
||||
|
||||
this.height = Math.round(imageryHolder.getBoundingClientRect().height);
|
||||
},
|
||||
setScale(timeSystem) {
|
||||
if (!this.width) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (timeSystem === undefined) {
|
||||
timeSystem = this.timeContext.timeSystem();
|
||||
}
|
||||
|
||||
if (timeSystem.isUTCBased) {
|
||||
this.xScale = d3Scale.scaleUtc();
|
||||
this.xScale.domain(
|
||||
[new Date(this.viewBounds.start), new Date(this.viewBounds.end)]
|
||||
);
|
||||
} else {
|
||||
this.xScale = d3Scale.scaleLinear();
|
||||
this.xScale.domain(
|
||||
[this.viewBounds.start, this.viewBounds.end]
|
||||
);
|
||||
}
|
||||
|
||||
this.xScale.range([PADDING, this.width - PADDING * 2]);
|
||||
},
|
||||
isImageryInBounds(imageObj) {
|
||||
return (imageObj.time <= this.viewBounds.end) && (imageObj.time >= this.viewBounds.start);
|
||||
},
|
||||
getImageryContainer() {
|
||||
let containerHeight = 100;
|
||||
let containerWidth = this.imageHistory.length ? this.width : 200;
|
||||
let imageryContainer;
|
||||
|
||||
let existingContainer = this.$el.querySelector(`.${CONTAINER_CLASS}`);
|
||||
if (existingContainer) {
|
||||
imageryContainer = existingContainer;
|
||||
imageryContainer.style.maxWidth = `${containerWidth}px`;
|
||||
} else {
|
||||
let component = new Vue({
|
||||
components: {
|
||||
SwimLane
|
||||
},
|
||||
provide: {
|
||||
openmct: this.openmct
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isNested: true
|
||||
};
|
||||
},
|
||||
template: `<swim-lane :is-nested="isNested" :hide-label="true"><template slot="object"><div class="c-imagery-tsv-container"></div></template></swim-lane>`
|
||||
});
|
||||
|
||||
this.$refs.imageryHolder.appendChild(component.$mount().$el);
|
||||
|
||||
imageryContainer = component.$el.querySelector(`.${CONTAINER_CLASS}`);
|
||||
imageryContainer.style.maxWidth = `${containerWidth}px`;
|
||||
imageryContainer.style.height = `${containerHeight}px`;
|
||||
}
|
||||
|
||||
return imageryContainer;
|
||||
},
|
||||
isImageryWidthAcceptable() {
|
||||
// We're calculating if there is enough space between images to show the thumbnails.
|
||||
// This algorithm could probably be enhanced to check the x co-ordinate distance between 2 consecutive images, but
|
||||
// we will go with this for now assuming imagery is not sorted by asc time so it's difficult to calculate.
|
||||
// TODO: Use telemetry.requestCollection to get sorted telemetry
|
||||
const currentStart = this.viewBounds.start;
|
||||
const currentEnd = this.viewBounds.end;
|
||||
const rectX = this.xScale(currentStart);
|
||||
const rectY = this.xScale(currentEnd);
|
||||
const imageContainerWidth = this.imageHistory.length ? (rectY - rectX) / this.imageHistory.length : 0;
|
||||
|
||||
return imageContainerWidth < IMAGE_WIDTH_THRESHOLD;
|
||||
},
|
||||
drawImagery() {
|
||||
let imageryContainer = this.getImageryContainer();
|
||||
const showImagePlaceholders = this.isImageryWidthAcceptable();
|
||||
let index = 0;
|
||||
if (this.imageHistory.length) {
|
||||
this.imageHistory.forEach((currentImageObject) => {
|
||||
if (this.isImageryInBounds(currentImageObject)) {
|
||||
this.plotImagery(currentImageObject, showImagePlaceholders, imageryContainer, index);
|
||||
index = index + 1;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.plotNoItems(imageryContainer);
|
||||
}
|
||||
},
|
||||
plotNoItems(containerElement) {
|
||||
let textElement = document.createElement('text');
|
||||
textElement.classList.add(NO_ITEMS_CLASS);
|
||||
textElement.innerHTML = 'No images within timeframe';
|
||||
|
||||
containerElement.appendChild(textElement);
|
||||
},
|
||||
setNSAttributesForElement(element, attributes) {
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object.keys(attributes).forEach((key) => {
|
||||
element.setAttributeNS(null, key, attributes[key]);
|
||||
});
|
||||
},
|
||||
setStyles(element, styles) {
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object.keys(styles).forEach((key) => {
|
||||
element.style[key] = styles[key];
|
||||
});
|
||||
},
|
||||
getImageWrapper(item) {
|
||||
const id = `${ID_PREFIX}${item.time}`;
|
||||
|
||||
return this.$el.querySelector(`.c-imagery-tsv__contents div[id=${id}]`);
|
||||
},
|
||||
plotImagery(item, showImagePlaceholders, containerElement, index) {
|
||||
let existingImageWrapper = this.getImageWrapper(item);
|
||||
//imageWrapper wraps the vertical tick and the image
|
||||
if (existingImageWrapper) {
|
||||
this.updateExistingImageWrapper(existingImageWrapper, item, showImagePlaceholders);
|
||||
} else {
|
||||
let imageWrapper = this.createImageWrapper(index, item, showImagePlaceholders);
|
||||
containerElement.appendChild(imageWrapper);
|
||||
}
|
||||
},
|
||||
setImageDisplay(imageElement, showImagePlaceholders) {
|
||||
if (showImagePlaceholders) {
|
||||
imageElement.style.display = 'none';
|
||||
} else {
|
||||
imageElement.style.display = 'block';
|
||||
}
|
||||
},
|
||||
updateExistingImageWrapper(existingImageWrapper, item, showImagePlaceholders) {
|
||||
//Update the x co-ordinates of the image wrapper and the url of image
|
||||
//this is to avoid tearing down all elements completely and re-drawing them
|
||||
this.setNSAttributesForElement(existingImageWrapper, {
|
||||
'data-show-image-placeholders': showImagePlaceholders
|
||||
});
|
||||
existingImageWrapper.style.left = `${this.xScale(item.time)}px`;
|
||||
|
||||
let imageElement = existingImageWrapper.querySelector('img');
|
||||
this.setNSAttributesForElement(imageElement, {
|
||||
src: item.url
|
||||
});
|
||||
this.setImageDisplay(imageElement, showImagePlaceholders);
|
||||
},
|
||||
createImageWrapper(index, item, showImagePlaceholders) {
|
||||
const id = `${ID_PREFIX}${item.time}`;
|
||||
let imageWrapper = document.createElement('div');
|
||||
imageWrapper.classList.add(IMAGE_WRAPPER_CLASS);
|
||||
imageWrapper.style.left = `${this.xScale(item.time)}px`;
|
||||
this.setNSAttributesForElement(imageWrapper, {
|
||||
id,
|
||||
'data-show-image-placeholders': showImagePlaceholders
|
||||
});
|
||||
//create image vertical tick indicator
|
||||
let imageTickElement = document.createElement('div');
|
||||
imageTickElement.classList.add('c-imagery-tsv__image-handle');
|
||||
imageTickElement.style.width = '2px';
|
||||
imageTickElement.style.height = `${String(ROW_HEIGHT - 10)}px`;
|
||||
imageWrapper.appendChild(imageTickElement);
|
||||
|
||||
//create placeholder - this will also hold the actual image
|
||||
let imagePlaceholder = document.createElement('div');
|
||||
imagePlaceholder.classList.add('c-imagery-tsv__image-placeholder');
|
||||
imagePlaceholder.style.width = `${IMAGE_SIZE}px`;
|
||||
imagePlaceholder.style.height = `${IMAGE_SIZE}px`;
|
||||
imageWrapper.appendChild(imagePlaceholder);
|
||||
|
||||
//create image element
|
||||
let imageElement = document.createElement('img');
|
||||
this.setNSAttributesForElement(imageElement, {
|
||||
src: item.url
|
||||
});
|
||||
imageElement.style.width = `${IMAGE_SIZE}px`;
|
||||
imageElement.style.height = `${IMAGE_SIZE}px`;
|
||||
this.setImageDisplay(imageElement, showImagePlaceholders);
|
||||
|
||||
//handle mousedown event to show the image in a large view
|
||||
imageWrapper.addEventListener('mousedown', (e) => {
|
||||
if (e.button === 0) {
|
||||
this.expand(index);
|
||||
}
|
||||
});
|
||||
|
||||
imagePlaceholder.appendChild(imageElement);
|
||||
|
||||
return imageWrapper;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -57,7 +57,7 @@
|
||||
</div>
|
||||
<div ref="imageBG"
|
||||
class="c-imagery__main-image__bg"
|
||||
:class="{'paused unnsynced': isPaused,'stale':false }"
|
||||
:class="{'paused unnsynced': isPaused && !isFixed,'stale':false }"
|
||||
@click="expand"
|
||||
>
|
||||
<div class="image-wrapper"
|
||||
@@ -122,6 +122,7 @@
|
||||
</div>
|
||||
<div class="h-local-controls">
|
||||
<button
|
||||
v-if="!isFixed"
|
||||
class="c-button icon-pause pause-play"
|
||||
:class="{'is-paused': isPaused}"
|
||||
@click="paused(!isPaused, 'button')"
|
||||
@@ -129,12 +130,11 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c-imagery__thumbs-wrapper"
|
||||
:class="[
|
||||
{ 'is-paused': isPaused },
|
||||
{ 'is-autoscroll-off': !resizingWindow && !autoScroll && !isPaused }
|
||||
]"
|
||||
<div class="c-imagery__thumbs-wrapper"
|
||||
:class="[
|
||||
{ 'is-paused': isPaused && !isFixed },
|
||||
{ 'is-autoscroll-off': !resizingWindow && !autoScroll && !isPaused }
|
||||
]"
|
||||
>
|
||||
<div
|
||||
ref="thumbsWrapper"
|
||||
@@ -175,7 +175,8 @@ import moment from 'moment';
|
||||
import RelatedTelemetry from './RelatedTelemetry/RelatedTelemetry';
|
||||
import Compass from './Compass/Compass.vue';
|
||||
|
||||
const DEFAULT_DURATION_FORMATTER = 'duration';
|
||||
import imageryData from "../../imagery/mixins/imageryData";
|
||||
|
||||
const REFRESH_CSS_MS = 500;
|
||||
const DURATION_TRACK_MS = 1000;
|
||||
const ARROW_DOWN_DELAY_CHECK_MS = 400;
|
||||
@@ -197,40 +198,51 @@ export default {
|
||||
components: {
|
||||
Compass
|
||||
},
|
||||
mixins: [imageryData],
|
||||
inject: ['openmct', 'domainObject', 'objectPath', 'currentView'],
|
||||
props: {
|
||||
indexForFocusedImage: {
|
||||
type: Number,
|
||||
default() {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
let timeSystem = this.openmct.time.timeSystem();
|
||||
this.metadata = {};
|
||||
this.requestCount = 0;
|
||||
|
||||
return {
|
||||
autoScroll: true,
|
||||
durationFormatter: undefined,
|
||||
imageHistory: [],
|
||||
timeSystem: timeSystem,
|
||||
keyString: undefined,
|
||||
autoScroll: true,
|
||||
filters: {
|
||||
brightness: 100,
|
||||
contrast: 100
|
||||
},
|
||||
imageHistory: [],
|
||||
thumbnailClick: THUMBNAIL_CLICKED,
|
||||
isPaused: false,
|
||||
metadata: {},
|
||||
requestCount: 0,
|
||||
timeSystem: timeSystem,
|
||||
timeFormatter: undefined,
|
||||
refreshCSS: false,
|
||||
keyString: undefined,
|
||||
focusedImageIndex: undefined,
|
||||
focusedImageRelatedTelemetry: {},
|
||||
numericDuration: undefined,
|
||||
metadataEndpoints: {},
|
||||
relatedTelemetry: {},
|
||||
latestRelatedTelemetry: {},
|
||||
focusedImageNaturalAspectRatio: undefined,
|
||||
imageContainerWidth: undefined,
|
||||
imageContainerHeight: undefined,
|
||||
lockCompass: true,
|
||||
resizingWindow: false
|
||||
resizingWindow: false,
|
||||
timeContext: undefined
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
imageHistorySize() {
|
||||
return this.imageHistory.length;
|
||||
},
|
||||
compassRoseSizingClasses() {
|
||||
let compassRoseSizingClasses = '';
|
||||
if (this.sizedImageDimensions.width < 300) {
|
||||
@@ -256,10 +268,14 @@ export default {
|
||||
return age < cutoff && !this.refreshCSS;
|
||||
},
|
||||
canTrackDuration() {
|
||||
return this.openmct.time.clock() && this.timeSystem.isUTCBased;
|
||||
},
|
||||
focusedImageDownloadName() {
|
||||
return this.getImageDownloadName(this.focusedImage);
|
||||
let hasClock;
|
||||
if (this.timeContext) {
|
||||
hasClock = this.timeContext.clock();
|
||||
} else {
|
||||
hasClock = this.openmct.time.clock();
|
||||
}
|
||||
|
||||
return hasClock && this.timeSystem.isUTCBased;
|
||||
},
|
||||
isNextDisabled() {
|
||||
let disabled = false;
|
||||
@@ -380,9 +396,30 @@ export default {
|
||||
}
|
||||
|
||||
return sizedImageDimensions;
|
||||
},
|
||||
isFixed() {
|
||||
let clock;
|
||||
if (this.timeContext) {
|
||||
clock = this.timeContext.clock();
|
||||
} else {
|
||||
clock = this.openmct.time.clock();
|
||||
}
|
||||
|
||||
return clock === undefined;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
imageHistorySize(newSize, oldSize) {
|
||||
let imageIndex;
|
||||
if (this.indexForFocusedImage !== undefined) {
|
||||
imageIndex = this.initFocusedImageIndex;
|
||||
} else {
|
||||
imageIndex = newSize - 1;
|
||||
}
|
||||
|
||||
this.setFocusedImage(imageIndex, false);
|
||||
this.scrollToRight();
|
||||
},
|
||||
focusedImageIndex() {
|
||||
this.trackDuration();
|
||||
this.resetAgeCSS();
|
||||
@@ -391,18 +428,14 @@ export default {
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
// listen
|
||||
this.openmct.time.on('bounds', this.boundsChange);
|
||||
this.openmct.time.on('timeSystem', this.timeSystemChange);
|
||||
this.openmct.time.on('clock', this.clockChange);
|
||||
//We only need to use this till the user focuses an image manually
|
||||
if (this.indexForFocusedImage !== undefined) {
|
||||
this.initFocusedImageIndex = this.indexForFocusedImage;
|
||||
this.isPaused = true;
|
||||
}
|
||||
|
||||
// set
|
||||
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
|
||||
this.imageHints = { ...this.metadata.valuesForHints(['image'])[0] };
|
||||
this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
|
||||
this.imageFormatter = this.openmct.telemetry.getValueFormatter(this.imageHints);
|
||||
this.imageDownloadNameHints = { ...this.metadata.valuesForHints(['imageDownloadName'])[0]};
|
||||
this.setTimeContext = this.setTimeContext.bind(this);
|
||||
this.setTimeContext();
|
||||
|
||||
// related telemetry keys
|
||||
this.spacecraftPositionKeys = ['positionX', 'positionY', 'positionZ'];
|
||||
@@ -410,56 +443,48 @@ export default {
|
||||
this.cameraKeys = ['cameraPan', 'cameraTilt'];
|
||||
this.sunKeys = ['sunOrientation'];
|
||||
|
||||
// initialize
|
||||
this.timeKey = this.timeSystem.key;
|
||||
this.timeFormatter = this.getFormatter(this.timeKey);
|
||||
|
||||
// kickoff
|
||||
this.subscribe();
|
||||
this.requestHistory();
|
||||
|
||||
// related telemetry
|
||||
await this.initializeRelatedTelemetry();
|
||||
this.updateRelatedTelemetryForFocusedImage();
|
||||
await this.updateRelatedTelemetryForFocusedImage();
|
||||
this.trackLatestRelatedTelemetry();
|
||||
|
||||
// for scrolling through images quickly and resizing the object view
|
||||
_.debounce(this.updateRelatedTelemetryForFocusedImage, 400);
|
||||
_.debounce(this.resizeImageContainer, 400);
|
||||
this.updateRelatedTelemetryForFocusedImage = _.debounce(this.updateRelatedTelemetryForFocusedImage, 400);
|
||||
|
||||
this.imageContainerResizeObserver = new ResizeObserver(this.resizeImageContainer);
|
||||
this.imageContainerResizeObserver.observe(this.$refs.imageBG);
|
||||
// for resizing the object view
|
||||
this.resizeImageContainer = _.debounce(this.resizeImageContainer, 400);
|
||||
|
||||
if (this.$refs.imageBG) {
|
||||
this.imageContainerResizeObserver = new ResizeObserver(this.resizeImageContainer);
|
||||
this.imageContainerResizeObserver.observe(this.$refs.imageBG);
|
||||
}
|
||||
|
||||
// For adjusting scroll bar size and position when resizing thumbs wrapper
|
||||
this.handleScroll = _.debounce(this.handleScroll, SCROLL_LATENCY);
|
||||
this.handleThumbWindowResizeEnded = _.debounce(this.handleThumbWindowResizeEnded, SCROLL_LATENCY);
|
||||
this.handleThumbWindowResizeStart = _.debounce(this.handleThumbWindowResizeStart, SCROLL_LATENCY);
|
||||
|
||||
if (this.$refs.thumbsWrapper) {
|
||||
this.thumbWrapperResizeObserver = new ResizeObserver(this.handleThumbWindowResizeStart);
|
||||
this.thumbWrapperResizeObserver.observe(this.$refs.thumbsWrapper);
|
||||
}
|
||||
|
||||
this.thumbWrapperResizeObserver = new ResizeObserver(this.handleThumbWindowResizeStart);
|
||||
this.thumbWrapperResizeObserver.observe(this.$refs.thumbsWrapper);
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.unsubscribe) {
|
||||
this.unsubscribe();
|
||||
delete this.unsubscribe;
|
||||
this.stopFollowingTimeContext();
|
||||
|
||||
if (this.thumbWrapperResizeObserver) {
|
||||
this.thumbWrapperResizeObserver.disconnect();
|
||||
}
|
||||
|
||||
if (this.imageContainerResizeObserver) {
|
||||
this.imageContainerResizeObserver.disconnect();
|
||||
}
|
||||
|
||||
if (this.thumbWrapperResizeObserver) {
|
||||
this.thumbWrapperResizeObserver.disconnect();
|
||||
}
|
||||
|
||||
if (this.relatedTelemetry.hasRelatedTelemetry) {
|
||||
this.relatedTelemetry.destroy();
|
||||
}
|
||||
|
||||
this.stopDurationTracking();
|
||||
this.openmct.time.off('bounds', this.boundsChange);
|
||||
this.openmct.time.off('timeSystem', this.timeSystemChange);
|
||||
this.openmct.time.off('clock', this.clockChange);
|
||||
|
||||
// unsubscribe from related telemetry
|
||||
if (this.relatedTelemetry.hasRelatedTelemetry) {
|
||||
for (let key of this.relatedTelemetry.keys) {
|
||||
@@ -470,6 +495,21 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setTimeContext() {
|
||||
this.stopFollowingTimeContext();
|
||||
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
|
||||
//listen
|
||||
this.timeContext.on('timeSystem', this.trackDuration);
|
||||
this.timeContext.on('clock', this.trackDuration);
|
||||
this.timeContext.on("timeContext", this.setTimeContext);
|
||||
},
|
||||
stopFollowingTimeContext() {
|
||||
if (this.timeContext) {
|
||||
this.timeContext.off("timeSystem", this.trackDuration);
|
||||
this.timeContext.off("clock", this.trackDuration);
|
||||
this.timeContext.off("timeContext", this.setTimeContext);
|
||||
}
|
||||
},
|
||||
expand() {
|
||||
const actionCollection = this.openmct.actions.getActionsCollection(this.objectPath, this.currentView);
|
||||
const visibleActions = actionCollection.getVisibleActions();
|
||||
@@ -576,56 +616,6 @@ export default {
|
||||
focusElement() {
|
||||
this.$el.focus();
|
||||
},
|
||||
datumIsNotValid(datum) {
|
||||
if (this.imageHistory.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const datumURL = this.formatImageUrl(datum);
|
||||
const lastHistoryURL = this.formatImageUrl(this.imageHistory.slice(-1)[0]);
|
||||
|
||||
// datum is not valid if it matches the last datum in history,
|
||||
// or it is before the last datum in the history
|
||||
const datumTimeCheck = this.parseTime(datum);
|
||||
const historyTimeCheck = this.parseTime(this.imageHistory.slice(-1)[0]);
|
||||
const matchesLast = (datumTimeCheck === historyTimeCheck) && (datumURL === lastHistoryURL);
|
||||
const isStale = datumTimeCheck < historyTimeCheck;
|
||||
|
||||
return matchesLast || isStale;
|
||||
},
|
||||
formatImageUrl(datum) {
|
||||
if (!datum) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.imageFormatter.format(datum);
|
||||
},
|
||||
formatTime(datum) {
|
||||
if (!datum) {
|
||||
return;
|
||||
}
|
||||
|
||||
let dateTimeStr = this.timeFormatter.format(datum);
|
||||
|
||||
// Replace ISO "T" with a space to allow wrapping
|
||||
return dateTimeStr.replace("T", " ");
|
||||
},
|
||||
getImageDownloadName(datum) {
|
||||
let imageDownloadName = '';
|
||||
if (datum) {
|
||||
const key = this.imageDownloadNameHints.key;
|
||||
imageDownloadName = datum[key];
|
||||
}
|
||||
|
||||
return imageDownloadName;
|
||||
},
|
||||
parseTime(datum) {
|
||||
if (!datum) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.timeFormatter.parse(datum);
|
||||
},
|
||||
handleScroll() {
|
||||
const thumbsWrapper = this.$refs.thumbsWrapper;
|
||||
if (!thumbsWrapper || this.resizingWindow) {
|
||||
@@ -681,8 +671,17 @@ export default {
|
||||
});
|
||||
},
|
||||
setFocusedImage(index, thumbnailClick = false) {
|
||||
if (this.isPaused && !thumbnailClick) {
|
||||
if (thumbnailClick) {
|
||||
//We use the props till the user changes what they want to see
|
||||
this.initFocusedImageIndex = undefined;
|
||||
}
|
||||
|
||||
if (this.isPaused && !thumbnailClick && this.initFocusedImageIndex === undefined) {
|
||||
this.nextImageIndex = index;
|
||||
//this could happen if bounds changes
|
||||
if (this.focusedImageIndex > this.imageHistory.length - 1) {
|
||||
this.focusedImageIndex = index;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -693,70 +692,6 @@ export default {
|
||||
this.paused(true);
|
||||
}
|
||||
},
|
||||
boundsChange(bounds, isTick) {
|
||||
if (!isTick) {
|
||||
this.requestHistory();
|
||||
}
|
||||
},
|
||||
async requestHistory() {
|
||||
let bounds = this.openmct.time.bounds();
|
||||
this.requestCount++;
|
||||
const requestId = this.requestCount;
|
||||
this.imageHistory = [];
|
||||
|
||||
let data = await this.openmct.telemetry
|
||||
.request(this.domainObject, bounds) || [];
|
||||
|
||||
if (this.requestCount === requestId) {
|
||||
data.forEach((datum, index) => {
|
||||
this.updateHistory(datum, index === data.length - 1);
|
||||
});
|
||||
}
|
||||
},
|
||||
timeSystemChange(system) {
|
||||
this.timeSystem = this.openmct.time.timeSystem();
|
||||
this.timeKey = this.timeSystem.key;
|
||||
this.timeFormatter = this.getFormatter(this.timeKey);
|
||||
this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
|
||||
this.trackDuration();
|
||||
},
|
||||
clockChange(clock) {
|
||||
this.trackDuration();
|
||||
},
|
||||
subscribe() {
|
||||
this.unsubscribe = this.openmct.telemetry
|
||||
.subscribe(this.domainObject, (datum) => {
|
||||
let parsedTimestamp = this.parseTime(datum);
|
||||
let bounds = this.openmct.time.bounds();
|
||||
|
||||
if (parsedTimestamp >= bounds.start && parsedTimestamp <= bounds.end) {
|
||||
this.updateHistory(datum);
|
||||
}
|
||||
});
|
||||
},
|
||||
updateHistory(datum, setFocused = true) {
|
||||
if (this.datumIsNotValid(datum)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let image = { ...datum };
|
||||
image.formattedTime = this.formatTime(datum);
|
||||
image.url = this.formatImageUrl(datum);
|
||||
image.time = datum[this.timeKey];
|
||||
image.imageDownloadName = this.getImageDownloadName(datum);
|
||||
|
||||
this.imageHistory.push(image);
|
||||
if (setFocused) {
|
||||
this.setFocusedImage(this.imageHistory.length - 1);
|
||||
this.scrollToRight();
|
||||
}
|
||||
},
|
||||
getFormatter(key) {
|
||||
let metadataValue = this.metadata.value(key) || { format: key };
|
||||
let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);
|
||||
|
||||
return valueFormatter;
|
||||
},
|
||||
trackDuration() {
|
||||
if (this.canTrackDuration) {
|
||||
this.stopDurationTracking();
|
||||
@@ -772,8 +707,12 @@ export default {
|
||||
window.clearInterval(this.durationTracker);
|
||||
},
|
||||
updateDuration() {
|
||||
let currentTime = this.openmct.time.clock() && this.openmct.time.clock().currentValue();
|
||||
this.numericDuration = currentTime - this.parsedSelectedTime;
|
||||
let currentTime = this.timeContext.clock() && this.timeContext.clock().currentValue();
|
||||
if (currentTime === undefined) {
|
||||
this.numericDuration = currentTime;
|
||||
} else {
|
||||
this.numericDuration = currentTime - this.parsedSelectedTime;
|
||||
}
|
||||
},
|
||||
resetAgeCSS() {
|
||||
this.refreshCSS = true;
|
||||
@@ -876,6 +815,10 @@ export default {
|
||||
}, { once: true });
|
||||
},
|
||||
resizeImageContainer() {
|
||||
if (!this.$refs.imageBG) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.$refs.imageBG.clientWidth !== this.imageContainerWidth) {
|
||||
this.imageContainerWidth = this.$refs.imageBG.clientWidth;
|
||||
}
|
||||
@@ -312,3 +312,54 @@
|
||||
@include cArrowButtonSizing($dimOuter: 32px);
|
||||
}
|
||||
}
|
||||
|
||||
/*************************************** IMAGERY IN TIMESTRIP VIEWS */
|
||||
.c-imagery-tsv {
|
||||
div.c-imagery-tsv__image-wrapper {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
display: flex;
|
||||
z-index: 1;
|
||||
margin-top: 5px;
|
||||
|
||||
img {
|
||||
align-self: flex-end;
|
||||
}
|
||||
&:hover {
|
||||
z-index: 2;
|
||||
|
||||
filter: brightness(1) contrast(1) !important;
|
||||
[class*='__image-handle'] {
|
||||
background-color: $colorBodyFg;
|
||||
}
|
||||
|
||||
//[class*='__image-placeholder'] {
|
||||
// display: none;
|
||||
//}
|
||||
|
||||
img {
|
||||
display: block !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__no-items {
|
||||
fill: $colorBodyFg !important;
|
||||
}
|
||||
|
||||
&__image-handle {
|
||||
background-color: rgba($colorBodyFg, 0.5);
|
||||
}
|
||||
|
||||
&__image-placeholder {
|
||||
background-color: pushBack($colorBodyBg, 0.3);
|
||||
display: block;
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
&:hover div.c-imagery-tsv__image-wrapper {
|
||||
// TODO CH: convert to theme constants
|
||||
filter: brightness(0.5) contrast(0.7);
|
||||
}
|
||||
}
|
||||
190
src/plugins/imagery/mixins/imageryData.js
Normal file
190
src/plugins/imagery/mixins/imageryData.js
Normal file
@@ -0,0 +1,190 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
const DEFAULT_DURATION_FORMATTER = 'duration';
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'domainObject', 'objectPath'],
|
||||
mounted() {
|
||||
// listen
|
||||
this.boundsChange = this.boundsChange.bind(this);
|
||||
this.timeSystemChange = this.timeSystemChange.bind(this);
|
||||
this.setDataTimeContext = this.setDataTimeContext.bind(this);
|
||||
this.setDataTimeContext();
|
||||
|
||||
// set
|
||||
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
|
||||
this.imageHints = { ...this.metadata.valuesForHints(['image'])[0] };
|
||||
this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
|
||||
this.imageFormatter = this.openmct.telemetry.getValueFormatter(this.imageHints);
|
||||
this.imageDownloadNameHints = { ...this.metadata.valuesForHints(['imageDownloadName'])[0]};
|
||||
|
||||
// initialize
|
||||
this.timeKey = this.timeSystem.key;
|
||||
this.timeFormatter = this.getFormatter(this.timeKey);
|
||||
|
||||
// kickoff
|
||||
this.subscribe();
|
||||
this.requestHistory();
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.unsubscribe) {
|
||||
this.unsubscribe();
|
||||
delete this.unsubscribe;
|
||||
}
|
||||
|
||||
this.stopFollowingDataTimeContext();
|
||||
},
|
||||
methods: {
|
||||
setDataTimeContext() {
|
||||
this.stopFollowingDataTimeContext();
|
||||
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
|
||||
this.timeContext.on('bounds', this.boundsChange);
|
||||
this.boundsChange(this.timeContext.bounds());
|
||||
this.timeContext.on('timeSystem', this.timeSystemChange);
|
||||
this.timeContext.on("timeContext", this.setDataTimeContext);
|
||||
},
|
||||
stopFollowingDataTimeContext() {
|
||||
if (this.timeContext) {
|
||||
this.timeContext.off('bounds', this.boundsChange);
|
||||
this.timeContext.off('timeSystem', this.timeSystemChange);
|
||||
this.timeContext.off("timeContext", this.setDataTimeContext);
|
||||
}
|
||||
},
|
||||
datumIsNotValid(datum) {
|
||||
if (this.imageHistory.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const datumURL = this.formatImageUrl(datum);
|
||||
const lastHistoryURL = this.formatImageUrl(this.imageHistory.slice(-1)[0]);
|
||||
|
||||
// datum is not valid if it matches the last datum in history,
|
||||
// or it is before the last datum in the history
|
||||
const datumTimeCheck = this.parseTime(datum);
|
||||
const historyTimeCheck = this.parseTime(this.imageHistory.slice(-1)[0]);
|
||||
const matchesLast = (datumTimeCheck === historyTimeCheck) && (datumURL === lastHistoryURL);
|
||||
const isStale = datumTimeCheck < historyTimeCheck;
|
||||
|
||||
return matchesLast || isStale;
|
||||
},
|
||||
formatImageUrl(datum) {
|
||||
if (!datum) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.imageFormatter.format(datum);
|
||||
},
|
||||
formatTime(datum) {
|
||||
if (!datum) {
|
||||
return;
|
||||
}
|
||||
|
||||
let dateTimeStr = this.timeFormatter.format(datum);
|
||||
|
||||
// Replace ISO "T" with a space to allow wrapping
|
||||
return dateTimeStr.replace("T", " ");
|
||||
},
|
||||
getImageDownloadName(datum) {
|
||||
let imageDownloadName = '';
|
||||
if (datum) {
|
||||
const key = this.imageDownloadNameHints.key;
|
||||
imageDownloadName = datum[key];
|
||||
}
|
||||
|
||||
return imageDownloadName;
|
||||
},
|
||||
parseTime(datum) {
|
||||
if (!datum) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.timeFormatter.parse(datum);
|
||||
},
|
||||
boundsChange(bounds, isTick) {
|
||||
if (!isTick) {
|
||||
this.requestHistory();
|
||||
}
|
||||
},
|
||||
async requestHistory() {
|
||||
let bounds = this.timeContext.bounds();
|
||||
this.requestCount++;
|
||||
const requestId = this.requestCount;
|
||||
this.imageHistory = [];
|
||||
|
||||
let data = await this.openmct.telemetry
|
||||
.request(this.domainObject, bounds) || [];
|
||||
|
||||
if (this.requestCount === requestId) {
|
||||
let imagery = [];
|
||||
data.forEach((datum) => {
|
||||
let image = this.normalizeDatum(datum);
|
||||
if (image) {
|
||||
imagery.push(image);
|
||||
}
|
||||
});
|
||||
//this is to optimize anything that reacts to imageHistory length
|
||||
this.imageHistory = imagery;
|
||||
}
|
||||
},
|
||||
timeSystemChange() {
|
||||
this.timeSystem = this.timeContext.timeSystem();
|
||||
this.timeKey = this.timeSystem.key;
|
||||
this.timeFormatter = this.getFormatter(this.timeKey);
|
||||
this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
|
||||
},
|
||||
subscribe() {
|
||||
this.unsubscribe = this.openmct.telemetry
|
||||
.subscribe(this.domainObject, (datum) => {
|
||||
let parsedTimestamp = this.parseTime(datum);
|
||||
let bounds = this.timeContext.bounds();
|
||||
|
||||
if (parsedTimestamp >= bounds.start && parsedTimestamp <= bounds.end) {
|
||||
let image = this.normalizeDatum(datum);
|
||||
if (image) {
|
||||
this.imageHistory.push(image);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
normalizeDatum(datum) {
|
||||
if (this.datumIsNotValid(datum)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let image = { ...datum };
|
||||
image.formattedTime = this.formatTime(datum);
|
||||
image.url = this.formatImageUrl(datum);
|
||||
image.time = this.parseTime(image.formattedTime);
|
||||
image.imageDownloadName = this.getImageDownloadName(datum);
|
||||
|
||||
return image;
|
||||
},
|
||||
getFormatter(key) {
|
||||
let metadataValue = this.metadata.value(key) || { format: key };
|
||||
let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);
|
||||
|
||||
return valueFormatter;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -21,10 +21,12 @@
|
||||
*****************************************************************************/
|
||||
|
||||
import ImageryViewProvider from './ImageryViewProvider';
|
||||
import ImageryTimestripViewProvider from './ImageryTimestripViewProvider';
|
||||
|
||||
export default function () {
|
||||
return function install(openmct) {
|
||||
openmct.objectViews.addProvider(new ImageryViewProvider(openmct));
|
||||
openmct.objectViews.addProvider(new ImageryTimestripViewProvider(openmct));
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
import Vue from 'vue';
|
||||
import {
|
||||
createMouseEvent,
|
||||
createOpenMct,
|
||||
resetApplicationState,
|
||||
simulateKeyEvent
|
||||
@@ -32,19 +33,6 @@ 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;
|
||||
|
||||
function comparisonFunction(valueOne, valueTwo) {
|
||||
let larger = valueOne;
|
||||
let smaller = valueTwo;
|
||||
|
||||
if (larger < smaller) {
|
||||
larger = valueTwo;
|
||||
smaller = valueOne;
|
||||
}
|
||||
|
||||
return (larger - smaller) < TOLERANCE;
|
||||
}
|
||||
|
||||
function getImageInfo(doc) {
|
||||
let imageElement = doc.querySelectorAll(MAIN_IMAGE_CLASS)[0];
|
||||
@@ -84,15 +72,19 @@ function generateTelemetry(start, count) {
|
||||
return telemetry;
|
||||
}
|
||||
|
||||
describe("The Imagery View Layout", () => {
|
||||
describe("The Imagery View Layouts", () => {
|
||||
const imageryKey = 'example.imagery';
|
||||
const imageryForTimeStripKey = 'example.imagery.time-strip.view';
|
||||
const START = Date.now();
|
||||
const COUNT = 10;
|
||||
|
||||
let resolveFunction;
|
||||
// let resolveFunction;
|
||||
let originalRouterPath;
|
||||
let telemetryPromise;
|
||||
let telemetryPromiseResolve;
|
||||
let cleanupFirst;
|
||||
|
||||
let openmct;
|
||||
let appHolder;
|
||||
let parent;
|
||||
let child;
|
||||
let imageTelemetry = generateTelemetry(START - TEN_MINUTES, COUNT);
|
||||
@@ -116,51 +108,51 @@ describe("The Imagery View Layout", () => {
|
||||
"image": 1,
|
||||
"priority": 3
|
||||
},
|
||||
"source": "url",
|
||||
"relatedTelemetry": {
|
||||
"heading": {
|
||||
"comparisonFunction": comparisonFunction,
|
||||
"historical": {
|
||||
"telemetryObjectId": "heading",
|
||||
"valueKey": "value"
|
||||
}
|
||||
},
|
||||
"roll": {
|
||||
"comparisonFunction": comparisonFunction,
|
||||
"historical": {
|
||||
"telemetryObjectId": "roll",
|
||||
"valueKey": "value"
|
||||
}
|
||||
},
|
||||
"pitch": {
|
||||
"comparisonFunction": comparisonFunction,
|
||||
"historical": {
|
||||
"telemetryObjectId": "pitch",
|
||||
"valueKey": "value"
|
||||
}
|
||||
},
|
||||
"cameraPan": {
|
||||
"comparisonFunction": comparisonFunction,
|
||||
"historical": {
|
||||
"telemetryObjectId": "cameraPan",
|
||||
"valueKey": "value"
|
||||
}
|
||||
},
|
||||
"cameraTilt": {
|
||||
"comparisonFunction": comparisonFunction,
|
||||
"historical": {
|
||||
"telemetryObjectId": "cameraTilt",
|
||||
"valueKey": "value"
|
||||
}
|
||||
},
|
||||
"sunOrientation": {
|
||||
"comparisonFunction": comparisonFunction,
|
||||
"historical": {
|
||||
"telemetryObjectId": "sunOrientation",
|
||||
"valueKey": "value"
|
||||
}
|
||||
}
|
||||
}
|
||||
"source": "url"
|
||||
// "relatedTelemetry": {
|
||||
// "heading": {
|
||||
// "comparisonFunction": comparisonFunction,
|
||||
// "historical": {
|
||||
// "telemetryObjectId": "heading",
|
||||
// "valueKey": "value"
|
||||
// }
|
||||
// },
|
||||
// "roll": {
|
||||
// "comparisonFunction": comparisonFunction,
|
||||
// "historical": {
|
||||
// "telemetryObjectId": "roll",
|
||||
// "valueKey": "value"
|
||||
// }
|
||||
// },
|
||||
// "pitch": {
|
||||
// "comparisonFunction": comparisonFunction,
|
||||
// "historical": {
|
||||
// "telemetryObjectId": "pitch",
|
||||
// "valueKey": "value"
|
||||
// }
|
||||
// },
|
||||
// "cameraPan": {
|
||||
// "comparisonFunction": comparisonFunction,
|
||||
// "historical": {
|
||||
// "telemetryObjectId": "cameraPan",
|
||||
// "valueKey": "value"
|
||||
// }
|
||||
// },
|
||||
// "cameraTilt": {
|
||||
// "comparisonFunction": comparisonFunction,
|
||||
// "historical": {
|
||||
// "telemetryObjectId": "cameraTilt",
|
||||
// "valueKey": "value"
|
||||
// }
|
||||
// },
|
||||
// "sunOrientation": {
|
||||
// "comparisonFunction": comparisonFunction,
|
||||
// "historical": {
|
||||
// "telemetryObjectId": "sunOrientation",
|
||||
// "valueKey": "value"
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
},
|
||||
{
|
||||
"name": "Name",
|
||||
@@ -196,45 +188,130 @@ describe("The Imagery View Layout", () => {
|
||||
|
||||
// this setups up the app
|
||||
beforeEach((done) => {
|
||||
appHolder = document.createElement('div');
|
||||
appHolder.style.width = '640px';
|
||||
appHolder.style.height = '480px';
|
||||
cleanupFirst = [];
|
||||
|
||||
openmct = createOpenMct();
|
||||
openmct.time.timeSystem('utc', {
|
||||
start: START - (5 * ONE_MINUTE),
|
||||
end: START + (5 * ONE_MINUTE)
|
||||
});
|
||||
|
||||
openmct.install(openmct.plugins.MyItems());
|
||||
openmct.install(openmct.plugins.LocalTimeSystem());
|
||||
openmct.install(openmct.plugins.UTCTimeSystem());
|
||||
telemetryPromise = new Promise((resolve) => {
|
||||
telemetryPromiseResolve = resolve;
|
||||
});
|
||||
|
||||
spyOn(openmct.telemetry, 'request').and.callFake(() => {
|
||||
telemetryPromiseResolve(imageTelemetry);
|
||||
|
||||
return telemetryPromise;
|
||||
});
|
||||
|
||||
parent = document.createElement('div');
|
||||
child = document.createElement('div');
|
||||
parent.appendChild(child);
|
||||
parent.style.width = '640px';
|
||||
parent.style.height = '480px';
|
||||
|
||||
// document.querySelector('body').append(parent);
|
||||
child = document.createElement('div');
|
||||
child.style.width = '640px';
|
||||
child.style.height = '480px';
|
||||
|
||||
parent.appendChild(child);
|
||||
document.body.appendChild(parent);
|
||||
|
||||
spyOn(window, 'ResizeObserver').and.returnValue({
|
||||
observe() {},
|
||||
disconnect() {}
|
||||
});
|
||||
|
||||
spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([]));
|
||||
// spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([]));
|
||||
spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({}));
|
||||
|
||||
originalRouterPath = openmct.router.path;
|
||||
|
||||
openmct.on('start', done);
|
||||
openmct.start(appHolder);
|
||||
openmct.startHeadless();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
openmct.time.timeSystem('utc', {
|
||||
start: 0,
|
||||
end: 1
|
||||
});
|
||||
afterEach((done) => {
|
||||
openmct.router.path = originalRouterPath;
|
||||
|
||||
return resetApplicationState(openmct);
|
||||
// Needs to be in a timeout because plots use a bunch of setTimeouts, some of which can resolve during or after
|
||||
// teardown, which causes problems
|
||||
// This is hacky, we should find a better approach here.
|
||||
setTimeout(() => {
|
||||
//Cleanup code that needs to happen before dom elements start being destroyed
|
||||
cleanupFirst.forEach(cleanup => cleanup());
|
||||
cleanupFirst = [];
|
||||
document.body.removeChild(parent);
|
||||
|
||||
resetApplicationState(openmct).then(done).catch(done);
|
||||
});
|
||||
});
|
||||
|
||||
it("should provide an imagery time strip view when in a time strip", () => {
|
||||
openmct.router.path = [{
|
||||
identifier: {
|
||||
key: 'test-timestrip',
|
||||
namespace: ''
|
||||
},
|
||||
type: 'time-strip'
|
||||
}];
|
||||
|
||||
let applicableViews = openmct.objectViews.get(imageryObject, [imageryObject, {
|
||||
identifier: {
|
||||
key: 'test-timestrip',
|
||||
namespace: ''
|
||||
},
|
||||
type: 'time-strip'
|
||||
}]);
|
||||
let imageryView = applicableViews.find(
|
||||
viewProvider => viewProvider.key === imageryForTimeStripKey
|
||||
);
|
||||
|
||||
expect(imageryView).toBeDefined();
|
||||
});
|
||||
|
||||
it("should provide an imagery view only for imagery producing objects", () => {
|
||||
let applicableViews = openmct.objectViews.get(imageryObject, []);
|
||||
let applicableViews = openmct.objectViews.get(imageryObject, [imageryObject]);
|
||||
let imageryView = applicableViews.find(
|
||||
viewProvider => viewProvider.key === imageryKey
|
||||
);
|
||||
|
||||
expect(imageryView).toBeDefined();
|
||||
});
|
||||
|
||||
it("should not provide an imagery view when in a time strip", () => {
|
||||
openmct.router.path = [{
|
||||
identifier: {
|
||||
key: 'test-timestrip',
|
||||
namespace: ''
|
||||
},
|
||||
type: 'time-strip'
|
||||
}];
|
||||
|
||||
let applicableViews = openmct.objectViews.get(imageryObject, [imageryObject, {
|
||||
identifier: {
|
||||
key: 'test-timestrip',
|
||||
namespace: ''
|
||||
},
|
||||
type: 'time-strip'
|
||||
}]);
|
||||
let imageryView = applicableViews.find(
|
||||
viewProvider => viewProvider.key === imageryKey
|
||||
);
|
||||
|
||||
expect(imageryView).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should provide an imagery view when navigated to in the composition of a time strip", () => {
|
||||
openmct.router.path = [imageryObject];
|
||||
|
||||
let applicableViews = openmct.objectViews.get(imageryObject, [imageryObject, {
|
||||
identifier: {
|
||||
key: 'test-timestrip',
|
||||
namespace: ''
|
||||
},
|
||||
type: 'time-strip'
|
||||
}]);
|
||||
let imageryView = applicableViews.find(
|
||||
viewProvider => viewProvider.key === imageryKey
|
||||
);
|
||||
@@ -247,51 +324,53 @@ describe("The Imagery View Layout", () => {
|
||||
let imageryViewProvider;
|
||||
let imageryView;
|
||||
|
||||
beforeEach(async () => {
|
||||
let telemetryRequestResolve;
|
||||
let telemetryRequestPromise = new Promise((resolve) => {
|
||||
telemetryRequestResolve = resolve;
|
||||
});
|
||||
beforeEach(() => {
|
||||
|
||||
openmct.telemetry.request.and.callFake(() => {
|
||||
telemetryRequestResolve(imageTelemetry);
|
||||
|
||||
return telemetryRequestPromise;
|
||||
});
|
||||
|
||||
applicableViews = openmct.objectViews.get(imageryObject, []);
|
||||
applicableViews = openmct.objectViews.get(imageryObject, [imageryObject]);
|
||||
imageryViewProvider = applicableViews.find(viewProvider => viewProvider.key === imageryKey);
|
||||
imageryView = imageryViewProvider.view(imageryObject);
|
||||
imageryView = imageryViewProvider.view(imageryObject, [imageryObject]);
|
||||
imageryView.show(child);
|
||||
|
||||
await telemetryRequestPromise;
|
||||
return Vue.nextTick();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
openmct.time.stopClock();
|
||||
openmct.router.removeListener('change:hash', resolveFunction);
|
||||
// afterEach(() => {
|
||||
// openmct.time.stopClock();
|
||||
// openmct.router.removeListener('change:hash', resolveFunction);
|
||||
//
|
||||
// imageryView.destroy();
|
||||
// });
|
||||
|
||||
imageryView.destroy();
|
||||
});
|
||||
|
||||
it("on mount should show the the most recent image", () => {
|
||||
const imageInfo = getImageInfo(parent);
|
||||
|
||||
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 1].timeId)).not.toEqual(-1);
|
||||
});
|
||||
|
||||
xit("should show the clicked thumbnail as the main image", (done) => {
|
||||
const target = imageTelemetry[5].url;
|
||||
parent.querySelectorAll(`img[src='${target}']`)[0].click();
|
||||
it("on mount should show the the most recent image", (done) => {
|
||||
//Looks like we need Vue.nextTick here so that computed properties settle down
|
||||
Vue.nextTick(() => {
|
||||
const imageInfo = getImageInfo(parent);
|
||||
|
||||
expect(imageInfo.url.indexOf(imageTelemetry[5].timeId)).not.toEqual(-1);
|
||||
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 1].timeId)).not.toEqual(-1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should show the clicked thumbnail as the main image", (done) => {
|
||||
//Looks like we need Vue.nextTick here so that computed properties settle down
|
||||
Vue.nextTick(() => {
|
||||
const target = imageTelemetry[5].url;
|
||||
parent.querySelectorAll(`img[src='${target}']`)[0].click();
|
||||
Vue.nextTick(() => {
|
||||
const imageInfo = getImageInfo(parent);
|
||||
|
||||
expect(imageInfo.url.indexOf(imageTelemetry[5].timeId)).not.toEqual(-1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
xit("should show that an image is new", (done) => {
|
||||
openmct.time.clock('local', {
|
||||
start: -1000,
|
||||
end: 1000
|
||||
});
|
||||
|
||||
Vue.nextTick(() => {
|
||||
// used in code, need to wait to the 500ms here too
|
||||
setTimeout(() => {
|
||||
@@ -317,69 +396,148 @@ describe("The Imagery View Layout", () => {
|
||||
});
|
||||
});
|
||||
|
||||
xit("should navigate via arrow keys", (done) => {
|
||||
let keyOpts = {
|
||||
element: parent.querySelector('.c-imagery'),
|
||||
key: 'ArrowLeft',
|
||||
keyCode: 37,
|
||||
type: 'keyup'
|
||||
};
|
||||
|
||||
simulateKeyEvent(keyOpts);
|
||||
|
||||
it("should navigate via arrow keys", (done) => {
|
||||
Vue.nextTick(() => {
|
||||
const imageInfo = getImageInfo(parent);
|
||||
let keyOpts = {
|
||||
element: parent.querySelector('.c-imagery'),
|
||||
key: 'ArrowLeft',
|
||||
keyCode: 37,
|
||||
type: 'keyup'
|
||||
};
|
||||
|
||||
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 2].timeId)).not.toEqual(-1);
|
||||
done();
|
||||
simulateKeyEvent(keyOpts);
|
||||
|
||||
Vue.nextTick(() => {
|
||||
const imageInfo = getImageInfo(parent);
|
||||
|
||||
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 2].timeId)).not.toEqual(-1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should navigate via numerous arrow keys", (done) => {
|
||||
let element = parent.querySelector('.c-imagery');
|
||||
let type = 'keyup';
|
||||
let leftKeyOpts = {
|
||||
element,
|
||||
type,
|
||||
key: 'ArrowLeft',
|
||||
keyCode: 37
|
||||
};
|
||||
let rightKeyOpts = {
|
||||
element,
|
||||
type,
|
||||
key: 'ArrowRight',
|
||||
keyCode: 39
|
||||
};
|
||||
|
||||
// left thrice
|
||||
simulateKeyEvent(leftKeyOpts);
|
||||
simulateKeyEvent(leftKeyOpts);
|
||||
simulateKeyEvent(leftKeyOpts);
|
||||
// right once
|
||||
simulateKeyEvent(rightKeyOpts);
|
||||
|
||||
Vue.nextTick(() => {
|
||||
const imageInfo = getImageInfo(parent);
|
||||
let element = parent.querySelector('.c-imagery');
|
||||
let type = 'keyup';
|
||||
let leftKeyOpts = {
|
||||
element,
|
||||
type,
|
||||
key: 'ArrowLeft',
|
||||
keyCode: 37
|
||||
};
|
||||
let rightKeyOpts = {
|
||||
element,
|
||||
type,
|
||||
key: 'ArrowRight',
|
||||
keyCode: 39
|
||||
};
|
||||
|
||||
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 3].timeId)).not.toEqual(-1);
|
||||
// left thrice
|
||||
simulateKeyEvent(leftKeyOpts);
|
||||
simulateKeyEvent(leftKeyOpts);
|
||||
simulateKeyEvent(leftKeyOpts);
|
||||
// right once
|
||||
simulateKeyEvent(rightKeyOpts);
|
||||
|
||||
Vue.nextTick(() => {
|
||||
const imageInfo = getImageInfo(parent);
|
||||
|
||||
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 3].timeId)).not.toEqual(-1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
it ('shows an auto scroll button when scroll to left', (done) => {
|
||||
Vue.nextTick(() => {
|
||||
// to mock what a scroll would do
|
||||
imageryView._getInstance().$refs.ImageryContainer.autoScroll = false;
|
||||
Vue.nextTick(() => {
|
||||
let autoScrollButton = parent.querySelector('.c-imagery__auto-scroll-resume-button');
|
||||
expect(autoScrollButton).toBeTruthy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
it ('scrollToRight is called when clicking on auto scroll button', (done) => {
|
||||
Vue.nextTick(() => {
|
||||
// use spyon to spy the scroll function
|
||||
spyOn(imageryView._getInstance().$refs.ImageryContainer, 'scrollToRight');
|
||||
imageryView._getInstance().$refs.ImageryContainer.autoScroll = false;
|
||||
Vue.nextTick(() => {
|
||||
parent.querySelector('.c-imagery__auto-scroll-resume-button').click();
|
||||
expect(imageryView._getInstance().$refs.ImageryContainer.scrollToRight).toHaveBeenCalledWith('reset');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("imagery time strip view", () => {
|
||||
let applicableViews;
|
||||
let imageryViewProvider;
|
||||
let imageryView;
|
||||
let componentView;
|
||||
|
||||
beforeEach(() => {
|
||||
openmct.time.timeSystem('utc', {
|
||||
start: START - (5 * ONE_MINUTE),
|
||||
end: START + (5 * ONE_MINUTE)
|
||||
});
|
||||
|
||||
openmct.router.path = [{
|
||||
identifier: {
|
||||
key: 'test-timestrip',
|
||||
namespace: ''
|
||||
},
|
||||
type: 'time-strip'
|
||||
}];
|
||||
|
||||
applicableViews = openmct.objectViews.get(imageryObject, [imageryObject, {
|
||||
identifier: {
|
||||
key: 'test-timestrip',
|
||||
namespace: ''
|
||||
},
|
||||
type: 'time-strip'
|
||||
}]);
|
||||
imageryViewProvider = applicableViews.find(viewProvider => viewProvider.key === imageryForTimeStripKey);
|
||||
imageryView = imageryViewProvider.view(imageryObject, [imageryObject, {
|
||||
identifier: {
|
||||
key: 'test-timestrip',
|
||||
namespace: ''
|
||||
},
|
||||
type: 'time-strip'
|
||||
}]);
|
||||
imageryView.show(child);
|
||||
|
||||
componentView = imageryView.getComponent().$children[0];
|
||||
spyOn(componentView.previewAction, 'invoke').and.callThrough();
|
||||
|
||||
return Vue.nextTick();
|
||||
});
|
||||
|
||||
it("on mount should show imagery within the given bounds", (done) => {
|
||||
Vue.nextTick(() => {
|
||||
const imageElements = parent.querySelectorAll('.c-imagery-tsv__image-wrapper');
|
||||
expect(imageElements.length).toEqual(6);
|
||||
done();
|
||||
});
|
||||
});
|
||||
it ('shows an auto scroll button when scroll to left', async () => {
|
||||
// to mock what a scroll would do
|
||||
imageryView._getInstance().$refs.ImageryLayout.autoScroll = false;
|
||||
await Vue.nextTick();
|
||||
let autoScrollButton = parent.querySelector('.c-imagery__auto-scroll-resume-button');
|
||||
expect(autoScrollButton).toBeTruthy();
|
||||
});
|
||||
it ('scrollToRight is called when clicking on auto scroll button', async () => {
|
||||
// use spyon to spy the scroll function
|
||||
spyOn(imageryView._getInstance().$refs.ImageryLayout, 'scrollToRight');
|
||||
imageryView._getInstance().$refs.ImageryLayout.autoScroll = false;
|
||||
await Vue.nextTick();
|
||||
parent.querySelector('.c-imagery__auto-scroll-resume-button').click();
|
||||
expect(imageryView._getInstance().$refs.ImageryLayout.scrollToRight).toHaveBeenCalledWith('reset');
|
||||
|
||||
it("should show the clicked thumbnail as the preview image", (done) => {
|
||||
Vue.nextTick(() => {
|
||||
const mouseDownEvent = createMouseEvent("mousedown");
|
||||
let imageWrapper = parent.querySelectorAll(`.c-imagery-tsv__image-wrapper`);
|
||||
imageWrapper[2].dispatchEvent(mouseDownEvent);
|
||||
|
||||
Vue.nextTick(() => {
|
||||
expect(componentView.previewAction.invoke).toHaveBeenCalledWith([componentView.objectPath[0]], {
|
||||
indexForFocusedImage: 2,
|
||||
objectPath: componentView.objectPath
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -180,9 +180,13 @@ export default {
|
||||
this.openmct.notifications.alert(message);
|
||||
}
|
||||
|
||||
const relativeHash = hash.slice(hash.indexOf('#'));
|
||||
const url = new URL(relativeHash, `${location.protocol}//${location.host}${location.pathname}`);
|
||||
this.openmct.router.navigate(url.hash);
|
||||
if (this.openmct.editor.isEditing()) {
|
||||
this.previewEmbed();
|
||||
} else {
|
||||
const relativeHash = hash.slice(hash.indexOf('#'));
|
||||
const url = new URL(relativeHash, `${location.protocol}//${location.host}${location.pathname}`);
|
||||
this.openmct.router.navigate(url.hash);
|
||||
}
|
||||
},
|
||||
formatTime(unixTime, timeFormat) {
|
||||
return Moment.utc(unixTime).format(timeFormat);
|
||||
|
||||
72
src/plugins/notebook/monkeyPatchObjectAPIForNotebooks.js
Normal file
72
src/plugins/notebook/monkeyPatchObjectAPIForNotebooks.js
Normal file
@@ -0,0 +1,72 @@
|
||||
import {NOTEBOOK_TYPE} from './notebook-constants';
|
||||
|
||||
export default function (openmct) {
|
||||
const apiSave = openmct.objects.save.bind(openmct.objects);
|
||||
|
||||
openmct.objects.save = async (domainObject) => {
|
||||
if (domainObject.type !== NOTEBOOK_TYPE) {
|
||||
return apiSave(domainObject);
|
||||
}
|
||||
|
||||
const localMutable = openmct.objects._toMutable(domainObject);
|
||||
let result;
|
||||
|
||||
try {
|
||||
result = await apiSave(localMutable);
|
||||
} catch (error) {
|
||||
if (error instanceof openmct.objects.errors.Conflict) {
|
||||
result = resolveConflicts(localMutable, openmct);
|
||||
} else {
|
||||
result = Promise.reject(error);
|
||||
}
|
||||
} finally {
|
||||
openmct.objects.destroyMutable(localMutable);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
function resolveConflicts(localMutable, openmct) {
|
||||
return openmct.objects.getMutable(localMutable.identifier).then((remoteMutable) => {
|
||||
const localEntries = localMutable.configuration.entries;
|
||||
remoteMutable.$refresh(remoteMutable);
|
||||
applyLocalEntries(remoteMutable, localEntries);
|
||||
|
||||
openmct.objects.destroyMutable(remoteMutable);
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function applyLocalEntries(mutable, entries) {
|
||||
Object.entries(entries).forEach(([sectionKey, pagesInSection]) => {
|
||||
Object.entries(pagesInSection).forEach(([pageKey, localEntries]) => {
|
||||
const remoteEntries = mutable.configuration.entries[sectionKey][pageKey];
|
||||
const mergedEntries = [].concat(remoteEntries);
|
||||
let shouldMutate = false;
|
||||
|
||||
const locallyAddedEntries = _.differenceBy(localEntries, remoteEntries, 'id');
|
||||
const locallyModifiedEntries = _.differenceWith(localEntries, remoteEntries, (localEntry, remoteEntry) => {
|
||||
return localEntry.id === remoteEntry.id && localEntry.text === remoteEntry.text;
|
||||
});
|
||||
|
||||
locallyAddedEntries.forEach((localEntry) => {
|
||||
mergedEntries.push(localEntry);
|
||||
shouldMutate = true;
|
||||
});
|
||||
|
||||
locallyModifiedEntries.forEach((locallyModifiedEntry) => {
|
||||
let mergedEntry = mergedEntries.find(entry => entry.id === locallyModifiedEntry.id);
|
||||
if (mergedEntry !== undefined) {
|
||||
mergedEntry.text = locallyModifiedEntry.text;
|
||||
shouldMutate = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (shouldMutate) {
|
||||
mutable.$set(`configuration.entries.${sectionKey}.${pageKey}`, mergedEntries);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import CopyToNotebookAction from './actions/CopyToNotebookAction';
|
||||
import Notebook from './components/Notebook.vue';
|
||||
import NotebookSnapshotIndicator from './components/NotebookSnapshotIndicator.vue';
|
||||
import SnapshotContainer from './snapshot-container';
|
||||
import monkeyPatchObjectAPIForNotebooks from './monkeyPatchObjectAPIForNotebooks.js';
|
||||
|
||||
import { notebookImageMigration } from '../notebook/utils/notebook-migration';
|
||||
import { NOTEBOOK_TYPE } from './notebook-constants';
|
||||
@@ -165,5 +166,7 @@ export default function NotebookPlugin() {
|
||||
return domainObject;
|
||||
}
|
||||
});
|
||||
|
||||
monkeyPatchObjectAPIForNotebooks(openmct);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -154,6 +154,8 @@ describe("Notebook plugin:", () => {
|
||||
testObjectProvider.get.and.returnValue(Promise.resolve(notebookViewObject));
|
||||
openmct.objects.addProvider('test-namespace', testObjectProvider);
|
||||
testObjectProvider.observe.and.returnValue(() => {});
|
||||
testObjectProvider.create.and.returnValue(Promise.resolve(true));
|
||||
testObjectProvider.update.and.returnValue(Promise.resolve(true));
|
||||
|
||||
return openmct.objects.getMutable(notebookViewObject.identifier).then((mutableObject) => {
|
||||
mutableNotebookObject = mutableObject;
|
||||
|
||||
@@ -125,7 +125,7 @@ export function addNotebookEntry(openmct, domainObject, notebookStorage, embed =
|
||||
const newEntries = addEntryIntoPage(notebookStorage, entries, entry);
|
||||
|
||||
addDefaultClass(domainObject, openmct);
|
||||
openmct.objects.mutate(domainObject, 'configuration.entries', newEntries);
|
||||
domainObject.configuration.entries = newEntries;
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
@@ -15,12 +15,16 @@
|
||||
|
||||
port.onmessage = async function (event) {
|
||||
if (event.data.request === 'close') {
|
||||
console.log('Closing connection');
|
||||
connections.splice(event.data.connectionId - 1, 1);
|
||||
if (connections.length <= 0) {
|
||||
// abort any outstanding requests if there's nobody listening to it.
|
||||
controller.abort();
|
||||
}
|
||||
|
||||
console.log('Closed.');
|
||||
connected = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -29,68 +33,9 @@
|
||||
return;
|
||||
}
|
||||
|
||||
connected = true;
|
||||
|
||||
let url = event.data.url;
|
||||
let body = event.data.body;
|
||||
let error = false;
|
||||
// feed=continuous maintains an indefinitely open connection with a keep-alive of HEARTBEAT milliseconds until this client closes the connection
|
||||
// style=main_only returns only the current winning revision of the document
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": 'application/json'
|
||||
},
|
||||
signal,
|
||||
body
|
||||
});
|
||||
|
||||
let reader;
|
||||
|
||||
if (response.body === undefined) {
|
||||
error = true;
|
||||
} else {
|
||||
reader = response.body.getReader();
|
||||
}
|
||||
|
||||
while (!error) {
|
||||
const {done, value} = await reader.read();
|
||||
//done is true when we lose connection with the provider
|
||||
if (done) {
|
||||
error = true;
|
||||
}
|
||||
|
||||
if (value) {
|
||||
let chunk = new Uint8Array(value.length);
|
||||
chunk.set(value, 0);
|
||||
const decodedChunk = new TextDecoder("utf-8").decode(chunk).split('\n');
|
||||
if (decodedChunk.length && decodedChunk[decodedChunk.length - 1] === '') {
|
||||
decodedChunk.forEach((doc, index) => {
|
||||
try {
|
||||
if (doc) {
|
||||
const objectChanges = JSON.parse(doc);
|
||||
connections.forEach(function (connection) {
|
||||
connection.postMessage({
|
||||
objectChanges
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch (decodeError) {
|
||||
//do nothing;
|
||||
console.log(decodeError);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (error) {
|
||||
port.postMessage({
|
||||
error
|
||||
});
|
||||
}
|
||||
do {
|
||||
await self.listenForChanges(event.data.url, event.data.body, port);
|
||||
} while (connected);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -103,4 +48,64 @@
|
||||
console.log('Error on feed');
|
||||
};
|
||||
|
||||
self.listenForChanges = async function (url, body, port) {
|
||||
connected = true;
|
||||
let error = false;
|
||||
// feed=continuous maintains an indefinitely open connection with a keep-alive of HEARTBEAT milliseconds until this client closes the connection
|
||||
// style=main_only returns only the current winning revision of the document
|
||||
|
||||
console.log('Opening changes feed connection.');
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": 'application/json'
|
||||
},
|
||||
signal,
|
||||
body
|
||||
});
|
||||
|
||||
let reader;
|
||||
|
||||
if (response.body === undefined) {
|
||||
error = true;
|
||||
} else {
|
||||
reader = response.body.getReader();
|
||||
}
|
||||
|
||||
while (!error) {
|
||||
const {done, value} = await reader.read();
|
||||
//done is true when we lose connection with the provider
|
||||
if (done) {
|
||||
error = true;
|
||||
}
|
||||
|
||||
if (value) {
|
||||
let chunk = new Uint8Array(value.length);
|
||||
chunk.set(value, 0);
|
||||
const decodedChunk = new TextDecoder("utf-8").decode(chunk).split('\n');
|
||||
console.log('Received chunk');
|
||||
if (decodedChunk.length && decodedChunk[decodedChunk.length - 1] === '') {
|
||||
decodedChunk.forEach((doc, index) => {
|
||||
try {
|
||||
if (doc) {
|
||||
const objectChanges = JSON.parse(doc);
|
||||
connections.forEach(function (connection) {
|
||||
connection.postMessage({
|
||||
objectChanges
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch (decodeError) {
|
||||
//do nothing;
|
||||
console.log(decodeError);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
console.log('Done reading changes feed');
|
||||
};
|
||||
|
||||
}());
|
||||
|
||||
@@ -29,7 +29,7 @@ const ID = "_id";
|
||||
const HEARTBEAT = 50000;
|
||||
const ALL_DOCS = "_all_docs?include_docs=true";
|
||||
|
||||
export default class CouchObjectProvider {
|
||||
class CouchObjectProvider {
|
||||
constructor(openmct, options, namespace) {
|
||||
options = this._normalize(options);
|
||||
this.openmct = openmct;
|
||||
@@ -74,13 +74,6 @@ export default class CouchObjectProvider {
|
||||
if (event.data.type === 'connection') {
|
||||
this.changesFeedSharedWorkerConnectionId = event.data.connectionId;
|
||||
} else {
|
||||
const error = event.data.error;
|
||||
if (error && Object.keys(this.observers).length > 0) {
|
||||
this.observeObjectChanges();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let objectChanges = event.data.objectChanges;
|
||||
objectChanges.identifier = {
|
||||
namespace: this.namespace,
|
||||
@@ -126,11 +119,12 @@ export default class CouchObjectProvider {
|
||||
}
|
||||
|
||||
return fetch(this.url + '/' + subPath, fetchOptions)
|
||||
.then(response => response.json())
|
||||
.then(function (response) {
|
||||
return response;
|
||||
}, function () {
|
||||
return undefined;
|
||||
.then((response) => {
|
||||
if (response.status === CouchObjectProvider.HTTP_CONFLICT) {
|
||||
throw new this.openmct.objects.errors.Conflict(`Conflict persisting ${fetchOptions.body.name}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -561,12 +555,18 @@ export default class CouchObjectProvider {
|
||||
let intermediateResponse = this.getIntermediateResponse();
|
||||
const key = model.identifier.key;
|
||||
this.enqueueObject(key, model, intermediateResponse);
|
||||
this.objectQueue[key].pending = true;
|
||||
const queued = this.objectQueue[key].dequeue();
|
||||
let document = new CouchDocument(key, queued.model);
|
||||
this.request(key, "PUT", document).then((response) => {
|
||||
this.checkResponse(response, queued.intermediateResponse, key);
|
||||
});
|
||||
if (!this.objectQueue[key].pending) {
|
||||
this.objectQueue[key].pending = true;
|
||||
const queued = this.objectQueue[key].dequeue();
|
||||
let document = new CouchDocument(key, queued.model);
|
||||
this.request(key, "PUT", document).then((response) => {
|
||||
console.log('create check response', key);
|
||||
this.checkResponse(response, queued.intermediateResponse, key);
|
||||
}).catch(error => {
|
||||
queued.intermediateResponse.reject(error);
|
||||
this.objectQueue[key].pending = false;
|
||||
});
|
||||
}
|
||||
|
||||
return intermediateResponse.promise;
|
||||
}
|
||||
@@ -581,6 +581,9 @@ export default class CouchObjectProvider {
|
||||
let document = new CouchDocument(key, queued.model, this.objectQueue[key].rev);
|
||||
this.request(key, "PUT", document).then((response) => {
|
||||
this.checkResponse(response, queued.intermediateResponse, key);
|
||||
}).catch((error) => {
|
||||
queued.intermediateResponse.reject(error);
|
||||
this.objectQueue[key].pending = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -594,3 +597,7 @@ export default class CouchObjectProvider {
|
||||
return intermediateResponse.promise;
|
||||
}
|
||||
}
|
||||
|
||||
CouchObjectProvider.HTTP_CONFLICT = 409;
|
||||
|
||||
export default CouchObjectProvider;
|
||||
|
||||
@@ -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,12 +157,10 @@ export default {
|
||||
getPlanData(domainObject) {
|
||||
this.planData = getValidatedPlan(domainObject);
|
||||
},
|
||||
updateViewBounds() {
|
||||
this.viewBounds = this.openmct.time.bounds();
|
||||
//Add a 50% padding to the end bounds to look ahead
|
||||
let timespan = (this.viewBounds.end - this.viewBounds.start);
|
||||
let padding = timespan / 2;
|
||||
this.viewBounds.end = this.viewBounds.end + padding;
|
||||
updateViewBounds(bounds) {
|
||||
if (bounds) {
|
||||
this.viewBounds = Object.create(bounds);
|
||||
}
|
||||
|
||||
if (this.timeSystem === undefined) {
|
||||
this.timeSystem = this.openmct.time.timeSystem();
|
||||
|
||||
@@ -25,7 +25,9 @@ import Vue from 'vue';
|
||||
|
||||
export default function PlanViewProvider(openmct) {
|
||||
function isCompactView(objectPath) {
|
||||
return objectPath.find(object => object.type === 'time-strip') !== undefined;
|
||||
let isChildOfTimeStrip = objectPath.find(object => object.type === 'time-strip');
|
||||
|
||||
return isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -54,7 +56,8 @@ export default function PlanViewProvider(openmct) {
|
||||
},
|
||||
provide: {
|
||||
openmct,
|
||||
domainObject
|
||||
domainObject,
|
||||
path: objectPath
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -30,6 +30,7 @@ describe('the plugin', function () {
|
||||
let child;
|
||||
let openmct;
|
||||
let appHolder;
|
||||
let originalRouterPath;
|
||||
|
||||
beforeEach((done) => {
|
||||
appHolder = document.createElement('div');
|
||||
@@ -49,11 +50,15 @@ describe('the plugin', function () {
|
||||
child.style.height = '480px';
|
||||
element.appendChild(child);
|
||||
|
||||
originalRouterPath = openmct.router.path;
|
||||
|
||||
openmct.on('start', done);
|
||||
openmct.start(appHolder);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
openmct.router.path = originalRouterPath;
|
||||
|
||||
return resetApplicationState(openmct);
|
||||
});
|
||||
|
||||
@@ -78,8 +83,9 @@ describe('the plugin', function () {
|
||||
id: "test-object",
|
||||
type: "plan"
|
||||
};
|
||||
openmct.router.path = [testViewObject];
|
||||
|
||||
const applicableViews = openmct.objectViews.get(testViewObject, []);
|
||||
const applicableViews = openmct.objectViews.get(testViewObject, [testViewObject]);
|
||||
let planView = applicableViews.find((viewProvider) => viewProvider.key === 'plan.view');
|
||||
expect(planView).toBeDefined();
|
||||
});
|
||||
@@ -137,7 +143,9 @@ describe('the plugin', function () {
|
||||
}
|
||||
};
|
||||
|
||||
const applicableViews = openmct.objectViews.get(planDomainObject, []);
|
||||
openmct.router.path = [planDomainObject];
|
||||
|
||||
const applicableViews = openmct.objectViews.get(planDomainObject, [planDomainObject]);
|
||||
planView = applicableViews.find((viewProvider) => viewProvider.key === 'plan.view');
|
||||
let view = planView.view(planDomainObject, mockObjectPath);
|
||||
view.show(child, true);
|
||||
|
||||
@@ -35,12 +35,15 @@
|
||||
:tick-width="tickWidth"
|
||||
:single-series="seriesModels.length === 1"
|
||||
:series-model="seriesModels[0]"
|
||||
:style="{
|
||||
left: (plotWidth - tickWidth) + 'px'
|
||||
}"
|
||||
@yKeyChanged="setYAxisKey"
|
||||
@tickWidthChanged="onTickWidthChange"
|
||||
/>
|
||||
<div class="gl-plot-wrapper-display-area-and-x-axis"
|
||||
:style="{
|
||||
left: (tickWidth + 20) + 'px'
|
||||
left: (plotWidth + 20) + 'px'
|
||||
}"
|
||||
>
|
||||
|
||||
@@ -156,7 +159,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 +176,7 @@ export default {
|
||||
MctTicks,
|
||||
MctChart
|
||||
},
|
||||
inject: ['openmct', 'domainObject'],
|
||||
inject: ['openmct', 'domainObject', 'path'],
|
||||
props: {
|
||||
options: {
|
||||
type: Object,
|
||||
@@ -219,7 +222,8 @@ export default {
|
||||
isRealTime: this.openmct.time.clock() !== undefined,
|
||||
loaded: false,
|
||||
isTimeOutOfSync: false,
|
||||
showLimitLineLabels: undefined
|
||||
showLimitLineLabels: undefined,
|
||||
isFrozenOnMouseDown: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -235,24 +239,26 @@ export default {
|
||||
} else {
|
||||
return 'plot-legend-collapsed';
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
plotTickWidth(newTickWidth) {
|
||||
this.onTickWidthChange(newTickWidth, true);
|
||||
},
|
||||
plotWidth() {
|
||||
return this.plotTickWidth || this.tickWidth;
|
||||
}
|
||||
},
|
||||
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;
|
||||
|
||||
//this also updates bounds which loads series data
|
||||
this.setTimeContext();
|
||||
|
||||
this.listenTo(this.config.series, 'add', this.addSeries, this);
|
||||
this.listenTo(this.config.series, 'remove', this.removeSeries, this);
|
||||
|
||||
this.config.series.models.forEach(this.addSeries, this);
|
||||
|
||||
this.filterObserver = this.openmct.objects.observe(
|
||||
this.domainObject,
|
||||
'configuration.filters',
|
||||
@@ -261,7 +267,6 @@ export default {
|
||||
this.removeStatusListener = this.openmct.status.observe(this.domainObject.identifier, this.updateStatus);
|
||||
|
||||
this.openmct.objectViews.on('clearData', this.clearData);
|
||||
this.followTimeConductor();
|
||||
|
||||
this.loaded = true;
|
||||
|
||||
@@ -274,11 +279,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);
|
||||
@@ -326,9 +347,13 @@ export default {
|
||||
this.offsetWidth = this.$parent.$refs.plotWrapper.offsetWidth;
|
||||
|
||||
this.startLoading();
|
||||
const bounds = this.timeContext.bounds();
|
||||
const options = {
|
||||
size: this.$parent.$refs.plotWrapper.offsetWidth,
|
||||
domain: this.config.xAxis.get('key')
|
||||
domain: {
|
||||
max: bounds.start,
|
||||
min: bounds.end
|
||||
}
|
||||
};
|
||||
|
||||
series.load(options)
|
||||
@@ -337,16 +362,24 @@ export default {
|
||||
|
||||
loadMoreData(range, purge) {
|
||||
this.config.series.forEach(plotSeries => {
|
||||
this.startLoading();
|
||||
plotSeries.load({
|
||||
size: this.$parent.$refs.plotWrapper.offsetWidth,
|
||||
start: range.min,
|
||||
end: range.max,
|
||||
domain: this.config.xAxis.get('key')
|
||||
})
|
||||
.then(this.stopLoading());
|
||||
if (purge) {
|
||||
plotSeries.purgeRecordsOutsideRange(range);
|
||||
const domain = {
|
||||
max: range.start,
|
||||
min: range.end
|
||||
};
|
||||
if (!_.isEqual(domain, this.config.xAxis.get('key'))) {
|
||||
this.offsetWidth = this.$parent.$refs.plotWrapper.offsetWidth;
|
||||
|
||||
this.startLoading();
|
||||
plotSeries.load({
|
||||
size: this.offsetWidth,
|
||||
start: range.min,
|
||||
end: range.max,
|
||||
domain: this.config.xAxis.get('key')
|
||||
})
|
||||
.then(this.stopLoading());
|
||||
if (purge) {
|
||||
plotSeries.purgeRecordsOutsideRange(range);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -485,7 +518,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;
|
||||
@@ -574,7 +607,8 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
this.$emit('plotTickWidth', this.tickWidth);
|
||||
const id = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
this.$emit('plotTickWidth', this.tickWidth, id);
|
||||
},
|
||||
|
||||
trackMousePosition(event) {
|
||||
@@ -667,6 +701,11 @@ export default {
|
||||
|
||||
this.listenTo(window, 'mouseup', this.onMouseUp, this);
|
||||
this.listenTo(window, 'mousemove', this.trackMousePosition, this);
|
||||
|
||||
// track frozen state on mouseDown to be read on mouseUp
|
||||
const isFrozen = this.config.xAxis.get('frozen') === true && this.config.yAxis.get('frozen') === true;
|
||||
this.isFrozenOnMouseDown = isFrozen;
|
||||
|
||||
if (event.altKey) {
|
||||
return this.startPan(event);
|
||||
} else {
|
||||
@@ -687,7 +726,14 @@ export default {
|
||||
}
|
||||
|
||||
if (this.marquee) {
|
||||
return this.endMarquee(event);
|
||||
this.endMarquee(event);
|
||||
}
|
||||
|
||||
// resume the plot if no pan, zoom, or drag action is taken
|
||||
// needs to follow endMarquee so that plotHistory is pruned
|
||||
const isAction = Boolean(this.plotHistory.length);
|
||||
if (!isAction && !this.isFrozenOnMouseDown) {
|
||||
return this.play();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -958,7 +1004,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 +1039,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 +1052,7 @@ export default {
|
||||
configStore.deleteStore(this.config.id);
|
||||
|
||||
this.stopListening();
|
||||
|
||||
if (this.checkForSize) {
|
||||
clearInterval(this.checkForSize);
|
||||
delete this.checkForSize;
|
||||
@@ -1021,8 +1068,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) {
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -80,7 +80,7 @@ export default {
|
||||
components: {
|
||||
MctPlot
|
||||
},
|
||||
inject: ['openmct', 'domainObject'],
|
||||
inject: ['openmct', 'domainObject', 'path'],
|
||||
props: {
|
||||
options: {
|
||||
type: Object,
|
||||
|
||||
@@ -44,7 +44,9 @@ export default function PlotViewProvider(openmct) {
|
||||
}
|
||||
|
||||
function isCompactView(objectPath) {
|
||||
return objectPath.find(object => object.type === 'time-strip');
|
||||
let isChildOfTimeStrip = objectPath.find(object => object.type === 'time-strip');
|
||||
|
||||
return isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -68,7 +70,8 @@ export default function PlotViewProvider(openmct) {
|
||||
},
|
||||
provide: {
|
||||
openmct,
|
||||
domainObject
|
||||
domainObject,
|
||||
path: objectPath
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
|
||||
<script>
|
||||
import MctTicks from "../MctTicks.vue";
|
||||
import configStore from "../configuration/configStore";
|
||||
import configStore from "../configuration/ConfigStore";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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
|
||||
@@ -82,12 +82,17 @@ export default class PlotSeries extends Model {
|
||||
.openmct
|
||||
.telemetry
|
||||
.getMetadata(options.domainObject);
|
||||
|
||||
this.formats = options
|
||||
.openmct
|
||||
.telemetry
|
||||
.getFormatMap(this.metadata);
|
||||
|
||||
const range = this.metadata.valuesForHints(['range'])[0];
|
||||
//if the object is missing or doesn't have metadata for some reason
|
||||
let range = {};
|
||||
if (this.metadata) {
|
||||
range = this.metadata.valuesForHints(['range'])[0];
|
||||
}
|
||||
|
||||
return {
|
||||
name: options.domainObject.name,
|
||||
@@ -191,7 +196,10 @@ export default class PlotSeries extends Model {
|
||||
.uniq(true, point => [this.getXVal(point), this.getYVal(point)].join())
|
||||
.value();
|
||||
this.reset(newPoints);
|
||||
}.bind(this));
|
||||
}.bind(this))
|
||||
.catch((error) => {
|
||||
console.warn('Error fetching data', error);
|
||||
});
|
||||
/* eslint-enable you-dont-need-lodash-underscore/concat */
|
||||
}
|
||||
/**
|
||||
@@ -199,7 +207,9 @@ export default class PlotSeries extends Model {
|
||||
*/
|
||||
onXKeyChange(xKey) {
|
||||
const format = this.formats[xKey];
|
||||
this.getXVal = format.parse.bind(format);
|
||||
if (format) {
|
||||
this.getXVal = format.parse.bind(format);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Update y formatter on change, default to stepAfter interpolation if
|
||||
|
||||
@@ -23,8 +23,8 @@ import _ from 'lodash';
|
||||
|
||||
import PlotSeries from "./PlotSeries";
|
||||
import Collection from "./Collection";
|
||||
import Color from "../lib/Color";
|
||||
import ColorPalette from "../lib/ColorPalette";
|
||||
import Color from "@/ui/color/Color";
|
||||
import ColorPalette from "@/ui/color/ColorPalette";
|
||||
|
||||
export default class SeriesCollection extends Collection {
|
||||
|
||||
|
||||
@@ -184,7 +184,7 @@ export default class YAxisModel extends Model {
|
||||
this.set('values', yMetadata.values);
|
||||
if (!label) {
|
||||
const labelName = series.map(function (s) {
|
||||
return s.metadata.value(s.get('yKey')).name;
|
||||
return s.metadata ? s.metadata.value(s.get('yKey')).name : '';
|
||||
}).reduce(function (a, b) {
|
||||
if (a === undefined) {
|
||||
return b;
|
||||
@@ -204,7 +204,7 @@ export default class YAxisModel extends Model {
|
||||
}
|
||||
|
||||
const labelUnits = series.map(function (s) {
|
||||
return s.metadata.value(s.get('yKey')).units;
|
||||
return s.metadata ? s.metadata.value(s.get('yKey')).units : '';
|
||||
}).reduce(function (a, b) {
|
||||
if (a === undefined) {
|
||||
return b;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 '@/ui/color/ColorSwatch.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ColorSwatch
|
||||
},
|
||||
inject: ['openmct', 'domainObject', 'path'],
|
||||
props: {
|
||||
series: {
|
||||
|
||||
@@ -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 '@/ui/color/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();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -25,7 +25,9 @@ import Vue from 'vue';
|
||||
|
||||
export default function OverlayPlotViewProvider(openmct) {
|
||||
function isCompactView(objectPath) {
|
||||
return objectPath.find(object => object.type === 'time-strip');
|
||||
let isChildOfTimeStrip = objectPath.find(object => object.type === 'time-strip');
|
||||
|
||||
return isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -53,7 +55,8 @@ export default function OverlayPlotViewProvider(openmct) {
|
||||
},
|
||||
provide: {
|
||||
openmct,
|
||||
domainObject
|
||||
domainObject,
|
||||
path: objectPath
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import PlotViewProvider from './PlotViewProvider';
|
||||
import OverlayPlotViewProvider from './overlayPlot/OverlayPlotViewProvider';
|
||||
import StackedPlotViewProvider from './stackedPlot/StackedPlotViewProvider';
|
||||
@@ -63,7 +62,9 @@ export default function () {
|
||||
openmct.objectViews.addProvider(new StackedPlotViewProvider(openmct));
|
||||
openmct.objectViews.addProvider(new OverlayPlotViewProvider(openmct));
|
||||
openmct.objectViews.addProvider(new PlotViewProvider(openmct));
|
||||
|
||||
openmct.inspectorViews.addProvider(new PlotsInspectorViewProvider(openmct));
|
||||
|
||||
openmct.composition.addPolicy(new OverlayPlotCompositionPolicy(openmct).allow);
|
||||
openmct.composition.addPolicy(new StackedPlotCompositionPolicy(openmct).allow);
|
||||
};
|
||||
|
||||
@@ -24,7 +24,7 @@ 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 configStore from "./configuration/ConfigStore";
|
||||
import EventEmitter from "EventEmitter";
|
||||
import PlotOptions from "./inspector/PlotOptions.vue";
|
||||
import PlotConfigurationModel from "./configuration/PlotConfigurationModel";
|
||||
@@ -141,6 +141,7 @@ describe("the plugin", function () {
|
||||
|
||||
spyOn(window, 'ResizeObserver').and.returnValue({
|
||||
observe() {},
|
||||
unobserve() {},
|
||||
disconnect() {}
|
||||
});
|
||||
|
||||
@@ -312,6 +313,7 @@ describe("the plugin", function () {
|
||||
let plotView = applicableViews.find((viewProvider) => viewProvider.key === "plot-stacked");
|
||||
expect(plotView).toBeDefined();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("The single plot view", () => {
|
||||
@@ -442,6 +444,64 @@ describe("the plugin", function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('resume actions on errant click', () => {
|
||||
beforeEach(() => {
|
||||
openmct.time.clock('local', {
|
||||
start: -1000,
|
||||
end: 100
|
||||
});
|
||||
|
||||
return Vue.nextTick();
|
||||
});
|
||||
|
||||
it("clicking the plot view without movement resumes the plot while active", async () => {
|
||||
|
||||
const pauseEl = element.querySelectorAll(".c-button-set .icon-pause");
|
||||
// if the pause button is present, the chart is running
|
||||
expect(pauseEl.length).toBe(1);
|
||||
|
||||
// simulate an errant mouse click
|
||||
// the second item is the canvas we need to use
|
||||
const canvas = element.querySelectorAll("canvas")[1];
|
||||
const mouseDownEvent = new MouseEvent('mousedown');
|
||||
const mouseUpEvent = new MouseEvent('mouseup');
|
||||
canvas.dispatchEvent(mouseDownEvent);
|
||||
// mouseup event is bound to the window
|
||||
window.dispatchEvent(mouseUpEvent);
|
||||
await Vue.nextTick();
|
||||
|
||||
const pauseElAfterClick = element.querySelectorAll(".c-button-set .icon-pause");
|
||||
console.log('pauseElAfterClick', pauseElAfterClick);
|
||||
expect(pauseElAfterClick.length).toBe(1);
|
||||
|
||||
});
|
||||
|
||||
it("clicking the plot view without movement leaves the plot paused", async () => {
|
||||
|
||||
const pauseEl = element.querySelector(".c-button-set .icon-pause");
|
||||
// pause the plot
|
||||
pauseEl.dispatchEvent(createMouseEvent('click'));
|
||||
await Vue.nextTick();
|
||||
|
||||
const playEl = element.querySelectorAll('.c-button-set .is-paused');
|
||||
expect(playEl.length).toBe(1);
|
||||
|
||||
// simulate an errant mouse click
|
||||
// the second item is the canvas we need to use
|
||||
const canvas = element.querySelectorAll("canvas")[1];
|
||||
const mouseDownEvent = new MouseEvent('mousedown');
|
||||
const mouseUpEvent = new MouseEvent('mouseup');
|
||||
canvas.dispatchEvent(mouseDownEvent);
|
||||
// mouseup event is bound to the window
|
||||
window.dispatchEvent(mouseUpEvent);
|
||||
await Vue.nextTick();
|
||||
|
||||
const playElAfterChartClick = element.querySelectorAll(".c-button-set .is-paused");
|
||||
expect(playElAfterChartClick.length).toBe(1);
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
describe('controls in time strip view', () => {
|
||||
|
||||
it('zoom controls are hidden', () => {
|
||||
@@ -570,7 +630,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 +1023,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', () => {
|
||||
@@ -987,7 +1048,11 @@ describe("the plugin", function () {
|
||||
const yAxisProperties = editOptionsEl.querySelectorAll("div.grid-properties:first-of-type .l-inspector-part");
|
||||
expect(yAxisProperties.length).toEqual(3);
|
||||
});
|
||||
|
||||
it('renders color palette options', () => {
|
||||
const colorSwatch = editOptionsEl.querySelector(".c-click-swatch");
|
||||
expect(colorSwatch).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -75,7 +75,7 @@ export default {
|
||||
components: {
|
||||
StackedPlotItem
|
||||
},
|
||||
inject: ['openmct', 'domainObject', 'composition'],
|
||||
inject: ['openmct', 'domainObject', 'composition', 'path'],
|
||||
props: {
|
||||
options: {
|
||||
type: Object,
|
||||
@@ -90,7 +90,8 @@ export default {
|
||||
cursorGuide: false,
|
||||
gridLines: true,
|
||||
loading: false,
|
||||
compositionObjects: []
|
||||
compositionObjects: [],
|
||||
tickWidthMap: {}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -106,8 +107,6 @@ export default {
|
||||
|
||||
this.imageExporter = new ImageExporter(this.openmct);
|
||||
|
||||
this.tickWidthMap = {};
|
||||
|
||||
this.composition.on('add', this.addChild);
|
||||
this.composition.on('remove', this.removeChild);
|
||||
this.composition.on('reorder', this.compositionReorder);
|
||||
@@ -127,13 +126,15 @@ export default {
|
||||
addChild(child) {
|
||||
const id = this.openmct.objects.makeKeyString(child.identifier);
|
||||
|
||||
this.tickWidthMap[id] = 0;
|
||||
this.$set(this.tickWidthMap, id, 0);
|
||||
this.compositionObjects.push(child);
|
||||
},
|
||||
|
||||
removeChild(childIdentifier) {
|
||||
const id = this.openmct.objects.makeKeyString(childIdentifier);
|
||||
delete this.tickWidthMap[id];
|
||||
|
||||
this.$delete(this.tickWidthMap, id);
|
||||
|
||||
const childObj = this.compositionObjects.filter((c) => {
|
||||
const identifier = this.openmct.objects.makeKeyString(c.identifier);
|
||||
|
||||
@@ -191,14 +192,7 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
//update the tickWidth for this plotId, the computed max tick width of the stacked plot will be cascaded down
|
||||
//TODO: Might need to do this using $set
|
||||
this.tickWidthMap[plotId] = Math.max(width, this.tickWidthMap[plotId]);
|
||||
// const newTickWidth = Math.max(...Object.values(this.tickWidthMap));
|
||||
// if (newTickWidth !== tickWidth || width !== tickWidth) {
|
||||
// tickWidth = newTickWidth;
|
||||
// $scope.$broadcast('plot:tickWidth', tickWidth);
|
||||
// }
|
||||
this.$set(this.tickWidthMap, plotId, width);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -25,7 +25,9 @@ import Vue from 'vue';
|
||||
|
||||
export default function StackedPlotViewProvider(openmct) {
|
||||
function isCompactView(objectPath) {
|
||||
return objectPath.find(object => object.type === 'time-strip');
|
||||
let isChildOfTimeStrip = objectPath.find(object => object.type === 'time-strip');
|
||||
|
||||
return isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -55,7 +57,8 @@ export default function StackedPlotViewProvider(openmct) {
|
||||
provide: {
|
||||
openmct,
|
||||
domainObject,
|
||||
composition: openmct.composition.get(domainObject)
|
||||
composition: openmct.composition.get(domainObject),
|
||||
path: objectPath
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -36,6 +36,7 @@ define([
|
||||
'./URLIndicatorPlugin/URLIndicatorPlugin',
|
||||
'./telemetryMean/plugin',
|
||||
'./plot/plugin',
|
||||
'./charts/plugin',
|
||||
'./telemetryTable/plugin',
|
||||
'./staticRootPlugin/plugin',
|
||||
'./notebook/plugin',
|
||||
@@ -87,6 +88,7 @@ define([
|
||||
URLIndicatorPlugin,
|
||||
TelemetryMean,
|
||||
PlotPlugin,
|
||||
ChartPlugin,
|
||||
TelemetryTablePlugin,
|
||||
StaticRootPlugin,
|
||||
Notebook,
|
||||
@@ -189,6 +191,7 @@ define([
|
||||
plugins.ExampleImagery = ExampleImagery;
|
||||
plugins.ImageryPlugin = ImageryPlugin;
|
||||
plugins.Plot = PlotPlugin.default;
|
||||
plugins.Chart = ChartPlugin.default;
|
||||
plugins.TelemetryTable = TelemetryTablePlugin;
|
||||
|
||||
plugins.SummaryWidget = SummaryWidget;
|
||||
|
||||
@@ -7,7 +7,8 @@ define([
|
||||
'./eventHelpers',
|
||||
'objectUtils',
|
||||
'lodash',
|
||||
'zepto'
|
||||
'zepto',
|
||||
'@braintree/sanitize-url'
|
||||
], function (
|
||||
widgetTemplate,
|
||||
Rule,
|
||||
@@ -17,7 +18,8 @@ define([
|
||||
eventHelpers,
|
||||
objectUtils,
|
||||
_,
|
||||
$
|
||||
$,
|
||||
urlSanitizeLib
|
||||
) {
|
||||
|
||||
//default css configuration for new rules
|
||||
@@ -88,7 +90,7 @@ define([
|
||||
function toggleTestData() {
|
||||
self.outerWrapper.toggleClass('expanded-widget-test-data');
|
||||
self.toggleTestDataControl.toggleClass('c-disclosure-triangle--expanded');
|
||||
}
|
||||
}
|
||||
|
||||
this.listenTo(this.toggleTestDataControl, 'click', toggleTestData);
|
||||
|
||||
@@ -99,7 +101,7 @@ define([
|
||||
function toggleRules() {
|
||||
self.outerWrapper.toggleClass('expanded-widget-rules');
|
||||
self.toggleRulesControl.toggleClass('c-disclosure-triangle--expanded');
|
||||
}
|
||||
}
|
||||
|
||||
this.listenTo(this.toggleRulesControl, 'click', toggleRules);
|
||||
|
||||
@@ -114,7 +116,7 @@ define([
|
||||
*/
|
||||
SummaryWidget.prototype.addHyperlink = function (url, openNewTab) {
|
||||
if (url) {
|
||||
this.widgetButton.attr('href', url);
|
||||
this.widgetButton.attr('href', urlSanitizeLib.sanitizeUrl(url));
|
||||
} else {
|
||||
this.widgetButton.removeAttr('href');
|
||||
}
|
||||
@@ -317,7 +319,7 @@ define([
|
||||
expanded: 'true'
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
ruleConfig = this.domainObject.configuration.ruleConfigById[ruleId];
|
||||
this.rulesById[ruleId] = new Rule(ruleConfig, this.domainObject, this.openmct,
|
||||
@@ -345,7 +347,7 @@ define([
|
||||
ruleOrder.splice(targetIndex + 1, 0, event.draggingId);
|
||||
this.domainObject.configuration.ruleOrder = ruleOrder;
|
||||
this.updateDomainObject();
|
||||
}
|
||||
}
|
||||
|
||||
this.refreshRules();
|
||||
};
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
define([
|
||||
'./summary-widget.html'
|
||||
'./summary-widget.html',
|
||||
'@braintree/sanitize-url'
|
||||
], function (
|
||||
summaryWidgetTemplate
|
||||
summaryWidgetTemplate,
|
||||
urlSanitizeLib
|
||||
) {
|
||||
const WIDGET_ICON_CLASS = 'c-sw__icon js-sw__icon';
|
||||
|
||||
@@ -35,8 +37,9 @@ define([
|
||||
this.icon = this.container.querySelector('#widgetIcon');
|
||||
this.label = this.container.querySelector('.js-sw__label');
|
||||
|
||||
if (this.domainObject.url) {
|
||||
this.widget.setAttribute('href', this.domainObject.url);
|
||||
let url = this.domainObject.url;
|
||||
if (url) {
|
||||
this.widget.setAttribute('href', urlSanitizeLib.sanitizeUrl(url));
|
||||
} else {
|
||||
this.widget.removeAttribute('href');
|
||||
}
|
||||
|
||||
@@ -60,18 +60,17 @@ define([
|
||||
this.addTelemetryObject = this.addTelemetryObject.bind(this);
|
||||
this.removeTelemetryObject = this.removeTelemetryObject.bind(this);
|
||||
this.removeTelemetryCollection = this.removeTelemetryCollection.bind(this);
|
||||
this.incrementOutstandingRequests = this.incrementOutstandingRequests.bind(this);
|
||||
this.decrementOutstandingRequests = this.decrementOutstandingRequests.bind(this);
|
||||
this.resetRowsFromAllData = this.resetRowsFromAllData.bind(this);
|
||||
this.isTelemetryObject = this.isTelemetryObject.bind(this);
|
||||
this.refreshData = this.refreshData.bind(this);
|
||||
this.updateFilters = this.updateFilters.bind(this);
|
||||
this.clearData = this.clearData.bind(this);
|
||||
this.buildOptionsFromConfiguration = this.buildOptionsFromConfiguration.bind(this);
|
||||
|
||||
this.filterObserver = undefined;
|
||||
|
||||
this.createTableRowCollections();
|
||||
|
||||
openmct.time.on('bounds', this.refreshData);
|
||||
openmct.time.on('timeSystem', this.refreshData);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -141,8 +140,6 @@ define([
|
||||
let columnMap = this.getColumnMapForObject(keyString);
|
||||
let limitEvaluator = this.openmct.telemetry.limitEvaluator(telemetryObject);
|
||||
|
||||
this.incrementOutstandingRequests();
|
||||
|
||||
const telemetryProcessor = this.getTelemetryProcessor(keyString, columnMap, limitEvaluator);
|
||||
const telemetryRemover = this.getTelemetryRemover();
|
||||
|
||||
@@ -151,13 +148,13 @@ define([
|
||||
this.telemetryCollections[keyString] = this.openmct.telemetry
|
||||
.requestCollection(telemetryObject, requestOptions);
|
||||
|
||||
this.telemetryCollections[keyString].on('requestStarted', this.incrementOutstandingRequests);
|
||||
this.telemetryCollections[keyString].on('requestEnded', this.decrementOutstandingRequests);
|
||||
this.telemetryCollections[keyString].on('remove', telemetryRemover);
|
||||
this.telemetryCollections[keyString].on('add', telemetryProcessor);
|
||||
this.telemetryCollections[keyString].on('clear', this.tableRows.clear);
|
||||
this.telemetryCollections[keyString].on('clear', this.clearData);
|
||||
this.telemetryCollections[keyString].load();
|
||||
|
||||
this.decrementOutstandingRequests();
|
||||
|
||||
this.telemetryObjects[keyString] = {
|
||||
telemetryObject,
|
||||
keyString,
|
||||
@@ -268,17 +265,6 @@ define([
|
||||
this.emit('object-removed', objectIdentifier);
|
||||
}
|
||||
|
||||
refreshData(bounds, isTick) {
|
||||
if (!isTick && this.tableRows.outstandingRequests === 0) {
|
||||
this.tableRows.clear();
|
||||
this.tableRows.sortBy({
|
||||
key: this.openmct.time.timeSystem().key,
|
||||
direction: 'asc'
|
||||
});
|
||||
this.tableRows.resubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
clearData() {
|
||||
this.tableRows.clear();
|
||||
this.emit('refresh');
|
||||
@@ -378,9 +364,6 @@ define([
|
||||
let keystrings = Object.keys(this.telemetryCollections);
|
||||
keystrings.forEach(this.removeTelemetryCollection);
|
||||
|
||||
this.openmct.time.off('bounds', this.refreshData);
|
||||
this.openmct.time.off('timeSystem', this.refreshData);
|
||||
|
||||
if (this.filterObserver) {
|
||||
this.filterObserver();
|
||||
}
|
||||
|
||||
@@ -131,7 +131,8 @@ export default {
|
||||
objects.forEach(object => this.addColumnsForObject(object, false));
|
||||
},
|
||||
addColumnsForObject(telemetryObject) {
|
||||
let metadataValues = this.openmct.telemetry.getMetadata(telemetryObject).values();
|
||||
const metadata = this.openmct.telemetry.getMetadata(telemetryObject);
|
||||
let metadataValues = metadata ? metadata.values() : [];
|
||||
metadataValues.forEach(metadatum => {
|
||||
let column = new TelemetryTableColumn(this.openmct, metadatum);
|
||||
this.tableConfiguration.addSingleColumnForObject(telemetryObject, column);
|
||||
|
||||
@@ -105,7 +105,8 @@ export default {
|
||||
composition.load().then((domainObjects) => {
|
||||
domainObjects.forEach(telemetryObject => {
|
||||
let keyString = this.openmct.objects.makeKeyString(telemetryObject.identifier);
|
||||
let metadataValues = this.openmct.telemetry.getMetadata(telemetryObject).values();
|
||||
const metadata = this.openmct.telemetry.getMetadata(telemetryObject);
|
||||
let metadataValues = metadata ? metadata.values() : [];
|
||||
let filters = this.filteredTelemetry[keyString];
|
||||
|
||||
if (filters !== undefined) {
|
||||
|
||||
@@ -125,7 +125,6 @@
|
||||
<div
|
||||
class="c-table c-telemetry-table c-table--filterable c-table--sortable has-control-bar u-style-receiver js-style-receiver"
|
||||
:class="{
|
||||
'loading': loading,
|
||||
'is-paused' : paused
|
||||
}"
|
||||
>
|
||||
@@ -362,7 +361,7 @@ export default {
|
||||
autoScroll: true,
|
||||
sortOptions: {},
|
||||
filters: {},
|
||||
loading: true,
|
||||
loading: false,
|
||||
scrollable: undefined,
|
||||
tableEl: undefined,
|
||||
headersHolderEl: undefined,
|
||||
@@ -422,6 +421,14 @@ export default {
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
loading: {
|
||||
handler(isLoading) {
|
||||
if (this.viewActionsCollection) {
|
||||
let action = isLoading ? 'disable' : 'enable';
|
||||
this.viewActionsCollection[action](['export-csv-all']);
|
||||
}
|
||||
}
|
||||
},
|
||||
markedRows: {
|
||||
handler(newVal, oldVal) {
|
||||
this.$emit('marked-rows-updated', newVal, oldVal);
|
||||
@@ -1020,6 +1027,12 @@ export default {
|
||||
this.viewActionsCollection.disable(['export-csv-marked', 'unmark-all-rows']);
|
||||
}
|
||||
|
||||
if (this.loading) {
|
||||
this.viewActionsCollection.disable(['export-csv-all']);
|
||||
} else {
|
||||
this.viewActionsCollection.enable(['export-csv-all']);
|
||||
}
|
||||
|
||||
if (this.paused) {
|
||||
this.viewActionsCollection.hide(['pause-data']);
|
||||
this.viewActionsCollection.show(['play-data']);
|
||||
|
||||
@@ -222,6 +222,24 @@ describe("the plugin", () => {
|
||||
openmct.router.path = originalRouterPath;
|
||||
});
|
||||
|
||||
it("Shows no progress bar initially", () => {
|
||||
let progressBar = element.querySelector('.c-progress-bar');
|
||||
|
||||
expect(tableInstance.outstandingRequests).toBe(0);
|
||||
expect(progressBar).toBeNull();
|
||||
});
|
||||
|
||||
it("Shows a progress bar while making requests", async () => {
|
||||
tableInstance.incrementOutstandingRequests();
|
||||
await Vue.nextTick();
|
||||
|
||||
let progressBar = element.querySelector('.c-progress-bar');
|
||||
|
||||
expect(tableInstance.outstandingRequests).toBe(1);
|
||||
expect(progressBar).not.toBeNull();
|
||||
|
||||
});
|
||||
|
||||
it("Renders a row for every telemetry datum returned", () => {
|
||||
let rows = element.querySelectorAll('table.c-telemetry-table__body tr');
|
||||
expect(rows.length).toBe(3);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -231,8 +231,8 @@ export default {
|
||||
const panStart = bounds.start - percX * deltaTime;
|
||||
|
||||
return {
|
||||
start: panStart,
|
||||
end: panStart + deltaTime
|
||||
start: parseInt(panStart, 10),
|
||||
end: parseInt(panStart + deltaTime, 10)
|
||||
};
|
||||
},
|
||||
startZoom() {
|
||||
@@ -296,7 +296,7 @@ export default {
|
||||
const valueDelta = value - this.left;
|
||||
const offset = valueDelta / this.width * timeDelta;
|
||||
|
||||
return bounds.start + offset;
|
||||
return parseInt(bounds.start + offset, 10);
|
||||
},
|
||||
isChangingViewBounds() {
|
||||
return this.dragStartX && this.dragX && this.dragStartX !== this.dragX;
|
||||
|
||||
267
src/plugins/timeConductor/ConductorInputsFixed.vue
Normal file
267
src/plugins/timeConductor/ConductorInputsFixed.vue
Normal file
@@ -0,0 +1,267 @@
|
||||
<template>
|
||||
<form ref="fixedDeltaInput"
|
||||
class="c-conductor__inputs"
|
||||
>
|
||||
<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>
|
||||
</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.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>
|
||||
269
src/plugins/timeConductor/ConductorInputsRealtime.vue
Normal file
269
src/plugins/timeConductor/ConductorInputsRealtime.vue
Normal 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>
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
/*****************************************************************************
|
||||
* 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.setTimeOptions);
|
||||
},
|
||||
stopFollowingTimeContext() {
|
||||
if (this.timeContext) {
|
||||
this.timeContext.off('timeContext', this.setTimeContext);
|
||||
this.timeContext.off('clock', this.setTimeOptions);
|
||||
}
|
||||
},
|
||||
setTimeOptions(clock) {
|
||||
this.timeOptions.clockOffsets = this.timeOptions.clockOffsets || this.timeContext.clockOffsets();
|
||||
this.timeOptions.fixedOffsets = this.timeOptions.fixedOffsets || this.timeContext.bounds();
|
||||
|
||||
if (!this.timeOptions.mode) {
|
||||
this.mode = this.timeContext.clock() === undefined ? {key: 'fixed'} : {key: Object.create(this.timeContext.clock()).key};
|
||||
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>
|
||||
223
src/plugins/timeConductor/independent/Mode.vue
Normal file
223
src/plugins/timeConductor/independent/Mode.vue
Normal file
@@ -0,0 +1,223 @@
|
||||
/*****************************************************************************
|
||||
* 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 v-if="modes.length > 1"
|
||||
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>
|
||||
128
src/plugins/timeConductor/pluginSpec.js
Normal file
128
src/plugins/timeConductor/pluginSpec.js
Normal 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');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user