mirror of
https://github.com/nasa/openmct.git
synced 2025-06-22 17:08:57 +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:
@ -46,7 +46,7 @@ define([
|
||||
StatusAPI
|
||||
) {
|
||||
return {
|
||||
TimeAPI: TimeAPI,
|
||||
TimeAPI: TimeAPI.default,
|
||||
ObjectAPI: ObjectAPI,
|
||||
CompositionAPI: CompositionAPI,
|
||||
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.
|
||||
*****************************************************************************/
|
||||
|
||||
define(['EventEmitter'], function (EventEmitter) {
|
||||
|
||||
/**
|
||||
* The public API for setting and querying the temporal state of the
|
||||
* application. The concept of time is integral to Open MCT, and at least
|
||||
* one {@link TimeSystem}, as well as some default time bounds must be
|
||||
* registered and enabled via {@link TimeAPI.addTimeSystem} and
|
||||
* {@link TimeAPI.timeSystem} respectively for Open MCT to work.
|
||||
*
|
||||
* Time-sensitive views will typically respond to changes to bounds or other
|
||||
* properties of the time conductor and update the data displayed based on
|
||||
* the temporal state of the application. The current time bounds are also
|
||||
* used in queries for historical data.
|
||||
*
|
||||
* The TimeAPI extends the EventEmitter class. A number of events are
|
||||
* fired when properties of the time conductor change, which are documented
|
||||
* below.
|
||||
*
|
||||
* @interface
|
||||
* @memberof module:openmct
|
||||
*/
|
||||
function TimeAPI() {
|
||||
EventEmitter.call(this);
|
||||
|
||||
//The Time System
|
||||
this.system = undefined;
|
||||
//The Time Of Interest
|
||||
this.toi = undefined;
|
||||
|
||||
this.boundsVal = {
|
||||
start: undefined,
|
||||
end: undefined
|
||||
};
|
||||
|
||||
this.timeSystems = new Map();
|
||||
this.clocks = new Map();
|
||||
this.activeClock = undefined;
|
||||
this.offsets = undefined;
|
||||
|
||||
this.tick = this.tick.bind(this);
|
||||
import GlobalTimeContext from "./GlobalTimeContext";
|
||||
import IndependentTimeContext from "@/api/time/IndependentTimeContext";
|
||||
|
||||
/**
|
||||
* The public API for setting and querying the temporal state of the
|
||||
* application. The concept of time is integral to Open MCT, and at least
|
||||
* one {@link TimeSystem}, as well as some default time bounds must be
|
||||
* registered and enabled via {@link TimeAPI.addTimeSystem} and
|
||||
* {@link TimeAPI.timeSystem} respectively for Open MCT to work.
|
||||
*
|
||||
* Time-sensitive views will typically respond to changes to bounds or other
|
||||
* properties of the time conductor and update the data displayed based on
|
||||
* the temporal state of the application. The current time bounds are also
|
||||
* used in queries for historical data.
|
||||
*
|
||||
* The TimeAPI extends the GlobalTimeContext which in turn extends the TimeContext/EventEmitter class. A number of events are
|
||||
* fired when properties of the time conductor change, which are documented
|
||||
* below.
|
||||
*
|
||||
* @interface
|
||||
* @memberof module:openmct
|
||||
*/
|
||||
class TimeAPI extends GlobalTimeContext {
|
||||
constructor(openmct) {
|
||||
super();
|
||||
this.openmct = openmct;
|
||||
this.independentContexts = new Map();
|
||||
}
|
||||
|
||||
TimeAPI.prototype = Object.create(EventEmitter.prototype);
|
||||
|
||||
/**
|
||||
* A TimeSystem provides meaning to the values returned by the TimeAPI. Open
|
||||
* MCT supports multiple different types of time values, although all are
|
||||
@ -94,16 +78,16 @@ define(['EventEmitter'], function (EventEmitter) {
|
||||
* @memberof module:openmct.TimeAPI#
|
||||
* @param {TimeSystem} timeSystem A time system object.
|
||||
*/
|
||||
TimeAPI.prototype.addTimeSystem = function (timeSystem) {
|
||||
addTimeSystem(timeSystem) {
|
||||
this.timeSystems.set(timeSystem.key, timeSystem);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {TimeSystem[]}
|
||||
*/
|
||||
TimeAPI.prototype.getAllTimeSystems = function () {
|
||||
getAllTimeSystems() {
|
||||
return Array.from(this.timeSystems.values());
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Clocks provide a timing source that is used to
|
||||
@ -126,340 +110,81 @@ define(['EventEmitter'], function (EventEmitter) {
|
||||
* @memberof module:openmct.TimeAPI#
|
||||
* @param {Clock} clock
|
||||
*/
|
||||
TimeAPI.prototype.addClock = function (clock) {
|
||||
addClock(clock) {
|
||||
this.clocks.set(clock.key, clock);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @memberof module:openmct.TimeAPI#
|
||||
* @returns {Clock[]}
|
||||
* @memberof module:openmct.TimeAPI#
|
||||
*/
|
||||
TimeAPI.prototype.getAllClocks = function () {
|
||||
getAllClocks() {
|
||||
return Array.from(this.clocks.values());
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the given bounds. This can be used for pre-validation of bounds,
|
||||
* for example by views validating user inputs.
|
||||
* @param {TimeBounds} bounds The start and end time of the conductor.
|
||||
* @returns {string | true} A validation error, or true if valid
|
||||
* Get or set an independent time context which follows the TimeAPI timeSystem,
|
||||
* but with different offsets for a given domain object
|
||||
* @param {key | string} key The identifier key of the domain object these offsets are set for
|
||||
* @param {ClockOffsets | TimeBounds} value This maintains a sliding time window of a fixed width that automatically updates
|
||||
* @param {key | string} clockKey the real time clock key currently in use
|
||||
* @memberof module:openmct.TimeAPI#
|
||||
* @method validateBounds
|
||||
* @method addIndependentTimeContext
|
||||
*/
|
||||
TimeAPI.prototype.validateBounds = function (bounds) {
|
||||
if ((bounds.start === undefined)
|
||||
|| (bounds.end === undefined)
|
||||
|| isNaN(bounds.start)
|
||||
|| isNaN(bounds.end)
|
||||
) {
|
||||
return "Start and end must be specified as integer values";
|
||||
} else if (bounds.start > bounds.end) {
|
||||
return "Specified start date exceeds end bound";
|
||||
addIndependentContext(key, value, clockKey) {
|
||||
let timeContext = this.independentContexts.get(key);
|
||||
if (!timeContext) {
|
||||
timeContext = new IndependentTimeContext(this, key);
|
||||
this.independentContexts.set(key, timeContext);
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate the given offsets. This can be used for pre-validation of
|
||||
* offsets, for example by views validating user inputs.
|
||||
* @param {ClockOffsets} offsets The start and end offsets from a 'now' value.
|
||||
* @returns {string | true} A validation error, or true if valid
|
||||
* @memberof module:openmct.TimeAPI#
|
||||
* @method validateBounds
|
||||
*/
|
||||
TimeAPI.prototype.validateOffsets = function (offsets) {
|
||||
if ((offsets.start === undefined)
|
||||
|| (offsets.end === undefined)
|
||||
|| isNaN(offsets.start)
|
||||
|| isNaN(offsets.end)
|
||||
) {
|
||||
return "Start and end offsets must be specified as integer values";
|
||||
} else if (offsets.start >= offsets.end) {
|
||||
return "Specified start offset must be < end offset";
|
||||
if (clockKey) {
|
||||
timeContext.clock(clockKey, value);
|
||||
} else {
|
||||
timeContext.stopClock();
|
||||
timeContext.bounds(value);
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
this.emit('timeContext', key);
|
||||
|
||||
/**
|
||||
* @typedef {Object} TimeBounds
|
||||
* @property {number} start The start time displayed by the time conductor
|
||||
* in ms since epoch. Epoch determined by currently active time system
|
||||
* @property {number} end The end time displayed by the time conductor in ms
|
||||
* since epoch.
|
||||
* @memberof module:openmct.TimeAPI~
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get or set the start and end time of the time conductor. Basic validation
|
||||
* of bounds is performed.
|
||||
*
|
||||
* @param {module:openmct.TimeAPI~TimeConductorBounds} newBounds
|
||||
* @throws {Error} Validation error
|
||||
* @fires module:openmct.TimeAPI~bounds
|
||||
* @returns {module:openmct.TimeAPI~TimeConductorBounds}
|
||||
* @memberof module:openmct.TimeAPI#
|
||||
* @method bounds
|
||||
*/
|
||||
TimeAPI.prototype.bounds = function (newBounds) {
|
||||
if (arguments.length > 0) {
|
||||
const validationResult = this.validateBounds(newBounds);
|
||||
if (validationResult !== true) {
|
||||
throw new Error(validationResult);
|
||||
}
|
||||
|
||||
//Create a copy to avoid direct mutation of conductor bounds
|
||||
this.boundsVal = JSON.parse(JSON.stringify(newBounds));
|
||||
/**
|
||||
* The start time, end time, or both have been updated.
|
||||
* @event bounds
|
||||
* @memberof module:openmct.TimeAPI~
|
||||
* @property {TimeConductorBounds} bounds The newly updated bounds
|
||||
* @property {boolean} [tick] `true` if the bounds update was due to
|
||||
* a "tick" event (ie. was an automatic update), false otherwise.
|
||||
*/
|
||||
this.emit('bounds', this.boundsVal, false);
|
||||
|
||||
// If a bounds change results in a TOI outside of the current
|
||||
// bounds, unset it
|
||||
if (this.toi < newBounds.start || this.toi > newBounds.end) {
|
||||
this.timeOfInterest(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
//Return a copy to prevent direct mutation of time conductor bounds.
|
||||
return JSON.parse(JSON.stringify(this.boundsVal));
|
||||
};
|
||||
|
||||
/**
|
||||
* Get or set the time system of the TimeAPI.
|
||||
* @param {TimeSystem | string} timeSystem
|
||||
* @param {module:openmct.TimeAPI~TimeConductorBounds} bounds
|
||||
* @fires module:openmct.TimeAPI~timeSystem
|
||||
* @returns {TimeSystem} The currently applied time system
|
||||
* @memberof module:openmct.TimeAPI#
|
||||
* @method timeSystem
|
||||
*/
|
||||
TimeAPI.prototype.timeSystem = function (timeSystemOrKey, bounds) {
|
||||
if (arguments.length >= 1) {
|
||||
if (arguments.length === 1 && !this.activeClock) {
|
||||
throw new Error(
|
||||
"Must specify bounds when changing time system without "
|
||||
+ "an active clock."
|
||||
);
|
||||
}
|
||||
|
||||
let timeSystem;
|
||||
|
||||
if (timeSystemOrKey === undefined) {
|
||||
throw "Please provide a time system";
|
||||
}
|
||||
|
||||
if (typeof timeSystemOrKey === 'string') {
|
||||
timeSystem = this.timeSystems.get(timeSystemOrKey);
|
||||
|
||||
if (timeSystem === undefined) {
|
||||
throw "Unknown time system " + timeSystemOrKey + ". Has it been registered with 'addTimeSystem'?";
|
||||
}
|
||||
} else if (typeof timeSystemOrKey === 'object') {
|
||||
timeSystem = timeSystemOrKey;
|
||||
|
||||
if (!this.timeSystems.has(timeSystem.key)) {
|
||||
throw "Unknown time system " + timeSystem.key + ". Has it been registered with 'addTimeSystem'?";
|
||||
}
|
||||
} else {
|
||||
throw "Attempt to set invalid time system in Time API. Please provide a previously registered time system object or key";
|
||||
}
|
||||
|
||||
this.system = timeSystem;
|
||||
|
||||
/**
|
||||
* The time system used by the time
|
||||
* conductor has changed. A change in Time System will always be
|
||||
* followed by a bounds event specifying new query bounds.
|
||||
*
|
||||
* @event module:openmct.TimeAPI~timeSystem
|
||||
* @property {TimeSystem} The value of the currently applied
|
||||
* Time System
|
||||
* */
|
||||
this.emit('timeSystem', this.system);
|
||||
if (bounds) {
|
||||
this.bounds(bounds);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return this.system;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get or set the Time of Interest. The Time of Interest is a single point
|
||||
* in time, and constitutes the temporal focus of application views. It can
|
||||
* be manipulated by the user from the time conductor or from other views.
|
||||
* The time of interest can effectively be unset by assigning a value of
|
||||
* 'undefined'.
|
||||
* @fires module:openmct.TimeAPI~timeOfInterest
|
||||
* @param newTOI
|
||||
* @returns {number} the current time of interest
|
||||
* @memberof module:openmct.TimeAPI#
|
||||
* @method timeOfInterest
|
||||
*/
|
||||
TimeAPI.prototype.timeOfInterest = function (newTOI) {
|
||||
if (arguments.length > 0) {
|
||||
this.toi = newTOI;
|
||||
/**
|
||||
* The Time of Interest has moved.
|
||||
* @event timeOfInterest
|
||||
* @memberof module:openmct.TimeAPI~
|
||||
* @property {number} Current time of interest
|
||||
*/
|
||||
this.emit('timeOfInterest', this.toi);
|
||||
}
|
||||
|
||||
return this.toi;
|
||||
};
|
||||
|
||||
/**
|
||||
* Update bounds based on provided time and current offsets
|
||||
* @private
|
||||
* @param {number} timestamp A time from which boudns will be calculated
|
||||
* using current offsets.
|
||||
*/
|
||||
TimeAPI.prototype.tick = function (timestamp) {
|
||||
const newBounds = {
|
||||
start: timestamp + this.offsets.start,
|
||||
end: timestamp + this.offsets.end
|
||||
return () => {
|
||||
this.independentContexts.delete(key);
|
||||
timeContext.emit('timeContext', key);
|
||||
};
|
||||
|
||||
this.boundsVal = newBounds;
|
||||
this.emit('bounds', this.boundsVal, true);
|
||||
|
||||
// If a bounds change results in a TOI outside of the current
|
||||
// bounds, unset it
|
||||
if (this.toi < newBounds.start || this.toi > newBounds.end) {
|
||||
this.timeOfInterest(undefined);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the active clock. Tick source will be immediately subscribed to
|
||||
* and ticking will begin. Offsets from 'now' must also be provided. A clock
|
||||
* can be unset by calling {@link stopClock}.
|
||||
*
|
||||
* @param {Clock || string} The clock to activate, or its key
|
||||
* @param {ClockOffsets} offsets on each tick these will be used to calculate
|
||||
* the start and end bounds. This maintains a sliding time window of a fixed
|
||||
* width that automatically updates.
|
||||
* @fires module:openmct.TimeAPI~clock
|
||||
* @return {Clock} the currently active clock;
|
||||
* Get the independent time context which follows the TimeAPI timeSystem,
|
||||
* but with different offsets.
|
||||
* @param {key | string} key The identifier key of the domain object these offsets
|
||||
* @memberof module:openmct.TimeAPI#
|
||||
* @method getIndependentTimeContext
|
||||
*/
|
||||
TimeAPI.prototype.clock = function (keyOrClock, offsets) {
|
||||
if (arguments.length === 2) {
|
||||
let clock;
|
||||
|
||||
if (typeof keyOrClock === 'string') {
|
||||
clock = this.clocks.get(keyOrClock);
|
||||
if (clock === undefined) {
|
||||
throw "Unknown clock '" + keyOrClock + "'. Has it been registered with 'addClock'?";
|
||||
}
|
||||
} else if (typeof keyOrClock === 'object') {
|
||||
clock = keyOrClock;
|
||||
if (!this.clocks.has(clock.key)) {
|
||||
throw "Unknown clock '" + keyOrClock.key + "'. Has it been registered with 'addClock'?";
|
||||
}
|
||||
}
|
||||
|
||||
const previousClock = this.activeClock;
|
||||
if (previousClock !== undefined) {
|
||||
previousClock.off("tick", this.tick);
|
||||
}
|
||||
|
||||
this.activeClock = clock;
|
||||
|
||||
/**
|
||||
* The active clock has changed. Clock can be unset by calling {@link stopClock}
|
||||
* @event clock
|
||||
* @memberof module:openmct.TimeAPI~
|
||||
* @property {Clock} clock The newly activated clock, or undefined
|
||||
* if the system is no longer following a clock source
|
||||
*/
|
||||
this.emit("clock", this.activeClock);
|
||||
|
||||
if (this.activeClock !== undefined) {
|
||||
this.clockOffsets(offsets);
|
||||
this.activeClock.on("tick", this.tick);
|
||||
}
|
||||
|
||||
} else if (arguments.length === 1) {
|
||||
throw "When setting the clock, clock offsets must also be provided";
|
||||
}
|
||||
|
||||
return this.activeClock;
|
||||
};
|
||||
getIndependentContext(key) {
|
||||
return this.independentContexts.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clock offsets are used to calculate temporal bounds when the system is
|
||||
* ticking on a clock source.
|
||||
*
|
||||
* @typedef {object} ClockOffsets
|
||||
* @property {number} start A time span relative to the current value of the
|
||||
* ticking clock, from which start bounds will be calculated. This value must
|
||||
* be < 0. When a clock is active, bounds will be calculated automatically
|
||||
* based on the value provided by the clock, and the defined clock offsets.
|
||||
* @property {number} end A time span relative to the current value of the
|
||||
* ticking clock, from which end bounds will be calculated. This value must
|
||||
* be >= 0.
|
||||
* Get the a timeContext for a view based on it's objectPath. If there is any object in the objectPath with an independent time context, it will be returned.
|
||||
* Otherwise, the global time context will be returned.
|
||||
* @param { Array } objectPath The view's objectPath
|
||||
* @memberof module:openmct.TimeAPI#
|
||||
* @method getContextForView
|
||||
*/
|
||||
/**
|
||||
* Get or set the currently applied clock offsets. If no parameter is provided,
|
||||
* the current value will be returned. If provided, the new value will be
|
||||
* used as the new clock offsets.
|
||||
* @param {ClockOffsets} offsets
|
||||
* @returns {ClockOffsets}
|
||||
*/
|
||||
TimeAPI.prototype.clockOffsets = function (offsets) {
|
||||
if (arguments.length > 0) {
|
||||
getContextForView(objectPath) {
|
||||
let timeContext = this;
|
||||
|
||||
const validationResult = this.validateOffsets(offsets);
|
||||
if (validationResult !== true) {
|
||||
throw new Error(validationResult);
|
||||
objectPath.forEach(item => {
|
||||
const key = this.openmct.objects.makeKeyString(item.identifier);
|
||||
if (this.independentContexts.get(key)) {
|
||||
timeContext = this.independentContexts.get(key);
|
||||
}
|
||||
});
|
||||
|
||||
this.offsets = offsets;
|
||||
return timeContext;
|
||||
}
|
||||
|
||||
const currentValue = this.activeClock.currentValue();
|
||||
const newBounds = {
|
||||
start: currentValue + offsets.start,
|
||||
end: currentValue + offsets.end
|
||||
};
|
||||
}
|
||||
|
||||
this.bounds(newBounds);
|
||||
|
||||
/**
|
||||
* Event that is triggered when clock offsets change.
|
||||
* @event clockOffsets
|
||||
* @memberof module:openmct.TimeAPI~
|
||||
* @property {ClockOffsets} clockOffsets The newly activated clock
|
||||
* offsets.
|
||||
*/
|
||||
this.emit("clockOffsets", offsets);
|
||||
}
|
||||
|
||||
return this.offsets;
|
||||
};
|
||||
|
||||
/**
|
||||
* Stop the currently active clock from ticking, and unset it. This will
|
||||
* revert all views to showing a static time frame defined by the current
|
||||
* bounds.
|
||||
*/
|
||||
TimeAPI.prototype.stopClock = function () {
|
||||
if (this.activeClock) {
|
||||
this.clock(undefined, undefined);
|
||||
}
|
||||
};
|
||||
|
||||
return TimeAPI;
|
||||
});
|
||||
export default TimeAPI;
|
||||
|
@ -19,241 +19,243 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import TimeAPI from "./TimeAPI";
|
||||
import {createOpenMct} from "utils/testing";
|
||||
|
||||
define(['./TimeAPI'], function (TimeAPI) {
|
||||
describe("The Time API", function () {
|
||||
let api;
|
||||
let timeSystemKey;
|
||||
let timeSystem;
|
||||
let clockKey;
|
||||
let clock;
|
||||
let bounds;
|
||||
let eventListener;
|
||||
let toi;
|
||||
describe("The Time API", function () {
|
||||
let api;
|
||||
let timeSystemKey;
|
||||
let timeSystem;
|
||||
let clockKey;
|
||||
let clock;
|
||||
let bounds;
|
||||
let eventListener;
|
||||
let toi;
|
||||
let openmct;
|
||||
|
||||
beforeEach(function () {
|
||||
openmct = createOpenMct();
|
||||
api = new TimeAPI(openmct);
|
||||
timeSystemKey = "timeSystemKey";
|
||||
timeSystem = {key: timeSystemKey};
|
||||
clockKey = "someClockKey";
|
||||
clock = jasmine.createSpyObj("clock", [
|
||||
"on",
|
||||
"off",
|
||||
"currentValue"
|
||||
]);
|
||||
clock.currentValue.and.returnValue(100);
|
||||
clock.key = clockKey;
|
||||
bounds = {
|
||||
start: 0,
|
||||
end: 1
|
||||
};
|
||||
eventListener = jasmine.createSpy("eventListener");
|
||||
toi = 111;
|
||||
});
|
||||
|
||||
it("Supports setting and querying of time of interest", function () {
|
||||
expect(api.timeOfInterest()).not.toBe(toi);
|
||||
api.timeOfInterest(toi);
|
||||
expect(api.timeOfInterest()).toBe(toi);
|
||||
});
|
||||
|
||||
it("Allows setting of valid bounds", function () {
|
||||
bounds = {
|
||||
start: 0,
|
||||
end: 1
|
||||
};
|
||||
expect(api.bounds()).not.toBe(bounds);
|
||||
expect(api.bounds.bind(api, bounds)).not.toThrow();
|
||||
expect(api.bounds()).toEqual(bounds);
|
||||
});
|
||||
|
||||
it("Disallows setting of invalid bounds", function () {
|
||||
bounds = {
|
||||
start: 1,
|
||||
end: 0
|
||||
};
|
||||
expect(api.bounds()).not.toEqual(bounds);
|
||||
expect(api.bounds.bind(api, bounds)).toThrow();
|
||||
expect(api.bounds()).not.toEqual(bounds);
|
||||
|
||||
bounds = {start: 1};
|
||||
expect(api.bounds()).not.toEqual(bounds);
|
||||
expect(api.bounds.bind(api, bounds)).toThrow();
|
||||
expect(api.bounds()).not.toEqual(bounds);
|
||||
});
|
||||
|
||||
it("Allows setting of previously registered time system with bounds", function () {
|
||||
api.addTimeSystem(timeSystem);
|
||||
expect(api.timeSystem()).not.toBe(timeSystem);
|
||||
expect(function () {
|
||||
api.timeSystem(timeSystem, bounds);
|
||||
}).not.toThrow();
|
||||
expect(api.timeSystem()).toBe(timeSystem);
|
||||
});
|
||||
|
||||
it("Disallows setting of time system without bounds", function () {
|
||||
api.addTimeSystem(timeSystem);
|
||||
expect(api.timeSystem()).not.toBe(timeSystem);
|
||||
expect(function () {
|
||||
api.timeSystem(timeSystemKey);
|
||||
}).toThrow();
|
||||
expect(api.timeSystem()).not.toBe(timeSystem);
|
||||
});
|
||||
|
||||
it("allows setting of timesystem without bounds with clock", function () {
|
||||
api.addTimeSystem(timeSystem);
|
||||
api.addClock(clock);
|
||||
api.clock(clockKey, {
|
||||
start: 0,
|
||||
end: 1
|
||||
});
|
||||
expect(api.timeSystem()).not.toBe(timeSystem);
|
||||
expect(function () {
|
||||
api.timeSystem(timeSystemKey);
|
||||
}).not.toThrow();
|
||||
expect(api.timeSystem()).toBe(timeSystem);
|
||||
|
||||
});
|
||||
|
||||
it("Emits an event when time system changes", function () {
|
||||
api.addTimeSystem(timeSystem);
|
||||
expect(eventListener).not.toHaveBeenCalled();
|
||||
api.on("timeSystem", eventListener);
|
||||
api.timeSystem(timeSystemKey, bounds);
|
||||
expect(eventListener).toHaveBeenCalledWith(timeSystem);
|
||||
});
|
||||
|
||||
it("Emits an event when time of interest changes", function () {
|
||||
expect(eventListener).not.toHaveBeenCalled();
|
||||
api.on("timeOfInterest", eventListener);
|
||||
api.timeOfInterest(toi);
|
||||
expect(eventListener).toHaveBeenCalledWith(toi);
|
||||
});
|
||||
|
||||
it("Emits an event when bounds change", function () {
|
||||
expect(eventListener).not.toHaveBeenCalled();
|
||||
api.on("bounds", eventListener);
|
||||
api.bounds(bounds);
|
||||
expect(eventListener).toHaveBeenCalledWith(bounds, false);
|
||||
});
|
||||
|
||||
it("If bounds are set and TOI lies inside them, do not change TOI", function () {
|
||||
api.timeOfInterest(6);
|
||||
api.bounds({
|
||||
start: 1,
|
||||
end: 10
|
||||
});
|
||||
expect(api.timeOfInterest()).toEqual(6);
|
||||
});
|
||||
|
||||
it("If bounds are set and TOI lies outside them, reset TOI", function () {
|
||||
api.timeOfInterest(11);
|
||||
api.bounds({
|
||||
start: 1,
|
||||
end: 10
|
||||
});
|
||||
expect(api.timeOfInterest()).toBeUndefined();
|
||||
});
|
||||
|
||||
it("Maintains delta during tick", function () {
|
||||
});
|
||||
|
||||
it("Allows registered time system to be activated", function () {
|
||||
});
|
||||
|
||||
it("Allows a registered tick source to be activated", function () {
|
||||
const mockTickSource = jasmine.createSpyObj("mockTickSource", [
|
||||
"on",
|
||||
"off",
|
||||
"currentValue"
|
||||
]);
|
||||
mockTickSource.key = 'mockTickSource';
|
||||
});
|
||||
|
||||
describe(" when enabling a tick source", function () {
|
||||
let mockTickSource;
|
||||
let anotherMockTickSource;
|
||||
const mockOffsets = {
|
||||
start: 0,
|
||||
end: 1
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
api = new TimeAPI();
|
||||
timeSystemKey = "timeSystemKey";
|
||||
timeSystem = {key: timeSystemKey};
|
||||
clockKey = "someClockKey";
|
||||
clock = jasmine.createSpyObj("clock", [
|
||||
mockTickSource = jasmine.createSpyObj("clock", [
|
||||
"on",
|
||||
"off",
|
||||
"currentValue"
|
||||
]);
|
||||
clock.currentValue.and.returnValue(100);
|
||||
clock.key = clockKey;
|
||||
bounds = {
|
||||
start: 0,
|
||||
end: 1
|
||||
};
|
||||
eventListener = jasmine.createSpy("eventListener");
|
||||
toi = 111;
|
||||
});
|
||||
|
||||
it("Supports setting and querying of time of interest", function () {
|
||||
expect(api.timeOfInterest()).not.toBe(toi);
|
||||
api.timeOfInterest(toi);
|
||||
expect(api.timeOfInterest()).toBe(toi);
|
||||
});
|
||||
|
||||
it("Allows setting of valid bounds", function () {
|
||||
bounds = {
|
||||
start: 0,
|
||||
end: 1
|
||||
};
|
||||
expect(api.bounds()).not.toBe(bounds);
|
||||
expect(api.bounds.bind(api, bounds)).not.toThrow();
|
||||
expect(api.bounds()).toEqual(bounds);
|
||||
});
|
||||
|
||||
it("Disallows setting of invalid bounds", function () {
|
||||
bounds = {
|
||||
start: 1,
|
||||
end: 0
|
||||
};
|
||||
expect(api.bounds()).not.toEqual(bounds);
|
||||
expect(api.bounds.bind(api, bounds)).toThrow();
|
||||
expect(api.bounds()).not.toEqual(bounds);
|
||||
|
||||
bounds = {start: 1};
|
||||
expect(api.bounds()).not.toEqual(bounds);
|
||||
expect(api.bounds.bind(api, bounds)).toThrow();
|
||||
expect(api.bounds()).not.toEqual(bounds);
|
||||
});
|
||||
|
||||
it("Allows setting of previously registered time system with bounds", function () {
|
||||
api.addTimeSystem(timeSystem);
|
||||
expect(api.timeSystem()).not.toBe(timeSystem);
|
||||
expect(function () {
|
||||
api.timeSystem(timeSystem, bounds);
|
||||
}).not.toThrow();
|
||||
expect(api.timeSystem()).toBe(timeSystem);
|
||||
});
|
||||
|
||||
it("Disallows setting of time system without bounds", function () {
|
||||
api.addTimeSystem(timeSystem);
|
||||
expect(api.timeSystem()).not.toBe(timeSystem);
|
||||
expect(function () {
|
||||
api.timeSystem(timeSystemKey);
|
||||
}).toThrow();
|
||||
expect(api.timeSystem()).not.toBe(timeSystem);
|
||||
});
|
||||
|
||||
it("allows setting of timesystem without bounds with clock", function () {
|
||||
api.addTimeSystem(timeSystem);
|
||||
api.addClock(clock);
|
||||
api.clock(clockKey, {
|
||||
start: 0,
|
||||
end: 1
|
||||
});
|
||||
expect(api.timeSystem()).not.toBe(timeSystem);
|
||||
expect(function () {
|
||||
api.timeSystem(timeSystemKey);
|
||||
}).not.toThrow();
|
||||
expect(api.timeSystem()).toBe(timeSystem);
|
||||
|
||||
});
|
||||
|
||||
it("Emits an event when time system changes", function () {
|
||||
api.addTimeSystem(timeSystem);
|
||||
expect(eventListener).not.toHaveBeenCalled();
|
||||
api.on("timeSystem", eventListener);
|
||||
api.timeSystem(timeSystemKey, bounds);
|
||||
expect(eventListener).toHaveBeenCalledWith(timeSystem);
|
||||
});
|
||||
|
||||
it("Emits an event when time of interest changes", function () {
|
||||
expect(eventListener).not.toHaveBeenCalled();
|
||||
api.on("timeOfInterest", eventListener);
|
||||
api.timeOfInterest(toi);
|
||||
expect(eventListener).toHaveBeenCalledWith(toi);
|
||||
});
|
||||
|
||||
it("Emits an event when bounds change", function () {
|
||||
expect(eventListener).not.toHaveBeenCalled();
|
||||
api.on("bounds", eventListener);
|
||||
api.bounds(bounds);
|
||||
expect(eventListener).toHaveBeenCalledWith(bounds, false);
|
||||
});
|
||||
|
||||
it("If bounds are set and TOI lies inside them, do not change TOI", function () {
|
||||
api.timeOfInterest(6);
|
||||
api.bounds({
|
||||
start: 1,
|
||||
end: 10
|
||||
});
|
||||
expect(api.timeOfInterest()).toEqual(6);
|
||||
});
|
||||
|
||||
it("If bounds are set and TOI lies outside them, reset TOI", function () {
|
||||
api.timeOfInterest(11);
|
||||
api.bounds({
|
||||
start: 1,
|
||||
end: 10
|
||||
});
|
||||
expect(api.timeOfInterest()).toBeUndefined();
|
||||
});
|
||||
|
||||
it("Maintains delta during tick", function () {
|
||||
});
|
||||
|
||||
it("Allows registered time system to be activated", function () {
|
||||
});
|
||||
|
||||
it("Allows a registered tick source to be activated", function () {
|
||||
const mockTickSource = jasmine.createSpyObj("mockTickSource", [
|
||||
"on",
|
||||
"off",
|
||||
"currentValue"
|
||||
]);
|
||||
mockTickSource.key = 'mockTickSource';
|
||||
});
|
||||
|
||||
describe(" when enabling a tick source", function () {
|
||||
let mockTickSource;
|
||||
let anotherMockTickSource;
|
||||
const mockOffsets = {
|
||||
start: 0,
|
||||
end: 1
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
mockTickSource = jasmine.createSpyObj("clock", [
|
||||
"on",
|
||||
"off",
|
||||
"currentValue"
|
||||
]);
|
||||
mockTickSource.currentValue.and.returnValue(10);
|
||||
mockTickSource.key = "mts";
|
||||
|
||||
anotherMockTickSource = jasmine.createSpyObj("clock", [
|
||||
"on",
|
||||
"off",
|
||||
"currentValue"
|
||||
]);
|
||||
anotherMockTickSource.key = "amts";
|
||||
anotherMockTickSource.currentValue.and.returnValue(10);
|
||||
|
||||
api.addClock(mockTickSource);
|
||||
api.addClock(anotherMockTickSource);
|
||||
});
|
||||
|
||||
it("sets bounds based on current value", function () {
|
||||
api.clock("mts", mockOffsets);
|
||||
expect(api.bounds()).toEqual({
|
||||
start: 10,
|
||||
end: 11
|
||||
});
|
||||
});
|
||||
|
||||
it("a new tick listener is registered", function () {
|
||||
api.clock("mts", mockOffsets);
|
||||
expect(mockTickSource.on).toHaveBeenCalledWith("tick", jasmine.any(Function));
|
||||
});
|
||||
|
||||
it("listener of existing tick source is reregistered", function () {
|
||||
api.clock("mts", mockOffsets);
|
||||
api.clock("amts", mockOffsets);
|
||||
expect(mockTickSource.off).toHaveBeenCalledWith("tick", jasmine.any(Function));
|
||||
});
|
||||
|
||||
it("Allows the active clock to be set and unset", function () {
|
||||
expect(api.clock()).toBeUndefined();
|
||||
api.clock("mts", mockOffsets);
|
||||
expect(api.clock()).toBeDefined();
|
||||
api.stopClock();
|
||||
expect(api.clock()).toBeUndefined();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it("on tick, observes offsets, and indicates tick in bounds callback", function () {
|
||||
const mockTickSource = jasmine.createSpyObj("clock", [
|
||||
"on",
|
||||
"off",
|
||||
"currentValue"
|
||||
]);
|
||||
mockTickSource.currentValue.and.returnValue(100);
|
||||
let tickCallback;
|
||||
const boundsCallback = jasmine.createSpy("boundsCallback");
|
||||
const clockOffsets = {
|
||||
start: -100,
|
||||
end: 100
|
||||
};
|
||||
mockTickSource.currentValue.and.returnValue(10);
|
||||
mockTickSource.key = "mts";
|
||||
|
||||
anotherMockTickSource = jasmine.createSpyObj("clock", [
|
||||
"on",
|
||||
"off",
|
||||
"currentValue"
|
||||
]);
|
||||
anotherMockTickSource.key = "amts";
|
||||
anotherMockTickSource.currentValue.and.returnValue(10);
|
||||
|
||||
api.addClock(mockTickSource);
|
||||
api.clock("mts", clockOffsets);
|
||||
|
||||
api.on("bounds", boundsCallback);
|
||||
|
||||
tickCallback = mockTickSource.on.calls.mostRecent().args[1];
|
||||
tickCallback(1000);
|
||||
expect(boundsCallback).toHaveBeenCalledWith({
|
||||
start: 900,
|
||||
end: 1100
|
||||
}, true);
|
||||
api.addClock(anotherMockTickSource);
|
||||
});
|
||||
|
||||
it("sets bounds based on current value", function () {
|
||||
api.clock("mts", mockOffsets);
|
||||
expect(api.bounds()).toEqual({
|
||||
start: 10,
|
||||
end: 11
|
||||
});
|
||||
});
|
||||
|
||||
it("a new tick listener is registered", function () {
|
||||
api.clock("mts", mockOffsets);
|
||||
expect(mockTickSource.on).toHaveBeenCalledWith("tick", jasmine.any(Function));
|
||||
});
|
||||
|
||||
it("listener of existing tick source is reregistered", function () {
|
||||
api.clock("mts", mockOffsets);
|
||||
api.clock("amts", mockOffsets);
|
||||
expect(mockTickSource.off).toHaveBeenCalledWith("tick", jasmine.any(Function));
|
||||
});
|
||||
|
||||
it("Allows the active clock to be set and unset", function () {
|
||||
expect(api.clock()).toBeUndefined();
|
||||
api.clock("mts", mockOffsets);
|
||||
expect(api.clock()).toBeDefined();
|
||||
api.stopClock();
|
||||
expect(api.clock()).toBeUndefined();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it("on tick, observes offsets, and indicates tick in bounds callback", function () {
|
||||
const mockTickSource = jasmine.createSpyObj("clock", [
|
||||
"on",
|
||||
"off",
|
||||
"currentValue"
|
||||
]);
|
||||
mockTickSource.currentValue.and.returnValue(100);
|
||||
let tickCallback;
|
||||
const boundsCallback = jasmine.createSpy("boundsCallback");
|
||||
const clockOffsets = {
|
||||
start: -100,
|
||||
end: 100
|
||||
};
|
||||
mockTickSource.key = "mts";
|
||||
|
||||
api.addClock(mockTickSource);
|
||||
api.clock("mts", clockOffsets);
|
||||
|
||||
api.on("bounds", boundsCallback);
|
||||
|
||||
tickCallback = mockTickSource.on.calls.mostRecent().args[1];
|
||||
tickCallback(1000);
|
||||
expect(boundsCallback).toHaveBeenCalledWith({
|
||||
start: 900,
|
||||
end: 1100
|
||||
}, true);
|
||||
});
|
||||
});
|
||||
|
360
src/api/time/TimeContext.js
Normal file
360
src/api/time/TimeContext.js
Normal file
@ -0,0 +1,360 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import EventEmitter from 'EventEmitter';
|
||||
|
||||
class TimeContext extends EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
//The Time System
|
||||
this.timeSystems = new Map();
|
||||
|
||||
this.system = undefined;
|
||||
|
||||
this.clocks = new Map();
|
||||
|
||||
this.boundsVal = {
|
||||
start: undefined,
|
||||
end: undefined
|
||||
};
|
||||
|
||||
this.activeClock = undefined;
|
||||
this.offsets = undefined;
|
||||
|
||||
this.tick = this.tick.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or set the time system of the TimeAPI.
|
||||
* @param {TimeSystem | string} timeSystem
|
||||
* @param {module:openmct.TimeAPI~TimeConductorBounds} bounds
|
||||
* @fires module:openmct.TimeAPI~timeSystem
|
||||
* @returns {TimeSystem} The currently applied time system
|
||||
* @memberof module:openmct.TimeAPI#
|
||||
* @method timeSystem
|
||||
*/
|
||||
timeSystem(timeSystemOrKey, bounds) {
|
||||
if (arguments.length >= 1) {
|
||||
if (arguments.length === 1 && !this.activeClock) {
|
||||
throw new Error(
|
||||
"Must specify bounds when changing time system without "
|
||||
+ "an active clock."
|
||||
);
|
||||
}
|
||||
|
||||
let timeSystem;
|
||||
|
||||
if (timeSystemOrKey === undefined) {
|
||||
throw "Please provide a time system";
|
||||
}
|
||||
|
||||
if (typeof timeSystemOrKey === 'string') {
|
||||
timeSystem = this.timeSystems.get(timeSystemOrKey);
|
||||
|
||||
if (timeSystem === undefined) {
|
||||
throw "Unknown time system " + timeSystemOrKey + ". Has it been registered with 'addTimeSystem'?";
|
||||
}
|
||||
} else if (typeof timeSystemOrKey === 'object') {
|
||||
timeSystem = timeSystemOrKey;
|
||||
|
||||
if (!this.timeSystems.has(timeSystem.key)) {
|
||||
throw "Unknown time system " + timeSystem.key + ". Has it been registered with 'addTimeSystem'?";
|
||||
}
|
||||
} else {
|
||||
throw "Attempt to set invalid time system in Time API. Please provide a previously registered time system object or key";
|
||||
}
|
||||
|
||||
this.system = timeSystem;
|
||||
|
||||
/**
|
||||
* The time system used by the time
|
||||
* conductor has changed. A change in Time System will always be
|
||||
* followed by a bounds event specifying new query bounds.
|
||||
*
|
||||
* @event module:openmct.TimeAPI~timeSystem
|
||||
* @property {TimeSystem} The value of the currently applied
|
||||
* Time System
|
||||
* */
|
||||
this.emit('timeSystem', this.system);
|
||||
if (bounds) {
|
||||
this.bounds(bounds);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return this.system;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clock offsets are used to calculate temporal bounds when the system is
|
||||
* ticking on a clock source.
|
||||
*
|
||||
* @typedef {object} ValidationResult
|
||||
* @property {boolean} valid Result of the validation - true or false.
|
||||
* @property {string} message An error message if valid is false.
|
||||
*/
|
||||
/**
|
||||
* Validate the given bounds. This can be used for pre-validation of bounds,
|
||||
* for example by views validating user inputs.
|
||||
* @param {TimeBounds} bounds The start and end time of the conductor.
|
||||
* @returns {ValidationResult} A validation error, or true if valid
|
||||
* @memberof module:openmct.TimeAPI#
|
||||
* @method validateBounds
|
||||
*/
|
||||
validateBounds(bounds) {
|
||||
if ((bounds.start === undefined)
|
||||
|| (bounds.end === undefined)
|
||||
|| isNaN(bounds.start)
|
||||
|| isNaN(bounds.end)
|
||||
) {
|
||||
return {
|
||||
valid: false,
|
||||
message: "Start and end must be specified as integer values"
|
||||
};
|
||||
} else if (bounds.start > bounds.end) {
|
||||
return {
|
||||
valid: false,
|
||||
message: "Specified start date exceeds end bound"
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
message: ''
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or set the start and end time of the time conductor. Basic validation
|
||||
* of bounds is performed.
|
||||
*
|
||||
* @param {module:openmct.TimeAPI~TimeConductorBounds} newBounds
|
||||
* @throws {Error} Validation error
|
||||
* @fires module:openmct.TimeAPI~bounds
|
||||
* @returns {module:openmct.TimeAPI~TimeConductorBounds}
|
||||
* @memberof module:openmct.TimeAPI#
|
||||
* @method bounds
|
||||
*/
|
||||
bounds(newBounds) {
|
||||
if (arguments.length > 0) {
|
||||
const validationResult = this.validateBounds(newBounds);
|
||||
if (validationResult.valid !== true) {
|
||||
throw new Error(validationResult.message);
|
||||
}
|
||||
|
||||
//Create a copy to avoid direct mutation of conductor bounds
|
||||
this.boundsVal = JSON.parse(JSON.stringify(newBounds));
|
||||
/**
|
||||
* The start time, end time, or both have been updated.
|
||||
* @event bounds
|
||||
* @memberof module:openmct.TimeAPI~
|
||||
* @property {TimeConductorBounds} bounds The newly updated bounds
|
||||
* @property {boolean} [tick] `true` if the bounds update was due to
|
||||
* a "tick" event (ie. was an automatic update), false otherwise.
|
||||
*/
|
||||
this.emit('bounds', this.boundsVal, false);
|
||||
}
|
||||
|
||||
//Return a copy to prevent direct mutation of time conductor bounds.
|
||||
return JSON.parse(JSON.stringify(this.boundsVal));
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the given offsets. This can be used for pre-validation of
|
||||
* offsets, for example by views validating user inputs.
|
||||
* @param {ClockOffsets} offsets The start and end offsets from a 'now' value.
|
||||
* @returns { ValidationResult } A validation error, and true/false if valid or not
|
||||
* @memberof module:openmct.TimeAPI#
|
||||
* @method validateOffsets
|
||||
*/
|
||||
validateOffsets(offsets) {
|
||||
if ((offsets.start === undefined)
|
||||
|| (offsets.end === undefined)
|
||||
|| isNaN(offsets.start)
|
||||
|| isNaN(offsets.end)
|
||||
) {
|
||||
return {
|
||||
valid: false,
|
||||
message: "Start and end offsets must be specified as integer values"
|
||||
};
|
||||
} else if (offsets.start >= offsets.end) {
|
||||
return {
|
||||
valid: false,
|
||||
message: "Specified start offset must be < end offset"
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
message: ''
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} TimeBounds
|
||||
* @property {number} start The start time displayed by the time conductor
|
||||
* in ms since epoch. Epoch determined by currently active time system
|
||||
* @property {number} end The end time displayed by the time conductor in ms
|
||||
* since epoch.
|
||||
* @memberof module:openmct.TimeAPI~
|
||||
*/
|
||||
|
||||
/**
|
||||
* Clock offsets are used to calculate temporal bounds when the system is
|
||||
* ticking on a clock source.
|
||||
*
|
||||
* @typedef {object} ClockOffsets
|
||||
* @property {number} start A time span relative to the current value of the
|
||||
* ticking clock, from which start bounds will be calculated. This value must
|
||||
* be < 0. When a clock is active, bounds will be calculated automatically
|
||||
* based on the value provided by the clock, and the defined clock offsets.
|
||||
* @property {number} end A time span relative to the current value of the
|
||||
* ticking clock, from which end bounds will be calculated. This value must
|
||||
* be >= 0.
|
||||
*/
|
||||
/**
|
||||
* Get or set the currently applied clock offsets. If no parameter is provided,
|
||||
* the current value will be returned. If provided, the new value will be
|
||||
* used as the new clock offsets.
|
||||
* @param {ClockOffsets} offsets
|
||||
* @returns {ClockOffsets}
|
||||
*/
|
||||
clockOffsets(offsets) {
|
||||
if (arguments.length > 0) {
|
||||
|
||||
const validationResult = this.validateOffsets(offsets);
|
||||
if (validationResult.valid !== true) {
|
||||
throw new Error(validationResult.message);
|
||||
}
|
||||
|
||||
this.offsets = offsets;
|
||||
|
||||
const currentValue = this.activeClock.currentValue();
|
||||
const newBounds = {
|
||||
start: currentValue + offsets.start,
|
||||
end: currentValue + offsets.end
|
||||
};
|
||||
|
||||
this.bounds(newBounds);
|
||||
|
||||
/**
|
||||
* Event that is triggered when clock offsets change.
|
||||
* @event clockOffsets
|
||||
* @memberof module:openmct.TimeAPI~
|
||||
* @property {ClockOffsets} clockOffsets The newly activated clock
|
||||
* offsets.
|
||||
*/
|
||||
this.emit("clockOffsets", offsets);
|
||||
}
|
||||
|
||||
return this.offsets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the currently active clock from ticking, and unset it. This will
|
||||
* revert all views to showing a static time frame defined by the current
|
||||
* bounds.
|
||||
*/
|
||||
stopClock() {
|
||||
if (this.activeClock) {
|
||||
this.clock(undefined, undefined);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the active clock. Tick source will be immediately subscribed to
|
||||
* and ticking will begin. Offsets from 'now' must also be provided. A clock
|
||||
* can be unset by calling {@link stopClock}.
|
||||
*
|
||||
* @param {Clock || string} keyOrClock The clock to activate, or its key
|
||||
* @param {ClockOffsets} offsets on each tick these will be used to calculate
|
||||
* the start and end bounds. This maintains a sliding time window of a fixed
|
||||
* width that automatically updates.
|
||||
* @fires module:openmct.TimeAPI~clock
|
||||
* @return {Clock} the currently active clock;
|
||||
*/
|
||||
clock(keyOrClock, offsets) {
|
||||
if (arguments.length === 2) {
|
||||
let clock;
|
||||
|
||||
if (typeof keyOrClock === 'string') {
|
||||
clock = this.clocks.get(keyOrClock);
|
||||
if (clock === undefined) {
|
||||
throw "Unknown clock '" + keyOrClock + "'. Has it been registered with 'addClock'?";
|
||||
}
|
||||
} else if (typeof keyOrClock === 'object') {
|
||||
clock = keyOrClock;
|
||||
if (!this.clocks.has(clock.key)) {
|
||||
throw "Unknown clock '" + keyOrClock.key + "'. Has it been registered with 'addClock'?";
|
||||
}
|
||||
}
|
||||
|
||||
const previousClock = this.activeClock;
|
||||
if (previousClock !== undefined) {
|
||||
previousClock.off("tick", this.tick);
|
||||
}
|
||||
|
||||
this.activeClock = clock;
|
||||
|
||||
/**
|
||||
* The active clock has changed. Clock can be unset by calling {@link stopClock}
|
||||
* @event clock
|
||||
* @memberof module:openmct.TimeAPI~
|
||||
* @property {Clock} clock The newly activated clock, or undefined
|
||||
* if the system is no longer following a clock source
|
||||
*/
|
||||
this.emit("clock", this.activeClock);
|
||||
|
||||
if (this.activeClock !== undefined) {
|
||||
this.clockOffsets(offsets);
|
||||
this.activeClock.on("tick", this.tick);
|
||||
}
|
||||
|
||||
} else if (arguments.length === 1) {
|
||||
throw "When setting the clock, clock offsets must also be provided";
|
||||
}
|
||||
|
||||
return this.activeClock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update bounds based on provided time and current offsets
|
||||
* @param {number} timestamp A time from which bounds will be calculated
|
||||
* using current offsets.
|
||||
*/
|
||||
tick(timestamp) {
|
||||
if (!this.activeClock) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newBounds = {
|
||||
start: timestamp + this.offsets.start,
|
||||
end: timestamp + this.offsets.end
|
||||
};
|
||||
|
||||
this.boundsVal = newBounds;
|
||||
this.emit('bounds', this.boundsVal, true);
|
||||
}
|
||||
}
|
||||
|
||||
export default TimeContext;
|
155
src/api/time/independentTimeAPISpec.js
Normal file
155
src/api/time/independentTimeAPISpec.js
Normal file
@ -0,0 +1,155 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import TimeAPI from "./TimeAPI";
|
||||
import {createOpenMct} from "utils/testing";
|
||||
describe("The Independent Time API", function () {
|
||||
let api;
|
||||
let domainObjectKey;
|
||||
let clockKey;
|
||||
let clock;
|
||||
let bounds;
|
||||
let independentBounds;
|
||||
let eventListener;
|
||||
let openmct;
|
||||
|
||||
beforeEach(function () {
|
||||
openmct = createOpenMct();
|
||||
api = new TimeAPI(openmct);
|
||||
clockKey = "someClockKey";
|
||||
clock = jasmine.createSpyObj("clock", [
|
||||
"on",
|
||||
"off",
|
||||
"currentValue"
|
||||
]);
|
||||
clock.currentValue.and.returnValue(100);
|
||||
clock.key = clockKey;
|
||||
api.addClock(clock);
|
||||
domainObjectKey = 'test-key';
|
||||
bounds = {
|
||||
start: 0,
|
||||
end: 1
|
||||
};
|
||||
api.bounds(bounds);
|
||||
independentBounds = {
|
||||
start: 10,
|
||||
end: 11
|
||||
};
|
||||
eventListener = jasmine.createSpy("eventListener");
|
||||
});
|
||||
|
||||
it("Creates an independent time context", () => {
|
||||
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
|
||||
let timeContext = api.getIndependentContext(domainObjectKey);
|
||||
expect(timeContext.bounds()).toEqual(independentBounds);
|
||||
destroyTimeContext();
|
||||
});
|
||||
|
||||
it("Gets an independent time context given the objectPath", () => {
|
||||
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
|
||||
let timeContext = api.getContextForView([{
|
||||
identifier: {
|
||||
namespace: '',
|
||||
key: 'blah'
|
||||
}
|
||||
}, { identifier: domainObjectKey }]);
|
||||
expect(timeContext.bounds()).toEqual(independentBounds);
|
||||
destroyTimeContext();
|
||||
});
|
||||
|
||||
it("defaults to the global time context given the objectPath", () => {
|
||||
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
|
||||
let timeContext = api.getContextForView([{
|
||||
identifier: {
|
||||
namespace: '',
|
||||
key: 'blah'
|
||||
}
|
||||
}]);
|
||||
expect(timeContext.bounds()).toEqual(bounds);
|
||||
destroyTimeContext();
|
||||
});
|
||||
|
||||
it("Allows setting of valid bounds", function () {
|
||||
bounds = {
|
||||
start: 0,
|
||||
end: 1
|
||||
};
|
||||
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
|
||||
let timeContext = api.getContextForView([{identifier: domainObjectKey}]);
|
||||
expect(timeContext.bounds()).not.toEqual(bounds);
|
||||
timeContext.bounds(bounds);
|
||||
expect(timeContext.bounds()).toEqual(bounds);
|
||||
destroyTimeContext();
|
||||
});
|
||||
|
||||
it("Disallows setting of invalid bounds", function () {
|
||||
bounds = {
|
||||
start: 1,
|
||||
end: 0
|
||||
};
|
||||
|
||||
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
|
||||
let timeContext = api.getContextForView([{identifier: domainObjectKey}]);
|
||||
expect(timeContext.bounds()).not.toBe(bounds);
|
||||
|
||||
expect(timeContext.bounds.bind(timeContext, bounds)).toThrow();
|
||||
expect(timeContext.bounds()).not.toEqual(bounds);
|
||||
|
||||
bounds = {start: 1};
|
||||
expect(timeContext.bounds()).not.toEqual(bounds);
|
||||
expect(timeContext.bounds.bind(timeContext, bounds)).toThrow();
|
||||
expect(timeContext.bounds()).not.toEqual(bounds);
|
||||
destroyTimeContext();
|
||||
});
|
||||
|
||||
it("Emits an event when bounds change", function () {
|
||||
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
|
||||
let timeContext = api.getContextForView([{identifier: domainObjectKey}]);
|
||||
expect(eventListener).not.toHaveBeenCalled();
|
||||
timeContext.on('bounds', eventListener);
|
||||
timeContext.bounds(bounds);
|
||||
expect(eventListener).toHaveBeenCalledWith(bounds, false);
|
||||
destroyTimeContext();
|
||||
});
|
||||
|
||||
describe(" when using real time clock", function () {
|
||||
const mockOffsets = {
|
||||
start: 10,
|
||||
end: 11
|
||||
};
|
||||
|
||||
it("Emits an event when bounds change based on current value", function () {
|
||||
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
|
||||
let timeContext = api.getContextForView([{identifier: domainObjectKey}]);
|
||||
expect(eventListener).not.toHaveBeenCalled();
|
||||
timeContext.clock('someClockKey', mockOffsets);
|
||||
timeContext.on('bounds', eventListener);
|
||||
timeContext.tick(10);
|
||||
expect(eventListener).toHaveBeenCalledWith({
|
||||
start: 20,
|
||||
end: 21
|
||||
}, true);
|
||||
destroyTimeContext();
|
||||
});
|
||||
|
||||
});
|
||||
});
|
Reference in New Issue
Block a user