diff --git a/src/MCT.js b/src/MCT.js
index dc4d273fb7..352d103e63 100644
--- a/src/MCT.js
+++ b/src/MCT.js
@@ -136,7 +136,7 @@ define([
* @memberof module:openmct.MCT#
* @name conductor
*/
- this.time = new api.TimeAPI();
+ this.time = new api.TimeAPI(this);
/**
* An interface for interacting with the composition of domain objects.
diff --git a/src/api/api.js b/src/api/api.js
index 72332087f3..f3f7db28cd 100644
--- a/src/api/api.js
+++ b/src/api/api.js
@@ -46,7 +46,7 @@ define([
StatusAPI
) {
return {
- TimeAPI: TimeAPI,
+ TimeAPI: TimeAPI.default,
ObjectAPI: ObjectAPI,
CompositionAPI: CompositionAPI,
TypeRegistry: TypeRegistry,
diff --git a/src/api/time/GlobalTimeContext.js b/src/api/time/GlobalTimeContext.js
new file mode 100644
index 0000000000..8ad5a24723
--- /dev/null
+++ b/src/api/time/GlobalTimeContext.js
@@ -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;
diff --git a/src/api/time/IndependentTimeContext.js b/src/api/time/IndependentTimeContext.js
new file mode 100644
index 0000000000..78c4b39352
--- /dev/null
+++ b/src/api/time/IndependentTimeContext.js
@@ -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;
diff --git a/src/api/time/TimeAPI.js b/src/api/time/TimeAPI.js
index 75f19ff660..34cf6483e0 100644
--- a/src/api/time/TimeAPI.js
+++ b/src/api/time/TimeAPI.js
@@ -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;
diff --git a/src/api/time/TimeAPISpec.js b/src/api/time/TimeAPISpec.js
index 303d30548c..97bd583f5c 100644
--- a/src/api/time/TimeAPISpec.js
+++ b/src/api/time/TimeAPISpec.js
@@ -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);
});
});
diff --git a/src/api/time/TimeContext.js b/src/api/time/TimeContext.js
new file mode 100644
index 0000000000..3307342d73
--- /dev/null
+++ b/src/api/time/TimeContext.js
@@ -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;
diff --git a/src/api/time/independentTimeAPISpec.js b/src/api/time/independentTimeAPISpec.js
new file mode 100644
index 0000000000..0e932867d7
--- /dev/null
+++ b/src/api/time/independentTimeAPISpec.js
@@ -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();
+ });
+
+ });
+});
diff --git a/src/plugins/imagery/components/ImageryTimeView.vue b/src/plugins/imagery/components/ImageryTimeView.vue
index d34a0bf506..3f65cb2b68 100644
--- a/src/plugins/imagery/components/ImageryTimeView.vue
+++ b/src/plugins/imagery/components/ImageryTimeView.vue
@@ -40,7 +40,6 @@ import PreviewAction from "@/ui/preview/PreviewAction";
import _ from "lodash";
const PADDING = 1;
-const RESIZE_POLL_INTERVAL = 200;
const ROW_HEIGHT = 100;
const IMAGE_WIDTH_THRESHOLD = 40;
diff --git a/src/plugins/imagery/pluginSpec.js b/src/plugins/imagery/pluginSpec.js
index 9c8e6fe394..d64aba9ab8 100644
--- a/src/plugins/imagery/pluginSpec.js
+++ b/src/plugins/imagery/pluginSpec.js
@@ -32,19 +32,19 @@ const TEN_MINUTES = ONE_MINUTE * 10;
const MAIN_IMAGE_CLASS = '.js-imageryView-image';
const NEW_IMAGE_CLASS = '.c-imagery__age.c-imagery--new';
const REFRESH_CSS_MS = 500;
-const TOLERANCE = 0.50;
+// const TOLERANCE = 0.50;
-function comparisonFunction(valueOne, valueTwo) {
- let larger = valueOne;
- let smaller = valueTwo;
-
- if (larger < smaller) {
- larger = valueTwo;
- smaller = valueOne;
- }
-
- return (larger - smaller) < TOLERANCE;
-}
+// function comparisonFunction(valueOne, valueTwo) {
+// let larger = valueOne;
+// let smaller = valueTwo;
+//
+// if (larger < smaller) {
+// larger = valueTwo;
+// smaller = valueOne;
+// }
+//
+// return (larger - smaller) < TOLERANCE;
+// }
function getImageInfo(doc) {
let imageElement = doc.querySelectorAll(MAIN_IMAGE_CLASS)[0];
diff --git a/src/plugins/plan/Plan.vue b/src/plugins/plan/Plan.vue
index 92fd29ba87..85f6628196 100644
--- a/src/plugins/plan/Plan.vue
+++ b/src/plugins/plan/Plan.vue
@@ -67,7 +67,7 @@ export default {
TimelineAxis,
SwimLane
},
- inject: ['openmct', 'domainObject'],
+ inject: ['openmct', 'domainObject', 'path'],
props: {
options: {
type: Object,
@@ -99,21 +99,37 @@ export default {
this.canvasContext = this.canvas.getContext('2d');
this.setDimensions();
- this.updateViewBounds();
- this.openmct.time.on("timeSystem", this.setScaleAndPlotActivities);
- this.openmct.time.on("bounds", this.updateViewBounds);
+ this.setTimeContext();
this.resizeTimer = setInterval(this.resize, RESIZE_POLL_INTERVAL);
this.unlisten = this.openmct.objects.observe(this.domainObject, '*', this.observeForChanges);
},
beforeDestroy() {
clearInterval(this.resizeTimer);
- this.openmct.time.off("timeSystem", this.setScaleAndPlotActivities);
- this.openmct.time.off("bounds", this.updateViewBounds);
+ this.stopFollowingTimeContext();
if (this.unlisten) {
this.unlisten();
}
},
methods: {
+ setTimeContext() {
+ this.stopFollowingTimeContext();
+ this.timeContext = this.openmct.time.getContextForView(this.path);
+ this.timeContext.on("timeContext", this.setTimeContext);
+ this.followTimeContext();
+ },
+ followTimeContext() {
+ this.updateViewBounds(this.timeContext.bounds());
+
+ this.timeContext.on("timeSystem", this.setScaleAndPlotActivities);
+ this.timeContext.on("bounds", this.updateViewBounds);
+ },
+ stopFollowingTimeContext() {
+ if (this.timeContext) {
+ this.timeContext.off("timeSystem", this.setScaleAndPlotActivities);
+ this.timeContext.off("bounds", this.updateViewBounds);
+ this.timeContext.off("timeContext", this.setTimeContext);
+ }
+ },
observeForChanges(mutatedObject) {
this.getPlanData(mutatedObject);
this.setScaleAndPlotActivities();
@@ -141,13 +157,9 @@ export default {
getPlanData(domainObject) {
this.planData = getValidatedPlan(domainObject);
},
- updateViewBounds() {
- this.viewBounds = this.openmct.time.bounds();
- if (!this.options.compact) {
- //Add a 50% padding to the end bounds to look ahead
- let timespan = (this.viewBounds.end - this.viewBounds.start);
- let padding = timespan / 2;
- this.viewBounds.end = this.viewBounds.end + padding;
+ updateViewBounds(bounds) {
+ if (bounds) {
+ this.viewBounds = Object.create(bounds);
}
if (this.timeSystem === undefined) {
diff --git a/src/plugins/plan/PlanViewProvider.js b/src/plugins/plan/PlanViewProvider.js
index ebd1c1e24a..d80bc86434 100644
--- a/src/plugins/plan/PlanViewProvider.js
+++ b/src/plugins/plan/PlanViewProvider.js
@@ -54,7 +54,8 @@ export default function PlanViewProvider(openmct) {
},
provide: {
openmct,
- domainObject
+ domainObject,
+ path: objectPath
},
data() {
return {
diff --git a/src/plugins/plot/MctPlot.vue b/src/plugins/plot/MctPlot.vue
index 40cddda8a9..16917ad22e 100644
--- a/src/plugins/plot/MctPlot.vue
+++ b/src/plugins/plot/MctPlot.vue
@@ -173,7 +173,7 @@ export default {
MctTicks,
MctChart
},
- inject: ['openmct', 'domainObject'],
+ inject: ['openmct', 'domainObject', 'path'],
props: {
options: {
type: Object,
@@ -244,6 +244,9 @@ export default {
},
mounted() {
eventHelpers.extend(this);
+ this.updateRealTime = this.updateRealTime.bind(this);
+ this.updateDisplayBounds = this.updateDisplayBounds.bind(this);
+ this.setTimeContext = this.setTimeContext.bind(this);
this.config = this.getConfig();
this.legend = this.config.legend;
@@ -261,7 +264,7 @@ export default {
this.removeStatusListener = this.openmct.status.observe(this.domainObject.identifier, this.updateStatus);
this.openmct.objectViews.on('clearData', this.clearData);
- this.followTimeConductor();
+ this.setTimeContext();
this.loaded = true;
@@ -274,11 +277,27 @@ export default {
this.destroy();
},
methods: {
- followTimeConductor() {
- this.openmct.time.on('clock', this.updateRealTime);
- this.openmct.time.on('bounds', this.updateDisplayBounds);
+ setTimeContext() {
+ this.stopFollowingTimeContext();
+
+ this.timeContext = this.openmct.time.getContextForView(this.path);
+ this.timeContext.on('timeContext', this.setTimeContext);
+ this.followTimeContext();
+
+ },
+ followTimeContext() {
+ this.updateDisplayBounds(this.timeContext.bounds());
+ this.timeContext.on('clock', this.updateRealTime);
+ this.timeContext.on('bounds', this.updateDisplayBounds);
this.synchronized(true);
},
+ stopFollowingTimeContext() {
+ if (this.timeContext) {
+ this.timeContext.off("clock", this.updateRealTime);
+ this.timeContext.off("bounds", this.updateDisplayBounds);
+ this.timeContext.off("timeContext", this.setTimeContext);
+ }
+ },
getConfig() {
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
let config = configStore.get(configId);
@@ -485,7 +504,7 @@ export default {
* displays can update accordingly.
*/
synchronized(value) {
- const isLocalClock = this.openmct.time.clock();
+ const isLocalClock = this.timeContext.clock();
if (typeof value !== 'undefined') {
this._synchronized = value;
@@ -958,7 +977,7 @@ export default {
},
showSynchronizeDialog() {
- const isLocalClock = this.openmct.time.clock();
+ const isLocalClock = this.timeContext.clock();
if (isLocalClock !== undefined) {
const message = `
This action will change the Time Conductor to Fixed Timespan mode with this plot view's current time bounds.
@@ -993,9 +1012,9 @@ export default {
},
synchronizeTimeConductor() {
- this.openmct.time.stopClock();
+ this.timeContext.stopClock();
const range = this.config.xAxis.get('displayRange');
- this.openmct.time.bounds({
+ this.timeContext.bounds({
start: range.min,
end: range.max
});
@@ -1006,6 +1025,7 @@ export default {
configStore.deleteStore(this.config.id);
this.stopListening();
+
if (this.checkForSize) {
clearInterval(this.checkForSize);
delete this.checkForSize;
@@ -1021,8 +1041,7 @@ export default {
this.plotContainerResizeObserver.disconnect();
- this.openmct.time.off('clock', this.updateRealTime);
- this.openmct.time.off('bounds', this.updateDisplayBounds);
+ this.stopFollowingTimeContext();
this.openmct.objectViews.off('clearData', this.clearData);
},
updateStatus(status) {
diff --git a/src/plugins/plot/Plot.vue b/src/plugins/plot/Plot.vue
index c8c1e3ffc1..79ee10bbf3 100644
--- a/src/plugins/plot/Plot.vue
+++ b/src/plugins/plot/Plot.vue
@@ -80,7 +80,7 @@ export default {
components: {
MctPlot
},
- inject: ['openmct', 'domainObject'],
+ inject: ['openmct', 'domainObject', 'path'],
props: {
options: {
type: Object,
diff --git a/src/plugins/plot/PlotViewProvider.js b/src/plugins/plot/PlotViewProvider.js
index 22b73f2f5d..10fc5024e6 100644
--- a/src/plugins/plot/PlotViewProvider.js
+++ b/src/plugins/plot/PlotViewProvider.js
@@ -68,7 +68,8 @@ export default function PlotViewProvider(openmct) {
},
provide: {
openmct,
- domainObject
+ domainObject,
+ path: objectPath
},
data() {
return {
diff --git a/src/plugins/plot/overlayPlot/OverlayPlotViewProvider.js b/src/plugins/plot/overlayPlot/OverlayPlotViewProvider.js
index 415184281c..729eed8a3b 100644
--- a/src/plugins/plot/overlayPlot/OverlayPlotViewProvider.js
+++ b/src/plugins/plot/overlayPlot/OverlayPlotViewProvider.js
@@ -53,7 +53,8 @@ export default function OverlayPlotViewProvider(openmct) {
},
provide: {
openmct,
- domainObject
+ domainObject,
+ path: objectPath
},
data() {
return {
diff --git a/src/plugins/plot/pluginSpec.js b/src/plugins/plot/pluginSpec.js
index d10a5edeff..96dc95b68e 100644
--- a/src/plugins/plot/pluginSpec.js
+++ b/src/plugins/plot/pluginSpec.js
@@ -570,7 +570,8 @@ describe("the plugin", function () {
provide: {
openmct: openmct,
domainObject: stackedPlotObject,
- composition: openmct.composition.get(stackedPlotObject)
+ composition: openmct.composition.get(stackedPlotObject),
+ path: [stackedPlotObject]
},
template: "