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: "" }); diff --git a/src/plugins/plot/stackedPlot/StackedPlot.vue b/src/plugins/plot/stackedPlot/StackedPlot.vue index d342ce8d52..7d46a29136 100644 --- a/src/plugins/plot/stackedPlot/StackedPlot.vue +++ b/src/plugins/plot/stackedPlot/StackedPlot.vue @@ -75,7 +75,7 @@ export default { components: { StackedPlotItem }, - inject: ['openmct', 'domainObject', 'composition'], + inject: ['openmct', 'domainObject', 'composition', 'path'], props: { options: { type: Object, diff --git a/src/plugins/plot/stackedPlot/StackedPlotItem.vue b/src/plugins/plot/stackedPlot/StackedPlotItem.vue index 2284f2a6da..08f6ecea60 100644 --- a/src/plugins/plot/stackedPlot/StackedPlotItem.vue +++ b/src/plugins/plot/stackedPlot/StackedPlotItem.vue @@ -28,7 +28,7 @@ import MctPlot from '../MctPlot.vue'; import Vue from "vue"; export default { - inject: ['openmct', 'domainObject'], + inject: ['openmct', 'domainObject', 'path'], props: { object: { type: Object, @@ -94,6 +94,7 @@ export default { const openmct = this.openmct; const object = this.object; + const path = this.path; const getProps = this.getProps; let viewContainer = document.createElement('div'); @@ -106,7 +107,8 @@ export default { }, provide: { openmct, - domainObject: object + domainObject: object, + path }, data() { return { diff --git a/src/plugins/plot/stackedPlot/StackedPlotViewProvider.js b/src/plugins/plot/stackedPlot/StackedPlotViewProvider.js index cb998148b4..c69ec7e684 100644 --- a/src/plugins/plot/stackedPlot/StackedPlotViewProvider.js +++ b/src/plugins/plot/stackedPlot/StackedPlotViewProvider.js @@ -55,7 +55,8 @@ export default function StackedPlotViewProvider(openmct) { provide: { openmct, domainObject, - composition: openmct.composition.get(domainObject) + composition: openmct.composition.get(domainObject), + path: objectPath }, data() { return { diff --git a/src/plugins/timeConductor/Conductor.vue b/src/plugins/timeConductor/Conductor.vue index 123a669621..38e2b1a0d2 100644 --- a/src/plugins/timeConductor/Conductor.vue +++ b/src/plugins/timeConductor/Conductor.vue @@ -29,144 +29,36 @@ isFixed ? 'is-fixed-mode' : 'is-realtime-mode' ]" > -
-
- - - -
- -
- Start -
- - -
- -
- -
- - -
- -
- -
- {{ isFixed ? 'End' : 'Updated' }} -
- - -
- -
- -
- - -
- - - -
-
- - - -
- -
+
+ + + + +
+
+ + + +
@@ -174,23 +66,23 @@ import _ from 'lodash'; import ConductorMode from './ConductorMode.vue'; import ConductorTimeSystem from './ConductorTimeSystem.vue'; -import DatePicker from './DatePicker.vue'; import ConductorAxis from './ConductorAxis.vue'; import ConductorModeIcon from './ConductorModeIcon.vue'; import ConductorHistory from './ConductorHistory.vue'; -import TimePopup from './timePopup.vue'; +import ConductorInputsFixed from "./ConductorInputsFixed.vue"; +import ConductorInputsRealtime from "./ConductorInputsRealtime.vue"; const DEFAULT_DURATION_FORMATTER = 'duration'; export default { components: { + ConductorInputsRealtime, + ConductorInputsFixed, ConductorMode, ConductorTimeSystem, - DatePicker, ConductorAxis, ConductorModeIcon, - ConductorHistory, - TimePopup + ConductorHistory }, inject: ['openmct', 'configuration'], data() { @@ -242,7 +134,6 @@ export default { this.openmct.time.on('bounds', _.throttle(this.handleNewBounds, 300)); this.openmct.time.on('timeSystem', this.setTimeSystem); this.openmct.time.on('clock', this.setViewFromClock); - this.openmct.time.on('clockOffsets', this.setViewFromOffsets); }, beforeDestroy() { document.removeEventListener('keydown', this.handleKeyDown); @@ -297,42 +188,8 @@ export default { timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER); this.isUTCBased = timeSystem.isUTCBased; }, - setOffsetsFromView($event) { - if (this.$refs.conductorForm.checkValidity()) { - let startOffset = 0 - this.durationFormatter.parse(this.offsets.start); - let endOffset = this.durationFormatter.parse(this.offsets.end); - - this.openmct.time.clockOffsets({ - start: startOffset, - end: endOffset - }); - } - - if ($event) { - $event.preventDefault(); - - return false; - } - }, - setBoundsFromView($event) { - if (this.$refs.conductorForm.checkValidity()) { - let start = this.timeFormatter.parse(this.formattedBounds.start); - let end = this.timeFormatter.parse(this.formattedBounds.end); - - this.openmct.time.bounds({ - start: start, - end: end - }); - } - - if ($event) { - $event.preventDefault(); - - return false; - } - }, setViewFromClock(clock) { - this.clearAllValidation(); + // this.clearAllValidation(); this.isFixed = clock === undefined; }, setViewFromBounds(bounds) { @@ -341,158 +198,16 @@ export default { this.viewBounds.start = bounds.start; this.viewBounds.end = bounds.end; }, - setViewFromOffsets(offsets) { - this.offsets.start = this.durationFormatter.format(Math.abs(offsets.start)); - this.offsets.end = this.durationFormatter.format(Math.abs(offsets.end)); - }, - updateTimeFromConductor() { - if (this.isFixed) { - this.setBoundsFromView(); - } else { - this.setOffsetsFromView(); - } - }, - getBoundsLimit() { - const configuration = this.configuration.menuOptions - .filter(option => option.timeSystem === this.timeSystem.key) - .find(option => option.limit); - - const limit = configuration ? configuration.limit : undefined; - - return limit; - }, - clearAllValidation() { - if (this.isFixed) { - [this.$refs.startDate, this.$refs.endDate].forEach(this.clearValidationForInput); - } else { - [this.$refs.startOffset, this.$refs.endOffset].forEach(this.clearValidationForInput); - } - }, - clearValidationForInput(input) { - input.setCustomValidity(''); - input.title = ''; - }, - validateAllBounds(ref) { - if (!this.areBoundsFormatsValid()) { - return false; - } - - let validationResult = true; - const currentInput = this.$refs[ref]; - - return [this.$refs.startDate, this.$refs.endDate].every((input) => { - let boundsValues = { - start: this.timeFormatter.parse(this.formattedBounds.start), - end: this.timeFormatter.parse(this.formattedBounds.end) - }; - const limit = this.getBoundsLimit(); - - if ( - this.timeSystem.isUTCBased - && limit - && boundsValues.end - boundsValues.start > limit - ) { - if (input === currentInput) { - validationResult = "Start and end difference exceeds allowable limit"; - } - } else { - if (input === currentInput) { - validationResult = this.openmct.time.validateBounds(boundsValues); - } - } - - return this.handleValidationResults(input, validationResult); - }); - }, - areBoundsFormatsValid() { - let validationResult = true; - - return [this.$refs.startDate, this.$refs.endDate].every((input) => { - const formattedDate = input === this.$refs.startDate - ? this.formattedBounds.start - : this.formattedBounds.end - ; - - if (!this.timeFormatter.validate(formattedDate)) { - validationResult = 'Invalid date'; - } - - return this.handleValidationResults(input, validationResult); - }); - }, - validateAllOffsets(event) { - return [this.$refs.startOffset, this.$refs.endOffset].every((input) => { - let validationResult = true; - let formattedOffset; - - if (input === this.$refs.startOffset) { - formattedOffset = this.offsets.start; - } else { - formattedOffset = this.offsets.end; - } - - if (!this.durationFormatter.validate(formattedOffset)) { - validationResult = 'Offsets must be in the format hh:mm:ss and less than 24 hours in duration'; - } else { - let offsetValues = { - start: 0 - this.durationFormatter.parse(this.offsets.start), - end: this.durationFormatter.parse(this.offsets.end) - }; - validationResult = this.openmct.time.validateOffsets(offsetValues); - } - - return this.handleValidationResults(input, validationResult); - }); - }, - handleValidationResults(input, validationResult) { - if (validationResult !== true) { - input.setCustomValidity(validationResult); - input.title = validationResult; - - return false; - } else { - input.setCustomValidity(''); - input.title = ''; - - return true; - } - }, - submitForm() { - // Allow Vue model to catch up to user input. - // Submitting form will cause validation messages to display (but only if triggered by button click) - this.$nextTick(() => this.$refs.submitButton.click()); - }, getFormatter(key) { return this.openmct.telemetry.getValueFormatter({ format: key }).formatter; }, - startDateSelected(date) { - this.formattedBounds.start = this.timeFormatter.format(date); - this.validateAllBounds('startDate'); - this.submitForm(); + saveClockOffsets(offsets) { + this.openmct.time.clockOffsets(offsets); }, - endDateSelected(date) { - this.formattedBounds.end = this.timeFormatter.format(date); - this.validateAllBounds('endDate'); - this.submitForm(); - }, - hideAllTimePopups() { - this.showTCInputStart = false; - this.showTCInputEnd = false; - }, - showTimePopupStart() { - this.hideAllTimePopups(); - this.showTCInputStart = !this.showTCInputStart; - }, - showTimePopupEnd() { - this.hideAllTimePopups(); - this.showTCInputEnd = !this.showTCInputEnd; - }, - timePopUpdate({ type, hours, minutes, seconds }) { - this.offsets[type] = [hours, minutes, seconds].join(':'); - this.setOffsetsFromView(); - this.hideAllTimePopups(); + saveFixedOffsets(bounds) { + this.openmct.time.bounds(bounds); } } }; diff --git a/src/plugins/timeConductor/ConductorInputsFixed.vue b/src/plugins/timeConductor/ConductorInputsFixed.vue new file mode 100644 index 0000000000..2675fa6af9 --- /dev/null +++ b/src/plugins/timeConductor/ConductorInputsFixed.vue @@ -0,0 +1,280 @@ + + + diff --git a/src/plugins/timeConductor/ConductorInputsRealtime.vue b/src/plugins/timeConductor/ConductorInputsRealtime.vue new file mode 100644 index 0000000000..16815f8b14 --- /dev/null +++ b/src/plugins/timeConductor/ConductorInputsRealtime.vue @@ -0,0 +1,269 @@ + + + diff --git a/src/plugins/timeConductor/DatePicker.vue b/src/plugins/timeConductor/DatePicker.vue index fafdbe2b11..3a1e856902 100644 --- a/src/plugins/timeConductor/DatePicker.vue +++ b/src/plugins/timeConductor/DatePicker.vue @@ -22,7 +22,8 @@