Compare commits
	
		
			28 Commits
		
	
	
		
			composable
			...
			independen
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | c67f2e908b | ||
|   | d252cc02fd | ||
|   | 545952ef41 | ||
|   | 024ce4dcca | ||
|   | aafb306ec0 | ||
|   | bc600ce3a5 | ||
|   | 8c6c73d85c | ||
|   | 72a5d55f3a | ||
|   | 3a6890bf91 | ||
|   | e4da7e1d70 | ||
|   | 503b4ac485 | ||
|   | bfd68f6a92 | ||
|   | c832422f2b | ||
|   | 1c124f44d5 | ||
|   | e4e7c0948d | ||
|   | 187dc16189 | ||
|   | c238f6583b | ||
|   | 144a8d3a70 | ||
|   | 1a2ad6ed96 | ||
|   | 3a7237de6b | ||
|   | 45977c81d6 | ||
|   | fdddafe9a7 | ||
|   | a5b3e4289c | ||
|   | 3bef6186c8 | ||
|   | a22d1bf87b | ||
|   | 64c9d3dc9e | ||
|   | d838765467 | ||
|   | efa3be9519 | 
| @@ -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. | ||||
|   | ||||
| @@ -46,7 +46,7 @@ define([ | ||||
|     StatusAPI | ||||
| ) { | ||||
|     return { | ||||
|         TimeAPI: TimeAPI, | ||||
|         TimeAPI: TimeAPI.default, | ||||
|         ObjectAPI: ObjectAPI, | ||||
|         CompositionAPI: CompositionAPI, | ||||
|         TypeRegistry: TypeRegistry, | ||||
|   | ||||
							
								
								
									
										180
									
								
								src/api/time/GlobalTimeContext.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										180
									
								
								src/api/time/GlobalTimeContext.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,180 @@ | ||||
| /***************************************************************************** | ||||
|  * 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"; | ||||
|  | ||||
| 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) { | ||||
|             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); | ||||
|  | ||||
|             // 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)); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 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 | ||||
|      * @private | ||||
|      * @param {number} timestamp A time from which bounds will be calculated | ||||
|      * using current offsets. | ||||
|      */ | ||||
|     tick(timestamp) { | ||||
|         const newBounds = { | ||||
|             start: timestamp + this.offsets.start, | ||||
|             end: timestamp + this.offsets.end | ||||
|         }; | ||||
|  | ||||
|         this.boundsVal = newBounds; | ||||
|         this.emit('bounds', this.boundsVal, true); | ||||
|  | ||||
|         // If a bounds change results in a TOI outside of the current | ||||
|         // bounds, unset it | ||||
|         if (this.toi < newBounds.start || this.toi > newBounds.end) { | ||||
|             this.timeOfInterest(undefined); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 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; | ||||
							
								
								
									
										141
									
								
								src/api/time/IndependentTimeContext.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										141
									
								
								src/api/time/IndependentTimeContext.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,141 @@ | ||||
| /***************************************************************************** | ||||
|  * 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"; | ||||
|  | ||||
| class IndependentTimeContext extends TimeContext { | ||||
|     constructor(globalTimeContext, key) { | ||||
|         super(); | ||||
|         this.key = key; | ||||
|         this.tick = this.tick.bind(this); | ||||
|  | ||||
|         this.globalTimeContext = globalTimeContext; | ||||
|         this.globalTimeContext.on('tick', this.tick); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 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)); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 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'?"; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             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); | ||||
|             } | ||||
|  | ||||
|         } 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 | ||||
|      * @private | ||||
|      * @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 IndependentTimeContext; | ||||
| @@ -20,51 +20,35 @@ | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define(['EventEmitter'], function (EventEmitter) { | ||||
|  | ||||
|     /** | ||||
|      * The public API for setting and querying the temporal state of the | ||||
|      * application. The concept of time is integral to Open MCT, and at least | ||||
|      * one {@link TimeSystem}, as well as some default time bounds must be | ||||
|      * registered and enabled via {@link TimeAPI.addTimeSystem} and | ||||
|      * {@link TimeAPI.timeSystem} respectively for Open MCT to work. | ||||
|      * | ||||
|      * Time-sensitive views will typically respond to changes to bounds or other | ||||
|      * properties of the time conductor and update the data displayed based on | ||||
|      * the temporal state of the application. The current time bounds are also | ||||
|      * used in queries for historical data. | ||||
|      * | ||||
|      * The TimeAPI extends the EventEmitter class. A number of events are | ||||
|      * fired when properties of the time conductor change, which are documented | ||||
|      * below. | ||||
|      * | ||||
|      * @interface | ||||
|      * @memberof module:openmct | ||||
|      */ | ||||
|     function TimeAPI() { | ||||
|         EventEmitter.call(this); | ||||
|  | ||||
|         //The Time System | ||||
|         this.system = undefined; | ||||
|         //The Time Of Interest | ||||
|         this.toi = undefined; | ||||
|  | ||||
|         this.boundsVal = { | ||||
|             start: undefined, | ||||
|             end: undefined | ||||
|         }; | ||||
|  | ||||
|         this.timeSystems = new Map(); | ||||
|         this.clocks = new Map(); | ||||
|         this.activeClock = undefined; | ||||
|         this.offsets = undefined; | ||||
|  | ||||
|         this.tick = this.tick.bind(this); | ||||
| import GlobalTimeContext from "./GlobalTimeContext"; | ||||
| import IndependentTimeContext from "@/api/time/IndependentTimeContext"; | ||||
|  | ||||
| /** | ||||
| * The public API for setting and querying the temporal state of the | ||||
| * application. The concept of time is integral to Open MCT, and at least | ||||
| * one {@link TimeSystem}, as well as some default time bounds must be | ||||
| * registered and enabled via {@link TimeAPI.addTimeSystem} and | ||||
| * {@link TimeAPI.timeSystem} respectively for Open MCT to work. | ||||
| * | ||||
| * Time-sensitive views will typically respond to changes to bounds or other | ||||
| * properties of the time conductor and update the data displayed based on | ||||
| * the temporal state of the application. The current time bounds are also | ||||
| * used in queries for historical data. | ||||
| * | ||||
| * The TimeAPI extends the 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,80 @@ 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 observer 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.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 observer 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 timeContext for a view based on it's objectPath. If there is any object in the objectPath with an independent time context, it will be returned. | ||||
|      * Otherwise, the global time context will be returned. | ||||
|      * @param { Array } objectPath The view's objectPath | ||||
|      * @memberof module:openmct.TimeAPI# | ||||
|      * @method getContextForView | ||||
|      */ | ||||
|     /** | ||||
|      * Get or set the currently applied clock offsets. If no parameter is provided, | ||||
|      * the current value will be returned. If provided, the new value will be | ||||
|      * used as the new clock offsets. | ||||
|      * @param {ClockOffsets} offsets | ||||
|      * @returns {ClockOffsets} | ||||
|      */ | ||||
|     TimeAPI.prototype.clockOffsets = function (offsets) { | ||||
|         if (arguments.length > 0) { | ||||
|     getContextForView(objectPath) { | ||||
|         let timeContext = this; | ||||
|  | ||||
|             const validationResult = this.validateOffsets(offsets); | ||||
|             if (validationResult !== true) { | ||||
|                 throw new Error(validationResult); | ||||
|         objectPath.forEach(item => { | ||||
|             const key = this.openmct.objects.makeKeyString(item.identifier); | ||||
|             if (this.independentContexts.get(key)) { | ||||
|                 timeContext = this.independentContexts.get(key); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|             this.offsets = offsets; | ||||
|         return timeContext; | ||||
|     } | ||||
|  | ||||
|             const currentValue = this.activeClock.currentValue(); | ||||
|             const newBounds = { | ||||
|                 start: currentValue + offsets.start, | ||||
|                 end: currentValue + offsets.end | ||||
|             }; | ||||
| } | ||||
|  | ||||
|             this.bounds(newBounds); | ||||
|  | ||||
|             /** | ||||
|              * Event that is triggered when clock offsets change. | ||||
|              * @event clockOffsets | ||||
|              * @memberof module:openmct.TimeAPI~ | ||||
|              * @property {ClockOffsets} clockOffsets The newly activated clock | ||||
|              * offsets. | ||||
|              */ | ||||
|             this.emit("clockOffsets", offsets); | ||||
|         } | ||||
|  | ||||
|         return this.offsets; | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Stop the currently active clock from ticking, and unset it. This will | ||||
|      * revert all views to showing a static time frame defined by the current | ||||
|      * bounds. | ||||
|      */ | ||||
|     TimeAPI.prototype.stopClock = function () { | ||||
|         if (this.activeClock) { | ||||
|             this.clock(undefined, undefined); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     return TimeAPI; | ||||
| }); | ||||
| export default TimeAPI; | ||||
|   | ||||
| @@ -19,241 +19,243 @@ | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
| import TimeAPI from "./TimeAPI"; | ||||
| import {createOpenMct} from "utils/testing"; | ||||
|  | ||||
| define(['./TimeAPI'], function (TimeAPI) { | ||||
|     describe("The Time API", function () { | ||||
|         let api; | ||||
|         let timeSystemKey; | ||||
|         let timeSystem; | ||||
|         let clockKey; | ||||
|         let clock; | ||||
|         let bounds; | ||||
|         let eventListener; | ||||
|         let toi; | ||||
| describe("The Time API", function () { | ||||
|     let api; | ||||
|     let timeSystemKey; | ||||
|     let timeSystem; | ||||
|     let clockKey; | ||||
|     let clock; | ||||
|     let bounds; | ||||
|     let eventListener; | ||||
|     let toi; | ||||
|     let openmct; | ||||
|  | ||||
|     beforeEach(function () { | ||||
|         openmct = createOpenMct(); | ||||
|         api = new TimeAPI(openmct); | ||||
|         timeSystemKey = "timeSystemKey"; | ||||
|         timeSystem = {key: timeSystemKey}; | ||||
|         clockKey = "someClockKey"; | ||||
|         clock = jasmine.createSpyObj("clock", [ | ||||
|             "on", | ||||
|             "off", | ||||
|             "currentValue" | ||||
|         ]); | ||||
|         clock.currentValue.and.returnValue(100); | ||||
|         clock.key = clockKey; | ||||
|         bounds = { | ||||
|             start: 0, | ||||
|             end: 1 | ||||
|         }; | ||||
|         eventListener = jasmine.createSpy("eventListener"); | ||||
|         toi = 111; | ||||
|     }); | ||||
|  | ||||
|     it("Supports setting and querying of time of interest", function () { | ||||
|         expect(api.timeOfInterest()).not.toBe(toi); | ||||
|         api.timeOfInterest(toi); | ||||
|         expect(api.timeOfInterest()).toBe(toi); | ||||
|     }); | ||||
|  | ||||
|     it("Allows setting of valid bounds", function () { | ||||
|         bounds = { | ||||
|             start: 0, | ||||
|             end: 1 | ||||
|         }; | ||||
|         expect(api.bounds()).not.toBe(bounds); | ||||
|         expect(api.bounds.bind(api, bounds)).not.toThrow(); | ||||
|         expect(api.bounds()).toEqual(bounds); | ||||
|     }); | ||||
|  | ||||
|     it("Disallows setting of invalid bounds", function () { | ||||
|         bounds = { | ||||
|             start: 1, | ||||
|             end: 0 | ||||
|         }; | ||||
|         expect(api.bounds()).not.toEqual(bounds); | ||||
|         expect(api.bounds.bind(api, bounds)).toThrow(); | ||||
|         expect(api.bounds()).not.toEqual(bounds); | ||||
|  | ||||
|         bounds = {start: 1}; | ||||
|         expect(api.bounds()).not.toEqual(bounds); | ||||
|         expect(api.bounds.bind(api, bounds)).toThrow(); | ||||
|         expect(api.bounds()).not.toEqual(bounds); | ||||
|     }); | ||||
|  | ||||
|     it("Allows setting of previously registered time system with bounds", function () { | ||||
|         api.addTimeSystem(timeSystem); | ||||
|         expect(api.timeSystem()).not.toBe(timeSystem); | ||||
|         expect(function () { | ||||
|             api.timeSystem(timeSystem, bounds); | ||||
|         }).not.toThrow(); | ||||
|         expect(api.timeSystem()).toBe(timeSystem); | ||||
|     }); | ||||
|  | ||||
|     it("Disallows setting of time system without bounds", function () { | ||||
|         api.addTimeSystem(timeSystem); | ||||
|         expect(api.timeSystem()).not.toBe(timeSystem); | ||||
|         expect(function () { | ||||
|             api.timeSystem(timeSystemKey); | ||||
|         }).toThrow(); | ||||
|         expect(api.timeSystem()).not.toBe(timeSystem); | ||||
|     }); | ||||
|  | ||||
|     it("allows setting of timesystem without bounds with clock", function () { | ||||
|         api.addTimeSystem(timeSystem); | ||||
|         api.addClock(clock); | ||||
|         api.clock(clockKey, { | ||||
|             start: 0, | ||||
|             end: 1 | ||||
|         }); | ||||
|         expect(api.timeSystem()).not.toBe(timeSystem); | ||||
|         expect(function () { | ||||
|             api.timeSystem(timeSystemKey); | ||||
|         }).not.toThrow(); | ||||
|         expect(api.timeSystem()).toBe(timeSystem); | ||||
|  | ||||
|     }); | ||||
|  | ||||
|     it("Emits an event when time system changes", function () { | ||||
|         api.addTimeSystem(timeSystem); | ||||
|         expect(eventListener).not.toHaveBeenCalled(); | ||||
|         api.on("timeSystem", eventListener); | ||||
|         api.timeSystem(timeSystemKey, bounds); | ||||
|         expect(eventListener).toHaveBeenCalledWith(timeSystem); | ||||
|     }); | ||||
|  | ||||
|     it("Emits an event when time of interest changes", function () { | ||||
|         expect(eventListener).not.toHaveBeenCalled(); | ||||
|         api.on("timeOfInterest", eventListener); | ||||
|         api.timeOfInterest(toi); | ||||
|         expect(eventListener).toHaveBeenCalledWith(toi); | ||||
|     }); | ||||
|  | ||||
|     it("Emits an event when bounds change", function () { | ||||
|         expect(eventListener).not.toHaveBeenCalled(); | ||||
|         api.on("bounds", eventListener); | ||||
|         api.bounds(bounds); | ||||
|         expect(eventListener).toHaveBeenCalledWith(bounds, false); | ||||
|     }); | ||||
|  | ||||
|     it("If bounds are set and TOI lies inside them, do not change TOI", function () { | ||||
|         api.timeOfInterest(6); | ||||
|         api.bounds({ | ||||
|             start: 1, | ||||
|             end: 10 | ||||
|         }); | ||||
|         expect(api.timeOfInterest()).toEqual(6); | ||||
|     }); | ||||
|  | ||||
|     it("If bounds are set and TOI lies outside them, reset TOI", function () { | ||||
|         api.timeOfInterest(11); | ||||
|         api.bounds({ | ||||
|             start: 1, | ||||
|             end: 10 | ||||
|         }); | ||||
|         expect(api.timeOfInterest()).toBeUndefined(); | ||||
|     }); | ||||
|  | ||||
|     it("Maintains delta during tick", function () { | ||||
|     }); | ||||
|  | ||||
|     it("Allows registered time system to be activated", function () { | ||||
|     }); | ||||
|  | ||||
|     it("Allows a registered tick source to be activated", function () { | ||||
|         const mockTickSource = jasmine.createSpyObj("mockTickSource", [ | ||||
|             "on", | ||||
|             "off", | ||||
|             "currentValue" | ||||
|         ]); | ||||
|         mockTickSource.key = 'mockTickSource'; | ||||
|     }); | ||||
|  | ||||
|     describe(" when enabling a tick source", function () { | ||||
|         let mockTickSource; | ||||
|         let anotherMockTickSource; | ||||
|         const mockOffsets = { | ||||
|             start: 0, | ||||
|             end: 1 | ||||
|         }; | ||||
|  | ||||
|         beforeEach(function () { | ||||
|             api = new TimeAPI(); | ||||
|             timeSystemKey = "timeSystemKey"; | ||||
|             timeSystem = {key: timeSystemKey}; | ||||
|             clockKey = "someClockKey"; | ||||
|             clock = jasmine.createSpyObj("clock", [ | ||||
|             mockTickSource = jasmine.createSpyObj("clock", [ | ||||
|                 "on", | ||||
|                 "off", | ||||
|                 "currentValue" | ||||
|             ]); | ||||
|             clock.currentValue.and.returnValue(100); | ||||
|             clock.key = clockKey; | ||||
|             bounds = { | ||||
|                 start: 0, | ||||
|                 end: 1 | ||||
|             }; | ||||
|             eventListener = jasmine.createSpy("eventListener"); | ||||
|             toi = 111; | ||||
|         }); | ||||
|  | ||||
|         it("Supports setting and querying of time of interest", function () { | ||||
|             expect(api.timeOfInterest()).not.toBe(toi); | ||||
|             api.timeOfInterest(toi); | ||||
|             expect(api.timeOfInterest()).toBe(toi); | ||||
|         }); | ||||
|  | ||||
|         it("Allows setting of valid bounds", function () { | ||||
|             bounds = { | ||||
|                 start: 0, | ||||
|                 end: 1 | ||||
|             }; | ||||
|             expect(api.bounds()).not.toBe(bounds); | ||||
|             expect(api.bounds.bind(api, bounds)).not.toThrow(); | ||||
|             expect(api.bounds()).toEqual(bounds); | ||||
|         }); | ||||
|  | ||||
|         it("Disallows setting of invalid bounds", function () { | ||||
|             bounds = { | ||||
|                 start: 1, | ||||
|                 end: 0 | ||||
|             }; | ||||
|             expect(api.bounds()).not.toEqual(bounds); | ||||
|             expect(api.bounds.bind(api, bounds)).toThrow(); | ||||
|             expect(api.bounds()).not.toEqual(bounds); | ||||
|  | ||||
|             bounds = {start: 1}; | ||||
|             expect(api.bounds()).not.toEqual(bounds); | ||||
|             expect(api.bounds.bind(api, bounds)).toThrow(); | ||||
|             expect(api.bounds()).not.toEqual(bounds); | ||||
|         }); | ||||
|  | ||||
|         it("Allows setting of previously registered time system with bounds", function () { | ||||
|             api.addTimeSystem(timeSystem); | ||||
|             expect(api.timeSystem()).not.toBe(timeSystem); | ||||
|             expect(function () { | ||||
|                 api.timeSystem(timeSystem, bounds); | ||||
|             }).not.toThrow(); | ||||
|             expect(api.timeSystem()).toBe(timeSystem); | ||||
|         }); | ||||
|  | ||||
|         it("Disallows setting of time system without bounds", function () { | ||||
|             api.addTimeSystem(timeSystem); | ||||
|             expect(api.timeSystem()).not.toBe(timeSystem); | ||||
|             expect(function () { | ||||
|                 api.timeSystem(timeSystemKey); | ||||
|             }).toThrow(); | ||||
|             expect(api.timeSystem()).not.toBe(timeSystem); | ||||
|         }); | ||||
|  | ||||
|         it("allows setting of timesystem without bounds with clock", function () { | ||||
|             api.addTimeSystem(timeSystem); | ||||
|             api.addClock(clock); | ||||
|             api.clock(clockKey, { | ||||
|                 start: 0, | ||||
|                 end: 1 | ||||
|             }); | ||||
|             expect(api.timeSystem()).not.toBe(timeSystem); | ||||
|             expect(function () { | ||||
|                 api.timeSystem(timeSystemKey); | ||||
|             }).not.toThrow(); | ||||
|             expect(api.timeSystem()).toBe(timeSystem); | ||||
|  | ||||
|         }); | ||||
|  | ||||
|         it("Emits an event when time system changes", function () { | ||||
|             api.addTimeSystem(timeSystem); | ||||
|             expect(eventListener).not.toHaveBeenCalled(); | ||||
|             api.on("timeSystem", eventListener); | ||||
|             api.timeSystem(timeSystemKey, bounds); | ||||
|             expect(eventListener).toHaveBeenCalledWith(timeSystem); | ||||
|         }); | ||||
|  | ||||
|         it("Emits an event when time of interest changes", function () { | ||||
|             expect(eventListener).not.toHaveBeenCalled(); | ||||
|             api.on("timeOfInterest", eventListener); | ||||
|             api.timeOfInterest(toi); | ||||
|             expect(eventListener).toHaveBeenCalledWith(toi); | ||||
|         }); | ||||
|  | ||||
|         it("Emits an event when bounds change", function () { | ||||
|             expect(eventListener).not.toHaveBeenCalled(); | ||||
|             api.on("bounds", eventListener); | ||||
|             api.bounds(bounds); | ||||
|             expect(eventListener).toHaveBeenCalledWith(bounds, false); | ||||
|         }); | ||||
|  | ||||
|         it("If bounds are set and TOI lies inside them, do not change TOI", function () { | ||||
|             api.timeOfInterest(6); | ||||
|             api.bounds({ | ||||
|                 start: 1, | ||||
|                 end: 10 | ||||
|             }); | ||||
|             expect(api.timeOfInterest()).toEqual(6); | ||||
|         }); | ||||
|  | ||||
|         it("If bounds are set and TOI lies outside them, reset TOI", function () { | ||||
|             api.timeOfInterest(11); | ||||
|             api.bounds({ | ||||
|                 start: 1, | ||||
|                 end: 10 | ||||
|             }); | ||||
|             expect(api.timeOfInterest()).toBeUndefined(); | ||||
|         }); | ||||
|  | ||||
|         it("Maintains delta during tick", function () { | ||||
|         }); | ||||
|  | ||||
|         it("Allows registered time system to be activated", function () { | ||||
|         }); | ||||
|  | ||||
|         it("Allows a registered tick source to be activated", function () { | ||||
|             const mockTickSource = jasmine.createSpyObj("mockTickSource", [ | ||||
|                 "on", | ||||
|                 "off", | ||||
|                 "currentValue" | ||||
|             ]); | ||||
|             mockTickSource.key = 'mockTickSource'; | ||||
|         }); | ||||
|  | ||||
|         describe(" when enabling a tick source", function () { | ||||
|             let mockTickSource; | ||||
|             let anotherMockTickSource; | ||||
|             const mockOffsets = { | ||||
|                 start: 0, | ||||
|                 end: 1 | ||||
|             }; | ||||
|  | ||||
|             beforeEach(function () { | ||||
|                 mockTickSource = jasmine.createSpyObj("clock", [ | ||||
|                     "on", | ||||
|                     "off", | ||||
|                     "currentValue" | ||||
|                 ]); | ||||
|                 mockTickSource.currentValue.and.returnValue(10); | ||||
|                 mockTickSource.key = "mts"; | ||||
|  | ||||
|                 anotherMockTickSource = jasmine.createSpyObj("clock", [ | ||||
|                     "on", | ||||
|                     "off", | ||||
|                     "currentValue" | ||||
|                 ]); | ||||
|                 anotherMockTickSource.key = "amts"; | ||||
|                 anotherMockTickSource.currentValue.and.returnValue(10); | ||||
|  | ||||
|                 api.addClock(mockTickSource); | ||||
|                 api.addClock(anotherMockTickSource); | ||||
|             }); | ||||
|  | ||||
|             it("sets bounds based on current value", function () { | ||||
|                 api.clock("mts", mockOffsets); | ||||
|                 expect(api.bounds()).toEqual({ | ||||
|                     start: 10, | ||||
|                     end: 11 | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|             it("a new tick listener is registered", function () { | ||||
|                 api.clock("mts", mockOffsets); | ||||
|                 expect(mockTickSource.on).toHaveBeenCalledWith("tick", jasmine.any(Function)); | ||||
|             }); | ||||
|  | ||||
|             it("listener of existing tick source is reregistered", function () { | ||||
|                 api.clock("mts", mockOffsets); | ||||
|                 api.clock("amts", mockOffsets); | ||||
|                 expect(mockTickSource.off).toHaveBeenCalledWith("tick", jasmine.any(Function)); | ||||
|             }); | ||||
|  | ||||
|             it("Allows the active clock to be set and unset", function () { | ||||
|                 expect(api.clock()).toBeUndefined(); | ||||
|                 api.clock("mts", mockOffsets); | ||||
|                 expect(api.clock()).toBeDefined(); | ||||
|                 api.stopClock(); | ||||
|                 expect(api.clock()).toBeUndefined(); | ||||
|             }); | ||||
|  | ||||
|         }); | ||||
|  | ||||
|         it("on tick, observes offsets, and indicates tick in bounds callback", function () { | ||||
|             const mockTickSource = jasmine.createSpyObj("clock", [ | ||||
|                 "on", | ||||
|                 "off", | ||||
|                 "currentValue" | ||||
|             ]); | ||||
|             mockTickSource.currentValue.and.returnValue(100); | ||||
|             let tickCallback; | ||||
|             const boundsCallback = jasmine.createSpy("boundsCallback"); | ||||
|             const clockOffsets = { | ||||
|                 start: -100, | ||||
|                 end: 100 | ||||
|             }; | ||||
|             mockTickSource.currentValue.and.returnValue(10); | ||||
|             mockTickSource.key = "mts"; | ||||
|  | ||||
|             anotherMockTickSource = jasmine.createSpyObj("clock", [ | ||||
|                 "on", | ||||
|                 "off", | ||||
|                 "currentValue" | ||||
|             ]); | ||||
|             anotherMockTickSource.key = "amts"; | ||||
|             anotherMockTickSource.currentValue.and.returnValue(10); | ||||
|  | ||||
|             api.addClock(mockTickSource); | ||||
|             api.clock("mts", clockOffsets); | ||||
|  | ||||
|             api.on("bounds", boundsCallback); | ||||
|  | ||||
|             tickCallback = mockTickSource.on.calls.mostRecent().args[1]; | ||||
|             tickCallback(1000); | ||||
|             expect(boundsCallback).toHaveBeenCalledWith({ | ||||
|                 start: 900, | ||||
|                 end: 1100 | ||||
|             }, true); | ||||
|             api.addClock(anotherMockTickSource); | ||||
|         }); | ||||
|  | ||||
|         it("sets bounds based on current value", function () { | ||||
|             api.clock("mts", mockOffsets); | ||||
|             expect(api.bounds()).toEqual({ | ||||
|                 start: 10, | ||||
|                 end: 11 | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         it("a new tick listener is registered", function () { | ||||
|             api.clock("mts", mockOffsets); | ||||
|             expect(mockTickSource.on).toHaveBeenCalledWith("tick", jasmine.any(Function)); | ||||
|         }); | ||||
|  | ||||
|         it("listener of existing tick source is reregistered", function () { | ||||
|             api.clock("mts", mockOffsets); | ||||
|             api.clock("amts", mockOffsets); | ||||
|             expect(mockTickSource.off).toHaveBeenCalledWith("tick", jasmine.any(Function)); | ||||
|         }); | ||||
|  | ||||
|         it("Allows the active clock to be set and unset", function () { | ||||
|             expect(api.clock()).toBeUndefined(); | ||||
|             api.clock("mts", mockOffsets); | ||||
|             expect(api.clock()).toBeDefined(); | ||||
|             api.stopClock(); | ||||
|             expect(api.clock()).toBeUndefined(); | ||||
|         }); | ||||
|  | ||||
|     }); | ||||
|  | ||||
|     it("on tick, observes offsets, and indicates tick in bounds callback", function () { | ||||
|         const mockTickSource = jasmine.createSpyObj("clock", [ | ||||
|             "on", | ||||
|             "off", | ||||
|             "currentValue" | ||||
|         ]); | ||||
|         mockTickSource.currentValue.and.returnValue(100); | ||||
|         let tickCallback; | ||||
|         const boundsCallback = jasmine.createSpy("boundsCallback"); | ||||
|         const clockOffsets = { | ||||
|             start: -100, | ||||
|             end: 100 | ||||
|         }; | ||||
|         mockTickSource.key = "mts"; | ||||
|  | ||||
|         api.addClock(mockTickSource); | ||||
|         api.clock("mts", clockOffsets); | ||||
|  | ||||
|         api.on("bounds", boundsCallback); | ||||
|  | ||||
|         tickCallback = mockTickSource.on.calls.mostRecent().args[1]; | ||||
|         tickCallback(1000); | ||||
|         expect(boundsCallback).toHaveBeenCalledWith({ | ||||
|             start: 900, | ||||
|             end: 1100 | ||||
|         }, true); | ||||
|     }); | ||||
| }); | ||||
|   | ||||
							
								
								
									
										250
									
								
								src/api/time/TimeContext.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										250
									
								
								src/api/time/TimeContext.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,250 @@ | ||||
| /***************************************************************************** | ||||
|  * 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: '' | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 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); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| export default TimeContext; | ||||
							
								
								
									
										155
									
								
								src/api/time/independentTimeAPISpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										155
									
								
								src/api/time/independentTimeAPISpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,155 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2021, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| import TimeAPI from "./TimeAPI"; | ||||
| import {createOpenMct} from "utils/testing"; | ||||
| describe("The Independent Time API", function () { | ||||
|     let api; | ||||
|     let domainObjectKey; | ||||
|     let clockKey; | ||||
|     let clock; | ||||
|     let bounds; | ||||
|     let independentBounds; | ||||
|     let eventListener; | ||||
|     let openmct; | ||||
|  | ||||
|     beforeEach(function () { | ||||
|         openmct = createOpenMct(); | ||||
|         api = new TimeAPI(openmct); | ||||
|         clockKey = "someClockKey"; | ||||
|         clock = jasmine.createSpyObj("clock", [ | ||||
|             "on", | ||||
|             "off", | ||||
|             "currentValue" | ||||
|         ]); | ||||
|         clock.currentValue.and.returnValue(100); | ||||
|         clock.key = clockKey; | ||||
|         api.addClock(clock); | ||||
|         domainObjectKey = 'test-key'; | ||||
|         bounds = { | ||||
|             start: 0, | ||||
|             end: 1 | ||||
|         }; | ||||
|         api.bounds(bounds); | ||||
|         independentBounds = { | ||||
|             start: 10, | ||||
|             end: 11 | ||||
|         }; | ||||
|         eventListener = jasmine.createSpy("eventListener"); | ||||
|     }); | ||||
|  | ||||
|     it("Creates an independent time context", () => { | ||||
|         let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds); | ||||
|         let timeContext = api.getIndependentContext(domainObjectKey); | ||||
|         expect(timeContext.bounds()).toEqual(independentBounds); | ||||
|         destroyTimeContext(); | ||||
|     }); | ||||
|  | ||||
|     it("Gets an independent time context given the objectPath", () => { | ||||
|         let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds); | ||||
|         let timeContext = api.getContextForView([{ | ||||
|             identifier: { | ||||
|                 namespace: '', | ||||
|                 key: 'blah' | ||||
|             } | ||||
|         }, { identifier: domainObjectKey }]); | ||||
|         expect(timeContext.bounds()).toEqual(independentBounds); | ||||
|         destroyTimeContext(); | ||||
|     }); | ||||
|  | ||||
|     it("defaults to the global time context given the objectPath", () => { | ||||
|         let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds); | ||||
|         let timeContext = api.getContextForView([{ | ||||
|             identifier: { | ||||
|                 namespace: '', | ||||
|                 key: 'blah' | ||||
|             } | ||||
|         }]); | ||||
|         expect(timeContext.bounds()).toEqual(bounds); | ||||
|         destroyTimeContext(); | ||||
|     }); | ||||
|  | ||||
|     it("Allows setting of valid bounds", function () { | ||||
|         bounds = { | ||||
|             start: 0, | ||||
|             end: 1 | ||||
|         }; | ||||
|         let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds); | ||||
|         let timeContext = api.getContextForView([{identifier: domainObjectKey}]); | ||||
|         expect(timeContext.bounds()).not.toEqual(bounds); | ||||
|         timeContext.bounds(bounds); | ||||
|         expect(timeContext.bounds()).toEqual(bounds); | ||||
|         destroyTimeContext(); | ||||
|     }); | ||||
|  | ||||
|     it("Disallows setting of invalid bounds", function () { | ||||
|         bounds = { | ||||
|             start: 1, | ||||
|             end: 0 | ||||
|         }; | ||||
|  | ||||
|         let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds); | ||||
|         let timeContext = api.getContextForView([{identifier: domainObjectKey}]); | ||||
|         expect(timeContext.bounds()).not.toBe(bounds); | ||||
|  | ||||
|         expect(timeContext.bounds.bind(timeContext, bounds)).toThrow(); | ||||
|         expect(timeContext.bounds()).not.toEqual(bounds); | ||||
|  | ||||
|         bounds = {start: 1}; | ||||
|         expect(timeContext.bounds()).not.toEqual(bounds); | ||||
|         expect(timeContext.bounds.bind(timeContext, bounds)).toThrow(); | ||||
|         expect(timeContext.bounds()).not.toEqual(bounds); | ||||
|         destroyTimeContext(); | ||||
|     }); | ||||
|  | ||||
|     it("Emits an event when bounds change", function () { | ||||
|         let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds); | ||||
|         let timeContext = api.getContextForView([{identifier: domainObjectKey}]); | ||||
|         expect(eventListener).not.toHaveBeenCalled(); | ||||
|         timeContext.on('bounds', eventListener); | ||||
|         timeContext.bounds(bounds); | ||||
|         expect(eventListener).toHaveBeenCalledWith(bounds, false); | ||||
|         destroyTimeContext(); | ||||
|     }); | ||||
|  | ||||
|     describe(" when using real time clock", function () { | ||||
|         const mockOffsets = { | ||||
|             start: 10, | ||||
|             end: 11 | ||||
|         }; | ||||
|  | ||||
|         it("Emits an event when bounds change based on current value", function () { | ||||
|             let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds); | ||||
|             let timeContext = api.getContextForView([{identifier: domainObjectKey}]); | ||||
|             expect(eventListener).not.toHaveBeenCalled(); | ||||
|             timeContext.clock('someClockKey', mockOffsets); | ||||
|             timeContext.on('bounds', eventListener); | ||||
|             timeContext.tick(10); | ||||
|             expect(eventListener).toHaveBeenCalledWith({ | ||||
|                 start: 20, | ||||
|                 end: 21 | ||||
|             }, true); | ||||
|             destroyTimeContext(); | ||||
|         }); | ||||
|  | ||||
|     }); | ||||
| }); | ||||
| @@ -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,8 +157,11 @@ export default { | ||||
|         getPlanData(domainObject) { | ||||
|             this.planData = getValidatedPlan(domainObject); | ||||
|         }, | ||||
|         updateViewBounds() { | ||||
|             this.viewBounds = this.openmct.time.bounds(); | ||||
|         updateViewBounds(bounds) { | ||||
|             if (bounds) { | ||||
|                 this.viewBounds = Object.assign({}, bounds); | ||||
|             } | ||||
|  | ||||
|             //Add a 50% padding to the end bounds to look ahead | ||||
|             let timespan = (this.viewBounds.end - this.viewBounds.start); | ||||
|             let padding = timespan / 2; | ||||
|   | ||||
| @@ -54,7 +54,8 @@ export default function PlanViewProvider(openmct) { | ||||
|                         }, | ||||
|                         provide: { | ||||
|                             openmct, | ||||
|                             domainObject | ||||
|                             domainObject, | ||||
|                             path: objectPath | ||||
|                         }, | ||||
|                         data() { | ||||
|                             return { | ||||
|   | ||||
| @@ -173,7 +173,7 @@ export default { | ||||
|         MctTicks, | ||||
|         MctChart | ||||
|     }, | ||||
|     inject: ['openmct', 'domainObject'], | ||||
|     inject: ['openmct', 'domainObject', 'path'], | ||||
|     props: { | ||||
|         options: { | ||||
|             type: Object, | ||||
| @@ -249,6 +249,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(); | ||||
|  | ||||
| @@ -265,7 +268,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; | ||||
|  | ||||
| @@ -278,11 +281,27 @@ export default { | ||||
|         this.destroy(); | ||||
|     }, | ||||
|     methods: { | ||||
|         followTimeConductor() { | ||||
|             this.openmct.time.on('clock', this.updateRealTime); | ||||
|             this.openmct.time.on('bounds', this.updateDisplayBounds); | ||||
|         setTimeContext(updatedKey) { | ||||
|             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); | ||||
| @@ -467,7 +486,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; | ||||
| @@ -948,7 +967,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. | ||||
| @@ -983,9 +1002,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 | ||||
|             }); | ||||
| @@ -996,6 +1015,7 @@ export default { | ||||
|             configStore.deleteStore(this.config.id); | ||||
|  | ||||
|             this.stopListening(); | ||||
|  | ||||
|             if (this.checkForSize) { | ||||
|                 clearInterval(this.checkForSize); | ||||
|                 delete this.checkForSize; | ||||
| @@ -1011,8 +1031,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) { | ||||
|   | ||||
| @@ -80,7 +80,7 @@ export default { | ||||
|     components: { | ||||
|         MctPlot | ||||
|     }, | ||||
|     inject: ['openmct', 'domainObject'], | ||||
|     inject: ['openmct', 'domainObject', 'path'], | ||||
|     props: { | ||||
|         options: { | ||||
|             type: Object, | ||||
|   | ||||
| @@ -68,7 +68,8 @@ export default function PlotViewProvider(openmct) { | ||||
|                         }, | ||||
|                         provide: { | ||||
|                             openmct, | ||||
|                             domainObject | ||||
|                             domainObject, | ||||
|                             path: objectPath | ||||
|                         }, | ||||
|                         data() { | ||||
|                             return { | ||||
|   | ||||
| @@ -53,7 +53,8 @@ export default function OverlayPlotViewProvider(openmct) { | ||||
|                         }, | ||||
|                         provide: { | ||||
|                             openmct, | ||||
|                             domainObject | ||||
|                             domainObject, | ||||
|                             path: objectPath | ||||
|                         }, | ||||
|                         data() { | ||||
|                             return { | ||||
|   | ||||
| @@ -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: "<stacked-plot></stacked-plot>" | ||||
|             }); | ||||
|   | ||||
| @@ -75,7 +75,7 @@ export default { | ||||
|     components: { | ||||
|         StackedPlotItem | ||||
|     }, | ||||
|     inject: ['openmct', 'domainObject', 'composition'], | ||||
|     inject: ['openmct', 'domainObject', 'composition', 'path'], | ||||
|     props: { | ||||
|         options: { | ||||
|             type: Object, | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -29,144 +29,36 @@ | ||||
|         isFixed ? 'is-fixed-mode' : 'is-realtime-mode' | ||||
|     ]" | ||||
| > | ||||
|     <form | ||||
|         ref="conductorForm" | ||||
|         class="u-contents" | ||||
|         @submit.prevent="updateTimeFromConductor" | ||||
|     > | ||||
|         <div class="c-conductor__time-bounds"> | ||||
|             <button | ||||
|                 ref="submitButton" | ||||
|                 class="c-input--submit" | ||||
|                 type="submit" | ||||
|             ></button> | ||||
|             <ConductorModeIcon class="c-conductor__mode-icon" /> | ||||
|  | ||||
|             <div | ||||
|                 v-if="isFixed" | ||||
|                 class="c-ctrl-wrapper c-conductor-input c-conductor__start-fixed" | ||||
|             > | ||||
|                 <!-- Fixed start --> | ||||
|                 <div class="c-conductor__start-fixed__label"> | ||||
|                     Start | ||||
|                 </div> | ||||
|                 <input | ||||
|                     ref="startDate" | ||||
|                     v-model="formattedBounds.start" | ||||
|                     class="c-input--datetime" | ||||
|                     type="text" | ||||
|                     autocorrect="off" | ||||
|                     spellcheck="false" | ||||
|                     @change="validateAllBounds('startDate'); submitForm()" | ||||
|                 > | ||||
|                 <date-picker | ||||
|                     v-if="isFixed && isUTCBased" | ||||
|                     :default-date-time="formattedBounds.start" | ||||
|                     :formatter="timeFormatter" | ||||
|                     @date-selected="startDateSelected" | ||||
|                 /> | ||||
|             </div> | ||||
|  | ||||
|             <div | ||||
|                 v-if="!isFixed" | ||||
|                 class="c-ctrl-wrapper c-conductor-input c-conductor__start-delta" | ||||
|             > | ||||
|                 <!-- RT start --> | ||||
|                 <div class="c-direction-indicator icon-minus"></div> | ||||
|                 <time-popup | ||||
|                     v-if="showTCInputStart" | ||||
|                     class="pr-tc-input-menu--start" | ||||
|                     :type="'start'" | ||||
|                     :offset="offsets.start" | ||||
|                     @focus.native="$event.target.select()" | ||||
|                     @hide="hideAllTimePopups" | ||||
|                     @update="timePopUpdate" | ||||
|                 /> | ||||
|                 <button | ||||
|                     ref="startOffset" | ||||
|                     class="c-button c-conductor__delta-button" | ||||
|                     @click.prevent="showTimePopupStart" | ||||
|                 > | ||||
|                     {{ offsets.start }} | ||||
|                 </button> | ||||
|             </div> | ||||
|  | ||||
|             <div class="c-ctrl-wrapper c-conductor-input c-conductor__end-fixed"> | ||||
|                 <!-- Fixed end and RT 'last update' display --> | ||||
|                 <div class="c-conductor__end-fixed__label"> | ||||
|                     {{ isFixed ? 'End' : 'Updated' }} | ||||
|                 </div> | ||||
|                 <input | ||||
|                     ref="endDate" | ||||
|                     v-model="formattedBounds.end" | ||||
|                     class="c-input--datetime" | ||||
|                     type="text" | ||||
|                     autocorrect="off" | ||||
|                     spellcheck="false" | ||||
|                     :disabled="!isFixed" | ||||
|                     @change="validateAllBounds('endDate'); submitForm()" | ||||
|                 > | ||||
|                 <date-picker | ||||
|                     v-if="isFixed && isUTCBased" | ||||
|                     class="c-ctrl-wrapper--menus-left" | ||||
|                     :default-date-time="formattedBounds.end" | ||||
|                     :formatter="timeFormatter" | ||||
|                     @date-selected="endDateSelected" | ||||
|                 /> | ||||
|             </div> | ||||
|  | ||||
|             <div | ||||
|                 v-if="!isFixed" | ||||
|                 class="c-ctrl-wrapper c-conductor-input c-conductor__end-delta" | ||||
|             > | ||||
|                 <!-- RT end --> | ||||
|                 <div class="c-direction-indicator icon-plus"></div> | ||||
|                 <time-popup | ||||
|                     v-if="showTCInputEnd" | ||||
|                     class="pr-tc-input-menu--end" | ||||
|                     :type="'end'" | ||||
|                     :offset="offsets.end" | ||||
|                     @focus.native="$event.target.select()" | ||||
|                     @hide="hideAllTimePopups" | ||||
|                     @update="timePopUpdate" | ||||
|                 /> | ||||
|                 <button | ||||
|                     ref="endOffset" | ||||
|                     class="c-button c-conductor__delta-button" | ||||
|                     @click.prevent="showTimePopupEnd" | ||||
|                 > | ||||
|                     {{ offsets.end }} | ||||
|                 </button> | ||||
|             </div> | ||||
|  | ||||
|             <conductor-axis | ||||
|                 class="c-conductor__ticks" | ||||
|                 :view-bounds="viewBounds" | ||||
|                 :is-fixed="isFixed" | ||||
|                 :alt-pressed="altPressed" | ||||
|                 @endPan="endPan" | ||||
|                 @endZoom="endZoom" | ||||
|                 @panAxis="pan" | ||||
|                 @zoomAxis="zoom" | ||||
|             /> | ||||
|  | ||||
|         </div> | ||||
|         <div class="c-conductor__controls"> | ||||
|             <ConductorMode class="c-conductor__mode-select" /> | ||||
|             <ConductorTimeSystem class="c-conductor__time-system-select" /> | ||||
|             <ConductorHistory | ||||
|                 class="c-conductor__history-select" | ||||
|                 :offsets="openmct.time.clockOffsets()" | ||||
|                 :bounds="bounds" | ||||
|                 :time-system="timeSystem" | ||||
|                 :mode="timeMode" | ||||
|             /> | ||||
|         </div> | ||||
|         <input | ||||
|             type="submit" | ||||
|             class="invisible" | ||||
|         > | ||||
|     </form> | ||||
|     <div class="c-conductor__time-bounds"> | ||||
|         <conductor-inputs-fixed v-if="isFixed" | ||||
|                                 @updated="saveFixedOffsets" | ||||
|         /> | ||||
|         <conductor-inputs-realtime v-else | ||||
|                                    @updated="saveClockOffsets" | ||||
|         /> | ||||
|         <ConductorModeIcon class="c-conductor__mode-icon" /> | ||||
|         <conductor-axis | ||||
|             class="c-conductor__ticks" | ||||
|             :view-bounds="viewBounds" | ||||
|             :is-fixed="isFixed" | ||||
|             :alt-pressed="altPressed" | ||||
|             @endPan="endPan" | ||||
|             @endZoom="endZoom" | ||||
|             @panAxis="pan" | ||||
|             @zoomAxis="zoom" | ||||
|         /> | ||||
|     </div> | ||||
|     <div class="c-conductor__controls"> | ||||
|         <ConductorMode class="c-conductor__mode-select" /> | ||||
|         <ConductorTimeSystem class="c-conductor__time-system-select" /> | ||||
|         <ConductorHistory | ||||
|             class="c-conductor__history-select" | ||||
|             :offsets="openmct.time.clockOffsets()" | ||||
|             :bounds="bounds" | ||||
|             :time-system="timeSystem" | ||||
|             :mode="timeMode" | ||||
|         /> | ||||
|     </div> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| @@ -174,23 +66,23 @@ | ||||
| import _ from 'lodash'; | ||||
| import ConductorMode from './ConductorMode.vue'; | ||||
| import ConductorTimeSystem from './ConductorTimeSystem.vue'; | ||||
| import DatePicker from './DatePicker.vue'; | ||||
| import ConductorAxis from './ConductorAxis.vue'; | ||||
| import ConductorModeIcon from './ConductorModeIcon.vue'; | ||||
| import ConductorHistory from './ConductorHistory.vue'; | ||||
| import TimePopup from './timePopup.vue'; | ||||
| import ConductorInputsFixed from "./ConductorInputsFixed.vue"; | ||||
| import ConductorInputsRealtime from "./ConductorInputsRealtime.vue"; | ||||
|  | ||||
| const DEFAULT_DURATION_FORMATTER = 'duration'; | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
|         ConductorInputsRealtime, | ||||
|         ConductorInputsFixed, | ||||
|         ConductorMode, | ||||
|         ConductorTimeSystem, | ||||
|         DatePicker, | ||||
|         ConductorAxis, | ||||
|         ConductorModeIcon, | ||||
|         ConductorHistory, | ||||
|         TimePopup | ||||
|         ConductorHistory | ||||
|     }, | ||||
|     inject: ['openmct', 'configuration'], | ||||
|     data() { | ||||
| @@ -242,7 +134,6 @@ export default { | ||||
|         this.openmct.time.on('bounds', _.throttle(this.handleNewBounds, 300)); | ||||
|         this.openmct.time.on('timeSystem', this.setTimeSystem); | ||||
|         this.openmct.time.on('clock', this.setViewFromClock); | ||||
|         this.openmct.time.on('clockOffsets', this.setViewFromOffsets); | ||||
|     }, | ||||
|     beforeDestroy() { | ||||
|         document.removeEventListener('keydown', this.handleKeyDown); | ||||
| @@ -297,42 +188,8 @@ export default { | ||||
|                 timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER); | ||||
|             this.isUTCBased = timeSystem.isUTCBased; | ||||
|         }, | ||||
|         setOffsetsFromView($event) { | ||||
|             if (this.$refs.conductorForm.checkValidity()) { | ||||
|                 let startOffset = 0 - this.durationFormatter.parse(this.offsets.start); | ||||
|                 let endOffset = this.durationFormatter.parse(this.offsets.end); | ||||
|  | ||||
|                 this.openmct.time.clockOffsets({ | ||||
|                     start: startOffset, | ||||
|                     end: endOffset | ||||
|                 }); | ||||
|             } | ||||
|  | ||||
|             if ($event) { | ||||
|                 $event.preventDefault(); | ||||
|  | ||||
|                 return false; | ||||
|             } | ||||
|         }, | ||||
|         setBoundsFromView($event) { | ||||
|             if (this.$refs.conductorForm.checkValidity()) { | ||||
|                 let start = this.timeFormatter.parse(this.formattedBounds.start); | ||||
|                 let end = this.timeFormatter.parse(this.formattedBounds.end); | ||||
|  | ||||
|                 this.openmct.time.bounds({ | ||||
|                     start: start, | ||||
|                     end: end | ||||
|                 }); | ||||
|             } | ||||
|  | ||||
|             if ($event) { | ||||
|                 $event.preventDefault(); | ||||
|  | ||||
|                 return false; | ||||
|             } | ||||
|         }, | ||||
|         setViewFromClock(clock) { | ||||
|             this.clearAllValidation(); | ||||
|             // this.clearAllValidation(); | ||||
|             this.isFixed = clock === undefined; | ||||
|         }, | ||||
|         setViewFromBounds(bounds) { | ||||
| @@ -341,158 +198,16 @@ export default { | ||||
|             this.viewBounds.start = bounds.start; | ||||
|             this.viewBounds.end = bounds.end; | ||||
|         }, | ||||
|         setViewFromOffsets(offsets) { | ||||
|             this.offsets.start = this.durationFormatter.format(Math.abs(offsets.start)); | ||||
|             this.offsets.end = this.durationFormatter.format(Math.abs(offsets.end)); | ||||
|         }, | ||||
|         updateTimeFromConductor() { | ||||
|             if (this.isFixed) { | ||||
|                 this.setBoundsFromView(); | ||||
|             } else { | ||||
|                 this.setOffsetsFromView(); | ||||
|             } | ||||
|         }, | ||||
|         getBoundsLimit() { | ||||
|             const configuration = this.configuration.menuOptions | ||||
|                 .filter(option => option.timeSystem === this.timeSystem.key) | ||||
|                 .find(option => option.limit); | ||||
|  | ||||
|             const limit = configuration ? configuration.limit : undefined; | ||||
|  | ||||
|             return limit; | ||||
|         }, | ||||
|         clearAllValidation() { | ||||
|             if (this.isFixed) { | ||||
|                 [this.$refs.startDate, this.$refs.endDate].forEach(this.clearValidationForInput); | ||||
|             } else { | ||||
|                 [this.$refs.startOffset, this.$refs.endOffset].forEach(this.clearValidationForInput); | ||||
|             } | ||||
|         }, | ||||
|         clearValidationForInput(input) { | ||||
|             input.setCustomValidity(''); | ||||
|             input.title = ''; | ||||
|         }, | ||||
|         validateAllBounds(ref) { | ||||
|             if (!this.areBoundsFormatsValid()) { | ||||
|                 return false; | ||||
|             } | ||||
|  | ||||
|             let validationResult = true; | ||||
|             const currentInput = this.$refs[ref]; | ||||
|  | ||||
|             return [this.$refs.startDate, this.$refs.endDate].every((input) => { | ||||
|                 let boundsValues = { | ||||
|                     start: this.timeFormatter.parse(this.formattedBounds.start), | ||||
|                     end: this.timeFormatter.parse(this.formattedBounds.end) | ||||
|                 }; | ||||
|                 const limit = this.getBoundsLimit(); | ||||
|  | ||||
|                 if ( | ||||
|                     this.timeSystem.isUTCBased | ||||
|                     && limit | ||||
|                     && boundsValues.end - boundsValues.start > limit | ||||
|                 ) { | ||||
|                     if (input === currentInput) { | ||||
|                         validationResult = "Start and end difference exceeds allowable limit"; | ||||
|                     } | ||||
|                 } else { | ||||
|                     if (input === currentInput) { | ||||
|                         validationResult = this.openmct.time.validateBounds(boundsValues); | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 return this.handleValidationResults(input, validationResult); | ||||
|             }); | ||||
|         }, | ||||
|         areBoundsFormatsValid() { | ||||
|             let validationResult = true; | ||||
|  | ||||
|             return [this.$refs.startDate, this.$refs.endDate].every((input) => { | ||||
|                 const formattedDate = input === this.$refs.startDate | ||||
|                     ? this.formattedBounds.start | ||||
|                     : this.formattedBounds.end | ||||
|                 ; | ||||
|  | ||||
|                 if (!this.timeFormatter.validate(formattedDate)) { | ||||
|                     validationResult = 'Invalid date'; | ||||
|                 } | ||||
|  | ||||
|                 return this.handleValidationResults(input, validationResult); | ||||
|             }); | ||||
|         }, | ||||
|         validateAllOffsets(event) { | ||||
|             return [this.$refs.startOffset, this.$refs.endOffset].every((input) => { | ||||
|                 let validationResult = true; | ||||
|                 let formattedOffset; | ||||
|  | ||||
|                 if (input === this.$refs.startOffset) { | ||||
|                     formattedOffset = this.offsets.start; | ||||
|                 } else { | ||||
|                     formattedOffset = this.offsets.end; | ||||
|                 } | ||||
|  | ||||
|                 if (!this.durationFormatter.validate(formattedOffset)) { | ||||
|                     validationResult = 'Offsets must be in the format hh:mm:ss and less than 24 hours in duration'; | ||||
|                 } else { | ||||
|                     let offsetValues = { | ||||
|                         start: 0 - this.durationFormatter.parse(this.offsets.start), | ||||
|                         end: this.durationFormatter.parse(this.offsets.end) | ||||
|                     }; | ||||
|                     validationResult = this.openmct.time.validateOffsets(offsetValues); | ||||
|                 } | ||||
|  | ||||
|                 return this.handleValidationResults(input, validationResult); | ||||
|             }); | ||||
|         }, | ||||
|         handleValidationResults(input, validationResult) { | ||||
|             if (validationResult !== true) { | ||||
|                 input.setCustomValidity(validationResult); | ||||
|                 input.title = validationResult; | ||||
|  | ||||
|                 return false; | ||||
|             } else { | ||||
|                 input.setCustomValidity(''); | ||||
|                 input.title = ''; | ||||
|  | ||||
|                 return true; | ||||
|             } | ||||
|         }, | ||||
|         submitForm() { | ||||
|             // Allow Vue model to catch up to user input. | ||||
|             // Submitting form will cause validation messages to display (but only if triggered by button click) | ||||
|             this.$nextTick(() => this.$refs.submitButton.click()); | ||||
|         }, | ||||
|         getFormatter(key) { | ||||
|             return this.openmct.telemetry.getValueFormatter({ | ||||
|                 format: key | ||||
|             }).formatter; | ||||
|         }, | ||||
|         startDateSelected(date) { | ||||
|             this.formattedBounds.start = this.timeFormatter.format(date); | ||||
|             this.validateAllBounds('startDate'); | ||||
|             this.submitForm(); | ||||
|         saveClockOffsets(offsets) { | ||||
|             this.openmct.time.clockOffsets(offsets); | ||||
|         }, | ||||
|         endDateSelected(date) { | ||||
|             this.formattedBounds.end = this.timeFormatter.format(date); | ||||
|             this.validateAllBounds('endDate'); | ||||
|             this.submitForm(); | ||||
|         }, | ||||
|         hideAllTimePopups() { | ||||
|             this.showTCInputStart = false; | ||||
|             this.showTCInputEnd = false; | ||||
|         }, | ||||
|         showTimePopupStart() { | ||||
|             this.hideAllTimePopups(); | ||||
|             this.showTCInputStart = !this.showTCInputStart; | ||||
|         }, | ||||
|         showTimePopupEnd() { | ||||
|             this.hideAllTimePopups(); | ||||
|             this.showTCInputEnd = !this.showTCInputEnd; | ||||
|         }, | ||||
|         timePopUpdate({ type, hours, minutes, seconds }) { | ||||
|             this.offsets[type] = [hours, minutes, seconds].join(':'); | ||||
|             this.setOffsetsFromView(); | ||||
|             this.hideAllTimePopups(); | ||||
|         saveFixedOffsets(bounds) { | ||||
|             this.openmct.time.bounds(bounds); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|   | ||||
							
								
								
									
										298
									
								
								src/plugins/timeConductor/ConductorInputsFixed.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										298
									
								
								src/plugins/timeConductor/ConductorInputsFixed.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,298 @@ | ||||
| <template> | ||||
| <form ref="fixedDeltaInput" | ||||
|       class="u-contents" | ||||
|       @submit.prevent="updateTimeFromConductor" | ||||
| > | ||||
|     <button | ||||
|         ref="submitButton" | ||||
|         class="c-input--submit" | ||||
|         type="submit" | ||||
|     ></button> | ||||
|     <div | ||||
|         class="c-ctrl-wrapper c-conductor-input c-conductor__start-fixed" | ||||
|     > | ||||
|         <!-- Fixed start --> | ||||
|         <div class="c-conductor__start-fixed__label"> | ||||
|             Start | ||||
|         </div> | ||||
|         <input | ||||
|             ref="startDate" | ||||
|             v-model="formattedBounds.start" | ||||
|             class="c-input--datetime" | ||||
|             type="text" | ||||
|             autocorrect="off" | ||||
|             spellcheck="false" | ||||
|             @change="validateAllBounds('startDate'); submitForm()" | ||||
|         > | ||||
|         <date-picker | ||||
|             v-if="isUTCBased" | ||||
|             class="c-ctrl-wrapper--menus-left" | ||||
|             :bottom="offsets !== undefined" | ||||
|             :default-date-time="formattedBounds.start" | ||||
|             :formatter="timeFormatter" | ||||
|             @date-selected="startDateSelected" | ||||
|         /> | ||||
|     </div> | ||||
|     <div class="c-ctrl-wrapper c-conductor-input c-conductor__end-fixed"> | ||||
|         <!-- Fixed end and RT 'last update' display --> | ||||
|         <div class="c-conductor__end-fixed__label"> | ||||
|             End | ||||
|         </div> | ||||
|         <input | ||||
|             ref="endDate" | ||||
|             v-model="formattedBounds.end" | ||||
|             class="c-input--datetime" | ||||
|             type="text" | ||||
|             autocorrect="off" | ||||
|             spellcheck="false" | ||||
|             @change="validateAllBounds('endDate'); submitForm()" | ||||
|         > | ||||
|         <date-picker | ||||
|             v-if="isUTCBased" | ||||
|             class="c-ctrl-wrapper--menus-left" | ||||
|             :bottom="offsets !== undefined" | ||||
|             :default-date-time="formattedBounds.end" | ||||
|             :formatter="timeFormatter" | ||||
|             @date-selected="endDateSelected" | ||||
|         /> | ||||
|     </div> | ||||
|     <input | ||||
|         type="submit" | ||||
|         class="invisible" | ||||
|     > | ||||
| </form> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
|  | ||||
| import DatePicker from "./DatePicker.vue"; | ||||
| import _ from "lodash"; | ||||
|  | ||||
| const DEFAULT_DURATION_FORMATTER = 'duration'; | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
|         DatePicker | ||||
|     }, | ||||
|     inject: ['openmct'], | ||||
|     props: { | ||||
|         keyString: { | ||||
|             type: String, | ||||
|             default() { | ||||
|                 return undefined; | ||||
|             } | ||||
|         }, | ||||
|         offsets: { | ||||
|             type: Object, | ||||
|             default() { | ||||
|                 return undefined; | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
|         let timeSystem = this.openmct.time.timeSystem(); | ||||
|         let durationFormatter = this.getFormatter(timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER); | ||||
|         let timeFormatter = this.getFormatter(timeSystem.timeFormat); | ||||
|         let bounds = this.bounds || this.openmct.time.bounds(); | ||||
|  | ||||
|         return { | ||||
|             showTCInputStart: true, | ||||
|             showTCInputEnd: true, | ||||
|             durationFormatter, | ||||
|             timeFormatter, | ||||
|             bounds: { | ||||
|                 start: bounds.start, | ||||
|                 end: bounds.end | ||||
|             }, | ||||
|             formattedBounds: { | ||||
|                 start: timeFormatter.format(bounds.start), | ||||
|                 end: timeFormatter.format(bounds.end) | ||||
|             }, | ||||
|             isUTCBased: timeSystem.isUTCBased | ||||
|         }; | ||||
|     }, | ||||
|     watch: { | ||||
|         offsets: { | ||||
|             handler(newOffsets) { | ||||
|                 this.handleTimeSync(newOffsets); | ||||
|             }, | ||||
|             deep: true | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.handleTimeSync(this.offsets); | ||||
|         this.setTimeSystem(JSON.parse(JSON.stringify(this.openmct.time.timeSystem()))); | ||||
|         this.openmct.time.on('bounds', _.throttle(this.handleNewBounds, 300)); | ||||
|         this.openmct.time.on('timeSystem', this.setTimeSystem); | ||||
|         this.openmct.time.on('clock', this.clearAllValidation); | ||||
|     }, | ||||
|     beforeDestroy() { | ||||
|         this.openmct.time.off('bounds', _.throttle(this.handleNewBounds, 300)); | ||||
|         this.openmct.time.off('timeSystem', this.setTimeSystem); | ||||
|         this.openmct.time.off('clock', this.clearAllValidation); | ||||
|     }, | ||||
|     methods: { | ||||
|         handleTimeSync(offsets) { | ||||
|             if (offsets) { | ||||
|                 this.initializeIndependentTime(offsets); | ||||
|             } else { | ||||
|                 this.syncTime(); | ||||
|             } | ||||
|         }, | ||||
|         initializeIndependentTime(offsets) { | ||||
|             if (offsets) { | ||||
|                 this.setBounds(offsets); | ||||
|                 this.setViewFromBounds(offsets); | ||||
|             } | ||||
|         }, | ||||
|         syncTime() { | ||||
|             this.setTimeSystem(JSON.parse(JSON.stringify(this.openmct.time.timeSystem()))); | ||||
|             this.handleNewBounds(this.openmct.time.bounds()); | ||||
|         }, | ||||
|         handleNewBounds(bounds) { | ||||
|             if (!this.offsets) { | ||||
|                 this.setBounds(bounds); | ||||
|                 this.setViewFromBounds(bounds); | ||||
|             } | ||||
|         }, | ||||
|         clearAllValidation() { | ||||
|             [this.$refs.startDate, this.$refs.endDate].forEach(this.clearValidationForInput); | ||||
|         }, | ||||
|         clearValidationForInput(input) { | ||||
|             input.setCustomValidity(''); | ||||
|             input.title = ''; | ||||
|         }, | ||||
|         setBounds(bounds) { | ||||
|             this.bounds = bounds; | ||||
|         }, | ||||
|         setViewFromBounds(bounds) { | ||||
|             this.formattedBounds.start = this.timeFormatter.format(bounds.start); | ||||
|             this.formattedBounds.end = this.timeFormatter.format(bounds.end); | ||||
|         }, | ||||
|         setTimeSystem(timeSystem) { | ||||
|             this.timeSystem = timeSystem; | ||||
|             this.timeFormatter = this.getFormatter(timeSystem.timeFormat); | ||||
|             this.durationFormatter = this.getFormatter( | ||||
|                 timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER); | ||||
|             this.isUTCBased = timeSystem.isUTCBased; | ||||
|         }, | ||||
|         getFormatter(key) { | ||||
|             return this.openmct.telemetry.getValueFormatter({ | ||||
|                 format: key | ||||
|             }).formatter; | ||||
|         }, | ||||
|         setBoundsFromView($event) { | ||||
|             if (this.$refs.fixedDeltaInput.checkValidity()) { | ||||
|                 let start = this.timeFormatter.parse(this.formattedBounds.start); | ||||
|                 let end = this.timeFormatter.parse(this.formattedBounds.end); | ||||
|  | ||||
|                 this.$emit('updated', { | ||||
|                     start: start, | ||||
|                     end: end | ||||
|                 }); | ||||
|             } | ||||
|  | ||||
|             if ($event) { | ||||
|                 $event.preventDefault(); | ||||
|  | ||||
|                 return false; | ||||
|             } | ||||
|         }, | ||||
|         submitForm() { | ||||
|         // Allow Vue model to catch up to user input. | ||||
|         // Submitting form will cause validation messages to display (but only if triggered by button click) | ||||
|             this.$nextTick(() => this.$refs.submitButton.click()); | ||||
|         }, | ||||
|         updateTimeFromConductor() { | ||||
|             this.setBoundsFromView(); | ||||
|         }, | ||||
|         validateAllBounds(ref) { | ||||
|             if (!this.areBoundsFormatsValid()) { | ||||
|                 return false; | ||||
|             } | ||||
|  | ||||
|             let validationResult = { | ||||
|                 valid: true | ||||
|             }; | ||||
|             const currentInput = this.$refs[ref]; | ||||
|  | ||||
|             return [this.$refs.startDate, this.$refs.endDate].every((input) => { | ||||
|                 let boundsValues = { | ||||
|                     start: this.timeFormatter.parse(this.formattedBounds.start), | ||||
|                     end: this.timeFormatter.parse(this.formattedBounds.end) | ||||
|                 }; | ||||
|                 //TODO: Do we need limits here? We have conductor limits disabled right now | ||||
|                 // const limit = this.getBoundsLimit(); | ||||
|                 const limit = false; | ||||
|  | ||||
|                 if (this.timeSystem.isUTCBased && limit | ||||
|                     && boundsValues.end - boundsValues.start > limit) { | ||||
|                     if (input === currentInput) { | ||||
|                         validationResult = { | ||||
|                             valid: false, | ||||
|                             message: "Start and end difference exceeds allowable limit" | ||||
|                         }; | ||||
|                     } | ||||
|                 } else { | ||||
|                     if (input === currentInput) { | ||||
|                         validationResult = this.openmct.time.validateBounds(boundsValues); | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 return this.handleValidationResults(input, validationResult); | ||||
|             }); | ||||
|         }, | ||||
|         areBoundsFormatsValid() { | ||||
|             let validationResult = { | ||||
|                 valid: true | ||||
|             }; | ||||
|  | ||||
|             return [this.$refs.startDate, this.$refs.endDate].every((input) => { | ||||
|                 const formattedDate = input === this.$refs.startDate | ||||
|                     ? this.formattedBounds.start | ||||
|                     : this.formattedBounds.end | ||||
|           ; | ||||
|  | ||||
|                 if (!this.timeFormatter.validate(formattedDate)) { | ||||
|                     validationResult = { | ||||
|                         valid: false, | ||||
|                         message: 'Invalid date' | ||||
|                     }; | ||||
|                 } | ||||
|  | ||||
|                 return this.handleValidationResults(input, validationResult); | ||||
|             }); | ||||
|         }, | ||||
|         getBoundsLimit() { | ||||
|             const configuration = this.configuration.menuOptions | ||||
|                 .filter(option => option.timeSystem === this.timeSystem.key) | ||||
|                 .find(option => option.limit); | ||||
|  | ||||
|             const limit = configuration ? configuration.limit : undefined; | ||||
|  | ||||
|             return limit; | ||||
|         }, | ||||
|         handleValidationResults(input, validationResult) { | ||||
|             if (validationResult.valid !== true) { | ||||
|                 input.setCustomValidity(validationResult.message); | ||||
|                 input.title = validationResult.message; | ||||
|             } else { | ||||
|                 input.setCustomValidity(''); | ||||
|                 input.title = ''; | ||||
|             } | ||||
|  | ||||
|             return validationResult.valid; | ||||
|         }, | ||||
|         startDateSelected(date) { | ||||
|             this.formattedBounds.start = this.timeFormatter.format(date); | ||||
|             this.validateAllBounds('startDate'); | ||||
|             this.submitForm(); | ||||
|         }, | ||||
|         endDateSelected(date) { | ||||
|             this.formattedBounds.end = this.timeFormatter.format(date); | ||||
|             this.validateAllBounds('endDate'); | ||||
|             this.submitForm(); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| </script> | ||||
							
								
								
									
										290
									
								
								src/plugins/timeConductor/ConductorInputsRealtime.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										290
									
								
								src/plugins/timeConductor/ConductorInputsRealtime.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,290 @@ | ||||
| <template> | ||||
| <form ref="deltaInput" | ||||
|       class="u-contents" | ||||
| > | ||||
|     <div | ||||
|         class="c-ctrl-wrapper c-conductor-input c-conductor__start-delta" | ||||
|     > | ||||
|         <!-- RT start --> | ||||
|         <div class="c-direction-indicator icon-minus"></div> | ||||
|         <time-popup | ||||
|             v-if="showTCInputStart" | ||||
|             class="pr-tc-input-menu--start" | ||||
|             :bottom="realtimeOffsets !== undefined" | ||||
|             :type="'start'" | ||||
|             :offset="offsets.start" | ||||
|             @focus.native="$event.target.select()" | ||||
|             @hide="hideAllTimePopups" | ||||
|             @update="timePopUpdate" | ||||
|         /> | ||||
|         <button | ||||
|             ref="startOffset" | ||||
|             class="c-button c-conductor__delta-button" | ||||
|             title="Set the time offset after now" | ||||
|             @click.prevent="showTimePopupStart" | ||||
|         > | ||||
|             {{ offsets.start }} | ||||
|         </button> | ||||
|     </div> | ||||
|     <div class="c-ctrl-wrapper c-conductor-input c-conductor__end-fixed"> | ||||
|         <!-- RT 'last update' display --> | ||||
|         <div class="c-conductor__end-fixed__label"> | ||||
|             Current | ||||
|         </div> | ||||
|         <input | ||||
|             ref="endDate" | ||||
|             v-model="formattedBounds.end" | ||||
|             class="c-input--datetime" | ||||
|             type="text" | ||||
|             autocorrect="off" | ||||
|             spellcheck="false" | ||||
|             :disabled="true" | ||||
|         > | ||||
|     </div> | ||||
|     <div | ||||
|         class="c-ctrl-wrapper c-conductor-input c-conductor__end-delta" | ||||
|     > | ||||
|         <!-- RT end --> | ||||
|         <div class="c-direction-indicator icon-plus"></div> | ||||
|         <time-popup | ||||
|             v-if="showTCInputEnd" | ||||
|             class="pr-tc-input-menu--end" | ||||
|             :bottom="realtimeOffsets !== undefined" | ||||
|             :type="'end'" | ||||
|             :offset="offsets.end" | ||||
|             @focus.native="$event.target.select()" | ||||
|             @hide="hideAllTimePopups" | ||||
|             @update="timePopUpdate" | ||||
|         /> | ||||
|         <button | ||||
|             ref="endOffset" | ||||
|             class="c-button c-conductor__delta-button" | ||||
|             title="Set the time offset preceding now" | ||||
|             @click.prevent="showTimePopupEnd" | ||||
|         > | ||||
|             {{ offsets.end }} | ||||
|         </button> | ||||
|     </div> | ||||
| </form> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
|  | ||||
| import timePopup from "./timePopup.vue"; | ||||
| import _ from "lodash"; | ||||
|  | ||||
| const DEFAULT_DURATION_FORMATTER = 'duration'; | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
|         timePopup | ||||
|     }, | ||||
|     inject: ['openmct'], | ||||
|     props: { | ||||
|         keyString: { | ||||
|             type: String, | ||||
|             default() { | ||||
|                 return undefined; | ||||
|             } | ||||
|         }, | ||||
|         realtimeOffsets: { | ||||
|             type: Object, | ||||
|             default() { | ||||
|                 return undefined; | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
|         let timeSystem = this.openmct.time.timeSystem(); | ||||
|         let durationFormatter = this.getFormatter(timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER); | ||||
|         let timeFormatter = this.getFormatter(timeSystem.timeFormat); | ||||
|         let bounds = this.bounds || this.openmct.time.bounds(); | ||||
|         let offsets = this.openmct.time.clockOffsets(); | ||||
|  | ||||
|         return { | ||||
|             showTCInputStart: false, | ||||
|             showTCInputEnd: false, | ||||
|             durationFormatter, | ||||
|             timeFormatter, | ||||
|             bounds: { | ||||
|                 start: bounds.start, | ||||
|                 end: bounds.end | ||||
|             }, | ||||
|             offsets: { | ||||
|                 start: offsets && durationFormatter.format(Math.abs(offsets.start)), | ||||
|                 end: offsets && durationFormatter.format(Math.abs(offsets.end)) | ||||
|             }, | ||||
|             formattedBounds: { | ||||
|                 start: timeFormatter.format(bounds.start), | ||||
|                 end: timeFormatter.format(bounds.end) | ||||
|             }, | ||||
|             isUTCBased: timeSystem.isUTCBased | ||||
|         }; | ||||
|     }, | ||||
|     watch: { | ||||
|         realtimeOffsets: { | ||||
|             handler(newOffsets) { | ||||
|                 this.handleTimeSync(newOffsets); | ||||
|             }, | ||||
|             deep: true | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.setTimeContext(); | ||||
|         this.handleTimeSync(this.realtimeOffsets); | ||||
|     }, | ||||
|     beforeDestroy() { | ||||
|         this.stopFollowingTime(); | ||||
|     }, | ||||
|     methods: { | ||||
|         followTime() { | ||||
|             this.timeContext.on('bounds', _.throttle(this.handleNewBounds, 300)); | ||||
|             this.timeContext.on('timeSystem', this.setTimeSystem); | ||||
|             this.timeContext.on('clock', this.clearAllValidation); | ||||
|             this.timeContext.on('clockOffsets', this.setViewFromOffsets); | ||||
|         }, | ||||
|         stopFollowingTime() { | ||||
|             if (this.timeContext) { | ||||
|                 this.timeContext.off('bounds', _.throttle(this.handleNewBounds, 300)); | ||||
|                 this.timeContext.off('timeSystem', this.setTimeSystem); | ||||
|                 this.timeContext.off('clock', this.clearAllValidation); | ||||
|                 this.timeContext.off('clockOffsets', this.setViewFromOffsets); | ||||
|                 this.timeContext.off('timeContext', this.setTimeContext); | ||||
|             } | ||||
|         }, | ||||
|         handleTimeSync(offsets) { | ||||
|             if (offsets) { | ||||
|                 this.setViewFromOffsets(offsets); | ||||
|             } else { | ||||
|                 this.syncTime(); | ||||
|             } | ||||
|         }, | ||||
|         setTimeContext() { | ||||
|             this.stopFollowingTime(); | ||||
|             this.timeContext = this.openmct.time.getContextForView(this.keyString ? [{identifier: this.keyString}] : []); | ||||
|             this.timeContext.on('timeContext', this.setTimeContext); | ||||
|             this.followTime(); | ||||
|         }, | ||||
|         syncTime() { | ||||
|             this.setTimeSystem(JSON.parse(JSON.stringify(this.openmct.time.timeSystem()))); | ||||
|             this.setViewFromOffsets(this.openmct.time.clockOffsets()); | ||||
|             this.handleNewBounds(this.openmct.time.bounds()); | ||||
|         }, | ||||
|         handleNewBounds(bounds) { | ||||
|             this.setBounds(bounds); | ||||
|             this.setViewFromBounds(bounds); | ||||
|         }, | ||||
|         clearAllValidation() { | ||||
|             [this.$refs.startOffset, this.$refs.endOffset].forEach(this.clearValidationForInput); | ||||
|         }, | ||||
|         clearValidationForInput(input) { | ||||
|             input.setCustomValidity(''); | ||||
|             input.title = ''; | ||||
|         }, | ||||
|         setViewFromOffsets(offsets) { | ||||
|             this.offsets.start = this.durationFormatter.format(Math.abs(offsets.start)); | ||||
|             this.offsets.end = this.durationFormatter.format(Math.abs(offsets.end)); | ||||
|         }, | ||||
|         setBounds(bounds) { | ||||
|             this.bounds = bounds; | ||||
|         }, | ||||
|         setViewFromBounds(bounds) { | ||||
|             this.formattedBounds.start = this.timeFormatter.format(bounds.start); | ||||
|             this.formattedBounds.end = this.timeFormatter.format(bounds.end); | ||||
|         }, | ||||
|         setTimeSystem(timeSystem) { | ||||
|             this.timeSystem = timeSystem; | ||||
|             this.timeFormatter = this.getFormatter(timeSystem.timeFormat); | ||||
|             this.durationFormatter = this.getFormatter( | ||||
|                 timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER); | ||||
|             this.isUTCBased = timeSystem.isUTCBased; | ||||
|         }, | ||||
|         getFormatter(key) { | ||||
|             return this.openmct.telemetry.getValueFormatter({ | ||||
|                 format: key | ||||
|             }).formatter; | ||||
|         }, | ||||
|         hideAllTimePopups() { | ||||
|             this.showTCInputStart = false; | ||||
|             this.showTCInputEnd = false; | ||||
|         }, | ||||
|         showTimePopupStart() { | ||||
|             this.hideAllTimePopups(); | ||||
|             this.showTCInputStart = !this.showTCInputStart; | ||||
|         }, | ||||
|         showTimePopupEnd() { | ||||
|             this.hideAllTimePopups(); | ||||
|             this.showTCInputEnd = !this.showTCInputEnd; | ||||
|         }, | ||||
|         timePopUpdate({ type, hours, minutes, seconds }) { | ||||
|             this.offsets[type] = [hours, minutes, seconds].join(':'); | ||||
|             this.setOffsetsFromView(); | ||||
|             this.hideAllTimePopups(); | ||||
|         }, | ||||
|         setOffsetsFromView($event) { | ||||
|             if (this.$refs.deltaInput.checkValidity()) { | ||||
|                 let startOffset = 0 - this.durationFormatter.parse(this.offsets.start); | ||||
|                 let endOffset = this.durationFormatter.parse(this.offsets.end); | ||||
|  | ||||
|                 this.$emit('updated', { | ||||
|                     start: startOffset, | ||||
|                     end: endOffset | ||||
|                 }); | ||||
|             } | ||||
|  | ||||
|             if ($event) { | ||||
|                 $event.preventDefault(); | ||||
|  | ||||
|                 return false; | ||||
|             } | ||||
|         }, | ||||
|         validateAllBounds(ref) { | ||||
|             if (!this.areBoundsFormatsValid()) { | ||||
|                 return false; | ||||
|             } | ||||
|  | ||||
|             let validationResult = { | ||||
|                 valid: true | ||||
|             }; | ||||
|             const currentInput = this.$refs[ref]; | ||||
|  | ||||
|             return [this.$refs.startDate, this.$refs.endDate].every((input) => { | ||||
|                 let boundsValues = { | ||||
|                     start: this.timeFormatter.parse(this.formattedBounds.start), | ||||
|                     end: this.timeFormatter.parse(this.formattedBounds.end) | ||||
|                 }; | ||||
|                 //TODO: Do we need limits here? We have conductor limits disabled right now | ||||
|                 // const limit = this.getBoundsLimit(); | ||||
|                 const limit = false; | ||||
|  | ||||
|                 if (this.timeSystem.isUTCBased && limit | ||||
|                     && boundsValues.end - boundsValues.start > limit) { | ||||
|                     if (input === currentInput) { | ||||
|                         validationResult = { | ||||
|                             valid: false, | ||||
|                             message: "Start and end difference exceeds allowable limit" | ||||
|                         }; | ||||
|                     } | ||||
|                 } else { | ||||
|                     if (input === currentInput) { | ||||
|                         validationResult = this.openmct.time.validateBounds(boundsValues); | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 return this.handleValidationResults(input, validationResult); | ||||
|             }); | ||||
|         }, | ||||
|         handleValidationResults(input, validationResult) { | ||||
|             if (validationResult.valid !== true) { | ||||
|                 input.setCustomValidity(validationResult.message); | ||||
|                 input.title = validationResult.message; | ||||
|             } else { | ||||
|                 input.setCustomValidity(''); | ||||
|                 input.title = ''; | ||||
|             } | ||||
|  | ||||
|             return validationResult.valid; | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| </script> | ||||
| @@ -22,7 +22,8 @@ | ||||
| <template> | ||||
| <div | ||||
|     ref="calendarHolder" | ||||
|     class="c-ctrl-wrapper c-ctrl-wrapper--menus-up c-datetime-picker__wrapper" | ||||
|     class="c-ctrl-wrapper c-datetime-picker__wrapper" | ||||
|     :class="{'c-ctrl-wrapper--menus-up': bottom !== true, 'c-ctrl-wrapper--menus-down': bottom === true}" | ||||
| > | ||||
|     <a | ||||
|         class="c-icon-button icon-calendar" | ||||
| @@ -118,6 +119,12 @@ export default { | ||||
|         formatter: { | ||||
|             type: Object, | ||||
|             required: true | ||||
|         }, | ||||
|         bottom: { | ||||
|             type: Boolean, | ||||
|             default() { | ||||
|                 return false; | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     data: function () { | ||||
|   | ||||
| @@ -50,13 +50,6 @@ | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     [class*='-delta'] { | ||||
|         &:before { | ||||
|             content: $glyph-icon-clock; | ||||
|             font-family: symbolsfont; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     &.is-fixed-mode { | ||||
|         .c-conductor-axis { | ||||
|             &__zoom-indicator { | ||||
| @@ -181,6 +174,18 @@ | ||||
|     } | ||||
| } | ||||
|  | ||||
| .c-conductor-holder--compact { | ||||
|     .c-conductor { | ||||
|         &__time-bounds { | ||||
|             display: flex; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     .is-realtime-mode .c-conductor__end-fixed { | ||||
|         display: none !important; | ||||
|     } | ||||
| } | ||||
|  | ||||
| .c-conductor-input { | ||||
|     color: $colorInputFg; | ||||
|     display: flex; | ||||
| @@ -250,18 +255,22 @@ | ||||
|     box-shadow: $shdwMenu; | ||||
|     padding: $interiorMargin; | ||||
|     position: absolute; | ||||
|     left: 8px; | ||||
|     bottom: 24px; | ||||
|     z-index: 99; | ||||
|  | ||||
|     &[class*='--start'] { | ||||
|         left: -25px; | ||||
|     } | ||||
|  | ||||
|     &[class*='--end'] { | ||||
|         right: 0; | ||||
|     &[class*='--bottom'] { | ||||
|         bottom: auto; | ||||
|         top: 24px; | ||||
|     } | ||||
| } | ||||
|  | ||||
| .l-shell__time-conductor .pr-tc-input-menu--end { | ||||
|     left: auto; | ||||
|     right: 0; | ||||
| } | ||||
|  | ||||
|  | ||||
| [class^='pr-time'] { | ||||
|     &[class*='label'] { | ||||
|         font-size: 0.8em; | ||||
|   | ||||
| @@ -0,0 +1,180 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT Web, Copyright (c) 2014-2021, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT Web is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT Web includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
| <template> | ||||
| <div | ||||
|     class="c-conductor" | ||||
|     :class="[ | ||||
|         isFixed ? 'is-fixed-mode' : 'is-realtime-mode' | ||||
|     ]" | ||||
| > | ||||
|     <div v-if="timeOptions" | ||||
|          class="c-conductor__time-bounds" | ||||
|     > | ||||
|         <ConductorModeIcon /> | ||||
|         <div class="c-conductor__controls"> | ||||
|             <Mode v-if="mode" | ||||
|                   class="c-conductor__mode-select" | ||||
|                   :key-string="domainObject.identifier.key" | ||||
|                   :mode="mode" | ||||
|                   @modeChanged="saveMode" | ||||
|             /> | ||||
|         </div> | ||||
|         <conductor-inputs-fixed v-if="isFixed" | ||||
|                                 :key-string="domainObject.identifier.key" | ||||
|                                 :offsets="timeOptions.fixedOffsets" | ||||
|                                 @updated="saveFixedOffets" | ||||
|         /> | ||||
|         <conductor-inputs-realtime v-else | ||||
|                                    :key-string="domainObject.identifier.key" | ||||
|                                    :realtime-offsets="timeOptions.clockOffsets" | ||||
|                                    @updated="saveClockOffsets" | ||||
|         /> | ||||
|  | ||||
|     </div> | ||||
| </div> | ||||
| </div></template> | ||||
|  | ||||
| <script> | ||||
| import ConductorInputsFixed from "../ConductorInputsFixed.vue"; | ||||
| import ConductorInputsRealtime from "../ConductorInputsRealtime.vue"; | ||||
| import ConductorModeIcon from "@/plugins/timeConductor/ConductorModeIcon.vue"; | ||||
| import Mode from "./Mode.vue"; | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
|         Mode, | ||||
|         ConductorModeIcon, | ||||
|         ConductorInputsRealtime, | ||||
|         ConductorInputsFixed | ||||
|     }, | ||||
|     inject: ['openmct', 'domainObject'], | ||||
|     props: { | ||||
|         options: { | ||||
|             type: Object, | ||||
|             default() { | ||||
|                 return undefined; | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             isFixed: this.openmct.time.clock() === undefined, | ||||
|             timeOptions: this.options || { | ||||
|                 clockOffsets: this.openmct.time.clockOffsets(), | ||||
|                 fixedOffsets: this.openmct.time.bounds() | ||||
|             }, | ||||
|             mode: undefined | ||||
|         }; | ||||
|     }, | ||||
|     watch: { | ||||
|         options: { | ||||
|             handler(newOptions) { | ||||
|                 if (this.timeOptions.start !== newOptions.start | ||||
|                     || this.timeOptions.end !== newOptions.end) { | ||||
|                     this.timeOptions = newOptions; | ||||
|                     this.registerIndependentTimeOffsets(); | ||||
|                 } | ||||
|             }, | ||||
|             deep: true | ||||
|  | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|         if (this.timeOptions.mode) { | ||||
|             this.mode = this.timeOptions.mode; | ||||
|         } else { | ||||
|             this.mode = this.openmct.time.clock() === undefined ? { key: 'fixed' } : { key: this.openmct.time.clock().key}; | ||||
|         } | ||||
|  | ||||
|         this.isFixed = this.mode.key === 'fixed'; | ||||
|         this.registerIndependentTimeOffsets(); | ||||
|         this.setTimeContext(); | ||||
|     }, | ||||
|     beforeDestroy() { | ||||
|         this.timeContext.off('clock', this.setViewFromClock); | ||||
|         this.destroyIndependentTime(); | ||||
|     }, | ||||
|     methods: { | ||||
|         setTimeContext() { | ||||
|             this.timeContext = this.openmct.time.getContextForView([this.domainObject]); | ||||
|             this.timeContext.on('clock', this.setViewFromClock); | ||||
|         }, | ||||
|         setViewFromClock(clock) { | ||||
|             if (!this.mode) { | ||||
|                 this.setTimeOptions(clock); | ||||
|             } | ||||
|         }, | ||||
|         setTimeOptions() { | ||||
|             if (!this.timeOptions || !this.timeOptions.clockOffsets) { | ||||
|                 this.timeOptions = { | ||||
|                     clockOffsets: this.openmct.time.clockOffsets(), | ||||
|                     fixedOffsets: this.openmct.time.bounds() | ||||
|                 }; | ||||
|             } | ||||
|  | ||||
|             this.registerIndependentTimeOffsets(); | ||||
|         }, | ||||
|         saveFixedOffets(offsets) { | ||||
|             const newOptions = Object.assign({}, this.timeOptions, { | ||||
|                 fixedOffsets: offsets | ||||
|             }); | ||||
|             this.updateTimeOptions(newOptions); | ||||
|         }, | ||||
|         saveClockOffsets(offsets) { | ||||
|             const newOptions = Object.assign({}, this.timeOptions, { | ||||
|                 clockOffsets: offsets | ||||
|             }); | ||||
|             this.updateTimeOptions(newOptions); | ||||
|         }, | ||||
|         saveMode(mode) { | ||||
|             this.mode = mode; | ||||
|             this.isFixed = this.mode.key === 'fixed'; | ||||
|             const newOptions = Object.assign({}, this.timeOptions, { | ||||
|                 mode: this.mode | ||||
|             }); | ||||
|             this.updateTimeOptions(newOptions); | ||||
|         }, | ||||
|         updateTimeOptions(options) { | ||||
|             this.timeOptions = options; | ||||
|             this.registerIndependentTimeOffsets(); | ||||
|             this.$emit('updated', options); | ||||
|         }, | ||||
|         registerIndependentTimeOffsets() { | ||||
|             let offsets; | ||||
|  | ||||
|             if (this.isFixed) { | ||||
|                 offsets = this.timeOptions.fixedOffsets; | ||||
|             } else { | ||||
|                 offsets = this.timeOptions.clockOffsets; | ||||
|             } | ||||
|  | ||||
|             const key = this.openmct.objects.makeKeyString(this.domainObject.identifier); | ||||
|             this.unregisterIndependentTime = this.openmct.time.addIndependentContext(key, offsets, this.mode.key === 'fixed' ? undefined : this.mode.key); | ||||
|         }, | ||||
|         destroyIndependentTime() { | ||||
|             if (this.unregisterIndependentTime) { | ||||
|                 this.unregisterIndependentTime(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| </script> | ||||
							
								
								
									
										193
									
								
								src/plugins/timeConductor/independent/Mode.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										193
									
								
								src/plugins/timeConductor/independent/Mode.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,193 @@ | ||||
| /***************************************************************************** | ||||
| * Open MCT Web, Copyright (c) 2014-2021, United States Government | ||||
| * as represented by the Administrator of the National Aeronautics and Space | ||||
| * Administration. All rights reserved. | ||||
| * | ||||
| * Open MCT Web is licensed under the Apache License, Version 2.0 (the | ||||
| * "License"); you may not use this file except in compliance with the License. | ||||
| * You may obtain a copy of the License at | ||||
| * http://www.apache.org/licenses/LICENSE-2.0. | ||||
| * | ||||
| * Unless required by applicable law or agreed to in writing, software | ||||
| * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
| * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
| * License for the specific language governing permissions and limitations | ||||
| * under the License. | ||||
| * | ||||
| * Open MCT Web includes source code licensed under additional open source | ||||
| * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
| * this source code distribution or the Licensing information page available | ||||
| * at runtime from the About dialog for additional information. | ||||
| *****************************************************************************/ | ||||
| <template> | ||||
| <div ref="modeButton" | ||||
|      class="c-ctrl-wrapper c-ctrl-wrapper--menus-up" | ||||
| > | ||||
|     <div class="c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left"> | ||||
|         <button | ||||
|             class="c-button--menu c-mode-button" | ||||
|             @click.prevent.stop="showModesMenu" | ||||
|         > | ||||
|             <span class="c-button__label">{{ selectedMode.name }}</span> | ||||
|         </button> | ||||
|     </div> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import toggleMixin from '../../../ui/mixins/toggle-mixin'; | ||||
|  | ||||
| export default { | ||||
|     mixins: [toggleMixin], | ||||
|     inject: ['openmct', 'domainObject'], | ||||
|     props: { | ||||
|         mode: { | ||||
|             type: Object, | ||||
|             default() { | ||||
|                 return { | ||||
|                     key: 'fixed' | ||||
|                 }; | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     data: function () { | ||||
|         let clock; | ||||
|         if (this.mode && this.mode.key === 'fixed') { | ||||
|             clock = undefined; | ||||
|         } else { | ||||
|             //We want the clock from the global time context here | ||||
|             clock = this.openmct.time.clock(); | ||||
|         } | ||||
|  | ||||
|         if (clock !== undefined) { | ||||
|             //Create copy of active clock so the time API does not get reactified. | ||||
|             clock = Object.create(clock); | ||||
|         } | ||||
|  | ||||
|         return { | ||||
|             selectedMode: this.getModeOptionForClock(clock), | ||||
|             modes: [] | ||||
|         }; | ||||
|     }, | ||||
|     mounted: function () { | ||||
|         this.timeContext = this.openmct.time.getContextForView([this.domainObject]); | ||||
|         if (this.mode) { | ||||
|             this.setViewFromClock(this.mode.key === 'fixed' ? undefined : this.mode); | ||||
|         } | ||||
|  | ||||
|         this.openmct.time.on('clock', this.setViewFromClock); | ||||
|     }, | ||||
|     destroyed: function () { | ||||
|         this.openmct.time.off('clock', this.setViewFromClock); | ||||
|     }, | ||||
|     methods: { | ||||
|         showModesMenu() { | ||||
|             const elementBoundingClientRect = this.$refs.modeButton.getBoundingClientRect(); | ||||
|             const x = elementBoundingClientRect.x; | ||||
|             const y = elementBoundingClientRect.y; | ||||
|  | ||||
|             const menuOptions = { | ||||
|                 menuClass: 'c-conductor__mode-menu', | ||||
|                 placement: this.openmct.menus.menuPlacement.TOP_RIGHT | ||||
|             }; | ||||
|             this.openmct.menus.showSuperMenu(x, y, this.modes, menuOptions); | ||||
|         }, | ||||
|  | ||||
|         getMenuOptions() { | ||||
|             let clocks = [{ | ||||
|                 name: 'Fixed Timespan', | ||||
|                 timeSystem: 'utc' | ||||
|             }]; | ||||
|             let currentGlobalClock = this.openmct.time.clock(); | ||||
|             if (currentGlobalClock !== undefined) { | ||||
|             //Create copy of active clock so the time API does not get reactified. | ||||
|                 currentGlobalClock = Object.assign({}, { | ||||
|                     name: currentGlobalClock.name, | ||||
|                     clock: currentGlobalClock.key, | ||||
|                     timeSystem: this.openmct.time.timeSystem().key | ||||
|                 }); | ||||
|  | ||||
|                 clocks.push(currentGlobalClock); | ||||
|             } | ||||
|  | ||||
|             return clocks; | ||||
|         }, | ||||
|         loadClocks() { | ||||
|             let clocks = this.getMenuOptions() | ||||
|                 .map(menuOption => menuOption.clock) | ||||
|                 .filter(isDefinedAndUnique) | ||||
|                 .map(this.getClock); | ||||
|  | ||||
|             /* | ||||
|          * Populate the modes menu with metadata from the available clocks | ||||
|          * "Fixed Mode" is always first, and has no defined clock | ||||
|          */ | ||||
|             this.modes = [undefined] | ||||
|                 .concat(clocks) | ||||
|                 .map(this.getModeOptionForClock); | ||||
|  | ||||
|             function isDefinedAndUnique(key, index, array) { | ||||
|                 return key !== undefined && array.indexOf(key) === index; | ||||
|             } | ||||
|         }, | ||||
|  | ||||
|         getModeOptionForClock(clock) { | ||||
|             if (clock === undefined) { | ||||
|                 const key = 'fixed'; | ||||
|  | ||||
|                 return { | ||||
|                     key, | ||||
|                     name: 'Fixed Timespan', | ||||
|                     description: 'Query and explore data that falls between two fixed datetimes.', | ||||
|                     cssClass: 'icon-tabular', | ||||
|                     onItemClicked: () => this.setOption(key) | ||||
|                 }; | ||||
|             } else { | ||||
|                 const key = clock.key; | ||||
|  | ||||
|                 return { | ||||
|                     key, | ||||
|                     name: clock.name, | ||||
|                     description: "Monitor streaming data in real-time. The Time " | ||||
|               + "Conductor and displays will automatically advance themselves based on this clock. " + clock.description, | ||||
|                     cssClass: clock.cssClass || 'icon-clock', | ||||
|                     onItemClicked: () => this.setOption(key) | ||||
|                 }; | ||||
|             } | ||||
|         }, | ||||
|  | ||||
|         getClock(key) { | ||||
|             return this.openmct.time.getAllClocks().filter(function (clock) { | ||||
|                 return clock.key === key; | ||||
|             })[0]; | ||||
|         }, | ||||
|  | ||||
|         setOption(clockKey) { | ||||
|             let key = clockKey; | ||||
|             if (clockKey === 'fixed') { | ||||
|                 key = undefined; | ||||
|             } | ||||
|  | ||||
|             const matchingOptions = this.getMenuOptions().filter(option => option.clock === key); | ||||
|             const clock = matchingOptions.length && matchingOptions[0].clock ? Object.assign({}, matchingOptions[0], { key: matchingOptions[0].clock }) : undefined; | ||||
|             this.selectedMode = this.getModeOptionForClock(clock); | ||||
|  | ||||
|             this.$emit('modeChanged', { key: clockKey }); | ||||
|         }, | ||||
|  | ||||
|         setViewFromClock(clock) { | ||||
|             this.loadClocks(); | ||||
|             //retain last selected mode | ||||
|             if (this.selectedMode) { | ||||
|                 const found = this.modes.find(mode => mode.key === this.selectedMode.key); | ||||
|  | ||||
|                 if (!found) { | ||||
|                     this.setOption(this.getModeOptionForClock(clock).key); | ||||
|                 } | ||||
|             } else { | ||||
|                 this.setOption(this.getModeOptionForClock(clock).key); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| </script> | ||||
							
								
								
									
										128
									
								
								src/plugins/timeConductor/pluginSpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								src/plugins/timeConductor/pluginSpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,128 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2021, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| import {createMouseEvent, createOpenMct, resetApplicationState} from "utils/testing"; | ||||
| import ConductorPlugin from "./plugin"; | ||||
| import Vue from 'vue'; | ||||
|  | ||||
| const THIRTY_SECONDS = 30 * 1000; | ||||
| const ONE_MINUTE = THIRTY_SECONDS * 2; | ||||
| const FIVE_MINUTES = ONE_MINUTE * 5; | ||||
| const FIFTEEN_MINUTES = FIVE_MINUTES * 3; | ||||
| const THIRTY_MINUTES = FIFTEEN_MINUTES * 2; | ||||
| const date = new Date('Jan 20 1978').getTime(); | ||||
|  | ||||
| describe('time conductor', () => { | ||||
|     let element; | ||||
|     let child; | ||||
|     let appHolder; | ||||
|     let openmct; | ||||
|     let config = { | ||||
|         menuOptions: [ | ||||
|             { | ||||
|                 name: "FixedTimeRange", | ||||
|                 timeSystem: 'utc', | ||||
|                 bounds: { | ||||
|                     start: date - THIRTY_MINUTES, | ||||
|                     end: date | ||||
|                 }, | ||||
|                 presets: [], | ||||
|                 records: 2 | ||||
|             }, | ||||
|             { | ||||
|                 name: "LocalClock", | ||||
|                 timeSystem: 'utc', | ||||
|                 clock: 'local', | ||||
|                 clockOffsets: { | ||||
|                     start: -THIRTY_MINUTES, | ||||
|                     end: THIRTY_SECONDS | ||||
|                 }, | ||||
|                 presets: [] | ||||
|             } | ||||
|         ] | ||||
|     }; | ||||
|  | ||||
|     beforeEach((done) => { | ||||
|         openmct = createOpenMct(); | ||||
|         openmct.install(new ConductorPlugin(config)); | ||||
|  | ||||
|         element = document.createElement('div'); | ||||
|         element.style.width = '640px'; | ||||
|         element.style.height = '480px'; | ||||
|         child = document.createElement('div'); | ||||
|         child.style.width = '640px'; | ||||
|         child.style.height = '480px'; | ||||
|         element.appendChild(child); | ||||
|  | ||||
|         openmct.on('start', () => { | ||||
|             openmct.time.bounds({ | ||||
|                 start: config.menuOptions[0].bounds.start, | ||||
|                 end: config.menuOptions[0].bounds.end | ||||
|             }); | ||||
|             Vue.nextTick(() => { | ||||
|                 done(); | ||||
|             }); | ||||
|         }); | ||||
|         appHolder = document.createElement("div"); | ||||
|         openmct.start(appHolder); | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         appHolder = undefined; | ||||
|         openmct = undefined; | ||||
|  | ||||
|         return resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|     it('shows delta inputs in fixed mode', () => { | ||||
|         const fixedModeEl = appHolder.querySelector('.is-fixed-mode'); | ||||
|         const dateTimeInputs = fixedModeEl.querySelectorAll('.c-input--datetime'); | ||||
|         expect(dateTimeInputs[0].value).toEqual('1978-01-20 07:30:00.000Z'); | ||||
|         expect(dateTimeInputs[1].value).toEqual('1978-01-20 08:00:00.000Z'); | ||||
|         expect(fixedModeEl.querySelector('.c-mode-button .c-button__label').innerHTML).toEqual('Fixed Timespan'); | ||||
|     }); | ||||
|  | ||||
|     describe('shows delta inputs in realtime mode', () => { | ||||
|         beforeEach((done) => { | ||||
|             const switcher = appHolder.querySelector('.c-mode-button'); | ||||
|             const clickEvent = createMouseEvent("click"); | ||||
|  | ||||
|             switcher.dispatchEvent(clickEvent); | ||||
|             Vue.nextTick(() => { | ||||
|                 const clockItem = document.querySelectorAll('.c-conductor__mode-menu li')[1]; | ||||
|                 clockItem.dispatchEvent(clickEvent); | ||||
|                 Vue.nextTick(() => { | ||||
|                     done(); | ||||
|                 }); | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         it('shows clock options', () => { | ||||
|             const realtimeModeEl = appHolder.querySelector('.is-realtime-mode'); | ||||
|             const dateTimeInputs = realtimeModeEl.querySelectorAll('.c-conductor__delta-button'); | ||||
|             expect(dateTimeInputs[0].innerHTML.replace(/[^(\d|:)]/g, '')).toEqual('00:30:00'); | ||||
|             expect(dateTimeInputs[1].innerHTML.replace(/[^(\d|:)]/g, '')).toEqual('00:00:30'); | ||||
|             expect(realtimeModeEl.querySelector('.c-mode-button .c-button__label').innerHTML).toEqual('Local Clock'); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
| }); | ||||
| @@ -1,6 +1,7 @@ | ||||
| <template> | ||||
| <div | ||||
|     class="pr-tc-input-menu" | ||||
|     :class="{'pr-tc-input-menu--bottom' : bottom === true}" | ||||
|     @keydown.enter.prevent | ||||
|     @keyup.enter.prevent="submit" | ||||
|     @keydown.esc.prevent | ||||
| @@ -88,6 +89,12 @@ export default { | ||||
|         offset: { | ||||
|             type: String, | ||||
|             required: true | ||||
|         }, | ||||
|         bottom: { | ||||
|             type: Boolean, | ||||
|             default() { | ||||
|                 return false; | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
|   | ||||
| @@ -24,6 +24,13 @@ | ||||
| <div ref="timelineHolder" | ||||
|      class="c-timeline-holder" | ||||
| > | ||||
|     <div v-if="useIndependentTime" | ||||
|          class="c-conductor-holder--compact" | ||||
|     > | ||||
|         <independent-time-conductor :options="timeOptions" | ||||
|                                     @updated="saveTimeOptions" | ||||
|         /> | ||||
|     </div> | ||||
|     <div class="c-timeline"> | ||||
|         <div v-for="timeSystemItem in timeSystems" | ||||
|              :key="timeSystemItem.timeSystem.key" | ||||
| @@ -67,6 +74,7 @@ import TimelineObjectView from './TimelineObjectView.vue'; | ||||
| import TimelineAxis from '../../ui/components/TimeSystemAxis.vue'; | ||||
| import SwimLane from "@/ui/components/swim-lane/SwimLane.vue"; | ||||
| import { getValidatedPlan } from "../plan/util"; | ||||
| import IndependentTimeConductor from "@/plugins/timeConductor/independent/IndependentTimeConductor.vue"; | ||||
|  | ||||
| const unknownObjectType = { | ||||
|     definition: { | ||||
| @@ -77,6 +85,7 @@ const unknownObjectType = { | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
|         IndependentTimeConductor, | ||||
|         TimelineObjectView, | ||||
|         TimelineAxis, | ||||
|         SwimLane | ||||
| @@ -86,15 +95,21 @@ export default { | ||||
|         return { | ||||
|             items: [], | ||||
|             timeSystems: [], | ||||
|             height: 0 | ||||
|             height: 0, | ||||
|             useIndependentTime: this.domainObject.configuration.useIndependentTime === true, | ||||
|             isFixed: this.openmct.time.clock() === undefined, | ||||
|             timeOptions: this.domainObject.configuration.timeOptions | ||||
|         }; | ||||
|     }, | ||||
|     beforeDestroy() { | ||||
|         this.composition.off('add', this.addItem); | ||||
|         this.composition.off('remove', this.removeItem); | ||||
|         this.composition.off('reorder', this.reorder); | ||||
|         this.openmct.time.off("bounds", this.updateViewBounds); | ||||
|         this.stopFollowingTimeContext(); | ||||
|  | ||||
|         if (this.unObserveTime) { | ||||
|             this.unObserveTime(); | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|         if (this.composition) { | ||||
| @@ -104,8 +119,10 @@ export default { | ||||
|             this.composition.load(); | ||||
|         } | ||||
|  | ||||
|         this.handleTimeSync(this.useIndependentTime); | ||||
|         this.setTimeContext(); | ||||
|         this.getTimeSystems(); | ||||
|         this.openmct.time.on("bounds", this.updateViewBounds); | ||||
|         this.unObserveTime = this.openmct.objects.observe(this.domainObject, 'configuration.useIndependentTime', this.handleTimeSync); | ||||
|     }, | ||||
|     methods: { | ||||
|         addItem(domainObject) { | ||||
| @@ -154,7 +171,7 @@ export default { | ||||
|             }); | ||||
|         }, | ||||
|         getBoundsForTimeSystem(timeSystem) { | ||||
|             const currentBounds = this.openmct.time.bounds(); | ||||
|             const currentBounds = this.timeContext.bounds(); | ||||
|  | ||||
|             //TODO: Some kind of translation via an offset? of current bounds to target timeSystem | ||||
|             return currentBounds; | ||||
| @@ -164,6 +181,28 @@ export default { | ||||
|             if (currentTimeSystem) { | ||||
|                 currentTimeSystem.bounds = bounds; | ||||
|             } | ||||
|         }, | ||||
|         handleTimeSync(useIndependentTime) { | ||||
|             this.useIndependentTime = useIndependentTime; | ||||
|         }, | ||||
|         setTimeContext() { | ||||
|             console.log('changing contexts'); | ||||
|             this.stopFollowingTimeContext(); | ||||
|  | ||||
|             this.timeContext = this.openmct.time.getContextForView(this.objectPath); | ||||
|             this.timeContext.on('timeContext', this.setTimeContext); | ||||
|             this.updateViewBounds(this.timeContext.bounds()); | ||||
|             this.timeContext.on('bounds', this.updateViewBounds); | ||||
|         }, | ||||
|         stopFollowingTimeContext() { | ||||
|             if (this.timeContext) { | ||||
|                 this.timeContext.off('bounds', this.updateViewBounds); | ||||
|                 this.timeContext.off('timeContext', this.setTimeContext); | ||||
|             } | ||||
|         }, | ||||
|         saveTimeOptions(options) { | ||||
|             this.timeOptions = options; | ||||
|             this.openmct.objects.mutate(this.domainObject, 'configuration.timeOptions', this.timeOptions); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|   | ||||
| @@ -0,0 +1,48 @@ | ||||
|  | ||||
| import TimelineOptions from "./TimelineOptions.vue"; | ||||
| import Vue from 'vue'; | ||||
|  | ||||
| export default function TimelineInspectorViewProvider(openmct) { | ||||
|     return { | ||||
|         key: 'timestrip-inspector', | ||||
|         name: 'Time Strip Inspector View', | ||||
|         canView: function (selection) { | ||||
|             if (selection.length === 0 || selection[0].length === 0) { | ||||
|                 return false; | ||||
|             } | ||||
|  | ||||
|             let object = selection[0][0].context.item; | ||||
|  | ||||
|             return object | ||||
|                 && object.type === 'time-strip'; | ||||
|         }, | ||||
|         view: function (selection) { | ||||
|             let component; | ||||
|  | ||||
|             return { | ||||
|                 show: function (element) { | ||||
|                     component = new Vue({ | ||||
|                         el: element, | ||||
|                         components: { | ||||
|                             TimelineOptions | ||||
|                         }, | ||||
|                         provide: { | ||||
|                             openmct, | ||||
|                             domainObject: selection[0][0].context.item | ||||
|                         }, | ||||
|                         template: '<timeline-options></timeline-options>' | ||||
|                     }); | ||||
|                 }, | ||||
|                 destroy: function () { | ||||
|                     if (component) { | ||||
|                         component.$destroy(); | ||||
|                         component = undefined; | ||||
|                     } | ||||
|                 } | ||||
|             }; | ||||
|         }, | ||||
|         priority: function () { | ||||
|             return 1; | ||||
|         } | ||||
|     }; | ||||
| } | ||||
							
								
								
									
										83
									
								
								src/plugins/timeline/inspector/TimelineOptions.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								src/plugins/timeline/inspector/TimelineOptions.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,83 @@ | ||||
| <!-- | ||||
|  Open MCT, Copyright (c) 2014-2020, United States Government | ||||
|  as represented by the Administrator of the National Aeronautics and Space | ||||
|  Administration. All rights reserved. | ||||
|  | ||||
|  Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  "License"); you may not use this file except in compliance with the License. | ||||
|  You may obtain a copy of the License at | ||||
|  http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  | ||||
|  Unless required by applicable law or agreed to in writing, software | ||||
|  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  License for the specific language governing permissions and limitations | ||||
|  under the License. | ||||
|  | ||||
|  Open MCT includes source code licensed under additional open source | ||||
|  licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  this source code distribution or the Licensing information page available | ||||
|  at runtime from the About dialog for additional information. | ||||
| --> | ||||
| <template> | ||||
| <div> | ||||
|     <ul v-if="canEdit" | ||||
|         class="grid-properties" | ||||
|     > | ||||
|         <li class="grid-row"> | ||||
|             <div class="grid-cell label" | ||||
|                  title="Use Independent time" | ||||
|             >Use Independent Time</div> | ||||
|             <div class="grid-cell value"> | ||||
|                 <input v-model="useIndependentTime" | ||||
|                        type="checkbox" | ||||
|                        @change="updateTimeOption" | ||||
|                 > | ||||
|             </div> | ||||
|         </li> | ||||
|     </ul> | ||||
|     <ul v-else | ||||
|         class="grid-properties" | ||||
|     > | ||||
|         <li class="grid-row"> | ||||
|             <div class="grid-cell label" | ||||
|                  title="Use Independent Time." | ||||
|             >Use Independent Time</div> | ||||
|             <div class="grid-cell value"> | ||||
|                 {{ useIndependentTime ? "Enabled" : "Disabled" }} | ||||
|             </div> | ||||
|         </li> | ||||
|     </ul> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| export default { | ||||
|     inject: ['openmct', 'domainObject'], | ||||
|     data() { | ||||
|         return { | ||||
|             useIndependentTime: this.domainObject.configuration && this.domainObject.configuration.useIndependentTime, | ||||
|             isEditing: this.openmct.editor.isEditing() | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
|         canEdit() { | ||||
|             return this.isEditing && !this.domainObject.locked; | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.openmct.editor.on('isEditing', this.setEditState); | ||||
|     }, | ||||
|     beforeDestroy() { | ||||
|         this.openmct.editor.off('isEditing', this.setEditState); | ||||
|     }, | ||||
|     methods: { | ||||
|         setEditState(isEditing) { | ||||
|             this.isEditing = isEditing; | ||||
|         }, | ||||
|         updateTimeOption() { | ||||
|             this.openmct.objects.mutate(this.domainObject, 'configuration.useIndependentTime', this.useIndependentTime); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| </script> | ||||
| @@ -20,7 +20,9 @@ | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| import TimelineViewProvider from '../timeline/TimelineViewProvider'; | ||||
| import TimelineViewProvider from './TimelineViewProvider'; | ||||
| import TimelineInspectorViewProvider from "./inspector/TimelineInspectorViewProvider"; | ||||
| import timelineInterceptor from "./timelineInterceptor"; | ||||
|  | ||||
| export default function () { | ||||
|     return function install(openmct) { | ||||
| @@ -32,9 +34,14 @@ export default function () { | ||||
|             cssClass: 'icon-timeline', | ||||
|             initialize: function (domainObject) { | ||||
|                 domainObject.composition = []; | ||||
|                 domainObject.configuration = { | ||||
|  | ||||
|                 }; | ||||
|             } | ||||
|         }); | ||||
|         timelineInterceptor(openmct); | ||||
|         openmct.objectViews.addProvider(new TimelineViewProvider(openmct)); | ||||
|         openmct.inspectorViews.addProvider(new TimelineInspectorViewProvider(openmct)); | ||||
|     }; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -24,7 +24,7 @@ import { createOpenMct, resetApplicationState } from "utils/testing"; | ||||
| import TimelinePlugin from "./plugin"; | ||||
| import Vue from 'vue'; | ||||
|  | ||||
| describe('the plugin', function () { | ||||
| xdescribe('the plugin', function () { | ||||
|     let objectDef; | ||||
|     let element; | ||||
|     let child; | ||||
| @@ -96,10 +96,15 @@ describe('the plugin', function () { | ||||
|  | ||||
|     describe('the view', () => { | ||||
|         let timelineView; | ||||
|         let testViewObject; | ||||
|  | ||||
|         beforeEach(() => { | ||||
|             const testViewObject = { | ||||
|             testViewObject = { | ||||
|                 id: "test-object", | ||||
|                 identifier: { | ||||
|                     key: "test-object", | ||||
|                     namespace: '' | ||||
|                 }, | ||||
|                 type: "time-strip" | ||||
|             }; | ||||
|  | ||||
| @@ -119,6 +124,102 @@ describe('the plugin', function () { | ||||
|             const el = element.querySelector('.c-timesystem-axis'); | ||||
|             expect(el).toBeDefined(); | ||||
|         }); | ||||
|  | ||||
|         it('does not show the independent time conductor based on configuration', () => { | ||||
|             const independentTimeConductorEl = element.querySelector('.c-timeline-holder > .c-conductor-holder--compact'); | ||||
|             expect(independentTimeConductorEl).toBeNull(); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe('the independent time conductor', () => { | ||||
|         let timelineView; | ||||
|         let testViewObject = { | ||||
|             id: "test-object", | ||||
|             identifier: { | ||||
|                 key: "test-object", | ||||
|                 namespace: '' | ||||
|             }, | ||||
|             type: "time-strip", | ||||
|             configuration: { | ||||
|                 useIndependentTime: true, | ||||
|                 timeOptions: { | ||||
|                     mode: 'local', | ||||
|                     fixedOffsets: { | ||||
|                         start: 10, | ||||
|                         end: 11 | ||||
|                     }, | ||||
|                     clockOffsets: { | ||||
|                         start: -(30 * 60 * 1000), | ||||
|                         end: (30 * 60 * 1000) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         beforeEach(done => { | ||||
|             const applicableViews = openmct.objectViews.get(testViewObject, mockObjectPath); | ||||
|             timelineView = applicableViews.find((viewProvider) => viewProvider.key === 'time-strip.view'); | ||||
|             let view = timelineView.view(testViewObject, element); | ||||
|             view.show(child, true); | ||||
|  | ||||
|             Vue.nextTick(done); | ||||
|         }); | ||||
|  | ||||
|         it('displays an independent time conductor with saved options - local clock', () => { | ||||
|  | ||||
|             return Vue.nextTick(() => { | ||||
|                 const independentTimeConductorEl = element.querySelector('.c-timeline-holder > .c-conductor-holder--compact'); | ||||
|                 expect(independentTimeConductorEl).toBeDefined(); | ||||
|  | ||||
|                 const independentTime = openmct.time.getIndependentTime(testViewObject.identifier.key); | ||||
|                 expect(independentTime).toEqual(testViewObject.configuration.timeOptions.clockOffsets); | ||||
|             }); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe('the independent time conductor', () => { | ||||
|         let timelineView; | ||||
|         let testViewObject2 = { | ||||
|             id: "test-object2", | ||||
|             identifier: { | ||||
|                 key: "test-object2", | ||||
|                 namespace: '' | ||||
|             }, | ||||
|             type: "time-strip", | ||||
|             configuration: { | ||||
|                 useIndependentTime: true, | ||||
|                 timeOptions: { | ||||
|                     mode: 'fixed', | ||||
|                     fixedOffsets: { | ||||
|                         start: 10, | ||||
|                         end: 11 | ||||
|                     }, | ||||
|                     clockOffsets: { | ||||
|                         start: -(30 * 60 * 1000), | ||||
|                         end: (30 * 60 * 1000) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         beforeEach((done) => { | ||||
|             const applicableViews = openmct.objectViews.get(testViewObject2, mockObjectPath); | ||||
|             timelineView = applicableViews.find((viewProvider) => viewProvider.key === 'time-strip.view'); | ||||
|             let view = timelineView.view(testViewObject2, element); | ||||
|             view.show(child, true); | ||||
|  | ||||
|             Vue.nextTick(done); | ||||
|         }); | ||||
|  | ||||
|         it('displays an independent time conductor with saved options - fixed timespan', () => { | ||||
|             return Vue.nextTick(() => { | ||||
|                 const independentTimeConductorEl = element.querySelector('.c-timeline-holder > .c-conductor-holder--compact'); | ||||
|                 expect(independentTimeConductorEl).toBeDefined(); | ||||
|  | ||||
|                 const independentTime = openmct.time.getIndependentTime(testViewObject2.identifier.key); | ||||
|                 expect(independentTime).toEqual(testViewObject2.configuration.timeOptions.fixedOffsets); | ||||
|             }); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
| }); | ||||
|   | ||||
| @@ -1,4 +1,10 @@ | ||||
| .c-timeline-holder { | ||||
|     @include abs(); | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     overflow-x: hidden; | ||||
| } | ||||
|  | ||||
|     > * + * { | ||||
|         margin-top: $interiorMargin; | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										38
									
								
								src/plugins/timeline/timelineInterceptor.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								src/plugins/timeline/timelineInterceptor.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2021, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| export default function timelineInterceptor(openmct) { | ||||
|  | ||||
|     openmct.objects.addGetInterceptor({ | ||||
|         appliesTo: (identifier, domainObject) => { | ||||
|             return domainObject && domainObject.type === 'time-strip'; | ||||
|         }, | ||||
|         invoke: (identifier, object) => { | ||||
|  | ||||
|             if (object && object.configuration === undefined) { | ||||
|                 object.configuration = {}; | ||||
|             } | ||||
|  | ||||
|             return object; | ||||
|         } | ||||
|     }); | ||||
| } | ||||
| @@ -582,6 +582,12 @@ | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     &[class*='--menus-bottom'] { | ||||
|         .c-menu { | ||||
|             top: auto; bottom: 100%; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     &[class*='--menus-left'], | ||||
|     &[class*='menus-to-left'] { | ||||
|         .c-menu { | ||||
|   | ||||
| @@ -247,7 +247,13 @@ | ||||
|  | ||||
|     &__time-conductor { | ||||
|         border-top: 1px solid $colorInteriorBorder; | ||||
|         display: flex; | ||||
|         flex-direction: column; | ||||
|         padding-top: $interiorMargin; | ||||
|  | ||||
|         > * + * { | ||||
|             margin-top: $interiorMargin; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     &__main { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user