Compare commits
	
		
			33 Commits
		
	
	
		
			fix-missin
			...
			independen
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | a79da2104a | ||
|   | 8ddddb0624 | ||
|   | 8bc35dea2e | ||
|   | a4a244f5e6 | ||
|   | f590ac2a34 | ||
|   | 042d6edc12 | ||
|   | 5a5b2b14f2 | ||
|   | 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; | ||||
							
								
								
									
										155
									
								
								src/api/time/IndependentTimeContext.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										155
									
								
								src/api/time/IndependentTimeContext.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 TimeContext from "./TimeContext"; | ||||
|  | ||||
| class IndependentTimeContext extends TimeContext { | ||||
|     constructor(globalTimeContext) { | ||||
|         super(); | ||||
|  | ||||
|         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) { | ||||
|             if (newBounds === undefined) { | ||||
|                 //clear the independent time context | ||||
|                 this.boundsVal = { | ||||
|                     start: undefined, | ||||
|                     end: undefined | ||||
|                 }; | ||||
|             } else { | ||||
|                 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 (this.boundsVal.start === undefined && this.boundsVal.end === undefined) { | ||||
|             return this.globalTimeContext.bounds(); | ||||
|         } else { | ||||
|             //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"; | ||||
|         } | ||||
|  | ||||
|         if (this.activeClock === undefined) { | ||||
|             return this.globalTimeContext.clock(); | ||||
|         } else { | ||||
|             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,36 @@ | ||||
|  * 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(); | ||||
|         this.viewSpecificContexts = new WeakMap(); | ||||
|     } | ||||
|  | ||||
|     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 +79,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 +111,107 @@ 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; | ||||
|  | ||||
|             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; | ||||
|     }; | ||||
|         return timeContext; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 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. | ||||
|      * 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 { ViewRegistry } view The view | ||||
|      * @memberof module:openmct.TimeAPI# | ||||
|      * @method getContextForView | ||||
|      */ | ||||
|     TimeAPI.prototype.stopClock = function () { | ||||
|         if (this.activeClock) { | ||||
|             this.clock(undefined, undefined); | ||||
|     getViewContext(view, objectPath) { | ||||
|         if (!view) { | ||||
|             return (this); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     return TimeAPI; | ||||
| }); | ||||
|         //How to deal with composition views? We need to traverse up the view chain to get to time contexts | ||||
|         if (this.viewSpecificContexts.has(view)) { | ||||
|             return this.viewSpecificContexts.get(view); | ||||
|         } else { | ||||
|             const viewSpecificContext = this.createContext(); | ||||
|             this.viewSpecificContexts.set(view, viewSpecificContext); | ||||
|  | ||||
|             return viewSpecificContext; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     createContext() { | ||||
|         return new IndependentTimeContext(this); //Pass in the global time context so it can react to events from it. | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| 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,12 +157,17 @@ export default { | ||||
|         getPlanData(domainObject) { | ||||
|             this.planData = getValidatedPlan(domainObject); | ||||
|         }, | ||||
|         updateViewBounds() { | ||||
|             this.viewBounds = this.openmct.time.bounds(); | ||||
|             //Add a 50% padding to the end bounds to look ahead | ||||
|             let timespan = (this.viewBounds.end - this.viewBounds.start); | ||||
|             let padding = timespan / 2; | ||||
|             this.viewBounds.end = this.viewBounds.end + padding; | ||||
|         updateViewBounds(bounds) { | ||||
|             if (bounds) { | ||||
|                 this.viewBounds = Object.create(bounds); | ||||
|  | ||||
|                 if (!this.options.compact) { | ||||
|                     //Add a 50% padding to the end bounds to look ahead | ||||
|                     let timespan = (this.viewBounds.end - this.viewBounds.start); | ||||
|                     let padding = timespan / 2; | ||||
|                     this.viewBounds.end = this.viewBounds.end + padding; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             if (this.timeSystem === undefined) { | ||||
|                 this.timeSystem = this.openmct.time.timeSystem(); | ||||
|   | ||||
| @@ -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); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|   | ||||
							
								
								
									
										277
									
								
								src/plugins/timeConductor/ConductorInputsFixed.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										277
									
								
								src/plugins/timeConductor/ConductorInputsFixed.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,277 @@ | ||||
| <template> | ||||
| <form ref="fixedDeltaInput" | ||||
|       class="c-conductor__inputs" | ||||
|       @submit.prevent="updateTimeFromConductor" | ||||
| > | ||||
|     <button | ||||
|         ref="submitButton" | ||||
|         class="c-input--submit" | ||||
|         type="submit" | ||||
|     ></button> | ||||
|     <div | ||||
|         class="c-ctrl-wrapper c-conductor-input c-conductor__start-fixed" | ||||
|     > | ||||
|         <!-- Fixed start --> | ||||
|         <div class="c-conductor__start-fixed__label"> | ||||
|             Start | ||||
|         </div> | ||||
|         <input | ||||
|             ref="startDate" | ||||
|             v-model="formattedBounds.start" | ||||
|             class="c-input--datetime" | ||||
|             type="text" | ||||
|             autocorrect="off" | ||||
|             spellcheck="false" | ||||
|             @change="validateAllBounds('startDate'); submitForm()" | ||||
|         > | ||||
|         <date-picker | ||||
|             v-if="isUTCBased" | ||||
|             class="c-ctrl-wrapper--menus-left" | ||||
|             :bottom="viewObject !== 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="viewObject !== 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: { | ||||
|         viewObject: { | ||||
|             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 | ||||
|         }; | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.handleNewBounds = _.throttle(this.handleNewBounds, 300); | ||||
|         this.setTimeSystem(JSON.parse(JSON.stringify(this.openmct.time.timeSystem()))); | ||||
|         this.openmct.time.on('timeSystem', this.setTimeSystem); | ||||
|         this.setTimeContext(); | ||||
|     }, | ||||
|     beforeDestroy() { | ||||
|         this.openmct.time.off('timeSystem', this.setTimeSystem); | ||||
|         this.stopFollowingTimeContext(); | ||||
|     }, | ||||
|     methods: { | ||||
|         setTimeContext() { | ||||
|             this.stopFollowingTimeContext(); | ||||
|             this.timeContext = this.openmct.time.getViewContext(this.viewObject); | ||||
|  | ||||
|             this.handleNewBounds(this.timeContext.bounds()); | ||||
|             this.timeContext.on('bounds', this.handleNewBounds); | ||||
|             this.timeContext.on('clock', this.clearAllValidation); | ||||
|         }, | ||||
|         stopFollowingTimeContext() { | ||||
|             if (this.timeContext) { | ||||
|                 this.timeContext.off('bounds', this.handleNewBounds); | ||||
|                 this.timeContext.off('clock', this.clearAllValidation); | ||||
|             } | ||||
|         }, | ||||
|         handleNewBounds(bounds) { | ||||
|             this.setBounds(bounds); | ||||
|             this.setViewFromBounds(bounds); | ||||
|         }, | ||||
|         clearAllValidation() { | ||||
|             [this.$refs.startDate, this.$refs.endDate].forEach(this.clearValidationForInput); | ||||
|         }, | ||||
|         clearValidationForInput(input) { | ||||
|             input.setCustomValidity(''); | ||||
|             input.title = ''; | ||||
|         }, | ||||
|         setBounds(bounds) { | ||||
|             this.bounds = bounds; | ||||
|         }, | ||||
|         setViewFromBounds(bounds) { | ||||
|             this.formattedBounds.start = this.timeFormatter.format(bounds.start); | ||||
|             this.formattedBounds.end = this.timeFormatter.format(bounds.end); | ||||
|         }, | ||||
|         setTimeSystem(timeSystem) { | ||||
|             this.timeSystem = timeSystem; | ||||
|             this.timeFormatter = this.getFormatter(timeSystem.timeFormat); | ||||
|             this.durationFormatter = this.getFormatter( | ||||
|                 timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER); | ||||
|             this.isUTCBased = timeSystem.isUTCBased; | ||||
|         }, | ||||
|         getFormatter(key) { | ||||
|             return this.openmct.telemetry.getValueFormatter({ | ||||
|                 format: key | ||||
|             }).formatter; | ||||
|         }, | ||||
|         setBoundsFromView($event) { | ||||
|             if (this.$refs.fixedDeltaInput.checkValidity()) { | ||||
|                 let start = this.timeFormatter.parse(this.formattedBounds.start); | ||||
|                 let end = this.timeFormatter.parse(this.formattedBounds.end); | ||||
|  | ||||
|                 this.$emit('updated', { | ||||
|                     start: start, | ||||
|                     end: end | ||||
|                 }); | ||||
|             } | ||||
|  | ||||
|             if ($event) { | ||||
|                 $event.preventDefault(); | ||||
|  | ||||
|                 return false; | ||||
|             } | ||||
|         }, | ||||
|         submitForm() { | ||||
|         // Allow Vue model to catch up to user input. | ||||
|         // Submitting form will cause validation messages to display (but only if triggered by button click) | ||||
|             this.$nextTick(() => this.$refs.submitButton.click()); | ||||
|         }, | ||||
|         updateTimeFromConductor() { | ||||
|             this.setBoundsFromView(); | ||||
|         }, | ||||
|         validateAllBounds(ref) { | ||||
|             if (!this.areBoundsFormatsValid()) { | ||||
|                 return false; | ||||
|             } | ||||
|  | ||||
|             let validationResult = { | ||||
|                 valid: true | ||||
|             }; | ||||
|             const currentInput = this.$refs[ref]; | ||||
|  | ||||
|             return [this.$refs.startDate, this.$refs.endDate].every((input) => { | ||||
|                 let boundsValues = { | ||||
|                     start: this.timeFormatter.parse(this.formattedBounds.start), | ||||
|                     end: this.timeFormatter.parse(this.formattedBounds.end) | ||||
|                 }; | ||||
|                 //TODO: Do we need limits here? We have conductor limits disabled right now | ||||
|                 // const limit = this.getBoundsLimit(); | ||||
|                 const limit = false; | ||||
|  | ||||
|                 if (this.timeSystem.isUTCBased && limit | ||||
|                     && boundsValues.end - boundsValues.start > limit) { | ||||
|                     if (input === currentInput) { | ||||
|                         validationResult = { | ||||
|                             valid: false, | ||||
|                             message: "Start and end difference exceeds allowable limit" | ||||
|                         }; | ||||
|                     } | ||||
|                 } else { | ||||
|                     if (input === currentInput) { | ||||
|                         validationResult = this.openmct.time.validateBounds(boundsValues); | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 return this.handleValidationResults(input, validationResult); | ||||
|             }); | ||||
|         }, | ||||
|         areBoundsFormatsValid() { | ||||
|             let validationResult = { | ||||
|                 valid: true | ||||
|             }; | ||||
|  | ||||
|             return [this.$refs.startDate, this.$refs.endDate].every((input) => { | ||||
|                 const formattedDate = input === this.$refs.startDate | ||||
|                     ? this.formattedBounds.start | ||||
|                     : this.formattedBounds.end | ||||
|           ; | ||||
|  | ||||
|                 if (!this.timeFormatter.validate(formattedDate)) { | ||||
|                     validationResult = { | ||||
|                         valid: false, | ||||
|                         message: 'Invalid date' | ||||
|                     }; | ||||
|                 } | ||||
|  | ||||
|                 return this.handleValidationResults(input, validationResult); | ||||
|             }); | ||||
|         }, | ||||
|         getBoundsLimit() { | ||||
|             const configuration = this.configuration.menuOptions | ||||
|                 .filter(option => option.timeSystem === this.timeSystem.key) | ||||
|                 .find(option => option.limit); | ||||
|  | ||||
|             const limit = configuration ? configuration.limit : undefined; | ||||
|  | ||||
|             return limit; | ||||
|         }, | ||||
|         handleValidationResults(input, validationResult) { | ||||
|             if (validationResult.valid !== true) { | ||||
|                 input.setCustomValidity(validationResult.message); | ||||
|                 input.title = validationResult.message; | ||||
|             } else { | ||||
|                 input.setCustomValidity(''); | ||||
|                 input.title = ''; | ||||
|             } | ||||
|  | ||||
|             return validationResult.valid; | ||||
|         }, | ||||
|         startDateSelected(date) { | ||||
|             this.formattedBounds.start = this.timeFormatter.format(date); | ||||
|             this.validateAllBounds('startDate'); | ||||
|             this.submitForm(); | ||||
|         }, | ||||
|         endDateSelected(date) { | ||||
|             this.formattedBounds.end = this.timeFormatter.format(date); | ||||
|             this.validateAllBounds('endDate'); | ||||
|             this.submitForm(); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| </script> | ||||
							
								
								
									
										267
									
								
								src/plugins/timeConductor/ConductorInputsRealtime.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										267
									
								
								src/plugins/timeConductor/ConductorInputsRealtime.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,267 @@ | ||||
| <template> | ||||
| <form ref="deltaInput" | ||||
|       class="c-conductor__inputs" | ||||
| > | ||||
|     <div | ||||
|         class="c-ctrl-wrapper c-conductor-input c-conductor__start-delta" | ||||
|     > | ||||
|         <!-- RT start --> | ||||
|         <div class="c-direction-indicator icon-minus"></div> | ||||
|         <time-popup | ||||
|             v-if="showTCInputStart" | ||||
|             class="pr-tc-input-menu--start" | ||||
|             :bottom="viewObject !== 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="viewObject !== 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: { | ||||
|         viewObject: { | ||||
|             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 | ||||
|         }; | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.handleNewBounds = _.throttle(this.handleNewBounds, 300); | ||||
|         this.setTimeSystem(JSON.parse(JSON.stringify(this.openmct.time.timeSystem()))); | ||||
|         this.openmct.time.on('timeSystem', this.setTimeSystem); | ||||
|         this.setTimeContext(); | ||||
|     }, | ||||
|     beforeDestroy() { | ||||
|         this.openmct.time.off('timeSystem', this.setTimeSystem); | ||||
|         this.stopFollowingTime(); | ||||
|     }, | ||||
|     methods: { | ||||
|         followTime() { | ||||
|             this.handleNewBounds(this.timeContext.bounds()); | ||||
|             this.setViewFromOffsets(this.timeContext.clockOffsets()); | ||||
|             this.timeContext.on('bounds', this.handleNewBounds); | ||||
|             this.timeContext.on('clock', this.clearAllValidation); | ||||
|             this.timeContext.on('clockOffsets', this.setViewFromOffsets); | ||||
|         }, | ||||
|         stopFollowingTime() { | ||||
|             if (this.timeContext) { | ||||
|                 this.timeContext.off('bounds', this.handleNewBounds); | ||||
|                 this.timeContext.off('clock', this.clearAllValidation); | ||||
|                 this.timeContext.off('clockOffsets', this.setViewFromOffsets); | ||||
|             } | ||||
|         }, | ||||
|         setTimeContext() { | ||||
|             this.stopFollowingTime(); | ||||
|             this.timeContext = this.openmct.time.getViewContext(this.viewObject); | ||||
|             this.followTime(); | ||||
|         }, | ||||
|         handleNewBounds(bounds) { | ||||
|             this.setBounds(bounds); | ||||
|             this.setViewFromBounds(bounds); | ||||
|         }, | ||||
|         clearAllValidation() { | ||||
|             [this.$refs.startOffset, this.$refs.endOffset].forEach(this.clearValidationForInput); | ||||
|         }, | ||||
|         clearValidationForInput(input) { | ||||
|             input.setCustomValidity(''); | ||||
|             input.title = ''; | ||||
|         }, | ||||
|         setViewFromOffsets(offsets) { | ||||
|             if (offsets) { | ||||
|                 this.offsets.start = this.durationFormatter.format(Math.abs(offsets.start)); | ||||
|                 this.offsets.end = this.durationFormatter.format(Math.abs(offsets.end)); | ||||
|             } | ||||
|         }, | ||||
|         setBounds(bounds) { | ||||
|             this.bounds = bounds; | ||||
|         }, | ||||
|         setViewFromBounds(bounds) { | ||||
|             this.formattedBounds.start = this.timeFormatter.format(bounds.start); | ||||
|             this.formattedBounds.end = this.timeFormatter.format(bounds.end); | ||||
|         }, | ||||
|         setTimeSystem(timeSystem) { | ||||
|             this.timeSystem = timeSystem; | ||||
|             this.timeFormatter = this.getFormatter(timeSystem.timeFormat); | ||||
|             this.durationFormatter = this.getFormatter( | ||||
|                 timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER); | ||||
|             this.isUTCBased = timeSystem.isUTCBased; | ||||
|         }, | ||||
|         getFormatter(key) { | ||||
|             return this.openmct.telemetry.getValueFormatter({ | ||||
|                 format: key | ||||
|             }).formatter; | ||||
|         }, | ||||
|         hideAllTimePopups() { | ||||
|             this.showTCInputStart = false; | ||||
|             this.showTCInputEnd = false; | ||||
|         }, | ||||
|         showTimePopupStart() { | ||||
|             this.hideAllTimePopups(); | ||||
|             this.showTCInputStart = !this.showTCInputStart; | ||||
|         }, | ||||
|         showTimePopupEnd() { | ||||
|             this.hideAllTimePopups(); | ||||
|             this.showTCInputEnd = !this.showTCInputEnd; | ||||
|         }, | ||||
|         timePopUpdate({ type, hours, minutes, seconds }) { | ||||
|             this.offsets[type] = [hours, minutes, seconds].join(':'); | ||||
|             this.setOffsetsFromView(); | ||||
|             this.hideAllTimePopups(); | ||||
|         }, | ||||
|         setOffsetsFromView($event) { | ||||
|             if (this.$refs.deltaInput.checkValidity()) { | ||||
|                 let startOffset = 0 - this.durationFormatter.parse(this.offsets.start); | ||||
|                 let endOffset = this.durationFormatter.parse(this.offsets.end); | ||||
|  | ||||
|                 this.$emit('updated', { | ||||
|                     start: startOffset, | ||||
|                     end: endOffset | ||||
|                 }); | ||||
|             } | ||||
|  | ||||
|             if ($event) { | ||||
|                 $event.preventDefault(); | ||||
|  | ||||
|                 return false; | ||||
|             } | ||||
|         }, | ||||
|         validateAllBounds(ref) { | ||||
|             if (!this.areBoundsFormatsValid()) { | ||||
|                 return false; | ||||
|             } | ||||
|  | ||||
|             let validationResult = { | ||||
|                 valid: true | ||||
|             }; | ||||
|             const currentInput = this.$refs[ref]; | ||||
|  | ||||
|             return [this.$refs.startDate, this.$refs.endDate].every((input) => { | ||||
|                 let boundsValues = { | ||||
|                     start: this.timeFormatter.parse(this.formattedBounds.start), | ||||
|                     end: this.timeFormatter.parse(this.formattedBounds.end) | ||||
|                 }; | ||||
|                 //TODO: Do we need limits here? We have conductor limits disabled right now | ||||
|                 // const limit = this.getBoundsLimit(); | ||||
|                 const limit = false; | ||||
|  | ||||
|                 if (this.timeSystem.isUTCBased && limit | ||||
|                     && boundsValues.end - boundsValues.start > limit) { | ||||
|                     if (input === currentInput) { | ||||
|                         validationResult = { | ||||
|                             valid: false, | ||||
|                             message: "Start and end difference exceeds allowable limit" | ||||
|                         }; | ||||
|                     } | ||||
|                 } else { | ||||
|                     if (input === currentInput) { | ||||
|                         validationResult = this.openmct.time.validateBounds(boundsValues); | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 return this.handleValidationResults(input, validationResult); | ||||
|             }); | ||||
|         }, | ||||
|         handleValidationResults(input, validationResult) { | ||||
|             if (validationResult.valid !== true) { | ||||
|                 input.setCustomValidity(validationResult.message); | ||||
|                 input.title = validationResult.message; | ||||
|             } else { | ||||
|                 input.setCustomValidity(''); | ||||
|                 input.title = ''; | ||||
|             } | ||||
|  | ||||
|             return validationResult.valid; | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| </script> | ||||
| @@ -22,7 +22,8 @@ | ||||
| <template> | ||||
| <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 () { | ||||
|   | ||||
| @@ -8,6 +8,10 @@ | ||||
|  | ||||
| /*********************************************** CONDUCTOR LAYOUT */ | ||||
| .c-conductor { | ||||
|     &__inputs { | ||||
|         display: contents; | ||||
|     } | ||||
|  | ||||
|     &__time-bounds { | ||||
|         display: grid; | ||||
|         grid-column-gap: $interiorMargin; | ||||
| @@ -50,13 +54,6 @@ | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     [class*='-delta'] { | ||||
|         &:before { | ||||
|             content: $glyph-icon-clock; | ||||
|             font-family: symbolsfont; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     &.is-fixed-mode { | ||||
|         .c-conductor-axis { | ||||
|             &__zoom-indicator { | ||||
| @@ -181,6 +178,27 @@ | ||||
|     } | ||||
| } | ||||
|  | ||||
| .c-conductor-holder--compact { | ||||
|     min-height: 22px; | ||||
|  | ||||
|     .c-conductor { | ||||
|         &__inputs, | ||||
|         &__time-bounds { | ||||
|             display: flex; | ||||
|         } | ||||
|  | ||||
|         &__inputs { | ||||
|             > * + * { | ||||
|                 margin-left: $interiorMarginSm; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     .is-realtime-mode .c-conductor__end-fixed { | ||||
|         display: none !important; | ||||
|     } | ||||
| } | ||||
|  | ||||
| .c-conductor-input { | ||||
|     color: $colorInputFg; | ||||
|     display: flex; | ||||
| @@ -250,18 +268,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,207 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT Web, Copyright (c) 2014-2021, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT Web is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT Web includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
| <template> | ||||
| <div | ||||
|     class="c-conductor" | ||||
|     :class="[ | ||||
|         isFixed ? 'is-fixed-mode' : independentTCEnabled ? 'is-realtime-mode' : 'is-fixed-mode' | ||||
|     ]" | ||||
| > | ||||
|     <div class="c-conductor__time-bounds"> | ||||
|         <toggle-switch | ||||
|             id="independentTCToggle" | ||||
|             :checked="independentTCEnabled" | ||||
|             :title="`${independentTCEnabled ? 'Disable' : 'Enable'} independent Time Conductor`" | ||||
|             @change="toggleIndependentTC" | ||||
|         /> | ||||
|  | ||||
|         <ConductorModeIcon /> | ||||
|  | ||||
|         <div v-if="timeOptions && independentTCEnabled && viewObject" | ||||
|              class="c-conductor__controls" | ||||
|         > | ||||
|             <Mode v-if="mode" | ||||
|                   class="c-conductor__mode-select" | ||||
|                   :domain-object="domainObject" | ||||
|                   :key-string="domainObject.identifier.key" | ||||
|                   :mode="timeOptions.mode" | ||||
|                   @modeChanged="saveMode" | ||||
|             /> | ||||
|  | ||||
|             <conductor-inputs-fixed v-if="isFixed" | ||||
|                                     :view-object="viewObject" | ||||
|                                     @updated="saveFixedOffsets" | ||||
|             /> | ||||
|  | ||||
|             <conductor-inputs-realtime v-else | ||||
|                                        :view-object="viewObject" | ||||
|                                        @updated="saveClockOffsets" | ||||
|             /> | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import ConductorInputsFixed from "../ConductorInputsFixed.vue"; | ||||
| import ConductorInputsRealtime from "../ConductorInputsRealtime.vue"; | ||||
| import ConductorModeIcon from "@/plugins/timeConductor/ConductorModeIcon.vue"; | ||||
| import ToggleSwitch from '../../../ui/components/ToggleSwitch.vue'; | ||||
| import Mode from "./Mode.vue"; | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
|         Mode, | ||||
|         ConductorModeIcon, | ||||
|         ConductorInputsRealtime, | ||||
|         ConductorInputsFixed, | ||||
|         ToggleSwitch | ||||
|     }, | ||||
|     inject: ['openmct'], | ||||
|     props: { | ||||
|         domainObject: { | ||||
|             type: Object, | ||||
|             required: true | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             timeOptions: this.domainObject.configuration.timeOptions || { | ||||
|                 clockOffsets: this.openmct.time.clockOffsets(), | ||||
|                 fixedOffsets: this.openmct.time.bounds() | ||||
|             }, | ||||
|             mode: undefined, | ||||
|             independentTCEnabled: this.domainObject.configuration.useIndependentTime === true, | ||||
|             viewObject: this.$parent.currentView | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
|         isFixed() { | ||||
|             if (!this.mode || !this.mode.key) { | ||||
|                 return this.openmct.time.clock() === undefined; | ||||
|             } else { | ||||
|                 return this.mode.key === 'fixed'; | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.setTimeContext(); | ||||
|  | ||||
|         if (this.timeOptions.mode) { | ||||
|             this.mode = this.timeOptions.mode; | ||||
|         } else { | ||||
|             this.mode = this.timeContext.clock() === undefined ? { key: 'fixed' } : { key: this.timeContext.clock().key}; | ||||
|         } | ||||
|  | ||||
|         this.registerIndependentTimeOffsets(); | ||||
|     }, | ||||
|     beforeDestroy() { | ||||
|         this.timeContext.off('clock', this.setViewFromClock); | ||||
|     }, | ||||
|     methods: { | ||||
|         toggleIndependentTC() { | ||||
|             this.independentTCEnabled = !this.independentTCEnabled; | ||||
|             if (this.independentTCEnabled) { | ||||
|                 this.saveMode(this.mode); | ||||
|             } else { | ||||
|                 this.clearIndependentTimeOffsets(); | ||||
|             } | ||||
|  | ||||
|             this.$emit('stateChanged', this.independentTCEnabled); | ||||
|         }, | ||||
|         setTimeContext() { | ||||
|             this.timeContext = this.openmct.time.getViewContext(this.viewObject); | ||||
|             this.timeContext.on('clock', this.setViewFromClock); | ||||
|         }, | ||||
|         setViewFromClock(clock) { | ||||
|             if (!this.timeOptions.mode) { | ||||
|                 this.setTimeOptions(clock); | ||||
|             } | ||||
|         }, | ||||
|         setTimeOptions() { | ||||
|             if (!this.timeOptions || !this.timeOptions.mode) { | ||||
|                 this.mode = this.timeContext.clock() === undefined ? { key: 'fixed' } : { key: this.timeContext.clock().key}; | ||||
|                 this.timeOptions = { | ||||
|                     clockOffsets: this.timeContext.clockOffsets(), | ||||
|                     fixedOffsets: this.timeContext.bounds() | ||||
|                 }; | ||||
|             } | ||||
|  | ||||
|             this.registerIndependentTimeOffsets(); | ||||
|         }, | ||||
|         saveFixedOffsets(offsets) { | ||||
|             const newOptions = Object.assign({}, this.timeOptions, { | ||||
|                 fixedOffsets: offsets | ||||
|             }); | ||||
|  | ||||
|             this.updateTimeOptions(newOptions); | ||||
|         }, | ||||
|         saveClockOffsets(offsets) { | ||||
|             const newOptions = Object.assign({}, this.timeOptions, { | ||||
|                 clockOffsets: offsets | ||||
|             }); | ||||
|  | ||||
|             this.updateTimeOptions(newOptions); | ||||
|         }, | ||||
|         saveMode(mode) { | ||||
|             this.mode = mode; | ||||
|             const newOptions = Object.assign({}, this.timeOptions, { | ||||
|                 mode: this.mode | ||||
|             }); | ||||
|             this.updateTimeOptions(newOptions); | ||||
|         }, | ||||
|         updateTimeOptions(options) { | ||||
|             this.timeOptions = options; | ||||
|             if (!this.timeOptions.mode) { | ||||
|                 this.timeOptions.mode = this.mode; | ||||
|             } | ||||
|  | ||||
|             this.registerIndependentTimeOffsets(); | ||||
|             this.$emit('updated', this.timeOptions); | ||||
|         }, | ||||
|         registerIndependentTimeOffsets() { | ||||
|             if (!this.timeOptions.mode) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             if (this.isFixed) { | ||||
|                 this.timeContext.stopClock(); | ||||
|                 this.timeContext.bounds(this.timeOptions.fixedOffsets); | ||||
|             } else { | ||||
|                 this.timeContext.clock(this.mode.key, this.timeOptions.clockOffsets); | ||||
|             } | ||||
|         }, | ||||
|         clearIndependentTimeOffsets() { | ||||
|             if (!this.timeOptions.mode) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             if (this.isFixed) { | ||||
|                 this.timeContext.stopClock(); | ||||
|                 this.timeContext.bounds(undefined); | ||||
|             } else { | ||||
|                 this.timeContext.stopClock(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| </script> | ||||
							
								
								
									
										214
									
								
								src/plugins/timeConductor/independent/Mode.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										214
									
								
								src/plugins/timeConductor/independent/Mode.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,214 @@ | ||||
| /***************************************************************************** | ||||
| * 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 v-if="selectedMode" | ||||
|                 class="c-button--menu c-mode-button" | ||||
|                 @click.prevent.stop="showModesMenu" | ||||
|         > | ||||
|             <span class="c-button__label">{{ selectedMode.name }}</span> | ||||
|         </button> | ||||
|     </div> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import toggleMixin from '../../../ui/mixins/toggle-mixin'; | ||||
|  | ||||
| export default { | ||||
|     mixins: [toggleMixin], | ||||
|     inject: ['openmct'], | ||||
|     props: { | ||||
|         domainObject: { | ||||
|             type: Object, | ||||
|             default() { | ||||
|                 return undefined; | ||||
|             } | ||||
|         }, | ||||
|         mode: { | ||||
|             type: Object, | ||||
|             default() { | ||||
|                 return undefined; | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     data: function () { | ||||
|         let clock; | ||||
|         if (this.mode && this.mode.key === 'fixed') { | ||||
|             clock = undefined; | ||||
|         } else { | ||||
|         //We want the clock from the global time context here | ||||
|             clock = this.openmct.time.clock(); | ||||
|         } | ||||
|  | ||||
|         if (clock !== undefined) { | ||||
|         //Create copy of active clock so the time API does not get reactified. | ||||
|             clock = Object.create(clock); | ||||
|         } | ||||
|  | ||||
|         return { | ||||
|             selectedMode: this.getModeOptionForClock(clock), | ||||
|             modes: [] | ||||
|         }; | ||||
|     }, | ||||
|     watch: { | ||||
|         mode: { | ||||
|             deep: true, | ||||
|             handler(newMode) { | ||||
|                 if (newMode) { | ||||
|                     this.setViewFromClock(newMode.key === 'fixed' ? undefined : newMode); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     mounted: function () { | ||||
|         if (this.mode) { | ||||
|             this.setViewFromClock(this.mode.key === 'fixed' ? undefined : this.mode); | ||||
|         } | ||||
|  | ||||
|         this.followTimeConductor(); | ||||
|     }, | ||||
|     destroyed: function () { | ||||
|         this.stopFollowTimeConductor(); | ||||
|     }, | ||||
|     methods: { | ||||
|         followTimeConductor() { | ||||
|             this.openmct.time.on('clock', this.setViewFromClock); | ||||
|         }, | ||||
|         stopFollowTimeConductor() { | ||||
|             this.openmct.time.off('clock', this.setViewFromClock); | ||||
|         }, | ||||
|         showModesMenu() { | ||||
|             const elementBoundingClientRect = this.$refs.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); | ||||
|  | ||||
|             if (this.mode) { | ||||
|                 this.$emit('modeChanged', { key: clockKey }); | ||||
|             } | ||||
|         }, | ||||
|  | ||||
|         setViewFromClock(clock) { | ||||
|             this.loadClocks(); | ||||
|             //retain the mode chosen by the user | ||||
|             if (this.mode) { | ||||
|                 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(Date.UTC(78, 0, 20, 0, 0, 0)).getTime(); | ||||
|  | ||||
| describe('time conductor', () => { | ||||
|     let element; | ||||
|     let child; | ||||
|     let appHolder; | ||||
|     let openmct; | ||||
|     let config = { | ||||
|         menuOptions: [ | ||||
|             { | ||||
|                 name: "FixedTimeRange", | ||||
|                 timeSystem: 'utc', | ||||
|                 bounds: { | ||||
|                     start: date - THIRTY_MINUTES, | ||||
|                     end: date | ||||
|                 }, | ||||
|                 presets: [], | ||||
|                 records: 2 | ||||
|             }, | ||||
|             { | ||||
|                 name: "LocalClock", | ||||
|                 timeSystem: 'utc', | ||||
|                 clock: 'local', | ||||
|                 clockOffsets: { | ||||
|                     start: -THIRTY_MINUTES, | ||||
|                     end: THIRTY_SECONDS | ||||
|                 }, | ||||
|                 presets: [] | ||||
|             } | ||||
|         ] | ||||
|     }; | ||||
|  | ||||
|     beforeEach((done) => { | ||||
|         openmct = createOpenMct(); | ||||
|         openmct.install(new ConductorPlugin(config)); | ||||
|  | ||||
|         element = document.createElement('div'); | ||||
|         element.style.width = '640px'; | ||||
|         element.style.height = '480px'; | ||||
|         child = document.createElement('div'); | ||||
|         child.style.width = '640px'; | ||||
|         child.style.height = '480px'; | ||||
|         element.appendChild(child); | ||||
|  | ||||
|         openmct.on('start', () => { | ||||
|             openmct.time.bounds({ | ||||
|                 start: config.menuOptions[0].bounds.start, | ||||
|                 end: config.menuOptions[0].bounds.end | ||||
|             }); | ||||
|             Vue.nextTick(() => { | ||||
|                 done(); | ||||
|             }); | ||||
|         }); | ||||
|         appHolder = document.createElement("div"); | ||||
|         openmct.start(appHolder); | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         appHolder = undefined; | ||||
|         openmct = undefined; | ||||
|  | ||||
|         return resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|     it('shows delta inputs in fixed mode', () => { | ||||
|         const fixedModeEl = appHolder.querySelector('.is-fixed-mode'); | ||||
|         const dateTimeInputs = fixedModeEl.querySelectorAll('.c-input--datetime'); | ||||
|         expect(dateTimeInputs[0].value).toEqual('1978-01-19 23:30:00.000Z'); | ||||
|         expect(dateTimeInputs[1].value).toEqual('1978-01-20 00:00:00.000Z'); | ||||
|         expect(fixedModeEl.querySelector('.c-mode-button .c-button__label').innerHTML).toEqual('Fixed Timespan'); | ||||
|     }); | ||||
|  | ||||
|     describe('shows delta inputs in realtime mode', () => { | ||||
|         beforeEach((done) => { | ||||
|             const switcher = appHolder.querySelector('.c-mode-button'); | ||||
|             const clickEvent = createMouseEvent("click"); | ||||
|  | ||||
|             switcher.dispatchEvent(clickEvent); | ||||
|             Vue.nextTick(() => { | ||||
|                 const clockItem = document.querySelectorAll('.c-conductor__mode-menu li')[1]; | ||||
|                 clockItem.dispatchEvent(clickEvent); | ||||
|                 Vue.nextTick(() => { | ||||
|                     done(); | ||||
|                 }); | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         it('shows clock options', () => { | ||||
|             const realtimeModeEl = appHolder.querySelector('.is-realtime-mode'); | ||||
|             const dateTimeInputs = realtimeModeEl.querySelectorAll('.c-conductor__delta-button'); | ||||
|             expect(dateTimeInputs[0].innerHTML.replace(/[^(\d|:)]/g, '')).toEqual('00:30:00'); | ||||
|             expect(dateTimeInputs[1].innerHTML.replace(/[^(\d|:)]/g, '')).toEqual('00:00:30'); | ||||
|             expect(realtimeModeEl.querySelector('.c-mode-button .c-button__label').innerHTML).toEqual('Local Clock'); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
| }); | ||||
| @@ -1,6 +1,7 @@ | ||||
| <template> | ||||
| <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() { | ||||
|   | ||||
| @@ -86,15 +86,16 @@ export default { | ||||
|         return { | ||||
|             items: [], | ||||
|             timeSystems: [], | ||||
|             height: 0 | ||||
|             height: 0, | ||||
|             useIndependentTime: this.domainObject.configuration.useIndependentTime === true, | ||||
|             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(); | ||||
|     }, | ||||
|     mounted() { | ||||
|         if (this.composition) { | ||||
| @@ -104,8 +105,8 @@ export default { | ||||
|             this.composition.load(); | ||||
|         } | ||||
|  | ||||
|         this.setTimeContext(); | ||||
|         this.getTimeSystems(); | ||||
|         this.openmct.time.on("bounds", this.updateViewBounds); | ||||
|     }, | ||||
|     methods: { | ||||
|         addItem(domainObject) { | ||||
| @@ -132,8 +133,8 @@ export default { | ||||
|         }, | ||||
|         removeItem(identifier) { | ||||
|             let index = this.items.findIndex(item => this.openmct.objects.areIdsEqual(identifier, item.domainObject.identifier)); | ||||
|             this.removeSelectable(this.items[index]); | ||||
|             this.items.splice(index, 1); | ||||
|             this.updateContentHeight(); | ||||
|         }, | ||||
|         reorder(reorderPlan) { | ||||
|             let oldItems = this.items.slice(); | ||||
| @@ -154,7 +155,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 +165,20 @@ export default { | ||||
|             if (currentTimeSystem) { | ||||
|                 currentTimeSystem.bounds = bounds; | ||||
|             } | ||||
|         }, | ||||
|         setTimeContext() { | ||||
|             this.stopFollowingTimeContext(); | ||||
|  | ||||
|             this.timeContext = this.openmct.time.getContextForView(this.objectPath); | ||||
|             this.timeContext.on('timeContext', this.setTimeContext); | ||||
|             this.updateViewBounds(this.timeContext.bounds()); | ||||
|             this.timeContext.on('bounds', this.updateViewBounds); | ||||
|         }, | ||||
|         stopFollowingTimeContext() { | ||||
|             if (this.timeContext) { | ||||
|                 this.timeContext.off('bounds', this.updateViewBounds); | ||||
|                 this.timeContext.off('timeContext', this.setTimeContext); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|   | ||||
| @@ -20,7 +20,8 @@ | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| import TimelineViewProvider from '../timeline/TimelineViewProvider'; | ||||
| import TimelineViewProvider from './TimelineViewProvider'; | ||||
| import timelineInterceptor from "./timelineInterceptor"; | ||||
|  | ||||
| export default function () { | ||||
|     return function install(openmct) { | ||||
| @@ -32,8 +33,12 @@ export default function () { | ||||
|             cssClass: 'icon-timeline', | ||||
|             initialize: function (domainObject) { | ||||
|                 domainObject.composition = []; | ||||
|                 domainObject.configuration = { | ||||
|                     useIndependentTime: false | ||||
|                 }; | ||||
|             } | ||||
|         }); | ||||
|         timelineInterceptor(openmct); | ||||
|         openmct.objectViews.addProvider(new TimelineViewProvider(openmct)); | ||||
|     }; | ||||
| } | ||||
|   | ||||
| @@ -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,106 @@ 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__controls'); | ||||
|             expect(independentTimeConductorEl).toBeNull(); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe('the independent time conductor', () => { | ||||
|         let timelineView; | ||||
|         let testViewObject = { | ||||
|             id: "test-object", | ||||
|             identifier: { | ||||
|                 key: "test-object", | ||||
|                 namespace: '' | ||||
|             }, | ||||
|             type: "time-strip", | ||||
|             configuration: { | ||||
|                 useIndependentTime: true, | ||||
|                 timeOptions: { | ||||
|                     mode: { | ||||
|                         key: 'local' | ||||
|                     }, | ||||
|                     fixedOffsets: { | ||||
|                         start: 10, | ||||
|                         end: 11 | ||||
|                     }, | ||||
|                     clockOffsets: { | ||||
|                         start: -(30 * 60 * 1000), | ||||
|                         end: (30 * 60 * 1000) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         beforeEach(done => { | ||||
|             const applicableViews = openmct.objectViews.get(testViewObject, mockObjectPath); | ||||
|             timelineView = applicableViews.find((viewProvider) => viewProvider.key === 'time-strip.view'); | ||||
|             let view = timelineView.view(testViewObject, element); | ||||
|             view.show(child, true); | ||||
|  | ||||
|             Vue.nextTick(done); | ||||
|         }); | ||||
|  | ||||
|         it('displays an independent time conductor with saved options - local clock', () => { | ||||
|  | ||||
|             return Vue.nextTick(() => { | ||||
|                 const independentTimeConductorEl = element.querySelector('.c-timeline-holder > .c-conductor__controls'); | ||||
|                 expect(independentTimeConductorEl).toBeDefined(); | ||||
|  | ||||
|                 const independentTimeContext = openmct.time.getIndependentContext(testViewObject.identifier.key); | ||||
|                 expect(independentTimeContext.clockOffsets()).toEqual(testViewObject.configuration.timeOptions.clockOffsets); | ||||
|             }); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe('the independent time conductor', () => { | ||||
|         let timelineView; | ||||
|         let testViewObject2 = { | ||||
|             id: "test-object2", | ||||
|             identifier: { | ||||
|                 key: "test-object2", | ||||
|                 namespace: '' | ||||
|             }, | ||||
|             type: "time-strip", | ||||
|             configuration: { | ||||
|                 useIndependentTime: true, | ||||
|                 timeOptions: { | ||||
|                     mode: { | ||||
|                         key: 'fixed' | ||||
|                     }, | ||||
|                     fixedOffsets: { | ||||
|                         start: 10, | ||||
|                         end: 11 | ||||
|                     }, | ||||
|                     clockOffsets: { | ||||
|                         start: -(30 * 60 * 1000), | ||||
|                         end: (30 * 60 * 1000) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         beforeEach((done) => { | ||||
|             const applicableViews = openmct.objectViews.get(testViewObject2, mockObjectPath); | ||||
|             timelineView = applicableViews.find((viewProvider) => viewProvider.key === 'time-strip.view'); | ||||
|             let view = timelineView.view(testViewObject2, element); | ||||
|             view.show(child, true); | ||||
|  | ||||
|             Vue.nextTick(done); | ||||
|         }); | ||||
|  | ||||
|         it('displays an independent time conductor with saved options - fixed timespan', () => { | ||||
|             return Vue.nextTick(() => { | ||||
|                 const independentTimeConductorEl = element.querySelector('.c-timeline-holder > .c-conductor__controls'); | ||||
|                 expect(independentTimeConductorEl).toBeDefined(); | ||||
|  | ||||
|                 const independentTimeContext = openmct.time.getIndependentContext(testViewObject2.identifier.key); | ||||
|                 expect(independentTimeContext.bounds()).toEqual(testViewObject2.configuration.timeOptions.fixedOffsets); | ||||
|             }); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
| }); | ||||
|   | ||||
| @@ -1,4 +1,10 @@ | ||||
| .c-timeline-holder { | ||||
|     @include abs(); | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     overflow-x: hidden; | ||||
| } | ||||
|  | ||||
|     > * + * { | ||||
|         margin-top: $interiorMargin; | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										40
									
								
								src/plugins/timeline/timelineInterceptor.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/plugins/timeline/timelineInterceptor.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2021, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| export default function timelineInterceptor(openmct) { | ||||
|  | ||||
|     openmct.objects.addGetInterceptor({ | ||||
|         appliesTo: (identifier, domainObject) => { | ||||
|             return domainObject && domainObject.type === 'time-strip'; | ||||
|         }, | ||||
|         invoke: (identifier, object) => { | ||||
|  | ||||
|             if (object && object.configuration === undefined) { | ||||
|                 object.configuration = { | ||||
|                     useIndependentTime: true | ||||
|                 }; | ||||
|             } | ||||
|  | ||||
|             return object; | ||||
|         } | ||||
|     }); | ||||
| } | ||||
| @@ -582,6 +582,12 @@ | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     &[class*='--menus-bottom'] { | ||||
|         .c-menu { | ||||
|             top: auto; bottom: 100%; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     &[class*='--menus-left'], | ||||
|     &[class*='menus-to-left'] { | ||||
|         .c-menu { | ||||
|   | ||||
| @@ -1,13 +1,29 @@ | ||||
| <template> | ||||
| <div></div> | ||||
| <div> | ||||
|     <div v-if="domainObject && domainObject.type === 'time-strip' && currentView" | ||||
|          class="c-conductor-holder--compact l-shell__main-independent-time-conductor" | ||||
|     > | ||||
|         <independent-time-conductor :domain-object="domainObject" | ||||
|                                     @stateChanged="updateIndependentTimeState" | ||||
|                                     @updated="saveTimeOptions" | ||||
|         /> | ||||
|     </div> | ||||
|     <div ref="objectViewWrapper" | ||||
|          class="l-shell__main-object-view" | ||||
|     ></div> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import _ from "lodash"; | ||||
| import StyleRuleManager from "@/plugins/condition/StyleRuleManager"; | ||||
| import {STYLE_CONSTANTS} from "@/plugins/condition/utils/constants"; | ||||
| import IndependentTimeConductor from '@/plugins/timeConductor/independent/IndependentTimeConductor.vue'; | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
|         IndependentTimeConductor | ||||
|     }, | ||||
|     inject: ["openmct"], | ||||
|     props: { | ||||
|         showEditView: Boolean, | ||||
| @@ -36,7 +52,8 @@ export default { | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             domainObject: this.defaultObject | ||||
|             domainObject: this.defaultObject, | ||||
|             currentView: undefined | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
| @@ -79,13 +96,13 @@ export default { | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.updateView(); | ||||
|         this.$el.addEventListener('dragover', this.onDragOver, { | ||||
|         this.$refs.objectViewWrapper.addEventListener('dragover', this.onDragOver, { | ||||
|             capture: true | ||||
|         }); | ||||
|         this.$el.addEventListener('drop', this.editIfEditable, { | ||||
|         this.$refs.objectViewWrapper.addEventListener('drop', this.editIfEditable, { | ||||
|             capture: true | ||||
|         }); | ||||
|         this.$el.addEventListener('drop', this.addObjectToParent); | ||||
|         this.$refs.objectViewWrapper.addEventListener('drop', this.addObjectToParent); | ||||
|         if (this.domainObject) { | ||||
|             //This is to apply styles to subobjects in a layout | ||||
|             this.initObjectStyles(); | ||||
| @@ -95,7 +112,9 @@ export default { | ||||
|         clear() { | ||||
|             if (this.currentView) { | ||||
|                 this.currentView.destroy(); | ||||
|                 this.$el.innerHTML = ''; | ||||
|                 if (this.$refs.objectViewWrapper) { | ||||
|                     this.$refs.objectViewWrapper.innerHTML = ''; | ||||
|                 } | ||||
|  | ||||
|                 if (this.releaseEditModeHandler) { | ||||
|                     this.releaseEditModeHandler(); | ||||
| @@ -118,8 +137,8 @@ export default { | ||||
|             this.openmct.objectViews.off('clearData', this.clearData); | ||||
|         }, | ||||
|         getStyleReceiver() { | ||||
|             let styleReceiver = this.$el.querySelector('.js-style-receiver') | ||||
|                 || this.$el.querySelector(':first-child'); | ||||
|             let styleReceiver = this.$refs.objectViewWrapper.querySelector('.js-style-receiver') | ||||
|                 || this.$refs.objectViewWrapper.querySelector(':first-child'); | ||||
|  | ||||
|             if (styleReceiver === null) { | ||||
|                 styleReceiver = undefined; | ||||
| @@ -183,7 +202,7 @@ export default { | ||||
|  | ||||
|             this.viewContainer = document.createElement('div'); | ||||
|             this.viewContainer.classList.add('l-angular-ov-wrapper'); | ||||
|             this.$el.append(this.viewContainer); | ||||
|             this.$refs.objectViewWrapper.append(this.viewContainer); | ||||
|             let provider = this.getViewProvider(); | ||||
|             if (!provider) { | ||||
|                 return; | ||||
| @@ -213,7 +232,7 @@ export default { | ||||
|  | ||||
|             if (immediatelySelect) { | ||||
|                 this.removeSelectable = this.openmct.selection.selectable( | ||||
|                     this.$el, this.getSelectionContext(), true); | ||||
|                     this.$refs.objectViewWrapper, this.getSelectionContext(), true); | ||||
|             } | ||||
|  | ||||
|             this.openmct.objectViews.on('clearData', this.clearData); | ||||
| @@ -388,6 +407,14 @@ export default { | ||||
|             if (elemToStyle !== undefined) { | ||||
|                 elemToStyle.dataset.font = newFont; | ||||
|             } | ||||
|         }, | ||||
|         updateIndependentTimeState(useIndependentTime) { | ||||
|             this.useIndependentTime = useIndependentTime; | ||||
|             this.openmct.objects.mutate(this.domainObject, 'configuration.useIndependentTime', this.useIndependentTime); | ||||
|         }, | ||||
|         saveTimeOptions(options) { | ||||
|             this.timeOptions = options; | ||||
|             this.openmct.objects.mutate(this.domainObject, 'configuration.timeOptions', this.timeOptions); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|   | ||||
| @@ -240,6 +240,14 @@ | ||||
|         overflow: auto; | ||||
|     } | ||||
|  | ||||
|     &__main-object-view { | ||||
|         height: 100%; | ||||
|     } | ||||
|  | ||||
|     &__main-independent-time-conductor { | ||||
|         margin-bottom: 5px; | ||||
|     } | ||||
|  | ||||
|     &__tree { | ||||
|         // Tree component within __pane-tree | ||||
|         flex: 1 1 auto !important; | ||||
| @@ -247,7 +255,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