mirror of
https://github.com/nasa/openmct.git
synced 2025-06-14 13:18:15 +00:00
Independent time conductor (#3988)
* Independent time API implementation * Independent time conductor in plan view Co-authored-by: charlesh88 <charles.f.hacskaylo@nasa.gov> Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov> Co-authored-by: Andrew Henry <akhenry@gmail.com>
This commit is contained in:
@ -136,7 +136,7 @@ define([
|
|||||||
* @memberof module:openmct.MCT#
|
* @memberof module:openmct.MCT#
|
||||||
* @name conductor
|
* @name conductor
|
||||||
*/
|
*/
|
||||||
this.time = new api.TimeAPI();
|
this.time = new api.TimeAPI(this);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An interface for interacting with the composition of domain objects.
|
* An interface for interacting with the composition of domain objects.
|
||||||
|
@ -46,7 +46,7 @@ define([
|
|||||||
StatusAPI
|
StatusAPI
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
TimeAPI: TimeAPI,
|
TimeAPI: TimeAPI.default,
|
||||||
ObjectAPI: ObjectAPI,
|
ObjectAPI: ObjectAPI,
|
||||||
CompositionAPI: CompositionAPI,
|
CompositionAPI: CompositionAPI,
|
||||||
TypeRegistry: TypeRegistry,
|
TypeRegistry: TypeRegistry,
|
||||||
|
106
src/api/time/GlobalTimeContext.js
Normal file
106
src/api/time/GlobalTimeContext.js
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* Open MCT includes source code licensed under additional open source
|
||||||
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
* this source code distribution or the Licensing information page available
|
||||||
|
* at runtime from the About dialog for additional information.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
import TimeContext from "./TimeContext";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The GlobalContext handles getting and setting time of the openmct application in general.
|
||||||
|
* Views will use this context unless they specify an alternate/independent time context
|
||||||
|
*/
|
||||||
|
class GlobalTimeContext extends TimeContext {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
//The Time Of Interest
|
||||||
|
this.toi = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get or set the start and end time of the time conductor. Basic validation
|
||||||
|
* of bounds is performed.
|
||||||
|
*
|
||||||
|
* @param {module:openmct.TimeAPI~TimeConductorBounds} newBounds
|
||||||
|
* @throws {Error} Validation error
|
||||||
|
* @fires module:openmct.TimeAPI~bounds
|
||||||
|
* @returns {module:openmct.TimeAPI~TimeConductorBounds}
|
||||||
|
* @memberof module:openmct.TimeAPI#
|
||||||
|
* @method bounds
|
||||||
|
*/
|
||||||
|
bounds(newBounds) {
|
||||||
|
if (arguments.length > 0) {
|
||||||
|
super.bounds.call(this, ...arguments);
|
||||||
|
// If a bounds change results in a TOI outside of the current
|
||||||
|
// bounds, unset it
|
||||||
|
if (this.toi < newBounds.start || this.toi > newBounds.end) {
|
||||||
|
this.timeOfInterest(undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Return a copy to prevent direct mutation of time conductor bounds.
|
||||||
|
return JSON.parse(JSON.stringify(this.boundsVal));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update bounds based on provided time and current offsets
|
||||||
|
* @private
|
||||||
|
* @param {number} timestamp A time from which bounds will be calculated
|
||||||
|
* using current offsets.
|
||||||
|
*/
|
||||||
|
tick(timestamp) {
|
||||||
|
super.tick.call(this, ...arguments);
|
||||||
|
|
||||||
|
// If a bounds change results in a TOI outside of the current
|
||||||
|
// bounds, unset it
|
||||||
|
if (this.toi < this.boundsVal.start || this.toi > this.boundsVal.end) {
|
||||||
|
this.timeOfInterest(undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get or set the Time of Interest. The Time of Interest is a single point
|
||||||
|
* in time, and constitutes the temporal focus of application views. It can
|
||||||
|
* be manipulated by the user from the time conductor or from other views.
|
||||||
|
* The time of interest can effectively be unset by assigning a value of
|
||||||
|
* 'undefined'.
|
||||||
|
* @fires module:openmct.TimeAPI~timeOfInterest
|
||||||
|
* @param newTOI
|
||||||
|
* @returns {number} the current time of interest
|
||||||
|
* @memberof module:openmct.TimeAPI#
|
||||||
|
* @method timeOfInterest
|
||||||
|
*/
|
||||||
|
timeOfInterest(newTOI) {
|
||||||
|
if (arguments.length > 0) {
|
||||||
|
this.toi = newTOI;
|
||||||
|
/**
|
||||||
|
* The Time of Interest has moved.
|
||||||
|
* @event timeOfInterest
|
||||||
|
* @memberof module:openmct.TimeAPI~
|
||||||
|
* @property {number} Current time of interest
|
||||||
|
*/
|
||||||
|
this.emit('timeOfInterest', this.toi);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.toi;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GlobalTimeContext;
|
94
src/api/time/IndependentTimeContext.js
Normal file
94
src/api/time/IndependentTimeContext.js
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* Open MCT includes source code licensed under additional open source
|
||||||
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
* this source code distribution or the Licensing information page available
|
||||||
|
* at runtime from the About dialog for additional information.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
import TimeContext from "./TimeContext";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The IndependentTimeContext handles getting and setting time of the openmct application in general.
|
||||||
|
* Views will use the GlobalTimeContext unless they specify an alternate/independent time context here.
|
||||||
|
*/
|
||||||
|
class IndependentTimeContext extends TimeContext {
|
||||||
|
constructor(globalTimeContext, key) {
|
||||||
|
super();
|
||||||
|
this.key = key;
|
||||||
|
|
||||||
|
this.globalTimeContext = globalTimeContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the active clock. Tick source will be immediately subscribed to
|
||||||
|
* and ticking will begin. Offsets from 'now' must also be provided. A clock
|
||||||
|
* can be unset by calling {@link stopClock}.
|
||||||
|
*
|
||||||
|
* @param {Clock || string} keyOrClock The clock to activate, or its key
|
||||||
|
* @param {ClockOffsets} offsets on each tick these will be used to calculate
|
||||||
|
* the start and end bounds. This maintains a sliding time window of a fixed
|
||||||
|
* width that automatically updates.
|
||||||
|
* @fires module:openmct.TimeAPI~clock
|
||||||
|
* @return {Clock} the currently active clock;
|
||||||
|
*/
|
||||||
|
clock(keyOrClock, offsets) {
|
||||||
|
if (arguments.length === 2) {
|
||||||
|
let clock;
|
||||||
|
|
||||||
|
if (typeof keyOrClock === 'string') {
|
||||||
|
clock = this.globalTimeContext.clocks.get(keyOrClock);
|
||||||
|
if (clock === undefined) {
|
||||||
|
throw "Unknown clock '" + keyOrClock + "'. Has it been registered with 'addClock'?";
|
||||||
|
}
|
||||||
|
} else if (typeof keyOrClock === 'object') {
|
||||||
|
clock = keyOrClock;
|
||||||
|
if (!this.globalTimeContext.clocks.has(clock.key)) {
|
||||||
|
throw "Unknown clock '" + keyOrClock.key + "'. Has it been registered with 'addClock'?";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const previousClock = this.activeClock;
|
||||||
|
if (previousClock !== undefined) {
|
||||||
|
previousClock.off("tick", this.tick);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.activeClock = clock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The active clock has changed. Clock can be unset by calling {@link stopClock}
|
||||||
|
* @event clock
|
||||||
|
* @memberof module:openmct.TimeAPI~
|
||||||
|
* @property {Clock} clock The newly activated clock, or undefined
|
||||||
|
* if the system is no longer following a clock source
|
||||||
|
*/
|
||||||
|
this.emit("clock", this.activeClock);
|
||||||
|
|
||||||
|
if (this.activeClock !== undefined) {
|
||||||
|
this.clockOffsets(offsets);
|
||||||
|
this.activeClock.on("tick", this.tick);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (arguments.length === 1) {
|
||||||
|
throw "When setting the clock, clock offsets must also be provided";
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.activeClock;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IndependentTimeContext;
|
@ -20,51 +20,35 @@
|
|||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
define(['EventEmitter'], function (EventEmitter) {
|
import GlobalTimeContext from "./GlobalTimeContext";
|
||||||
|
import IndependentTimeContext from "@/api/time/IndependentTimeContext";
|
||||||
/**
|
|
||||||
* The public API for setting and querying the temporal state of the
|
|
||||||
* application. The concept of time is integral to Open MCT, and at least
|
|
||||||
* one {@link TimeSystem}, as well as some default time bounds must be
|
|
||||||
* registered and enabled via {@link TimeAPI.addTimeSystem} and
|
|
||||||
* {@link TimeAPI.timeSystem} respectively for Open MCT to work.
|
|
||||||
*
|
|
||||||
* Time-sensitive views will typically respond to changes to bounds or other
|
|
||||||
* properties of the time conductor and update the data displayed based on
|
|
||||||
* the temporal state of the application. The current time bounds are also
|
|
||||||
* used in queries for historical data.
|
|
||||||
*
|
|
||||||
* The TimeAPI extends the EventEmitter class. A number of events are
|
|
||||||
* fired when properties of the time conductor change, which are documented
|
|
||||||
* below.
|
|
||||||
*
|
|
||||||
* @interface
|
|
||||||
* @memberof module:openmct
|
|
||||||
*/
|
|
||||||
function TimeAPI() {
|
|
||||||
EventEmitter.call(this);
|
|
||||||
|
|
||||||
//The Time System
|
|
||||||
this.system = undefined;
|
|
||||||
//The Time Of Interest
|
|
||||||
this.toi = undefined;
|
|
||||||
|
|
||||||
this.boundsVal = {
|
|
||||||
start: undefined,
|
|
||||||
end: undefined
|
|
||||||
};
|
|
||||||
|
|
||||||
this.timeSystems = new Map();
|
|
||||||
this.clocks = new Map();
|
|
||||||
this.activeClock = undefined;
|
|
||||||
this.offsets = undefined;
|
|
||||||
|
|
||||||
this.tick = this.tick.bind(this);
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The public API for setting and querying the temporal state of the
|
||||||
|
* application. The concept of time is integral to Open MCT, and at least
|
||||||
|
* one {@link TimeSystem}, as well as some default time bounds must be
|
||||||
|
* registered and enabled via {@link TimeAPI.addTimeSystem} and
|
||||||
|
* {@link TimeAPI.timeSystem} respectively for Open MCT to work.
|
||||||
|
*
|
||||||
|
* Time-sensitive views will typically respond to changes to bounds or other
|
||||||
|
* properties of the time conductor and update the data displayed based on
|
||||||
|
* the temporal state of the application. The current time bounds are also
|
||||||
|
* used in queries for historical data.
|
||||||
|
*
|
||||||
|
* The TimeAPI extends the GlobalTimeContext which in turn extends the TimeContext/EventEmitter class. A number of events are
|
||||||
|
* fired when properties of the time conductor change, which are documented
|
||||||
|
* below.
|
||||||
|
*
|
||||||
|
* @interface
|
||||||
|
* @memberof module:openmct
|
||||||
|
*/
|
||||||
|
class TimeAPI extends GlobalTimeContext {
|
||||||
|
constructor(openmct) {
|
||||||
|
super();
|
||||||
|
this.openmct = openmct;
|
||||||
|
this.independentContexts = new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
TimeAPI.prototype = Object.create(EventEmitter.prototype);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A TimeSystem provides meaning to the values returned by the TimeAPI. Open
|
* A TimeSystem provides meaning to the values returned by the TimeAPI. Open
|
||||||
* MCT supports multiple different types of time values, although all are
|
* MCT supports multiple different types of time values, although all are
|
||||||
@ -94,16 +78,16 @@ define(['EventEmitter'], function (EventEmitter) {
|
|||||||
* @memberof module:openmct.TimeAPI#
|
* @memberof module:openmct.TimeAPI#
|
||||||
* @param {TimeSystem} timeSystem A time system object.
|
* @param {TimeSystem} timeSystem A time system object.
|
||||||
*/
|
*/
|
||||||
TimeAPI.prototype.addTimeSystem = function (timeSystem) {
|
addTimeSystem(timeSystem) {
|
||||||
this.timeSystems.set(timeSystem.key, timeSystem);
|
this.timeSystems.set(timeSystem.key, timeSystem);
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {TimeSystem[]}
|
* @returns {TimeSystem[]}
|
||||||
*/
|
*/
|
||||||
TimeAPI.prototype.getAllTimeSystems = function () {
|
getAllTimeSystems() {
|
||||||
return Array.from(this.timeSystems.values());
|
return Array.from(this.timeSystems.values());
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clocks provide a timing source that is used to
|
* Clocks provide a timing source that is used to
|
||||||
@ -126,340 +110,81 @@ define(['EventEmitter'], function (EventEmitter) {
|
|||||||
* @memberof module:openmct.TimeAPI#
|
* @memberof module:openmct.TimeAPI#
|
||||||
* @param {Clock} clock
|
* @param {Clock} clock
|
||||||
*/
|
*/
|
||||||
TimeAPI.prototype.addClock = function (clock) {
|
addClock(clock) {
|
||||||
this.clocks.set(clock.key, clock);
|
this.clocks.set(clock.key, clock);
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @memberof module:openmct.TimeAPI#
|
* @memberof module:openmct.TimeAPI#
|
||||||
* @returns {Clock[]}
|
* @returns {Clock[]}
|
||||||
* @memberof module:openmct.TimeAPI#
|
* @memberof module:openmct.TimeAPI#
|
||||||
*/
|
*/
|
||||||
TimeAPI.prototype.getAllClocks = function () {
|
getAllClocks() {
|
||||||
return Array.from(this.clocks.values());
|
return Array.from(this.clocks.values());
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate the given bounds. This can be used for pre-validation of bounds,
|
* Get or set an independent time context which follows the TimeAPI timeSystem,
|
||||||
* for example by views validating user inputs.
|
* but with different offsets for a given domain object
|
||||||
* @param {TimeBounds} bounds The start and end time of the conductor.
|
* @param {key | string} key The identifier key of the domain object these offsets are set for
|
||||||
* @returns {string | true} A validation error, or true if valid
|
* @param {ClockOffsets | TimeBounds} value This maintains a sliding time window of a fixed width that automatically updates
|
||||||
|
* @param {key | string} clockKey the real time clock key currently in use
|
||||||
* @memberof module:openmct.TimeAPI#
|
* @memberof module:openmct.TimeAPI#
|
||||||
* @method validateBounds
|
* @method addIndependentTimeContext
|
||||||
*/
|
*/
|
||||||
TimeAPI.prototype.validateBounds = function (bounds) {
|
addIndependentContext(key, value, clockKey) {
|
||||||
if ((bounds.start === undefined)
|
let timeContext = this.independentContexts.get(key);
|
||||||
|| (bounds.end === undefined)
|
if (!timeContext) {
|
||||||
|| isNaN(bounds.start)
|
timeContext = new IndependentTimeContext(this, key);
|
||||||
|| isNaN(bounds.end)
|
this.independentContexts.set(key, timeContext);
|
||||||
) {
|
|
||||||
return "Start and end must be specified as integer values";
|
|
||||||
} else if (bounds.start > bounds.end) {
|
|
||||||
return "Specified start date exceeds end bound";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
if (clockKey) {
|
||||||
};
|
timeContext.clock(clockKey, value);
|
||||||
|
} else {
|
||||||
/**
|
timeContext.stopClock();
|
||||||
* Validate the given offsets. This can be used for pre-validation of
|
timeContext.bounds(value);
|
||||||
* offsets, for example by views validating user inputs.
|
|
||||||
* @param {ClockOffsets} offsets The start and end offsets from a 'now' value.
|
|
||||||
* @returns {string | true} A validation error, or true if valid
|
|
||||||
* @memberof module:openmct.TimeAPI#
|
|
||||||
* @method validateBounds
|
|
||||||
*/
|
|
||||||
TimeAPI.prototype.validateOffsets = function (offsets) {
|
|
||||||
if ((offsets.start === undefined)
|
|
||||||
|| (offsets.end === undefined)
|
|
||||||
|| isNaN(offsets.start)
|
|
||||||
|| isNaN(offsets.end)
|
|
||||||
) {
|
|
||||||
return "Start and end offsets must be specified as integer values";
|
|
||||||
} else if (offsets.start >= offsets.end) {
|
|
||||||
return "Specified start offset must be < end offset";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
this.emit('timeContext', key);
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
return () => {
|
||||||
* @typedef {Object} TimeBounds
|
this.independentContexts.delete(key);
|
||||||
* @property {number} start The start time displayed by the time conductor
|
timeContext.emit('timeContext', key);
|
||||||
* in ms since epoch. Epoch determined by currently active time system
|
|
||||||
* @property {number} end The end time displayed by the time conductor in ms
|
|
||||||
* since epoch.
|
|
||||||
* @memberof module:openmct.TimeAPI~
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get or set the start and end time of the time conductor. Basic validation
|
|
||||||
* of bounds is performed.
|
|
||||||
*
|
|
||||||
* @param {module:openmct.TimeAPI~TimeConductorBounds} newBounds
|
|
||||||
* @throws {Error} Validation error
|
|
||||||
* @fires module:openmct.TimeAPI~bounds
|
|
||||||
* @returns {module:openmct.TimeAPI~TimeConductorBounds}
|
|
||||||
* @memberof module:openmct.TimeAPI#
|
|
||||||
* @method bounds
|
|
||||||
*/
|
|
||||||
TimeAPI.prototype.bounds = function (newBounds) {
|
|
||||||
if (arguments.length > 0) {
|
|
||||||
const validationResult = this.validateBounds(newBounds);
|
|
||||||
if (validationResult !== true) {
|
|
||||||
throw new Error(validationResult);
|
|
||||||
}
|
|
||||||
|
|
||||||
//Create a copy to avoid direct mutation of conductor bounds
|
|
||||||
this.boundsVal = JSON.parse(JSON.stringify(newBounds));
|
|
||||||
/**
|
|
||||||
* The start time, end time, or both have been updated.
|
|
||||||
* @event bounds
|
|
||||||
* @memberof module:openmct.TimeAPI~
|
|
||||||
* @property {TimeConductorBounds} bounds The newly updated bounds
|
|
||||||
* @property {boolean} [tick] `true` if the bounds update was due to
|
|
||||||
* a "tick" event (ie. was an automatic update), false otherwise.
|
|
||||||
*/
|
|
||||||
this.emit('bounds', this.boundsVal, false);
|
|
||||||
|
|
||||||
// If a bounds change results in a TOI outside of the current
|
|
||||||
// bounds, unset it
|
|
||||||
if (this.toi < newBounds.start || this.toi > newBounds.end) {
|
|
||||||
this.timeOfInterest(undefined);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//Return a copy to prevent direct mutation of time conductor bounds.
|
|
||||||
return JSON.parse(JSON.stringify(this.boundsVal));
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get or set the time system of the TimeAPI.
|
|
||||||
* @param {TimeSystem | string} timeSystem
|
|
||||||
* @param {module:openmct.TimeAPI~TimeConductorBounds} bounds
|
|
||||||
* @fires module:openmct.TimeAPI~timeSystem
|
|
||||||
* @returns {TimeSystem} The currently applied time system
|
|
||||||
* @memberof module:openmct.TimeAPI#
|
|
||||||
* @method timeSystem
|
|
||||||
*/
|
|
||||||
TimeAPI.prototype.timeSystem = function (timeSystemOrKey, bounds) {
|
|
||||||
if (arguments.length >= 1) {
|
|
||||||
if (arguments.length === 1 && !this.activeClock) {
|
|
||||||
throw new Error(
|
|
||||||
"Must specify bounds when changing time system without "
|
|
||||||
+ "an active clock."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let timeSystem;
|
|
||||||
|
|
||||||
if (timeSystemOrKey === undefined) {
|
|
||||||
throw "Please provide a time system";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof timeSystemOrKey === 'string') {
|
|
||||||
timeSystem = this.timeSystems.get(timeSystemOrKey);
|
|
||||||
|
|
||||||
if (timeSystem === undefined) {
|
|
||||||
throw "Unknown time system " + timeSystemOrKey + ". Has it been registered with 'addTimeSystem'?";
|
|
||||||
}
|
|
||||||
} else if (typeof timeSystemOrKey === 'object') {
|
|
||||||
timeSystem = timeSystemOrKey;
|
|
||||||
|
|
||||||
if (!this.timeSystems.has(timeSystem.key)) {
|
|
||||||
throw "Unknown time system " + timeSystem.key + ". Has it been registered with 'addTimeSystem'?";
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw "Attempt to set invalid time system in Time API. Please provide a previously registered time system object or key";
|
|
||||||
}
|
|
||||||
|
|
||||||
this.system = timeSystem;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The time system used by the time
|
|
||||||
* conductor has changed. A change in Time System will always be
|
|
||||||
* followed by a bounds event specifying new query bounds.
|
|
||||||
*
|
|
||||||
* @event module:openmct.TimeAPI~timeSystem
|
|
||||||
* @property {TimeSystem} The value of the currently applied
|
|
||||||
* Time System
|
|
||||||
* */
|
|
||||||
this.emit('timeSystem', this.system);
|
|
||||||
if (bounds) {
|
|
||||||
this.bounds(bounds);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.system;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get or set the Time of Interest. The Time of Interest is a single point
|
|
||||||
* in time, and constitutes the temporal focus of application views. It can
|
|
||||||
* be manipulated by the user from the time conductor or from other views.
|
|
||||||
* The time of interest can effectively be unset by assigning a value of
|
|
||||||
* 'undefined'.
|
|
||||||
* @fires module:openmct.TimeAPI~timeOfInterest
|
|
||||||
* @param newTOI
|
|
||||||
* @returns {number} the current time of interest
|
|
||||||
* @memberof module:openmct.TimeAPI#
|
|
||||||
* @method timeOfInterest
|
|
||||||
*/
|
|
||||||
TimeAPI.prototype.timeOfInterest = function (newTOI) {
|
|
||||||
if (arguments.length > 0) {
|
|
||||||
this.toi = newTOI;
|
|
||||||
/**
|
|
||||||
* The Time of Interest has moved.
|
|
||||||
* @event timeOfInterest
|
|
||||||
* @memberof module:openmct.TimeAPI~
|
|
||||||
* @property {number} Current time of interest
|
|
||||||
*/
|
|
||||||
this.emit('timeOfInterest', this.toi);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.toi;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update bounds based on provided time and current offsets
|
|
||||||
* @private
|
|
||||||
* @param {number} timestamp A time from which boudns will be calculated
|
|
||||||
* using current offsets.
|
|
||||||
*/
|
|
||||||
TimeAPI.prototype.tick = function (timestamp) {
|
|
||||||
const newBounds = {
|
|
||||||
start: timestamp + this.offsets.start,
|
|
||||||
end: timestamp + this.offsets.end
|
|
||||||
};
|
};
|
||||||
|
}
|
||||||
this.boundsVal = newBounds;
|
|
||||||
this.emit('bounds', this.boundsVal, true);
|
|
||||||
|
|
||||||
// If a bounds change results in a TOI outside of the current
|
|
||||||
// bounds, unset it
|
|
||||||
if (this.toi < newBounds.start || this.toi > newBounds.end) {
|
|
||||||
this.timeOfInterest(undefined);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the active clock. Tick source will be immediately subscribed to
|
* Get the independent time context which follows the TimeAPI timeSystem,
|
||||||
* and ticking will begin. Offsets from 'now' must also be provided. A clock
|
* but with different offsets.
|
||||||
* can be unset by calling {@link stopClock}.
|
* @param {key | string} key The identifier key of the domain object these offsets
|
||||||
*
|
* @memberof module:openmct.TimeAPI#
|
||||||
* @param {Clock || string} The clock to activate, or its key
|
* @method getIndependentTimeContext
|
||||||
* @param {ClockOffsets} offsets on each tick these will be used to calculate
|
|
||||||
* the start and end bounds. This maintains a sliding time window of a fixed
|
|
||||||
* width that automatically updates.
|
|
||||||
* @fires module:openmct.TimeAPI~clock
|
|
||||||
* @return {Clock} the currently active clock;
|
|
||||||
*/
|
*/
|
||||||
TimeAPI.prototype.clock = function (keyOrClock, offsets) {
|
getIndependentContext(key) {
|
||||||
if (arguments.length === 2) {
|
return this.independentContexts.get(key);
|
||||||
let clock;
|
}
|
||||||
|
|
||||||
if (typeof keyOrClock === 'string') {
|
|
||||||
clock = this.clocks.get(keyOrClock);
|
|
||||||
if (clock === undefined) {
|
|
||||||
throw "Unknown clock '" + keyOrClock + "'. Has it been registered with 'addClock'?";
|
|
||||||
}
|
|
||||||
} else if (typeof keyOrClock === 'object') {
|
|
||||||
clock = keyOrClock;
|
|
||||||
if (!this.clocks.has(clock.key)) {
|
|
||||||
throw "Unknown clock '" + keyOrClock.key + "'. Has it been registered with 'addClock'?";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const previousClock = this.activeClock;
|
|
||||||
if (previousClock !== undefined) {
|
|
||||||
previousClock.off("tick", this.tick);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.activeClock = clock;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The active clock has changed. Clock can be unset by calling {@link stopClock}
|
|
||||||
* @event clock
|
|
||||||
* @memberof module:openmct.TimeAPI~
|
|
||||||
* @property {Clock} clock The newly activated clock, or undefined
|
|
||||||
* if the system is no longer following a clock source
|
|
||||||
*/
|
|
||||||
this.emit("clock", this.activeClock);
|
|
||||||
|
|
||||||
if (this.activeClock !== undefined) {
|
|
||||||
this.clockOffsets(offsets);
|
|
||||||
this.activeClock.on("tick", this.tick);
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if (arguments.length === 1) {
|
|
||||||
throw "When setting the clock, clock offsets must also be provided";
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.activeClock;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clock offsets are used to calculate temporal bounds when the system is
|
* Get the a timeContext for a view based on it's objectPath. If there is any object in the objectPath with an independent time context, it will be returned.
|
||||||
* ticking on a clock source.
|
* Otherwise, the global time context will be returned.
|
||||||
*
|
* @param { Array } objectPath The view's objectPath
|
||||||
* @typedef {object} ClockOffsets
|
* @memberof module:openmct.TimeAPI#
|
||||||
* @property {number} start A time span relative to the current value of the
|
* @method getContextForView
|
||||||
* ticking clock, from which start bounds will be calculated. This value must
|
|
||||||
* be < 0. When a clock is active, bounds will be calculated automatically
|
|
||||||
* based on the value provided by the clock, and the defined clock offsets.
|
|
||||||
* @property {number} end A time span relative to the current value of the
|
|
||||||
* ticking clock, from which end bounds will be calculated. This value must
|
|
||||||
* be >= 0.
|
|
||||||
*/
|
*/
|
||||||
/**
|
getContextForView(objectPath) {
|
||||||
* Get or set the currently applied clock offsets. If no parameter is provided,
|
let timeContext = this;
|
||||||
* the current value will be returned. If provided, the new value will be
|
|
||||||
* used as the new clock offsets.
|
|
||||||
* @param {ClockOffsets} offsets
|
|
||||||
* @returns {ClockOffsets}
|
|
||||||
*/
|
|
||||||
TimeAPI.prototype.clockOffsets = function (offsets) {
|
|
||||||
if (arguments.length > 0) {
|
|
||||||
|
|
||||||
const validationResult = this.validateOffsets(offsets);
|
objectPath.forEach(item => {
|
||||||
if (validationResult !== true) {
|
const key = this.openmct.objects.makeKeyString(item.identifier);
|
||||||
throw new Error(validationResult);
|
if (this.independentContexts.get(key)) {
|
||||||
|
timeContext = this.independentContexts.get(key);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.offsets = offsets;
|
return timeContext;
|
||||||
|
}
|
||||||
|
|
||||||
const currentValue = this.activeClock.currentValue();
|
}
|
||||||
const newBounds = {
|
|
||||||
start: currentValue + offsets.start,
|
|
||||||
end: currentValue + offsets.end
|
|
||||||
};
|
|
||||||
|
|
||||||
this.bounds(newBounds);
|
export default TimeAPI;
|
||||||
|
|
||||||
/**
|
|
||||||
* Event that is triggered when clock offsets change.
|
|
||||||
* @event clockOffsets
|
|
||||||
* @memberof module:openmct.TimeAPI~
|
|
||||||
* @property {ClockOffsets} clockOffsets The newly activated clock
|
|
||||||
* offsets.
|
|
||||||
*/
|
|
||||||
this.emit("clockOffsets", offsets);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.offsets;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stop the currently active clock from ticking, and unset it. This will
|
|
||||||
* revert all views to showing a static time frame defined by the current
|
|
||||||
* bounds.
|
|
||||||
*/
|
|
||||||
TimeAPI.prototype.stopClock = function () {
|
|
||||||
if (this.activeClock) {
|
|
||||||
this.clock(undefined, undefined);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return TimeAPI;
|
|
||||||
});
|
|
||||||
|
@ -19,241 +19,243 @@
|
|||||||
* this source code distribution or the Licensing information page available
|
* this source code distribution or the Licensing information page available
|
||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
import TimeAPI from "./TimeAPI";
|
||||||
|
import {createOpenMct} from "utils/testing";
|
||||||
|
|
||||||
define(['./TimeAPI'], function (TimeAPI) {
|
describe("The Time API", function () {
|
||||||
describe("The Time API", function () {
|
let api;
|
||||||
let api;
|
let timeSystemKey;
|
||||||
let timeSystemKey;
|
let timeSystem;
|
||||||
let timeSystem;
|
let clockKey;
|
||||||
let clockKey;
|
let clock;
|
||||||
let clock;
|
let bounds;
|
||||||
let bounds;
|
let eventListener;
|
||||||
let eventListener;
|
let toi;
|
||||||
let toi;
|
let openmct;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
openmct = createOpenMct();
|
||||||
|
api = new TimeAPI(openmct);
|
||||||
|
timeSystemKey = "timeSystemKey";
|
||||||
|
timeSystem = {key: timeSystemKey};
|
||||||
|
clockKey = "someClockKey";
|
||||||
|
clock = jasmine.createSpyObj("clock", [
|
||||||
|
"on",
|
||||||
|
"off",
|
||||||
|
"currentValue"
|
||||||
|
]);
|
||||||
|
clock.currentValue.and.returnValue(100);
|
||||||
|
clock.key = clockKey;
|
||||||
|
bounds = {
|
||||||
|
start: 0,
|
||||||
|
end: 1
|
||||||
|
};
|
||||||
|
eventListener = jasmine.createSpy("eventListener");
|
||||||
|
toi = 111;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Supports setting and querying of time of interest", function () {
|
||||||
|
expect(api.timeOfInterest()).not.toBe(toi);
|
||||||
|
api.timeOfInterest(toi);
|
||||||
|
expect(api.timeOfInterest()).toBe(toi);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Allows setting of valid bounds", function () {
|
||||||
|
bounds = {
|
||||||
|
start: 0,
|
||||||
|
end: 1
|
||||||
|
};
|
||||||
|
expect(api.bounds()).not.toBe(bounds);
|
||||||
|
expect(api.bounds.bind(api, bounds)).not.toThrow();
|
||||||
|
expect(api.bounds()).toEqual(bounds);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Disallows setting of invalid bounds", function () {
|
||||||
|
bounds = {
|
||||||
|
start: 1,
|
||||||
|
end: 0
|
||||||
|
};
|
||||||
|
expect(api.bounds()).not.toEqual(bounds);
|
||||||
|
expect(api.bounds.bind(api, bounds)).toThrow();
|
||||||
|
expect(api.bounds()).not.toEqual(bounds);
|
||||||
|
|
||||||
|
bounds = {start: 1};
|
||||||
|
expect(api.bounds()).not.toEqual(bounds);
|
||||||
|
expect(api.bounds.bind(api, bounds)).toThrow();
|
||||||
|
expect(api.bounds()).not.toEqual(bounds);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Allows setting of previously registered time system with bounds", function () {
|
||||||
|
api.addTimeSystem(timeSystem);
|
||||||
|
expect(api.timeSystem()).not.toBe(timeSystem);
|
||||||
|
expect(function () {
|
||||||
|
api.timeSystem(timeSystem, bounds);
|
||||||
|
}).not.toThrow();
|
||||||
|
expect(api.timeSystem()).toBe(timeSystem);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Disallows setting of time system without bounds", function () {
|
||||||
|
api.addTimeSystem(timeSystem);
|
||||||
|
expect(api.timeSystem()).not.toBe(timeSystem);
|
||||||
|
expect(function () {
|
||||||
|
api.timeSystem(timeSystemKey);
|
||||||
|
}).toThrow();
|
||||||
|
expect(api.timeSystem()).not.toBe(timeSystem);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows setting of timesystem without bounds with clock", function () {
|
||||||
|
api.addTimeSystem(timeSystem);
|
||||||
|
api.addClock(clock);
|
||||||
|
api.clock(clockKey, {
|
||||||
|
start: 0,
|
||||||
|
end: 1
|
||||||
|
});
|
||||||
|
expect(api.timeSystem()).not.toBe(timeSystem);
|
||||||
|
expect(function () {
|
||||||
|
api.timeSystem(timeSystemKey);
|
||||||
|
}).not.toThrow();
|
||||||
|
expect(api.timeSystem()).toBe(timeSystem);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Emits an event when time system changes", function () {
|
||||||
|
api.addTimeSystem(timeSystem);
|
||||||
|
expect(eventListener).not.toHaveBeenCalled();
|
||||||
|
api.on("timeSystem", eventListener);
|
||||||
|
api.timeSystem(timeSystemKey, bounds);
|
||||||
|
expect(eventListener).toHaveBeenCalledWith(timeSystem);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Emits an event when time of interest changes", function () {
|
||||||
|
expect(eventListener).not.toHaveBeenCalled();
|
||||||
|
api.on("timeOfInterest", eventListener);
|
||||||
|
api.timeOfInterest(toi);
|
||||||
|
expect(eventListener).toHaveBeenCalledWith(toi);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Emits an event when bounds change", function () {
|
||||||
|
expect(eventListener).not.toHaveBeenCalled();
|
||||||
|
api.on("bounds", eventListener);
|
||||||
|
api.bounds(bounds);
|
||||||
|
expect(eventListener).toHaveBeenCalledWith(bounds, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("If bounds are set and TOI lies inside them, do not change TOI", function () {
|
||||||
|
api.timeOfInterest(6);
|
||||||
|
api.bounds({
|
||||||
|
start: 1,
|
||||||
|
end: 10
|
||||||
|
});
|
||||||
|
expect(api.timeOfInterest()).toEqual(6);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("If bounds are set and TOI lies outside them, reset TOI", function () {
|
||||||
|
api.timeOfInterest(11);
|
||||||
|
api.bounds({
|
||||||
|
start: 1,
|
||||||
|
end: 10
|
||||||
|
});
|
||||||
|
expect(api.timeOfInterest()).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Maintains delta during tick", function () {
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Allows registered time system to be activated", function () {
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Allows a registered tick source to be activated", function () {
|
||||||
|
const mockTickSource = jasmine.createSpyObj("mockTickSource", [
|
||||||
|
"on",
|
||||||
|
"off",
|
||||||
|
"currentValue"
|
||||||
|
]);
|
||||||
|
mockTickSource.key = 'mockTickSource';
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(" when enabling a tick source", function () {
|
||||||
|
let mockTickSource;
|
||||||
|
let anotherMockTickSource;
|
||||||
|
const mockOffsets = {
|
||||||
|
start: 0,
|
||||||
|
end: 1
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
api = new TimeAPI();
|
mockTickSource = jasmine.createSpyObj("clock", [
|
||||||
timeSystemKey = "timeSystemKey";
|
|
||||||
timeSystem = {key: timeSystemKey};
|
|
||||||
clockKey = "someClockKey";
|
|
||||||
clock = jasmine.createSpyObj("clock", [
|
|
||||||
"on",
|
"on",
|
||||||
"off",
|
"off",
|
||||||
"currentValue"
|
"currentValue"
|
||||||
]);
|
]);
|
||||||
clock.currentValue.and.returnValue(100);
|
mockTickSource.currentValue.and.returnValue(10);
|
||||||
clock.key = clockKey;
|
|
||||||
bounds = {
|
|
||||||
start: 0,
|
|
||||||
end: 1
|
|
||||||
};
|
|
||||||
eventListener = jasmine.createSpy("eventListener");
|
|
||||||
toi = 111;
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Supports setting and querying of time of interest", function () {
|
|
||||||
expect(api.timeOfInterest()).not.toBe(toi);
|
|
||||||
api.timeOfInterest(toi);
|
|
||||||
expect(api.timeOfInterest()).toBe(toi);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Allows setting of valid bounds", function () {
|
|
||||||
bounds = {
|
|
||||||
start: 0,
|
|
||||||
end: 1
|
|
||||||
};
|
|
||||||
expect(api.bounds()).not.toBe(bounds);
|
|
||||||
expect(api.bounds.bind(api, bounds)).not.toThrow();
|
|
||||||
expect(api.bounds()).toEqual(bounds);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Disallows setting of invalid bounds", function () {
|
|
||||||
bounds = {
|
|
||||||
start: 1,
|
|
||||||
end: 0
|
|
||||||
};
|
|
||||||
expect(api.bounds()).not.toEqual(bounds);
|
|
||||||
expect(api.bounds.bind(api, bounds)).toThrow();
|
|
||||||
expect(api.bounds()).not.toEqual(bounds);
|
|
||||||
|
|
||||||
bounds = {start: 1};
|
|
||||||
expect(api.bounds()).not.toEqual(bounds);
|
|
||||||
expect(api.bounds.bind(api, bounds)).toThrow();
|
|
||||||
expect(api.bounds()).not.toEqual(bounds);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Allows setting of previously registered time system with bounds", function () {
|
|
||||||
api.addTimeSystem(timeSystem);
|
|
||||||
expect(api.timeSystem()).not.toBe(timeSystem);
|
|
||||||
expect(function () {
|
|
||||||
api.timeSystem(timeSystem, bounds);
|
|
||||||
}).not.toThrow();
|
|
||||||
expect(api.timeSystem()).toBe(timeSystem);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Disallows setting of time system without bounds", function () {
|
|
||||||
api.addTimeSystem(timeSystem);
|
|
||||||
expect(api.timeSystem()).not.toBe(timeSystem);
|
|
||||||
expect(function () {
|
|
||||||
api.timeSystem(timeSystemKey);
|
|
||||||
}).toThrow();
|
|
||||||
expect(api.timeSystem()).not.toBe(timeSystem);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("allows setting of timesystem without bounds with clock", function () {
|
|
||||||
api.addTimeSystem(timeSystem);
|
|
||||||
api.addClock(clock);
|
|
||||||
api.clock(clockKey, {
|
|
||||||
start: 0,
|
|
||||||
end: 1
|
|
||||||
});
|
|
||||||
expect(api.timeSystem()).not.toBe(timeSystem);
|
|
||||||
expect(function () {
|
|
||||||
api.timeSystem(timeSystemKey);
|
|
||||||
}).not.toThrow();
|
|
||||||
expect(api.timeSystem()).toBe(timeSystem);
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Emits an event when time system changes", function () {
|
|
||||||
api.addTimeSystem(timeSystem);
|
|
||||||
expect(eventListener).not.toHaveBeenCalled();
|
|
||||||
api.on("timeSystem", eventListener);
|
|
||||||
api.timeSystem(timeSystemKey, bounds);
|
|
||||||
expect(eventListener).toHaveBeenCalledWith(timeSystem);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Emits an event when time of interest changes", function () {
|
|
||||||
expect(eventListener).not.toHaveBeenCalled();
|
|
||||||
api.on("timeOfInterest", eventListener);
|
|
||||||
api.timeOfInterest(toi);
|
|
||||||
expect(eventListener).toHaveBeenCalledWith(toi);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Emits an event when bounds change", function () {
|
|
||||||
expect(eventListener).not.toHaveBeenCalled();
|
|
||||||
api.on("bounds", eventListener);
|
|
||||||
api.bounds(bounds);
|
|
||||||
expect(eventListener).toHaveBeenCalledWith(bounds, false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("If bounds are set and TOI lies inside them, do not change TOI", function () {
|
|
||||||
api.timeOfInterest(6);
|
|
||||||
api.bounds({
|
|
||||||
start: 1,
|
|
||||||
end: 10
|
|
||||||
});
|
|
||||||
expect(api.timeOfInterest()).toEqual(6);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("If bounds are set and TOI lies outside them, reset TOI", function () {
|
|
||||||
api.timeOfInterest(11);
|
|
||||||
api.bounds({
|
|
||||||
start: 1,
|
|
||||||
end: 10
|
|
||||||
});
|
|
||||||
expect(api.timeOfInterest()).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Maintains delta during tick", function () {
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Allows registered time system to be activated", function () {
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Allows a registered tick source to be activated", function () {
|
|
||||||
const mockTickSource = jasmine.createSpyObj("mockTickSource", [
|
|
||||||
"on",
|
|
||||||
"off",
|
|
||||||
"currentValue"
|
|
||||||
]);
|
|
||||||
mockTickSource.key = 'mockTickSource';
|
|
||||||
});
|
|
||||||
|
|
||||||
describe(" when enabling a tick source", function () {
|
|
||||||
let mockTickSource;
|
|
||||||
let anotherMockTickSource;
|
|
||||||
const mockOffsets = {
|
|
||||||
start: 0,
|
|
||||||
end: 1
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(function () {
|
|
||||||
mockTickSource = jasmine.createSpyObj("clock", [
|
|
||||||
"on",
|
|
||||||
"off",
|
|
||||||
"currentValue"
|
|
||||||
]);
|
|
||||||
mockTickSource.currentValue.and.returnValue(10);
|
|
||||||
mockTickSource.key = "mts";
|
|
||||||
|
|
||||||
anotherMockTickSource = jasmine.createSpyObj("clock", [
|
|
||||||
"on",
|
|
||||||
"off",
|
|
||||||
"currentValue"
|
|
||||||
]);
|
|
||||||
anotherMockTickSource.key = "amts";
|
|
||||||
anotherMockTickSource.currentValue.and.returnValue(10);
|
|
||||||
|
|
||||||
api.addClock(mockTickSource);
|
|
||||||
api.addClock(anotherMockTickSource);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("sets bounds based on current value", function () {
|
|
||||||
api.clock("mts", mockOffsets);
|
|
||||||
expect(api.bounds()).toEqual({
|
|
||||||
start: 10,
|
|
||||||
end: 11
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("a new tick listener is registered", function () {
|
|
||||||
api.clock("mts", mockOffsets);
|
|
||||||
expect(mockTickSource.on).toHaveBeenCalledWith("tick", jasmine.any(Function));
|
|
||||||
});
|
|
||||||
|
|
||||||
it("listener of existing tick source is reregistered", function () {
|
|
||||||
api.clock("mts", mockOffsets);
|
|
||||||
api.clock("amts", mockOffsets);
|
|
||||||
expect(mockTickSource.off).toHaveBeenCalledWith("tick", jasmine.any(Function));
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Allows the active clock to be set and unset", function () {
|
|
||||||
expect(api.clock()).toBeUndefined();
|
|
||||||
api.clock("mts", mockOffsets);
|
|
||||||
expect(api.clock()).toBeDefined();
|
|
||||||
api.stopClock();
|
|
||||||
expect(api.clock()).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
it("on tick, observes offsets, and indicates tick in bounds callback", function () {
|
|
||||||
const mockTickSource = jasmine.createSpyObj("clock", [
|
|
||||||
"on",
|
|
||||||
"off",
|
|
||||||
"currentValue"
|
|
||||||
]);
|
|
||||||
mockTickSource.currentValue.and.returnValue(100);
|
|
||||||
let tickCallback;
|
|
||||||
const boundsCallback = jasmine.createSpy("boundsCallback");
|
|
||||||
const clockOffsets = {
|
|
||||||
start: -100,
|
|
||||||
end: 100
|
|
||||||
};
|
|
||||||
mockTickSource.key = "mts";
|
mockTickSource.key = "mts";
|
||||||
|
|
||||||
|
anotherMockTickSource = jasmine.createSpyObj("clock", [
|
||||||
|
"on",
|
||||||
|
"off",
|
||||||
|
"currentValue"
|
||||||
|
]);
|
||||||
|
anotherMockTickSource.key = "amts";
|
||||||
|
anotherMockTickSource.currentValue.and.returnValue(10);
|
||||||
|
|
||||||
api.addClock(mockTickSource);
|
api.addClock(mockTickSource);
|
||||||
api.clock("mts", clockOffsets);
|
api.addClock(anotherMockTickSource);
|
||||||
|
|
||||||
api.on("bounds", boundsCallback);
|
|
||||||
|
|
||||||
tickCallback = mockTickSource.on.calls.mostRecent().args[1];
|
|
||||||
tickCallback(1000);
|
|
||||||
expect(boundsCallback).toHaveBeenCalledWith({
|
|
||||||
start: 900,
|
|
||||||
end: 1100
|
|
||||||
}, true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("sets bounds based on current value", function () {
|
||||||
|
api.clock("mts", mockOffsets);
|
||||||
|
expect(api.bounds()).toEqual({
|
||||||
|
start: 10,
|
||||||
|
end: 11
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("a new tick listener is registered", function () {
|
||||||
|
api.clock("mts", mockOffsets);
|
||||||
|
expect(mockTickSource.on).toHaveBeenCalledWith("tick", jasmine.any(Function));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("listener of existing tick source is reregistered", function () {
|
||||||
|
api.clock("mts", mockOffsets);
|
||||||
|
api.clock("amts", mockOffsets);
|
||||||
|
expect(mockTickSource.off).toHaveBeenCalledWith("tick", jasmine.any(Function));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Allows the active clock to be set and unset", function () {
|
||||||
|
expect(api.clock()).toBeUndefined();
|
||||||
|
api.clock("mts", mockOffsets);
|
||||||
|
expect(api.clock()).toBeDefined();
|
||||||
|
api.stopClock();
|
||||||
|
expect(api.clock()).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it("on tick, observes offsets, and indicates tick in bounds callback", function () {
|
||||||
|
const mockTickSource = jasmine.createSpyObj("clock", [
|
||||||
|
"on",
|
||||||
|
"off",
|
||||||
|
"currentValue"
|
||||||
|
]);
|
||||||
|
mockTickSource.currentValue.and.returnValue(100);
|
||||||
|
let tickCallback;
|
||||||
|
const boundsCallback = jasmine.createSpy("boundsCallback");
|
||||||
|
const clockOffsets = {
|
||||||
|
start: -100,
|
||||||
|
end: 100
|
||||||
|
};
|
||||||
|
mockTickSource.key = "mts";
|
||||||
|
|
||||||
|
api.addClock(mockTickSource);
|
||||||
|
api.clock("mts", clockOffsets);
|
||||||
|
|
||||||
|
api.on("bounds", boundsCallback);
|
||||||
|
|
||||||
|
tickCallback = mockTickSource.on.calls.mostRecent().args[1];
|
||||||
|
tickCallback(1000);
|
||||||
|
expect(boundsCallback).toHaveBeenCalledWith({
|
||||||
|
start: 900,
|
||||||
|
end: 1100
|
||||||
|
}, true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
360
src/api/time/TimeContext.js
Normal file
360
src/api/time/TimeContext.js
Normal file
@ -0,0 +1,360 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* Open MCT includes source code licensed under additional open source
|
||||||
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
* this source code distribution or the Licensing information page available
|
||||||
|
* at runtime from the About dialog for additional information.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
import EventEmitter from 'EventEmitter';
|
||||||
|
|
||||||
|
class TimeContext extends EventEmitter {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
//The Time System
|
||||||
|
this.timeSystems = new Map();
|
||||||
|
|
||||||
|
this.system = undefined;
|
||||||
|
|
||||||
|
this.clocks = new Map();
|
||||||
|
|
||||||
|
this.boundsVal = {
|
||||||
|
start: undefined,
|
||||||
|
end: undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
this.activeClock = undefined;
|
||||||
|
this.offsets = undefined;
|
||||||
|
|
||||||
|
this.tick = this.tick.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get or set the time system of the TimeAPI.
|
||||||
|
* @param {TimeSystem | string} timeSystem
|
||||||
|
* @param {module:openmct.TimeAPI~TimeConductorBounds} bounds
|
||||||
|
* @fires module:openmct.TimeAPI~timeSystem
|
||||||
|
* @returns {TimeSystem} The currently applied time system
|
||||||
|
* @memberof module:openmct.TimeAPI#
|
||||||
|
* @method timeSystem
|
||||||
|
*/
|
||||||
|
timeSystem(timeSystemOrKey, bounds) {
|
||||||
|
if (arguments.length >= 1) {
|
||||||
|
if (arguments.length === 1 && !this.activeClock) {
|
||||||
|
throw new Error(
|
||||||
|
"Must specify bounds when changing time system without "
|
||||||
|
+ "an active clock."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let timeSystem;
|
||||||
|
|
||||||
|
if (timeSystemOrKey === undefined) {
|
||||||
|
throw "Please provide a time system";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof timeSystemOrKey === 'string') {
|
||||||
|
timeSystem = this.timeSystems.get(timeSystemOrKey);
|
||||||
|
|
||||||
|
if (timeSystem === undefined) {
|
||||||
|
throw "Unknown time system " + timeSystemOrKey + ". Has it been registered with 'addTimeSystem'?";
|
||||||
|
}
|
||||||
|
} else if (typeof timeSystemOrKey === 'object') {
|
||||||
|
timeSystem = timeSystemOrKey;
|
||||||
|
|
||||||
|
if (!this.timeSystems.has(timeSystem.key)) {
|
||||||
|
throw "Unknown time system " + timeSystem.key + ". Has it been registered with 'addTimeSystem'?";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw "Attempt to set invalid time system in Time API. Please provide a previously registered time system object or key";
|
||||||
|
}
|
||||||
|
|
||||||
|
this.system = timeSystem;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The time system used by the time
|
||||||
|
* conductor has changed. A change in Time System will always be
|
||||||
|
* followed by a bounds event specifying new query bounds.
|
||||||
|
*
|
||||||
|
* @event module:openmct.TimeAPI~timeSystem
|
||||||
|
* @property {TimeSystem} The value of the currently applied
|
||||||
|
* Time System
|
||||||
|
* */
|
||||||
|
this.emit('timeSystem', this.system);
|
||||||
|
if (bounds) {
|
||||||
|
this.bounds(bounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.system;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clock offsets are used to calculate temporal bounds when the system is
|
||||||
|
* ticking on a clock source.
|
||||||
|
*
|
||||||
|
* @typedef {object} ValidationResult
|
||||||
|
* @property {boolean} valid Result of the validation - true or false.
|
||||||
|
* @property {string} message An error message if valid is false.
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Validate the given bounds. This can be used for pre-validation of bounds,
|
||||||
|
* for example by views validating user inputs.
|
||||||
|
* @param {TimeBounds} bounds The start and end time of the conductor.
|
||||||
|
* @returns {ValidationResult} A validation error, or true if valid
|
||||||
|
* @memberof module:openmct.TimeAPI#
|
||||||
|
* @method validateBounds
|
||||||
|
*/
|
||||||
|
validateBounds(bounds) {
|
||||||
|
if ((bounds.start === undefined)
|
||||||
|
|| (bounds.end === undefined)
|
||||||
|
|| isNaN(bounds.start)
|
||||||
|
|| isNaN(bounds.end)
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: "Start and end must be specified as integer values"
|
||||||
|
};
|
||||||
|
} else if (bounds.start > bounds.end) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: "Specified start date exceeds end bound"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: true,
|
||||||
|
message: ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get or set the start and end time of the time conductor. Basic validation
|
||||||
|
* of bounds is performed.
|
||||||
|
*
|
||||||
|
* @param {module:openmct.TimeAPI~TimeConductorBounds} newBounds
|
||||||
|
* @throws {Error} Validation error
|
||||||
|
* @fires module:openmct.TimeAPI~bounds
|
||||||
|
* @returns {module:openmct.TimeAPI~TimeConductorBounds}
|
||||||
|
* @memberof module:openmct.TimeAPI#
|
||||||
|
* @method bounds
|
||||||
|
*/
|
||||||
|
bounds(newBounds) {
|
||||||
|
if (arguments.length > 0) {
|
||||||
|
const validationResult = this.validateBounds(newBounds);
|
||||||
|
if (validationResult.valid !== true) {
|
||||||
|
throw new Error(validationResult.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Create a copy to avoid direct mutation of conductor bounds
|
||||||
|
this.boundsVal = JSON.parse(JSON.stringify(newBounds));
|
||||||
|
/**
|
||||||
|
* The start time, end time, or both have been updated.
|
||||||
|
* @event bounds
|
||||||
|
* @memberof module:openmct.TimeAPI~
|
||||||
|
* @property {TimeConductorBounds} bounds The newly updated bounds
|
||||||
|
* @property {boolean} [tick] `true` if the bounds update was due to
|
||||||
|
* a "tick" event (ie. was an automatic update), false otherwise.
|
||||||
|
*/
|
||||||
|
this.emit('bounds', this.boundsVal, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Return a copy to prevent direct mutation of time conductor bounds.
|
||||||
|
return JSON.parse(JSON.stringify(this.boundsVal));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the given offsets. This can be used for pre-validation of
|
||||||
|
* offsets, for example by views validating user inputs.
|
||||||
|
* @param {ClockOffsets} offsets The start and end offsets from a 'now' value.
|
||||||
|
* @returns { ValidationResult } A validation error, and true/false if valid or not
|
||||||
|
* @memberof module:openmct.TimeAPI#
|
||||||
|
* @method validateOffsets
|
||||||
|
*/
|
||||||
|
validateOffsets(offsets) {
|
||||||
|
if ((offsets.start === undefined)
|
||||||
|
|| (offsets.end === undefined)
|
||||||
|
|| isNaN(offsets.start)
|
||||||
|
|| isNaN(offsets.end)
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: "Start and end offsets must be specified as integer values"
|
||||||
|
};
|
||||||
|
} else if (offsets.start >= offsets.end) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: "Specified start offset must be < end offset"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: true,
|
||||||
|
message: ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} TimeBounds
|
||||||
|
* @property {number} start The start time displayed by the time conductor
|
||||||
|
* in ms since epoch. Epoch determined by currently active time system
|
||||||
|
* @property {number} end The end time displayed by the time conductor in ms
|
||||||
|
* since epoch.
|
||||||
|
* @memberof module:openmct.TimeAPI~
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clock offsets are used to calculate temporal bounds when the system is
|
||||||
|
* ticking on a clock source.
|
||||||
|
*
|
||||||
|
* @typedef {object} ClockOffsets
|
||||||
|
* @property {number} start A time span relative to the current value of the
|
||||||
|
* ticking clock, from which start bounds will be calculated. This value must
|
||||||
|
* be < 0. When a clock is active, bounds will be calculated automatically
|
||||||
|
* based on the value provided by the clock, and the defined clock offsets.
|
||||||
|
* @property {number} end A time span relative to the current value of the
|
||||||
|
* ticking clock, from which end bounds will be calculated. This value must
|
||||||
|
* be >= 0.
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Get or set the currently applied clock offsets. If no parameter is provided,
|
||||||
|
* the current value will be returned. If provided, the new value will be
|
||||||
|
* used as the new clock offsets.
|
||||||
|
* @param {ClockOffsets} offsets
|
||||||
|
* @returns {ClockOffsets}
|
||||||
|
*/
|
||||||
|
clockOffsets(offsets) {
|
||||||
|
if (arguments.length > 0) {
|
||||||
|
|
||||||
|
const validationResult = this.validateOffsets(offsets);
|
||||||
|
if (validationResult.valid !== true) {
|
||||||
|
throw new Error(validationResult.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.offsets = offsets;
|
||||||
|
|
||||||
|
const currentValue = this.activeClock.currentValue();
|
||||||
|
const newBounds = {
|
||||||
|
start: currentValue + offsets.start,
|
||||||
|
end: currentValue + offsets.end
|
||||||
|
};
|
||||||
|
|
||||||
|
this.bounds(newBounds);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event that is triggered when clock offsets change.
|
||||||
|
* @event clockOffsets
|
||||||
|
* @memberof module:openmct.TimeAPI~
|
||||||
|
* @property {ClockOffsets} clockOffsets The newly activated clock
|
||||||
|
* offsets.
|
||||||
|
*/
|
||||||
|
this.emit("clockOffsets", offsets);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.offsets;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the currently active clock from ticking, and unset it. This will
|
||||||
|
* revert all views to showing a static time frame defined by the current
|
||||||
|
* bounds.
|
||||||
|
*/
|
||||||
|
stopClock() {
|
||||||
|
if (this.activeClock) {
|
||||||
|
this.clock(undefined, undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the active clock. Tick source will be immediately subscribed to
|
||||||
|
* and ticking will begin. Offsets from 'now' must also be provided. A clock
|
||||||
|
* can be unset by calling {@link stopClock}.
|
||||||
|
*
|
||||||
|
* @param {Clock || string} keyOrClock The clock to activate, or its key
|
||||||
|
* @param {ClockOffsets} offsets on each tick these will be used to calculate
|
||||||
|
* the start and end bounds. This maintains a sliding time window of a fixed
|
||||||
|
* width that automatically updates.
|
||||||
|
* @fires module:openmct.TimeAPI~clock
|
||||||
|
* @return {Clock} the currently active clock;
|
||||||
|
*/
|
||||||
|
clock(keyOrClock, offsets) {
|
||||||
|
if (arguments.length === 2) {
|
||||||
|
let clock;
|
||||||
|
|
||||||
|
if (typeof keyOrClock === 'string') {
|
||||||
|
clock = this.clocks.get(keyOrClock);
|
||||||
|
if (clock === undefined) {
|
||||||
|
throw "Unknown clock '" + keyOrClock + "'. Has it been registered with 'addClock'?";
|
||||||
|
}
|
||||||
|
} else if (typeof keyOrClock === 'object') {
|
||||||
|
clock = keyOrClock;
|
||||||
|
if (!this.clocks.has(clock.key)) {
|
||||||
|
throw "Unknown clock '" + keyOrClock.key + "'. Has it been registered with 'addClock'?";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const previousClock = this.activeClock;
|
||||||
|
if (previousClock !== undefined) {
|
||||||
|
previousClock.off("tick", this.tick);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.activeClock = clock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The active clock has changed. Clock can be unset by calling {@link stopClock}
|
||||||
|
* @event clock
|
||||||
|
* @memberof module:openmct.TimeAPI~
|
||||||
|
* @property {Clock} clock The newly activated clock, or undefined
|
||||||
|
* if the system is no longer following a clock source
|
||||||
|
*/
|
||||||
|
this.emit("clock", this.activeClock);
|
||||||
|
|
||||||
|
if (this.activeClock !== undefined) {
|
||||||
|
this.clockOffsets(offsets);
|
||||||
|
this.activeClock.on("tick", this.tick);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (arguments.length === 1) {
|
||||||
|
throw "When setting the clock, clock offsets must also be provided";
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.activeClock;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update bounds based on provided time and current offsets
|
||||||
|
* @param {number} timestamp A time from which bounds will be calculated
|
||||||
|
* using current offsets.
|
||||||
|
*/
|
||||||
|
tick(timestamp) {
|
||||||
|
if (!this.activeClock) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newBounds = {
|
||||||
|
start: timestamp + this.offsets.start,
|
||||||
|
end: timestamp + this.offsets.end
|
||||||
|
};
|
||||||
|
|
||||||
|
this.boundsVal = newBounds;
|
||||||
|
this.emit('bounds', this.boundsVal, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TimeContext;
|
155
src/api/time/independentTimeAPISpec.js
Normal file
155
src/api/time/independentTimeAPISpec.js
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* Open MCT includes source code licensed under additional open source
|
||||||
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
* this source code distribution or the Licensing information page available
|
||||||
|
* at runtime from the About dialog for additional information.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
import TimeAPI from "./TimeAPI";
|
||||||
|
import {createOpenMct} from "utils/testing";
|
||||||
|
describe("The Independent Time API", function () {
|
||||||
|
let api;
|
||||||
|
let domainObjectKey;
|
||||||
|
let clockKey;
|
||||||
|
let clock;
|
||||||
|
let bounds;
|
||||||
|
let independentBounds;
|
||||||
|
let eventListener;
|
||||||
|
let openmct;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
openmct = createOpenMct();
|
||||||
|
api = new TimeAPI(openmct);
|
||||||
|
clockKey = "someClockKey";
|
||||||
|
clock = jasmine.createSpyObj("clock", [
|
||||||
|
"on",
|
||||||
|
"off",
|
||||||
|
"currentValue"
|
||||||
|
]);
|
||||||
|
clock.currentValue.and.returnValue(100);
|
||||||
|
clock.key = clockKey;
|
||||||
|
api.addClock(clock);
|
||||||
|
domainObjectKey = 'test-key';
|
||||||
|
bounds = {
|
||||||
|
start: 0,
|
||||||
|
end: 1
|
||||||
|
};
|
||||||
|
api.bounds(bounds);
|
||||||
|
independentBounds = {
|
||||||
|
start: 10,
|
||||||
|
end: 11
|
||||||
|
};
|
||||||
|
eventListener = jasmine.createSpy("eventListener");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Creates an independent time context", () => {
|
||||||
|
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
|
||||||
|
let timeContext = api.getIndependentContext(domainObjectKey);
|
||||||
|
expect(timeContext.bounds()).toEqual(independentBounds);
|
||||||
|
destroyTimeContext();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Gets an independent time context given the objectPath", () => {
|
||||||
|
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
|
||||||
|
let timeContext = api.getContextForView([{
|
||||||
|
identifier: {
|
||||||
|
namespace: '',
|
||||||
|
key: 'blah'
|
||||||
|
}
|
||||||
|
}, { identifier: domainObjectKey }]);
|
||||||
|
expect(timeContext.bounds()).toEqual(independentBounds);
|
||||||
|
destroyTimeContext();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("defaults to the global time context given the objectPath", () => {
|
||||||
|
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
|
||||||
|
let timeContext = api.getContextForView([{
|
||||||
|
identifier: {
|
||||||
|
namespace: '',
|
||||||
|
key: 'blah'
|
||||||
|
}
|
||||||
|
}]);
|
||||||
|
expect(timeContext.bounds()).toEqual(bounds);
|
||||||
|
destroyTimeContext();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Allows setting of valid bounds", function () {
|
||||||
|
bounds = {
|
||||||
|
start: 0,
|
||||||
|
end: 1
|
||||||
|
};
|
||||||
|
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
|
||||||
|
let timeContext = api.getContextForView([{identifier: domainObjectKey}]);
|
||||||
|
expect(timeContext.bounds()).not.toEqual(bounds);
|
||||||
|
timeContext.bounds(bounds);
|
||||||
|
expect(timeContext.bounds()).toEqual(bounds);
|
||||||
|
destroyTimeContext();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Disallows setting of invalid bounds", function () {
|
||||||
|
bounds = {
|
||||||
|
start: 1,
|
||||||
|
end: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
|
||||||
|
let timeContext = api.getContextForView([{identifier: domainObjectKey}]);
|
||||||
|
expect(timeContext.bounds()).not.toBe(bounds);
|
||||||
|
|
||||||
|
expect(timeContext.bounds.bind(timeContext, bounds)).toThrow();
|
||||||
|
expect(timeContext.bounds()).not.toEqual(bounds);
|
||||||
|
|
||||||
|
bounds = {start: 1};
|
||||||
|
expect(timeContext.bounds()).not.toEqual(bounds);
|
||||||
|
expect(timeContext.bounds.bind(timeContext, bounds)).toThrow();
|
||||||
|
expect(timeContext.bounds()).not.toEqual(bounds);
|
||||||
|
destroyTimeContext();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Emits an event when bounds change", function () {
|
||||||
|
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
|
||||||
|
let timeContext = api.getContextForView([{identifier: domainObjectKey}]);
|
||||||
|
expect(eventListener).not.toHaveBeenCalled();
|
||||||
|
timeContext.on('bounds', eventListener);
|
||||||
|
timeContext.bounds(bounds);
|
||||||
|
expect(eventListener).toHaveBeenCalledWith(bounds, false);
|
||||||
|
destroyTimeContext();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(" when using real time clock", function () {
|
||||||
|
const mockOffsets = {
|
||||||
|
start: 10,
|
||||||
|
end: 11
|
||||||
|
};
|
||||||
|
|
||||||
|
it("Emits an event when bounds change based on current value", function () {
|
||||||
|
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
|
||||||
|
let timeContext = api.getContextForView([{identifier: domainObjectKey}]);
|
||||||
|
expect(eventListener).not.toHaveBeenCalled();
|
||||||
|
timeContext.clock('someClockKey', mockOffsets);
|
||||||
|
timeContext.on('bounds', eventListener);
|
||||||
|
timeContext.tick(10);
|
||||||
|
expect(eventListener).toHaveBeenCalledWith({
|
||||||
|
start: 20,
|
||||||
|
end: 21
|
||||||
|
}, true);
|
||||||
|
destroyTimeContext();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
@ -40,7 +40,6 @@ import PreviewAction from "@/ui/preview/PreviewAction";
|
|||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
|
|
||||||
const PADDING = 1;
|
const PADDING = 1;
|
||||||
const RESIZE_POLL_INTERVAL = 200;
|
|
||||||
const ROW_HEIGHT = 100;
|
const ROW_HEIGHT = 100;
|
||||||
const IMAGE_WIDTH_THRESHOLD = 40;
|
const IMAGE_WIDTH_THRESHOLD = 40;
|
||||||
|
|
||||||
|
@ -32,19 +32,19 @@ const TEN_MINUTES = ONE_MINUTE * 10;
|
|||||||
const MAIN_IMAGE_CLASS = '.js-imageryView-image';
|
const MAIN_IMAGE_CLASS = '.js-imageryView-image';
|
||||||
const NEW_IMAGE_CLASS = '.c-imagery__age.c-imagery--new';
|
const NEW_IMAGE_CLASS = '.c-imagery__age.c-imagery--new';
|
||||||
const REFRESH_CSS_MS = 500;
|
const REFRESH_CSS_MS = 500;
|
||||||
const TOLERANCE = 0.50;
|
// const TOLERANCE = 0.50;
|
||||||
|
|
||||||
function comparisonFunction(valueOne, valueTwo) {
|
// function comparisonFunction(valueOne, valueTwo) {
|
||||||
let larger = valueOne;
|
// let larger = valueOne;
|
||||||
let smaller = valueTwo;
|
// let smaller = valueTwo;
|
||||||
|
//
|
||||||
if (larger < smaller) {
|
// if (larger < smaller) {
|
||||||
larger = valueTwo;
|
// larger = valueTwo;
|
||||||
smaller = valueOne;
|
// smaller = valueOne;
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
return (larger - smaller) < TOLERANCE;
|
// return (larger - smaller) < TOLERANCE;
|
||||||
}
|
// }
|
||||||
|
|
||||||
function getImageInfo(doc) {
|
function getImageInfo(doc) {
|
||||||
let imageElement = doc.querySelectorAll(MAIN_IMAGE_CLASS)[0];
|
let imageElement = doc.querySelectorAll(MAIN_IMAGE_CLASS)[0];
|
||||||
|
@ -67,7 +67,7 @@ export default {
|
|||||||
TimelineAxis,
|
TimelineAxis,
|
||||||
SwimLane
|
SwimLane
|
||||||
},
|
},
|
||||||
inject: ['openmct', 'domainObject'],
|
inject: ['openmct', 'domainObject', 'path'],
|
||||||
props: {
|
props: {
|
||||||
options: {
|
options: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@ -99,21 +99,37 @@ export default {
|
|||||||
this.canvasContext = this.canvas.getContext('2d');
|
this.canvasContext = this.canvas.getContext('2d');
|
||||||
|
|
||||||
this.setDimensions();
|
this.setDimensions();
|
||||||
this.updateViewBounds();
|
this.setTimeContext();
|
||||||
this.openmct.time.on("timeSystem", this.setScaleAndPlotActivities);
|
|
||||||
this.openmct.time.on("bounds", this.updateViewBounds);
|
|
||||||
this.resizeTimer = setInterval(this.resize, RESIZE_POLL_INTERVAL);
|
this.resizeTimer = setInterval(this.resize, RESIZE_POLL_INTERVAL);
|
||||||
this.unlisten = this.openmct.objects.observe(this.domainObject, '*', this.observeForChanges);
|
this.unlisten = this.openmct.objects.observe(this.domainObject, '*', this.observeForChanges);
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
clearInterval(this.resizeTimer);
|
clearInterval(this.resizeTimer);
|
||||||
this.openmct.time.off("timeSystem", this.setScaleAndPlotActivities);
|
this.stopFollowingTimeContext();
|
||||||
this.openmct.time.off("bounds", this.updateViewBounds);
|
|
||||||
if (this.unlisten) {
|
if (this.unlisten) {
|
||||||
this.unlisten();
|
this.unlisten();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
setTimeContext() {
|
||||||
|
this.stopFollowingTimeContext();
|
||||||
|
this.timeContext = this.openmct.time.getContextForView(this.path);
|
||||||
|
this.timeContext.on("timeContext", this.setTimeContext);
|
||||||
|
this.followTimeContext();
|
||||||
|
},
|
||||||
|
followTimeContext() {
|
||||||
|
this.updateViewBounds(this.timeContext.bounds());
|
||||||
|
|
||||||
|
this.timeContext.on("timeSystem", this.setScaleAndPlotActivities);
|
||||||
|
this.timeContext.on("bounds", this.updateViewBounds);
|
||||||
|
},
|
||||||
|
stopFollowingTimeContext() {
|
||||||
|
if (this.timeContext) {
|
||||||
|
this.timeContext.off("timeSystem", this.setScaleAndPlotActivities);
|
||||||
|
this.timeContext.off("bounds", this.updateViewBounds);
|
||||||
|
this.timeContext.off("timeContext", this.setTimeContext);
|
||||||
|
}
|
||||||
|
},
|
||||||
observeForChanges(mutatedObject) {
|
observeForChanges(mutatedObject) {
|
||||||
this.getPlanData(mutatedObject);
|
this.getPlanData(mutatedObject);
|
||||||
this.setScaleAndPlotActivities();
|
this.setScaleAndPlotActivities();
|
||||||
@ -141,13 +157,9 @@ export default {
|
|||||||
getPlanData(domainObject) {
|
getPlanData(domainObject) {
|
||||||
this.planData = getValidatedPlan(domainObject);
|
this.planData = getValidatedPlan(domainObject);
|
||||||
},
|
},
|
||||||
updateViewBounds() {
|
updateViewBounds(bounds) {
|
||||||
this.viewBounds = this.openmct.time.bounds();
|
if (bounds) {
|
||||||
if (!this.options.compact) {
|
this.viewBounds = Object.create(bounds);
|
||||||
//Add a 50% padding to the end bounds to look ahead
|
|
||||||
let timespan = (this.viewBounds.end - this.viewBounds.start);
|
|
||||||
let padding = timespan / 2;
|
|
||||||
this.viewBounds.end = this.viewBounds.end + padding;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.timeSystem === undefined) {
|
if (this.timeSystem === undefined) {
|
||||||
|
@ -54,7 +54,8 @@ export default function PlanViewProvider(openmct) {
|
|||||||
},
|
},
|
||||||
provide: {
|
provide: {
|
||||||
openmct,
|
openmct,
|
||||||
domainObject
|
domainObject,
|
||||||
|
path: objectPath
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -173,7 +173,7 @@ export default {
|
|||||||
MctTicks,
|
MctTicks,
|
||||||
MctChart
|
MctChart
|
||||||
},
|
},
|
||||||
inject: ['openmct', 'domainObject'],
|
inject: ['openmct', 'domainObject', 'path'],
|
||||||
props: {
|
props: {
|
||||||
options: {
|
options: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@ -244,6 +244,9 @@ export default {
|
|||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
eventHelpers.extend(this);
|
eventHelpers.extend(this);
|
||||||
|
this.updateRealTime = this.updateRealTime.bind(this);
|
||||||
|
this.updateDisplayBounds = this.updateDisplayBounds.bind(this);
|
||||||
|
this.setTimeContext = this.setTimeContext.bind(this);
|
||||||
|
|
||||||
this.config = this.getConfig();
|
this.config = this.getConfig();
|
||||||
this.legend = this.config.legend;
|
this.legend = this.config.legend;
|
||||||
@ -261,7 +264,7 @@ export default {
|
|||||||
this.removeStatusListener = this.openmct.status.observe(this.domainObject.identifier, this.updateStatus);
|
this.removeStatusListener = this.openmct.status.observe(this.domainObject.identifier, this.updateStatus);
|
||||||
|
|
||||||
this.openmct.objectViews.on('clearData', this.clearData);
|
this.openmct.objectViews.on('clearData', this.clearData);
|
||||||
this.followTimeConductor();
|
this.setTimeContext();
|
||||||
|
|
||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
|
|
||||||
@ -274,11 +277,27 @@ export default {
|
|||||||
this.destroy();
|
this.destroy();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
followTimeConductor() {
|
setTimeContext() {
|
||||||
this.openmct.time.on('clock', this.updateRealTime);
|
this.stopFollowingTimeContext();
|
||||||
this.openmct.time.on('bounds', this.updateDisplayBounds);
|
|
||||||
|
this.timeContext = this.openmct.time.getContextForView(this.path);
|
||||||
|
this.timeContext.on('timeContext', this.setTimeContext);
|
||||||
|
this.followTimeContext();
|
||||||
|
|
||||||
|
},
|
||||||
|
followTimeContext() {
|
||||||
|
this.updateDisplayBounds(this.timeContext.bounds());
|
||||||
|
this.timeContext.on('clock', this.updateRealTime);
|
||||||
|
this.timeContext.on('bounds', this.updateDisplayBounds);
|
||||||
this.synchronized(true);
|
this.synchronized(true);
|
||||||
},
|
},
|
||||||
|
stopFollowingTimeContext() {
|
||||||
|
if (this.timeContext) {
|
||||||
|
this.timeContext.off("clock", this.updateRealTime);
|
||||||
|
this.timeContext.off("bounds", this.updateDisplayBounds);
|
||||||
|
this.timeContext.off("timeContext", this.setTimeContext);
|
||||||
|
}
|
||||||
|
},
|
||||||
getConfig() {
|
getConfig() {
|
||||||
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||||
let config = configStore.get(configId);
|
let config = configStore.get(configId);
|
||||||
@ -485,7 +504,7 @@ export default {
|
|||||||
* displays can update accordingly.
|
* displays can update accordingly.
|
||||||
*/
|
*/
|
||||||
synchronized(value) {
|
synchronized(value) {
|
||||||
const isLocalClock = this.openmct.time.clock();
|
const isLocalClock = this.timeContext.clock();
|
||||||
|
|
||||||
if (typeof value !== 'undefined') {
|
if (typeof value !== 'undefined') {
|
||||||
this._synchronized = value;
|
this._synchronized = value;
|
||||||
@ -958,7 +977,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
showSynchronizeDialog() {
|
showSynchronizeDialog() {
|
||||||
const isLocalClock = this.openmct.time.clock();
|
const isLocalClock = this.timeContext.clock();
|
||||||
if (isLocalClock !== undefined) {
|
if (isLocalClock !== undefined) {
|
||||||
const message = `
|
const message = `
|
||||||
This action will change the Time Conductor to Fixed Timespan mode with this plot view's current time bounds.
|
This action will change the Time Conductor to Fixed Timespan mode with this plot view's current time bounds.
|
||||||
@ -993,9 +1012,9 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
synchronizeTimeConductor() {
|
synchronizeTimeConductor() {
|
||||||
this.openmct.time.stopClock();
|
this.timeContext.stopClock();
|
||||||
const range = this.config.xAxis.get('displayRange');
|
const range = this.config.xAxis.get('displayRange');
|
||||||
this.openmct.time.bounds({
|
this.timeContext.bounds({
|
||||||
start: range.min,
|
start: range.min,
|
||||||
end: range.max
|
end: range.max
|
||||||
});
|
});
|
||||||
@ -1006,6 +1025,7 @@ export default {
|
|||||||
configStore.deleteStore(this.config.id);
|
configStore.deleteStore(this.config.id);
|
||||||
|
|
||||||
this.stopListening();
|
this.stopListening();
|
||||||
|
|
||||||
if (this.checkForSize) {
|
if (this.checkForSize) {
|
||||||
clearInterval(this.checkForSize);
|
clearInterval(this.checkForSize);
|
||||||
delete this.checkForSize;
|
delete this.checkForSize;
|
||||||
@ -1021,8 +1041,7 @@ export default {
|
|||||||
|
|
||||||
this.plotContainerResizeObserver.disconnect();
|
this.plotContainerResizeObserver.disconnect();
|
||||||
|
|
||||||
this.openmct.time.off('clock', this.updateRealTime);
|
this.stopFollowingTimeContext();
|
||||||
this.openmct.time.off('bounds', this.updateDisplayBounds);
|
|
||||||
this.openmct.objectViews.off('clearData', this.clearData);
|
this.openmct.objectViews.off('clearData', this.clearData);
|
||||||
},
|
},
|
||||||
updateStatus(status) {
|
updateStatus(status) {
|
||||||
|
@ -80,7 +80,7 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
MctPlot
|
MctPlot
|
||||||
},
|
},
|
||||||
inject: ['openmct', 'domainObject'],
|
inject: ['openmct', 'domainObject', 'path'],
|
||||||
props: {
|
props: {
|
||||||
options: {
|
options: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
@ -68,7 +68,8 @@ export default function PlotViewProvider(openmct) {
|
|||||||
},
|
},
|
||||||
provide: {
|
provide: {
|
||||||
openmct,
|
openmct,
|
||||||
domainObject
|
domainObject,
|
||||||
|
path: objectPath
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -53,7 +53,8 @@ export default function OverlayPlotViewProvider(openmct) {
|
|||||||
},
|
},
|
||||||
provide: {
|
provide: {
|
||||||
openmct,
|
openmct,
|
||||||
domainObject
|
domainObject,
|
||||||
|
path: objectPath
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -570,7 +570,8 @@ describe("the plugin", function () {
|
|||||||
provide: {
|
provide: {
|
||||||
openmct: openmct,
|
openmct: openmct,
|
||||||
domainObject: stackedPlotObject,
|
domainObject: stackedPlotObject,
|
||||||
composition: openmct.composition.get(stackedPlotObject)
|
composition: openmct.composition.get(stackedPlotObject),
|
||||||
|
path: [stackedPlotObject]
|
||||||
},
|
},
|
||||||
template: "<stacked-plot></stacked-plot>"
|
template: "<stacked-plot></stacked-plot>"
|
||||||
});
|
});
|
||||||
|
@ -75,7 +75,7 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
StackedPlotItem
|
StackedPlotItem
|
||||||
},
|
},
|
||||||
inject: ['openmct', 'domainObject', 'composition'],
|
inject: ['openmct', 'domainObject', 'composition', 'path'],
|
||||||
props: {
|
props: {
|
||||||
options: {
|
options: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
@ -28,7 +28,7 @@ import MctPlot from '../MctPlot.vue';
|
|||||||
import Vue from "vue";
|
import Vue from "vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
inject: ['openmct', 'domainObject'],
|
inject: ['openmct', 'domainObject', 'path'],
|
||||||
props: {
|
props: {
|
||||||
object: {
|
object: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@ -94,6 +94,7 @@ export default {
|
|||||||
|
|
||||||
const openmct = this.openmct;
|
const openmct = this.openmct;
|
||||||
const object = this.object;
|
const object = this.object;
|
||||||
|
const path = this.path;
|
||||||
|
|
||||||
const getProps = this.getProps;
|
const getProps = this.getProps;
|
||||||
let viewContainer = document.createElement('div');
|
let viewContainer = document.createElement('div');
|
||||||
@ -106,7 +107,8 @@ export default {
|
|||||||
},
|
},
|
||||||
provide: {
|
provide: {
|
||||||
openmct,
|
openmct,
|
||||||
domainObject: object
|
domainObject: object,
|
||||||
|
path
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -55,7 +55,8 @@ export default function StackedPlotViewProvider(openmct) {
|
|||||||
provide: {
|
provide: {
|
||||||
openmct,
|
openmct,
|
||||||
domainObject,
|
domainObject,
|
||||||
composition: openmct.composition.get(domainObject)
|
composition: openmct.composition.get(domainObject),
|
||||||
|
path: objectPath
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -29,144 +29,36 @@
|
|||||||
isFixed ? 'is-fixed-mode' : 'is-realtime-mode'
|
isFixed ? 'is-fixed-mode' : 'is-realtime-mode'
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<form
|
<div class="c-conductor__time-bounds">
|
||||||
ref="conductorForm"
|
<conductor-inputs-fixed v-if="isFixed"
|
||||||
class="u-contents"
|
@updated="saveFixedOffsets"
|
||||||
@submit.prevent="updateTimeFromConductor"
|
/>
|
||||||
>
|
<conductor-inputs-realtime v-else
|
||||||
<div class="c-conductor__time-bounds">
|
@updated="saveClockOffsets"
|
||||||
<button
|
/>
|
||||||
ref="submitButton"
|
<ConductorModeIcon class="c-conductor__mode-icon" />
|
||||||
class="c-input--submit"
|
<conductor-axis
|
||||||
type="submit"
|
class="c-conductor__ticks"
|
||||||
></button>
|
:view-bounds="viewBounds"
|
||||||
<ConductorModeIcon class="c-conductor__mode-icon" />
|
:is-fixed="isFixed"
|
||||||
|
:alt-pressed="altPressed"
|
||||||
<div
|
@endPan="endPan"
|
||||||
v-if="isFixed"
|
@endZoom="endZoom"
|
||||||
class="c-ctrl-wrapper c-conductor-input c-conductor__start-fixed"
|
@panAxis="pan"
|
||||||
>
|
@zoomAxis="zoom"
|
||||||
<!-- Fixed start -->
|
/>
|
||||||
<div class="c-conductor__start-fixed__label">
|
</div>
|
||||||
Start
|
<div class="c-conductor__controls">
|
||||||
</div>
|
<ConductorMode class="c-conductor__mode-select" />
|
||||||
<input
|
<ConductorTimeSystem class="c-conductor__time-system-select" />
|
||||||
ref="startDate"
|
<ConductorHistory
|
||||||
v-model="formattedBounds.start"
|
class="c-conductor__history-select"
|
||||||
class="c-input--datetime"
|
:offsets="openmct.time.clockOffsets()"
|
||||||
type="text"
|
:bounds="bounds"
|
||||||
autocorrect="off"
|
:time-system="timeSystem"
|
||||||
spellcheck="false"
|
:mode="timeMode"
|
||||||
@change="validateAllBounds('startDate'); submitForm()"
|
/>
|
||||||
>
|
</div>
|
||||||
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -174,23 +66,23 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import ConductorMode from './ConductorMode.vue';
|
import ConductorMode from './ConductorMode.vue';
|
||||||
import ConductorTimeSystem from './ConductorTimeSystem.vue';
|
import ConductorTimeSystem from './ConductorTimeSystem.vue';
|
||||||
import DatePicker from './DatePicker.vue';
|
|
||||||
import ConductorAxis from './ConductorAxis.vue';
|
import ConductorAxis from './ConductorAxis.vue';
|
||||||
import ConductorModeIcon from './ConductorModeIcon.vue';
|
import ConductorModeIcon from './ConductorModeIcon.vue';
|
||||||
import ConductorHistory from './ConductorHistory.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';
|
const DEFAULT_DURATION_FORMATTER = 'duration';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
ConductorInputsRealtime,
|
||||||
|
ConductorInputsFixed,
|
||||||
ConductorMode,
|
ConductorMode,
|
||||||
ConductorTimeSystem,
|
ConductorTimeSystem,
|
||||||
DatePicker,
|
|
||||||
ConductorAxis,
|
ConductorAxis,
|
||||||
ConductorModeIcon,
|
ConductorModeIcon,
|
||||||
ConductorHistory,
|
ConductorHistory
|
||||||
TimePopup
|
|
||||||
},
|
},
|
||||||
inject: ['openmct', 'configuration'],
|
inject: ['openmct', 'configuration'],
|
||||||
data() {
|
data() {
|
||||||
@ -242,7 +134,6 @@ export default {
|
|||||||
this.openmct.time.on('bounds', _.throttle(this.handleNewBounds, 300));
|
this.openmct.time.on('bounds', _.throttle(this.handleNewBounds, 300));
|
||||||
this.openmct.time.on('timeSystem', this.setTimeSystem);
|
this.openmct.time.on('timeSystem', this.setTimeSystem);
|
||||||
this.openmct.time.on('clock', this.setViewFromClock);
|
this.openmct.time.on('clock', this.setViewFromClock);
|
||||||
this.openmct.time.on('clockOffsets', this.setViewFromOffsets);
|
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
document.removeEventListener('keydown', this.handleKeyDown);
|
document.removeEventListener('keydown', this.handleKeyDown);
|
||||||
@ -297,42 +188,8 @@ export default {
|
|||||||
timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
|
timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
|
||||||
this.isUTCBased = timeSystem.isUTCBased;
|
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) {
|
setViewFromClock(clock) {
|
||||||
this.clearAllValidation();
|
// this.clearAllValidation();
|
||||||
this.isFixed = clock === undefined;
|
this.isFixed = clock === undefined;
|
||||||
},
|
},
|
||||||
setViewFromBounds(bounds) {
|
setViewFromBounds(bounds) {
|
||||||
@ -341,158 +198,16 @@ export default {
|
|||||||
this.viewBounds.start = bounds.start;
|
this.viewBounds.start = bounds.start;
|
||||||
this.viewBounds.end = bounds.end;
|
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) {
|
getFormatter(key) {
|
||||||
return this.openmct.telemetry.getValueFormatter({
|
return this.openmct.telemetry.getValueFormatter({
|
||||||
format: key
|
format: key
|
||||||
}).formatter;
|
}).formatter;
|
||||||
},
|
},
|
||||||
startDateSelected(date) {
|
saveClockOffsets(offsets) {
|
||||||
this.formattedBounds.start = this.timeFormatter.format(date);
|
this.openmct.time.clockOffsets(offsets);
|
||||||
this.validateAllBounds('startDate');
|
|
||||||
this.submitForm();
|
|
||||||
},
|
},
|
||||||
endDateSelected(date) {
|
saveFixedOffsets(bounds) {
|
||||||
this.formattedBounds.end = this.timeFormatter.format(date);
|
this.openmct.time.bounds(bounds);
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
280
src/plugins/timeConductor/ConductorInputsFixed.vue
Normal file
280
src/plugins/timeConductor/ConductorInputsFixed.vue
Normal file
@ -0,0 +1,280 @@
|
|||||||
|
<template>
|
||||||
|
<form ref="fixedDeltaInput"
|
||||||
|
class="c-conductor__inputs"
|
||||||
|
@submit.prevent="updateTimeFromConductor"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
ref="submitButton"
|
||||||
|
class="c-input--submit"
|
||||||
|
type="submit"
|
||||||
|
></button>
|
||||||
|
<div
|
||||||
|
class="c-ctrl-wrapper c-conductor-input c-conductor__start-fixed"
|
||||||
|
>
|
||||||
|
<!-- Fixed start -->
|
||||||
|
<div class="c-conductor__start-fixed__label">
|
||||||
|
Start
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
ref="startDate"
|
||||||
|
v-model="formattedBounds.start"
|
||||||
|
class="c-input--datetime"
|
||||||
|
type="text"
|
||||||
|
autocorrect="off"
|
||||||
|
spellcheck="false"
|
||||||
|
@change="validateAllBounds('startDate'); submitForm()"
|
||||||
|
>
|
||||||
|
<date-picker
|
||||||
|
v-if="isUTCBased"
|
||||||
|
class="c-ctrl-wrapper--menus-left"
|
||||||
|
:bottom="keyString !== undefined"
|
||||||
|
:default-date-time="formattedBounds.start"
|
||||||
|
:formatter="timeFormatter"
|
||||||
|
@date-selected="startDateSelected"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="c-ctrl-wrapper c-conductor-input c-conductor__end-fixed">
|
||||||
|
<!-- Fixed end and RT 'last update' display -->
|
||||||
|
<div class="c-conductor__end-fixed__label">
|
||||||
|
End
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
ref="endDate"
|
||||||
|
v-model="formattedBounds.end"
|
||||||
|
class="c-input--datetime"
|
||||||
|
type="text"
|
||||||
|
autocorrect="off"
|
||||||
|
spellcheck="false"
|
||||||
|
@change="validateAllBounds('endDate'); submitForm()"
|
||||||
|
>
|
||||||
|
<date-picker
|
||||||
|
v-if="isUTCBased"
|
||||||
|
class="c-ctrl-wrapper--menus-left"
|
||||||
|
:bottom="keyString !== undefined"
|
||||||
|
:default-date-time="formattedBounds.end"
|
||||||
|
:formatter="timeFormatter"
|
||||||
|
@date-selected="endDateSelected"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="submit"
|
||||||
|
class="invisible"
|
||||||
|
>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
import DatePicker from "./DatePicker.vue";
|
||||||
|
import _ from "lodash";
|
||||||
|
|
||||||
|
const DEFAULT_DURATION_FORMATTER = 'duration';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
DatePicker
|
||||||
|
},
|
||||||
|
inject: ['openmct'],
|
||||||
|
props: {
|
||||||
|
keyString: {
|
||||||
|
type: String,
|
||||||
|
default() {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
let timeSystem = this.openmct.time.timeSystem();
|
||||||
|
let durationFormatter = this.getFormatter(timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
|
||||||
|
let timeFormatter = this.getFormatter(timeSystem.timeFormat);
|
||||||
|
let bounds = this.bounds || this.openmct.time.bounds();
|
||||||
|
|
||||||
|
return {
|
||||||
|
showTCInputStart: true,
|
||||||
|
showTCInputEnd: true,
|
||||||
|
durationFormatter,
|
||||||
|
timeFormatter,
|
||||||
|
bounds: {
|
||||||
|
start: bounds.start,
|
||||||
|
end: bounds.end
|
||||||
|
},
|
||||||
|
formattedBounds: {
|
||||||
|
start: timeFormatter.format(bounds.start),
|
||||||
|
end: timeFormatter.format(bounds.end)
|
||||||
|
},
|
||||||
|
isUTCBased: timeSystem.isUTCBased
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.handleNewBounds = _.throttle(this.handleNewBounds, 300);
|
||||||
|
this.setTimeSystem(JSON.parse(JSON.stringify(this.openmct.time.timeSystem())));
|
||||||
|
this.openmct.time.on('timeSystem', this.setTimeSystem);
|
||||||
|
this.setTimeContext();
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.clearAllValidation();
|
||||||
|
this.openmct.time.off('timeSystem', this.setTimeSystem);
|
||||||
|
this.stopFollowingTimeContext();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setTimeContext() {
|
||||||
|
this.stopFollowingTimeContext();
|
||||||
|
this.timeContext = this.openmct.time.getContextForView(this.keyString ? [{identifier: this.keyString}] : []);
|
||||||
|
this.timeContext.on('timeContext', this.setTimeContext);
|
||||||
|
|
||||||
|
this.handleNewBounds(this.timeContext.bounds());
|
||||||
|
this.timeContext.on('bounds', this.handleNewBounds);
|
||||||
|
this.timeContext.on('clock', this.clearAllValidation);
|
||||||
|
},
|
||||||
|
stopFollowingTimeContext() {
|
||||||
|
if (this.timeContext) {
|
||||||
|
this.timeContext.off('bounds', this.handleNewBounds);
|
||||||
|
this.timeContext.off('clock', this.clearAllValidation);
|
||||||
|
this.timeContext.off('timeContext', this.setTimeContext);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleNewBounds(bounds) {
|
||||||
|
this.setBounds(bounds);
|
||||||
|
this.setViewFromBounds(bounds);
|
||||||
|
},
|
||||||
|
clearAllValidation() {
|
||||||
|
[this.$refs.startDate, this.$refs.endDate].forEach(this.clearValidationForInput);
|
||||||
|
},
|
||||||
|
clearValidationForInput(input) {
|
||||||
|
input.setCustomValidity('');
|
||||||
|
input.title = '';
|
||||||
|
},
|
||||||
|
setBounds(bounds) {
|
||||||
|
this.bounds = bounds;
|
||||||
|
},
|
||||||
|
setViewFromBounds(bounds) {
|
||||||
|
this.formattedBounds.start = this.timeFormatter.format(bounds.start);
|
||||||
|
this.formattedBounds.end = this.timeFormatter.format(bounds.end);
|
||||||
|
},
|
||||||
|
setTimeSystem(timeSystem) {
|
||||||
|
this.timeSystem = timeSystem;
|
||||||
|
this.timeFormatter = this.getFormatter(timeSystem.timeFormat);
|
||||||
|
this.durationFormatter = this.getFormatter(
|
||||||
|
timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
|
||||||
|
this.isUTCBased = timeSystem.isUTCBased;
|
||||||
|
},
|
||||||
|
getFormatter(key) {
|
||||||
|
return this.openmct.telemetry.getValueFormatter({
|
||||||
|
format: key
|
||||||
|
}).formatter;
|
||||||
|
},
|
||||||
|
setBoundsFromView($event) {
|
||||||
|
if (this.$refs.fixedDeltaInput.checkValidity()) {
|
||||||
|
let start = this.timeFormatter.parse(this.formattedBounds.start);
|
||||||
|
let end = this.timeFormatter.parse(this.formattedBounds.end);
|
||||||
|
|
||||||
|
this.$emit('updated', {
|
||||||
|
start: start,
|
||||||
|
end: end
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($event) {
|
||||||
|
$event.preventDefault();
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
submitForm() {
|
||||||
|
// Allow Vue model to catch up to user input.
|
||||||
|
// Submitting form will cause validation messages to display (but only if triggered by button click)
|
||||||
|
this.$nextTick(() => this.$refs.submitButton.click());
|
||||||
|
},
|
||||||
|
updateTimeFromConductor() {
|
||||||
|
this.setBoundsFromView();
|
||||||
|
},
|
||||||
|
validateAllBounds(ref) {
|
||||||
|
if (!this.areBoundsFormatsValid()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let validationResult = {
|
||||||
|
valid: true
|
||||||
|
};
|
||||||
|
const currentInput = this.$refs[ref];
|
||||||
|
|
||||||
|
return [this.$refs.startDate, this.$refs.endDate].every((input) => {
|
||||||
|
let boundsValues = {
|
||||||
|
start: this.timeFormatter.parse(this.formattedBounds.start),
|
||||||
|
end: this.timeFormatter.parse(this.formattedBounds.end)
|
||||||
|
};
|
||||||
|
//TODO: Do we need limits here? We have conductor limits disabled right now
|
||||||
|
// const limit = this.getBoundsLimit();
|
||||||
|
const limit = false;
|
||||||
|
|
||||||
|
if (this.timeSystem.isUTCBased && limit
|
||||||
|
&& boundsValues.end - boundsValues.start > limit) {
|
||||||
|
if (input === currentInput) {
|
||||||
|
validationResult = {
|
||||||
|
valid: false,
|
||||||
|
message: "Start and end difference exceeds allowable limit"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (input === currentInput) {
|
||||||
|
validationResult = this.openmct.time.validateBounds(boundsValues);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.handleValidationResults(input, validationResult);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
areBoundsFormatsValid() {
|
||||||
|
let validationResult = {
|
||||||
|
valid: true
|
||||||
|
};
|
||||||
|
|
||||||
|
return [this.$refs.startDate, this.$refs.endDate].every((input) => {
|
||||||
|
const formattedDate = input === this.$refs.startDate
|
||||||
|
? this.formattedBounds.start
|
||||||
|
: this.formattedBounds.end
|
||||||
|
;
|
||||||
|
|
||||||
|
if (!this.timeFormatter.validate(formattedDate)) {
|
||||||
|
validationResult = {
|
||||||
|
valid: false,
|
||||||
|
message: 'Invalid date'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.handleValidationResults(input, validationResult);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getBoundsLimit() {
|
||||||
|
const configuration = this.configuration.menuOptions
|
||||||
|
.filter(option => option.timeSystem === this.timeSystem.key)
|
||||||
|
.find(option => option.limit);
|
||||||
|
|
||||||
|
const limit = configuration ? configuration.limit : undefined;
|
||||||
|
|
||||||
|
return limit;
|
||||||
|
},
|
||||||
|
handleValidationResults(input, validationResult) {
|
||||||
|
if (validationResult.valid !== true) {
|
||||||
|
input.setCustomValidity(validationResult.message);
|
||||||
|
input.title = validationResult.message;
|
||||||
|
} else {
|
||||||
|
input.setCustomValidity('');
|
||||||
|
input.title = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return validationResult.valid;
|
||||||
|
},
|
||||||
|
startDateSelected(date) {
|
||||||
|
this.formattedBounds.start = this.timeFormatter.format(date);
|
||||||
|
this.validateAllBounds('startDate');
|
||||||
|
this.submitForm();
|
||||||
|
},
|
||||||
|
endDateSelected(date) {
|
||||||
|
this.formattedBounds.end = this.timeFormatter.format(date);
|
||||||
|
this.validateAllBounds('endDate');
|
||||||
|
this.submitForm();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
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>
|
<template>
|
||||||
<div
|
<div
|
||||||
ref="calendarHolder"
|
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
|
<a
|
||||||
class="c-icon-button icon-calendar"
|
class="c-icon-button icon-calendar"
|
||||||
@ -118,6 +119,12 @@ export default {
|
|||||||
formatter: {
|
formatter: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
|
},
|
||||||
|
bottom: {
|
||||||
|
type: Boolean,
|
||||||
|
default() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data: function () {
|
data: function () {
|
||||||
|
@ -8,6 +8,10 @@
|
|||||||
|
|
||||||
/*********************************************** CONDUCTOR LAYOUT */
|
/*********************************************** CONDUCTOR LAYOUT */
|
||||||
.c-conductor {
|
.c-conductor {
|
||||||
|
&__inputs {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
&__time-bounds {
|
&__time-bounds {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-column-gap: $interiorMargin;
|
grid-column-gap: $interiorMargin;
|
||||||
@ -50,13 +54,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[class*='-delta'] {
|
|
||||||
&:before {
|
|
||||||
content: $glyph-icon-clock;
|
|
||||||
font-family: symbolsfont;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.is-fixed-mode {
|
&.is-fixed-mode {
|
||||||
.c-conductor-axis {
|
.c-conductor-axis {
|
||||||
&__zoom-indicator {
|
&__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 {
|
.c-conductor-input {
|
||||||
color: $colorInputFg;
|
color: $colorInputFg;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -250,18 +268,22 @@
|
|||||||
box-shadow: $shdwMenu;
|
box-shadow: $shdwMenu;
|
||||||
padding: $interiorMargin;
|
padding: $interiorMargin;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
left: 8px;
|
||||||
bottom: 24px;
|
bottom: 24px;
|
||||||
z-index: 99;
|
z-index: 99;
|
||||||
|
|
||||||
&[class*='--start'] {
|
&[class*='--bottom'] {
|
||||||
left: -25px;
|
bottom: auto;
|
||||||
}
|
top: 24px;
|
||||||
|
|
||||||
&[class*='--end'] {
|
|
||||||
right: 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.l-shell__time-conductor .pr-tc-input-menu--end {
|
||||||
|
left: auto;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
[class^='pr-time'] {
|
[class^='pr-time'] {
|
||||||
&[class*='label'] {
|
&[class*='label'] {
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
|
@ -0,0 +1,231 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT Web, Copyright (c) 2014-2021, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* Open MCT Web includes source code licensed under additional open source
|
||||||
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
* this source code distribution or the Licensing information page available
|
||||||
|
* at runtime from the About dialog for additional information.
|
||||||
|
*****************************************************************************/
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="c-conductor"
|
||||||
|
:class="[
|
||||||
|
isFixed ? 'is-fixed-mode' : independentTCEnabled ? 'is-realtime-mode' : 'is-fixed-mode'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<div class="c-conductor__time-bounds">
|
||||||
|
<toggle-switch
|
||||||
|
id="independentTCToggle"
|
||||||
|
:checked="independentTCEnabled"
|
||||||
|
:title="`${independentTCEnabled ? 'Disable' : 'Enable'} independent Time Conductor`"
|
||||||
|
@change="toggleIndependentTC"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ConductorModeIcon />
|
||||||
|
|
||||||
|
<div v-if="timeOptions && independentTCEnabled"
|
||||||
|
class="c-conductor__controls"
|
||||||
|
>
|
||||||
|
<Mode v-if="mode"
|
||||||
|
class="c-conductor__mode-select"
|
||||||
|
:key-string="domainObject.identifier.key"
|
||||||
|
:mode="timeOptions.mode"
|
||||||
|
:enabled="independentTCEnabled"
|
||||||
|
@modeChanged="saveMode"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<conductor-inputs-fixed v-if="isFixed"
|
||||||
|
:key-string="domainObject.identifier.key"
|
||||||
|
@updated="saveFixedOffsets"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<conductor-inputs-realtime v-else
|
||||||
|
:key-string="domainObject.identifier.key"
|
||||||
|
@updated="saveClockOffsets"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ConductorInputsFixed from "../ConductorInputsFixed.vue";
|
||||||
|
import ConductorInputsRealtime from "../ConductorInputsRealtime.vue";
|
||||||
|
import ConductorModeIcon from "@/plugins/timeConductor/ConductorModeIcon.vue";
|
||||||
|
import ToggleSwitch from '../../../ui/components/ToggleSwitch.vue';
|
||||||
|
import Mode from "./Mode.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Mode,
|
||||||
|
ConductorModeIcon,
|
||||||
|
ConductorInputsRealtime,
|
||||||
|
ConductorInputsFixed,
|
||||||
|
ToggleSwitch
|
||||||
|
},
|
||||||
|
inject: ['openmct'],
|
||||||
|
props: {
|
||||||
|
domainObject: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
timeOptions: this.domainObject.configuration.timeOptions || {
|
||||||
|
clockOffsets: this.openmct.time.clockOffsets(),
|
||||||
|
fixedOffsets: this.openmct.time.bounds()
|
||||||
|
},
|
||||||
|
mode: undefined,
|
||||||
|
independentTCEnabled: this.domainObject.configuration.useIndependentTime === true
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isFixed() {
|
||||||
|
if (!this.mode || !this.mode.key) {
|
||||||
|
return this.openmct.time.clock() === undefined;
|
||||||
|
} else {
|
||||||
|
return this.mode.key === 'fixed';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
domainObject: {
|
||||||
|
handler(domainObject) {
|
||||||
|
if (!domainObject.configuration.timeOptions || !this.independentTCEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.timeOptions.start !== domainObject.configuration.timeOptions.start
|
||||||
|
|| this.timeOptions.end !== domainObject.configuration.timeOptions.end) {
|
||||||
|
this.timeOptions = domainObject.configuration.timeOptions;
|
||||||
|
this.destroyIndependentTime();
|
||||||
|
this.registerIndependentTimeOffsets();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deep: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.setTimeContext();
|
||||||
|
|
||||||
|
if (this.timeOptions.mode) {
|
||||||
|
this.mode = this.timeOptions.mode;
|
||||||
|
} else {
|
||||||
|
this.timeOptions.mode = this.mode = this.timeContext.clock() === undefined ? { key: 'fixed' } : { key: Object.create(this.timeContext.clock()).key};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.independentTCEnabled) {
|
||||||
|
this.registerIndependentTimeOffsets();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.stopFollowingTimeContext();
|
||||||
|
this.destroyIndependentTime();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toggleIndependentTC() {
|
||||||
|
this.independentTCEnabled = !this.independentTCEnabled;
|
||||||
|
if (this.independentTCEnabled) {
|
||||||
|
this.registerIndependentTimeOffsets();
|
||||||
|
} else {
|
||||||
|
this.destroyIndependentTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$emit('stateChanged', this.independentTCEnabled);
|
||||||
|
},
|
||||||
|
setTimeContext() {
|
||||||
|
this.stopFollowingTimeContext();
|
||||||
|
this.timeContext = this.openmct.time.getContextForView([this.domainObject]);
|
||||||
|
this.timeContext.on('timeContext', this.setTimeContext);
|
||||||
|
this.timeContext.on('clock', this.setViewFromClock);
|
||||||
|
},
|
||||||
|
stopFollowingTimeContext() {
|
||||||
|
if (this.timeContext) {
|
||||||
|
this.timeContext.off('timeContext', this.setTimeContext);
|
||||||
|
this.timeContext.off('clock', this.setViewFromClock);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setViewFromClock(clock) {
|
||||||
|
if (!this.timeOptions.mode) {
|
||||||
|
this.setTimeOptions(clock);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setTimeOptions() {
|
||||||
|
if (!this.timeOptions || !this.timeOptions.mode) {
|
||||||
|
this.mode = this.timeContext.clock() === undefined ? { key: 'fixed' } : { key: Object.create(this.timeContext.clock()).key};
|
||||||
|
this.timeOptions = {
|
||||||
|
clockOffsets: this.timeContext.clockOffsets(),
|
||||||
|
fixedOffsets: this.timeContext.bounds()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
this.registerIndependentTimeOffsets();
|
||||||
|
},
|
||||||
|
saveFixedOffsets(offsets) {
|
||||||
|
const newOptions = Object.assign({}, this.timeOptions, {
|
||||||
|
fixedOffsets: offsets
|
||||||
|
});
|
||||||
|
|
||||||
|
this.updateTimeOptions(newOptions);
|
||||||
|
},
|
||||||
|
saveClockOffsets(offsets) {
|
||||||
|
const newOptions = Object.assign({}, this.timeOptions, {
|
||||||
|
clockOffsets: offsets
|
||||||
|
});
|
||||||
|
|
||||||
|
this.updateTimeOptions(newOptions);
|
||||||
|
},
|
||||||
|
saveMode(mode) {
|
||||||
|
this.mode = mode;
|
||||||
|
const newOptions = Object.assign({}, this.timeOptions, {
|
||||||
|
mode: this.mode
|
||||||
|
});
|
||||||
|
this.updateTimeOptions(newOptions);
|
||||||
|
},
|
||||||
|
updateTimeOptions(options) {
|
||||||
|
this.timeOptions = options;
|
||||||
|
if (!this.timeOptions.mode) {
|
||||||
|
this.timeOptions.mode = this.mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.registerIndependentTimeOffsets();
|
||||||
|
this.$emit('updated', this.timeOptions);
|
||||||
|
},
|
||||||
|
registerIndependentTimeOffsets() {
|
||||||
|
if (!this.timeOptions.mode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let offsets;
|
||||||
|
|
||||||
|
if (this.isFixed) {
|
||||||
|
offsets = this.timeOptions.fixedOffsets;
|
||||||
|
} else {
|
||||||
|
offsets = this.timeOptions.clockOffsets;
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||||
|
this.unregisterIndependentTime = this.openmct.time.addIndependentContext(key, offsets, this.isFixed ? undefined : this.mode.key);
|
||||||
|
},
|
||||||
|
destroyIndependentTime() {
|
||||||
|
if (this.unregisterIndependentTime) {
|
||||||
|
this.unregisterIndependentTime();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
222
src/plugins/timeConductor/independent/Mode.vue
Normal file
222
src/plugins/timeConductor/independent/Mode.vue
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT Web, Copyright (c) 2014-2021, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* Open MCT Web includes source code licensed under additional open source
|
||||||
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
* this source code distribution or the Licensing information page available
|
||||||
|
* at runtime from the About dialog for additional information.
|
||||||
|
*****************************************************************************/
|
||||||
|
<template>
|
||||||
|
<div ref="modeMenuButton"
|
||||||
|
class="c-ctrl-wrapper c-ctrl-wrapper--menus-up"
|
||||||
|
>
|
||||||
|
<div class="c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left">
|
||||||
|
<button v-if="selectedMode"
|
||||||
|
class="c-button--menu c-mode-button"
|
||||||
|
@click.prevent.stop="showModesMenu"
|
||||||
|
>
|
||||||
|
<span class="c-button__label">{{ selectedMode.name }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import toggleMixin from '../../../ui/mixins/toggle-mixin';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [toggleMixin],
|
||||||
|
inject: ['openmct'],
|
||||||
|
props: {
|
||||||
|
mode: {
|
||||||
|
type: Object,
|
||||||
|
default() {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data: function () {
|
||||||
|
let clock;
|
||||||
|
if (this.mode && this.mode.key === 'fixed') {
|
||||||
|
clock = undefined;
|
||||||
|
} else {
|
||||||
|
//We want the clock from the global time context here
|
||||||
|
clock = this.openmct.time.clock();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clock !== undefined) {
|
||||||
|
//Create copy of active clock so the time API does not get reactified.
|
||||||
|
clock = Object.create(clock);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
selectedMode: this.getModeOptionForClock(clock),
|
||||||
|
modes: []
|
||||||
|
};
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
mode: {
|
||||||
|
deep: true,
|
||||||
|
handler(newMode) {
|
||||||
|
if (newMode) {
|
||||||
|
this.setViewFromClock(newMode.key === 'fixed' ? undefined : newMode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enabled(newValue, oldValue) {
|
||||||
|
if (newValue !== undefined && (newValue !== oldValue) && (newValue === true)) {
|
||||||
|
this.setViewFromClock(this.mode.key === 'fixed' ? undefined : this.mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted: function () {
|
||||||
|
if (this.mode) {
|
||||||
|
this.setViewFromClock(this.mode.key === 'fixed' ? undefined : this.mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.followTimeConductor();
|
||||||
|
},
|
||||||
|
destroyed: function () {
|
||||||
|
this.stopFollowTimeConductor();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
followTimeConductor() {
|
||||||
|
this.openmct.time.on('clock', this.setViewFromClock);
|
||||||
|
},
|
||||||
|
stopFollowTimeConductor() {
|
||||||
|
this.openmct.time.off('clock', this.setViewFromClock);
|
||||||
|
},
|
||||||
|
showModesMenu() {
|
||||||
|
const elementBoundingClientRect = this.$refs.modeMenuButton.getBoundingClientRect();
|
||||||
|
const x = elementBoundingClientRect.x;
|
||||||
|
const y = elementBoundingClientRect.y;
|
||||||
|
|
||||||
|
const menuOptions = {
|
||||||
|
menuClass: 'c-conductor__mode-menu',
|
||||||
|
placement: this.openmct.menus.menuPlacement.TOP_RIGHT
|
||||||
|
};
|
||||||
|
this.openmct.menus.showSuperMenu(x, y, this.modes, menuOptions);
|
||||||
|
},
|
||||||
|
|
||||||
|
getMenuOptions() {
|
||||||
|
let clocks = [{
|
||||||
|
name: 'Fixed Timespan',
|
||||||
|
timeSystem: 'utc'
|
||||||
|
}];
|
||||||
|
let currentGlobalClock = this.openmct.time.clock();
|
||||||
|
if (currentGlobalClock !== undefined) {
|
||||||
|
//Create copy of active clock so the time API does not get reactified.
|
||||||
|
currentGlobalClock = Object.assign({}, {
|
||||||
|
name: currentGlobalClock.name,
|
||||||
|
clock: currentGlobalClock.key,
|
||||||
|
timeSystem: this.openmct.time.timeSystem().key
|
||||||
|
});
|
||||||
|
|
||||||
|
clocks.push(currentGlobalClock);
|
||||||
|
}
|
||||||
|
|
||||||
|
return clocks;
|
||||||
|
},
|
||||||
|
loadClocks() {
|
||||||
|
let clocks = this.getMenuOptions()
|
||||||
|
.map(menuOption => menuOption.clock)
|
||||||
|
.filter(isDefinedAndUnique)
|
||||||
|
.map(this.getClock);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Populate the modes menu with metadata from the available clocks
|
||||||
|
* "Fixed Mode" is always first, and has no defined clock
|
||||||
|
*/
|
||||||
|
this.modes = [undefined]
|
||||||
|
.concat(clocks)
|
||||||
|
.map(this.getModeOptionForClock);
|
||||||
|
|
||||||
|
function isDefinedAndUnique(key, index, array) {
|
||||||
|
return key !== undefined && array.indexOf(key) === index;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getModeOptionForClock(clock) {
|
||||||
|
if (clock === undefined) {
|
||||||
|
const key = 'fixed';
|
||||||
|
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
name: 'Fixed Timespan',
|
||||||
|
description: 'Query and explore data that falls between two fixed datetimes.',
|
||||||
|
cssClass: 'icon-tabular',
|
||||||
|
onItemClicked: () => this.setOption(key)
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const key = clock.key;
|
||||||
|
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
name: clock.name,
|
||||||
|
description: "Monitor streaming data in real-time. The Time "
|
||||||
|
+ "Conductor and displays will automatically advance themselves based on this clock. " + clock.description,
|
||||||
|
cssClass: clock.cssClass || 'icon-clock',
|
||||||
|
onItemClicked: () => this.setOption(key)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getClock(key) {
|
||||||
|
return this.openmct.time.getAllClocks().filter(function (clock) {
|
||||||
|
return clock.key === key;
|
||||||
|
})[0];
|
||||||
|
},
|
||||||
|
|
||||||
|
setOption(clockKey) {
|
||||||
|
let key = clockKey;
|
||||||
|
if (clockKey === 'fixed') {
|
||||||
|
key = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchingOptions = this.getMenuOptions().filter(option => option.clock === key);
|
||||||
|
const clock = matchingOptions.length && matchingOptions[0].clock ? Object.assign({}, matchingOptions[0], { key: matchingOptions[0].clock }) : undefined;
|
||||||
|
this.selectedMode = this.getModeOptionForClock(clock);
|
||||||
|
|
||||||
|
if (this.mode) {
|
||||||
|
this.$emit('modeChanged', { key: clockKey });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setViewFromClock(clock) {
|
||||||
|
this.loadClocks();
|
||||||
|
//retain the mode chosen by the user
|
||||||
|
if (this.mode) {
|
||||||
|
let found = this.modes.find(mode => mode.key === this.selectedMode.key);
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
found = this.modes.find(mode => mode.key === clock && clock.key);
|
||||||
|
this.setOption(found ? this.getModeOptionForClock(clock).key : this.getModeOptionForClock().key);
|
||||||
|
} else if (this.mode.key !== this.selectedMode.key) {
|
||||||
|
this.setOption(this.selectedMode.key);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.setOption(this.getModeOptionForClock(clock).key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@ -1,6 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="pr-tc-input-menu"
|
class="pr-tc-input-menu"
|
||||||
|
:class="{'pr-tc-input-menu--bottom' : bottom === true}"
|
||||||
@keydown.enter.prevent
|
@keydown.enter.prevent
|
||||||
@keyup.enter.prevent="submit"
|
@keyup.enter.prevent="submit"
|
||||||
@keydown.esc.prevent
|
@keydown.esc.prevent
|
||||||
@ -88,6 +89,12 @@ export default {
|
|||||||
offset: {
|
offset: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
|
},
|
||||||
|
bottom: {
|
||||||
|
type: Boolean,
|
||||||
|
default() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
|
@ -86,15 +86,16 @@ export default {
|
|||||||
return {
|
return {
|
||||||
items: [],
|
items: [],
|
||||||
timeSystems: [],
|
timeSystems: [],
|
||||||
height: 0
|
height: 0,
|
||||||
|
useIndependentTime: this.domainObject.configuration.useIndependentTime === true,
|
||||||
|
timeOptions: this.domainObject.configuration.timeOptions
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.composition.off('add', this.addItem);
|
this.composition.off('add', this.addItem);
|
||||||
this.composition.off('remove', this.removeItem);
|
this.composition.off('remove', this.removeItem);
|
||||||
this.composition.off('reorder', this.reorder);
|
this.composition.off('reorder', this.reorder);
|
||||||
this.openmct.time.off("bounds", this.updateViewBounds);
|
this.stopFollowingTimeContext();
|
||||||
|
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
if (this.composition) {
|
if (this.composition) {
|
||||||
@ -104,8 +105,8 @@ export default {
|
|||||||
this.composition.load();
|
this.composition.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.setTimeContext();
|
||||||
this.getTimeSystems();
|
this.getTimeSystems();
|
||||||
this.openmct.time.on("bounds", this.updateViewBounds);
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
addItem(domainObject) {
|
addItem(domainObject) {
|
||||||
@ -132,8 +133,8 @@ export default {
|
|||||||
},
|
},
|
||||||
removeItem(identifier) {
|
removeItem(identifier) {
|
||||||
let index = this.items.findIndex(item => this.openmct.objects.areIdsEqual(identifier, item.domainObject.identifier));
|
let index = this.items.findIndex(item => this.openmct.objects.areIdsEqual(identifier, item.domainObject.identifier));
|
||||||
this.removeSelectable(this.items[index]);
|
|
||||||
this.items.splice(index, 1);
|
this.items.splice(index, 1);
|
||||||
|
this.updateContentHeight();
|
||||||
},
|
},
|
||||||
reorder(reorderPlan) {
|
reorder(reorderPlan) {
|
||||||
let oldItems = this.items.slice();
|
let oldItems = this.items.slice();
|
||||||
@ -154,7 +155,7 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
getBoundsForTimeSystem(timeSystem) {
|
getBoundsForTimeSystem(timeSystem) {
|
||||||
const currentBounds = this.openmct.time.bounds();
|
const currentBounds = this.timeContext.bounds();
|
||||||
|
|
||||||
//TODO: Some kind of translation via an offset? of current bounds to target timeSystem
|
//TODO: Some kind of translation via an offset? of current bounds to target timeSystem
|
||||||
return currentBounds;
|
return currentBounds;
|
||||||
@ -164,6 +165,20 @@ export default {
|
|||||||
if (currentTimeSystem) {
|
if (currentTimeSystem) {
|
||||||
currentTimeSystem.bounds = bounds;
|
currentTimeSystem.bounds = bounds;
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
setTimeContext() {
|
||||||
|
this.stopFollowingTimeContext();
|
||||||
|
|
||||||
|
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
|
||||||
|
this.timeContext.on('timeContext', this.setTimeContext);
|
||||||
|
this.updateViewBounds(this.timeContext.bounds());
|
||||||
|
this.timeContext.on('bounds', this.updateViewBounds);
|
||||||
|
},
|
||||||
|
stopFollowingTimeContext() {
|
||||||
|
if (this.timeContext) {
|
||||||
|
this.timeContext.off('bounds', this.updateViewBounds);
|
||||||
|
this.timeContext.off('timeContext', this.setTimeContext);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -20,7 +20,8 @@
|
|||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
import TimelineViewProvider from '../timeline/TimelineViewProvider';
|
import TimelineViewProvider from './TimelineViewProvider';
|
||||||
|
import timelineInterceptor from "./timelineInterceptor";
|
||||||
|
|
||||||
export default function () {
|
export default function () {
|
||||||
return function install(openmct) {
|
return function install(openmct) {
|
||||||
@ -32,8 +33,12 @@ export default function () {
|
|||||||
cssClass: 'icon-timeline',
|
cssClass: 'icon-timeline',
|
||||||
initialize: function (domainObject) {
|
initialize: function (domainObject) {
|
||||||
domainObject.composition = [];
|
domainObject.composition = [];
|
||||||
|
domainObject.configuration = {
|
||||||
|
useIndependentTime: false
|
||||||
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
timelineInterceptor(openmct);
|
||||||
openmct.objectViews.addProvider(new TimelineViewProvider(openmct));
|
openmct.objectViews.addProvider(new TimelineViewProvider(openmct));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -96,10 +96,15 @@ describe('the plugin', function () {
|
|||||||
|
|
||||||
describe('the view', () => {
|
describe('the view', () => {
|
||||||
let timelineView;
|
let timelineView;
|
||||||
|
let testViewObject;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const testViewObject = {
|
testViewObject = {
|
||||||
id: "test-object",
|
id: "test-object",
|
||||||
|
identifier: {
|
||||||
|
key: "test-object",
|
||||||
|
namespace: ''
|
||||||
|
},
|
||||||
type: "time-strip"
|
type: "time-strip"
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -119,6 +124,106 @@ describe('the plugin', function () {
|
|||||||
const el = element.querySelector('.c-timesystem-axis');
|
const el = element.querySelector('.c-timesystem-axis');
|
||||||
expect(el).toBeDefined();
|
expect(el).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not show the independent time conductor based on configuration', () => {
|
||||||
|
const independentTimeConductorEl = element.querySelector('.c-timeline-holder > .c-conductor__controls');
|
||||||
|
expect(independentTimeConductorEl).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('the independent time conductor', () => {
|
||||||
|
let timelineView;
|
||||||
|
let testViewObject = {
|
||||||
|
id: "test-object",
|
||||||
|
identifier: {
|
||||||
|
key: "test-object",
|
||||||
|
namespace: ''
|
||||||
|
},
|
||||||
|
type: "time-strip",
|
||||||
|
configuration: {
|
||||||
|
useIndependentTime: true,
|
||||||
|
timeOptions: {
|
||||||
|
mode: {
|
||||||
|
key: 'local'
|
||||||
|
},
|
||||||
|
fixedOffsets: {
|
||||||
|
start: 10,
|
||||||
|
end: 11
|
||||||
|
},
|
||||||
|
clockOffsets: {
|
||||||
|
start: -(30 * 60 * 1000),
|
||||||
|
end: (30 * 60 * 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(done => {
|
||||||
|
const applicableViews = openmct.objectViews.get(testViewObject, mockObjectPath);
|
||||||
|
timelineView = applicableViews.find((viewProvider) => viewProvider.key === 'time-strip.view');
|
||||||
|
let view = timelineView.view(testViewObject, element);
|
||||||
|
view.show(child, true);
|
||||||
|
|
||||||
|
Vue.nextTick(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays an independent time conductor with saved options - local clock', () => {
|
||||||
|
|
||||||
|
return Vue.nextTick(() => {
|
||||||
|
const independentTimeConductorEl = element.querySelector('.c-timeline-holder > .c-conductor__controls');
|
||||||
|
expect(independentTimeConductorEl).toBeDefined();
|
||||||
|
|
||||||
|
const independentTimeContext = openmct.time.getIndependentContext(testViewObject.identifier.key);
|
||||||
|
expect(independentTimeContext.clockOffsets()).toEqual(testViewObject.configuration.timeOptions.clockOffsets);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('the independent time conductor', () => {
|
||||||
|
let timelineView;
|
||||||
|
let testViewObject2 = {
|
||||||
|
id: "test-object2",
|
||||||
|
identifier: {
|
||||||
|
key: "test-object2",
|
||||||
|
namespace: ''
|
||||||
|
},
|
||||||
|
type: "time-strip",
|
||||||
|
configuration: {
|
||||||
|
useIndependentTime: true,
|
||||||
|
timeOptions: {
|
||||||
|
mode: {
|
||||||
|
key: 'fixed'
|
||||||
|
},
|
||||||
|
fixedOffsets: {
|
||||||
|
start: 10,
|
||||||
|
end: 11
|
||||||
|
},
|
||||||
|
clockOffsets: {
|
||||||
|
start: -(30 * 60 * 1000),
|
||||||
|
end: (30 * 60 * 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach((done) => {
|
||||||
|
const applicableViews = openmct.objectViews.get(testViewObject2, mockObjectPath);
|
||||||
|
timelineView = applicableViews.find((viewProvider) => viewProvider.key === 'time-strip.view');
|
||||||
|
let view = timelineView.view(testViewObject2, element);
|
||||||
|
view.show(child, true);
|
||||||
|
|
||||||
|
Vue.nextTick(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays an independent time conductor with saved options - fixed timespan', () => {
|
||||||
|
return Vue.nextTick(() => {
|
||||||
|
const independentTimeConductorEl = element.querySelector('.c-timeline-holder > .c-conductor__controls');
|
||||||
|
expect(independentTimeConductorEl).toBeDefined();
|
||||||
|
|
||||||
|
const independentTimeContext = openmct.time.getIndependentContext(testViewObject2.identifier.key);
|
||||||
|
expect(independentTimeContext.bounds()).toEqual(testViewObject2.configuration.timeOptions.fixedOffsets);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -1,4 +1,10 @@
|
|||||||
.c-timeline-holder {
|
.c-timeline-holder {
|
||||||
@include abs();
|
@include abs();
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
|
||||||
|
> * + * {
|
||||||
|
margin-top: $interiorMargin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
40
src/plugins/timeline/timelineInterceptor.js
Normal file
40
src/plugins/timeline/timelineInterceptor.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* Open MCT includes source code licensed under additional open source
|
||||||
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
* this source code distribution or the Licensing information page available
|
||||||
|
* at runtime from the About dialog for additional information.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
export default function timelineInterceptor(openmct) {
|
||||||
|
|
||||||
|
openmct.objects.addGetInterceptor({
|
||||||
|
appliesTo: (identifier, domainObject) => {
|
||||||
|
return domainObject && domainObject.type === 'time-strip';
|
||||||
|
},
|
||||||
|
invoke: (identifier, object) => {
|
||||||
|
|
||||||
|
if (object && object.configuration === undefined) {
|
||||||
|
object.configuration = {
|
||||||
|
useIndependentTime: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
@ -582,6 +582,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&[class*='--menus-bottom'] {
|
||||||
|
.c-menu {
|
||||||
|
top: auto; bottom: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&[class*='--menus-left'],
|
&[class*='--menus-left'],
|
||||||
&[class*='menus-to-left'] {
|
&[class*='menus-to-left'] {
|
||||||
.c-menu {
|
.c-menu {
|
||||||
|
@ -1,13 +1,29 @@
|
|||||||
<template>
|
<template>
|
||||||
<div></div>
|
<div>
|
||||||
|
<div v-if="domainObject && domainObject.type === 'time-strip'"
|
||||||
|
class="c-conductor-holder--compact l-shell__main-independent-time-conductor"
|
||||||
|
>
|
||||||
|
<independent-time-conductor :domain-object="domainObject"
|
||||||
|
@stateChanged="updateIndependentTimeState"
|
||||||
|
@updated="saveTimeOptions"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div ref="objectViewWrapper"
|
||||||
|
:class="objectViewStyle"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import StyleRuleManager from "@/plugins/condition/StyleRuleManager";
|
import StyleRuleManager from "@/plugins/condition/StyleRuleManager";
|
||||||
import {STYLE_CONSTANTS} from "@/plugins/condition/utils/constants";
|
import {STYLE_CONSTANTS} from "@/plugins/condition/utils/constants";
|
||||||
|
import IndependentTimeConductor from '@/plugins/timeConductor/independent/IndependentTimeConductor.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
components: {
|
||||||
|
IndependentTimeConductor
|
||||||
|
},
|
||||||
inject: ["openmct"],
|
inject: ["openmct"],
|
||||||
props: {
|
props: {
|
||||||
showEditView: Boolean,
|
showEditView: Boolean,
|
||||||
@ -48,6 +64,13 @@ export default {
|
|||||||
},
|
},
|
||||||
font() {
|
font() {
|
||||||
return this.objectFontStyle ? this.objectFontStyle.font : this.layoutFont;
|
return this.objectFontStyle ? this.objectFontStyle.font : this.layoutFont;
|
||||||
|
},
|
||||||
|
objectViewStyle() {
|
||||||
|
if (this.domainObject && this.domainObject.type === 'time-strip') {
|
||||||
|
return 'l-shell__main-object-view';
|
||||||
|
} else {
|
||||||
|
return 'u-contents';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
destroyed() {
|
destroyed() {
|
||||||
@ -79,13 +102,13 @@ export default {
|
|||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.updateView();
|
this.updateView();
|
||||||
this.$el.addEventListener('dragover', this.onDragOver, {
|
this.$refs.objectViewWrapper.addEventListener('dragover', this.onDragOver, {
|
||||||
capture: true
|
capture: true
|
||||||
});
|
});
|
||||||
this.$el.addEventListener('drop', this.editIfEditable, {
|
this.$refs.objectViewWrapper.addEventListener('drop', this.editIfEditable, {
|
||||||
capture: true
|
capture: true
|
||||||
});
|
});
|
||||||
this.$el.addEventListener('drop', this.addObjectToParent);
|
this.$refs.objectViewWrapper.addEventListener('drop', this.addObjectToParent);
|
||||||
if (this.domainObject) {
|
if (this.domainObject) {
|
||||||
//This is to apply styles to subobjects in a layout
|
//This is to apply styles to subobjects in a layout
|
||||||
this.initObjectStyles();
|
this.initObjectStyles();
|
||||||
@ -95,7 +118,9 @@ export default {
|
|||||||
clear() {
|
clear() {
|
||||||
if (this.currentView) {
|
if (this.currentView) {
|
||||||
this.currentView.destroy();
|
this.currentView.destroy();
|
||||||
this.$el.innerHTML = '';
|
if (this.$refs.objectViewWrapper) {
|
||||||
|
this.$refs.objectViewWrapper.innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
if (this.releaseEditModeHandler) {
|
if (this.releaseEditModeHandler) {
|
||||||
this.releaseEditModeHandler();
|
this.releaseEditModeHandler();
|
||||||
@ -118,8 +143,8 @@ export default {
|
|||||||
this.openmct.objectViews.off('clearData', this.clearData);
|
this.openmct.objectViews.off('clearData', this.clearData);
|
||||||
},
|
},
|
||||||
getStyleReceiver() {
|
getStyleReceiver() {
|
||||||
let styleReceiver = this.$el.querySelector('.js-style-receiver')
|
let styleReceiver = this.$refs.objectViewWrapper.querySelector('.js-style-receiver')
|
||||||
|| this.$el.querySelector(':first-child');
|
|| this.$refs.objectViewWrapper.querySelector(':first-child');
|
||||||
|
|
||||||
if (styleReceiver === null) {
|
if (styleReceiver === null) {
|
||||||
styleReceiver = undefined;
|
styleReceiver = undefined;
|
||||||
@ -183,7 +208,7 @@ export default {
|
|||||||
|
|
||||||
this.viewContainer = document.createElement('div');
|
this.viewContainer = document.createElement('div');
|
||||||
this.viewContainer.classList.add('l-angular-ov-wrapper');
|
this.viewContainer.classList.add('l-angular-ov-wrapper');
|
||||||
this.$el.append(this.viewContainer);
|
this.$refs.objectViewWrapper.append(this.viewContainer);
|
||||||
let provider = this.getViewProvider();
|
let provider = this.getViewProvider();
|
||||||
if (!provider) {
|
if (!provider) {
|
||||||
return;
|
return;
|
||||||
@ -213,7 +238,7 @@ export default {
|
|||||||
|
|
||||||
if (immediatelySelect) {
|
if (immediatelySelect) {
|
||||||
this.removeSelectable = this.openmct.selection.selectable(
|
this.removeSelectable = this.openmct.selection.selectable(
|
||||||
this.$el, this.getSelectionContext(), true);
|
this.$refs.objectViewWrapper, this.getSelectionContext(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.openmct.objectViews.on('clearData', this.clearData);
|
this.openmct.objectViews.on('clearData', this.clearData);
|
||||||
@ -388,6 +413,13 @@ export default {
|
|||||||
if (elemToStyle !== undefined) {
|
if (elemToStyle !== undefined) {
|
||||||
elemToStyle.dataset.font = newFont;
|
elemToStyle.dataset.font = newFont;
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
//Should the domainObject be updated in the Independent Time conductor component itself?
|
||||||
|
updateIndependentTimeState(useIndependentTime) {
|
||||||
|
this.openmct.objects.mutate(this.domainObject, 'configuration.useIndependentTime', useIndependentTime);
|
||||||
|
},
|
||||||
|
saveTimeOptions(options) {
|
||||||
|
this.openmct.objects.mutate(this.domainObject, 'configuration.timeOptions', options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -240,6 +240,14 @@
|
|||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__main-object-view {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__main-independent-time-conductor {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
&__tree {
|
&__tree {
|
||||||
// Tree component within __pane-tree
|
// Tree component within __pane-tree
|
||||||
flex: 1 1 auto !important;
|
flex: 1 1 auto !important;
|
||||||
@ -247,7 +255,13 @@
|
|||||||
|
|
||||||
&__time-conductor {
|
&__time-conductor {
|
||||||
border-top: 1px solid $colorInteriorBorder;
|
border-top: 1px solid $colorInteriorBorder;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
padding-top: $interiorMargin;
|
padding-top: $interiorMargin;
|
||||||
|
|
||||||
|
> * + * {
|
||||||
|
margin-top: $interiorMargin;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__main {
|
&__main {
|
||||||
|
Reference in New Issue
Block a user