Compare commits
	
		
			65 Commits
		
	
	
		
			default-se
			...
			time-condu
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | a7fb75aec6 | ||
|   | bbac4df4b8 | ||
|   | ed2d728f1d | ||
|   | a4ae654de9 | ||
|   | 378769fca7 | ||
|   | 2aac221e51 | ||
|   | 1055733ff5 | ||
|   | dc642f826e | ||
|   | 920b7a0c72 | ||
|   | ba7e7321df | ||
|   | 30ed5d8fdb | ||
|   | 5dc8f2b0d2 | ||
|   | 8161ed7ea6 | ||
|   | 9644208e6d | ||
|   | 42b6eb158a | ||
|   | bac9991855 | ||
|   | 1f545cb969 | ||
|   | 9271259a4c | ||
|   | 5f51a7cc90 | ||
|   | 445b9f3788 | ||
|   | acc0abc903 | ||
|   | 27dfda904e | ||
|   | 7aad1101b4 | ||
|   | 46d8d95583 | ||
|   | f6bd76be0e | ||
|   | b527bf3810 | ||
|   | b729c5132b | ||
|   | 4793fae5d1 | ||
|   | 26ba2f889e | ||
|   | a4956edf7b | ||
|   | 987e0c698c | ||
|   | a4200e81d9 | ||
|   | 2b5706f757 | ||
|   | 2c2d674b99 | ||
|   | aecbbf24b0 | ||
|   | 0ad6a595b0 | ||
|   | a6ac54383d | ||
|   | 490f25add8 | ||
|   | 694255db6b | ||
|   | 6910ae0a2b | ||
|   | b3bf7f2db1 | ||
|   | 348ba9085b | ||
|   | 9d6a5e2e17 | ||
|   | 5cb0e2e885 | ||
|   | ee277f3547 | ||
|   | bf77d240c7 | ||
|   | 684a3d2807 | ||
|   | 27e01ef13f | ||
|   | 4a2b1640e9 | ||
|   | 38cb92b203 | ||
|   | 0792ae0ae4 | ||
|   | d462f5d763 | ||
|   | cb8e97dc1c | ||
|   | aed3c19edd | ||
|   | a19c5cfd52 | ||
|   | 03e5241041 | ||
|   | 29ed251685 | ||
|   | 60433a12b8 | ||
|   | 69b8dd6c37 | ||
|   | e6c78c1826 | ||
|   | a1657817dc | ||
|   | 29538e6e78 | ||
|   | 68e9152d6a | ||
|   | 02b2b47411 | ||
|   | 70624c2c5c | 
							
								
								
									
										35
									
								
								index.html
									
									
									
									
									
								
							
							
						
						
									
										35
									
								
								index.html
									
									
									
									
									
								
							| @@ -34,8 +34,8 @@ | ||||
|     <body> | ||||
|     </body> | ||||
|     <script> | ||||
|         const FIVE_MINUTES = 5 * 60 * 1000; | ||||
|         const THIRTY_MINUTES = 30 * 60 * 1000; | ||||
|         const THIRTY_SECONDS = 30 * 1000; | ||||
|         const THIRTY_MINUTES = THIRTY_SECONDS * 60; | ||||
|  | ||||
|         [ | ||||
|             'example/eventGenerator' | ||||
| @@ -63,7 +63,34 @@ | ||||
|                     bounds: { | ||||
|                         start: Date.now() - THIRTY_MINUTES, | ||||
|                         end: Date.now() | ||||
|                     } | ||||
|                     }, | ||||
|                     presets: [ | ||||
|                         { | ||||
|                             label: 'Last Day', | ||||
|                             bounds: { | ||||
|                                 start: Date.now() - 1000 * 60 * 60 * 24, | ||||
|                                 end: Date.now() | ||||
|                             } | ||||
|                         }, | ||||
|                         { | ||||
|                             label: 'Last 2 hours', | ||||
|                             bounds: { | ||||
|                                 start: Date.now() - 1000 * 60 * 60 * 2, | ||||
|                                 end: Date.now() | ||||
|                             } | ||||
|                         }, | ||||
|                         { | ||||
|                             label: 'Last hour', | ||||
|                             bounds: { | ||||
|                                 start: Date.now() - 1000 * 60 * 60, | ||||
|                                 end: Date.now() | ||||
|                             } | ||||
|                         } | ||||
|                     ], | ||||
|                     // maximum entries to retain in conductor history | ||||
|                     records: 10, | ||||
|                     // maximum duration between start and end bounds | ||||
|                     limit: 1000 * 60 * 60 * 24 | ||||
|                 }, | ||||
|                 { | ||||
|                     name: "Realtime", | ||||
| @@ -71,7 +98,7 @@ | ||||
|                     clock: 'local', | ||||
|                     clockOffsets: { | ||||
|                         start: - THIRTY_MINUTES, | ||||
|                         end: FIVE_MINUTES | ||||
|                         end: THIRTY_SECONDS | ||||
|                     } | ||||
|                 } | ||||
|             ] | ||||
|   | ||||
| @@ -41,7 +41,7 @@ define([], function () { | ||||
|         this.timeFormat = 'local-format'; | ||||
|         this.durationFormat = 'duration'; | ||||
|  | ||||
|         this.isUTCBased = false; | ||||
|         this.isUTCBased = true; | ||||
|     } | ||||
|  | ||||
|     return LocalTimeSystem; | ||||
|   | ||||
| @@ -22,7 +22,12 @@ | ||||
| <template> | ||||
| <div | ||||
|     class="c-conductor" | ||||
|     :class="[isFixed ? 'is-fixed-mode' : 'is-realtime-mode']" | ||||
|     :class="[ | ||||
|         { 'is-zooming': isZooming }, | ||||
|         { 'is-panning': isPanning }, | ||||
|         { 'alt-pressed': altPressed }, | ||||
|         isFixed ? 'is-fixed-mode' : 'is-realtime-mode' | ||||
|     ]" | ||||
| > | ||||
|     <form | ||||
|         ref="conductorForm" | ||||
| @@ -52,7 +57,7 @@ | ||||
|                     type="text" | ||||
|                     autocorrect="off" | ||||
|                     spellcheck="false" | ||||
|                     @change="validateAllBounds(); submitForm()" | ||||
|                     @change="validateAllBounds('startDate'); submitForm()" | ||||
|                 > | ||||
|                 <date-picker | ||||
|                     v-if="isFixed && isUTCBased" | ||||
| @@ -92,7 +97,7 @@ | ||||
|                     autocorrect="off" | ||||
|                     spellcheck="false" | ||||
|                     :disabled="!isFixed" | ||||
|                     @change="validateAllBounds(); submitForm()" | ||||
|                     @change="validateAllBounds('endDate'); submitForm()" | ||||
|                 > | ||||
|                 <date-picker | ||||
|                     v-if="isFixed && isUTCBased" | ||||
| @@ -122,14 +127,25 @@ | ||||
|  | ||||
|             <conductor-axis | ||||
|                 class="c-conductor__ticks" | ||||
|                 :bounds="rawBounds" | ||||
|                 @panAxis="setViewFromBounds" | ||||
|                 :view-bounds="viewBounds" | ||||
|                 :is-fixed="isFixed" | ||||
|                 :alt-pressed="altPressed" | ||||
|                 @endPan="endPan" | ||||
|                 @endZoom="endZoom" | ||||
|                 @panAxis="pan" | ||||
|                 @zoomAxis="zoom" | ||||
|             /> | ||||
|  | ||||
|         </div> | ||||
|         <div class="c-conductor__controls"> | ||||
|             <!-- Mode, time system menu buttons and duration slider --> | ||||
|             <ConductorMode class="c-conductor__mode-select" /> | ||||
|             <ConductorTimeSystem class="c-conductor__time-system-select" /> | ||||
|             <ConductorHistory | ||||
|                 v-if="isFixed" | ||||
|                 class="c-conductor__history-select" | ||||
|                 :bounds="openmct.time.bounds()" | ||||
|                 :time-system="timeSystem" | ||||
|             /> | ||||
|         </div> | ||||
|         <input | ||||
|             type="submit" | ||||
| @@ -145,6 +161,7 @@ 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' | ||||
|  | ||||
| const DEFAULT_DURATION_FORMATTER = 'duration'; | ||||
|  | ||||
| @@ -155,7 +172,8 @@ export default { | ||||
|         ConductorTimeSystem, | ||||
|         DatePicker, | ||||
|         ConductorAxis, | ||||
|         ConductorModeIcon | ||||
|         ConductorModeIcon, | ||||
|         ConductorHistory | ||||
|     }, | ||||
|     data() { | ||||
|         let bounds = this.openmct.time.bounds(); | ||||
| @@ -165,6 +183,7 @@ export default { | ||||
|         let durationFormatter = this.getFormatter(timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER); | ||||
|  | ||||
|         return { | ||||
|             timeSystem: timeSystem, | ||||
|             timeFormatter: timeFormatter, | ||||
|             durationFormatter: durationFormatter, | ||||
|             offsets: { | ||||
| @@ -175,29 +194,64 @@ export default { | ||||
|                 start: timeFormatter.format(bounds.start), | ||||
|                 end: timeFormatter.format(bounds.end) | ||||
|             }, | ||||
|             rawBounds: { | ||||
|             viewBounds: { | ||||
|                 start: bounds.start, | ||||
|                 end: bounds.end | ||||
|             }, | ||||
|             isFixed: this.openmct.time.clock() === undefined, | ||||
|             isUTCBased: timeSystem.isUTCBased, | ||||
|             showDatePicker: false | ||||
|             showDatePicker: false, | ||||
|             altPressed: false, | ||||
|             isPanning: false, | ||||
|             isZooming: false | ||||
|         } | ||||
|     }, | ||||
|     created() { | ||||
|         document.addEventListener('keydown', (e) => { | ||||
|             if (e.key === 'Alt') { | ||||
|                 this.altPressed = true; | ||||
|             } | ||||
|         }); | ||||
|         document.addEventListener('keyup', (e) => { | ||||
|             if (e.key === 'Alt') { | ||||
|                 this.altPressed = false; | ||||
|             } | ||||
|         }); | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.setTimeSystem(JSON.parse(JSON.stringify(this.openmct.time.timeSystem()))); | ||||
|  | ||||
|         this.openmct.time.on('bounds', this.setViewFromBounds); | ||||
|         this.openmct.time.on('timeSystem', this.setTimeSystem); | ||||
|         this.openmct.time.on('clock', this.setViewFromClock); | ||||
|         this.openmct.time.on('clockOffsets', this.setViewFromOffsets) | ||||
|     }, | ||||
|     methods: { | ||||
|         pan(bounds) { | ||||
|             this.isPanning = true; | ||||
|             this.setViewFromBounds(bounds); | ||||
|         }, | ||||
|         endPan(bounds) { | ||||
|             this.isPanning = false; | ||||
|             if (bounds) { | ||||
|                 this.openmct.time.bounds(bounds); | ||||
|             } | ||||
|         }, | ||||
|         zoom(bounds) { | ||||
|             this.isZooming = true; | ||||
|             this.formattedBounds.start = this.timeFormatter.format(bounds.start); | ||||
|             this.formattedBounds.end = this.timeFormatter.format(bounds.end); | ||||
|         }, | ||||
|         endZoom(bounds) { | ||||
|             const _bounds = bounds ? bounds : this.openmct.time.bounds(); | ||||
|             this.isZooming = false; | ||||
|  | ||||
|             this.openmct.time.bounds(_bounds); | ||||
|         }, | ||||
|         setTimeSystem(timeSystem) { | ||||
|             this.timeSystem = timeSystem | ||||
|             this.timeFormatter = this.getFormatter(timeSystem.timeFormat); | ||||
|             this.durationFormatter = this.getFormatter( | ||||
|                 timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER); | ||||
|  | ||||
|             this.isUTCBased = timeSystem.isUTCBased; | ||||
|         }, | ||||
|         setOffsetsFromView($event) { | ||||
| @@ -237,8 +291,8 @@ export default { | ||||
|         setViewFromBounds(bounds) { | ||||
|             this.formattedBounds.start = this.timeFormatter.format(bounds.start); | ||||
|             this.formattedBounds.end = this.timeFormatter.format(bounds.end); | ||||
|             this.rawBounds.start = bounds.start; | ||||
|             this.rawBounds.end = bounds.end; | ||||
|             this.viewBounds.start = bounds.start; | ||||
|             this.viewBounds.end = bounds.end; | ||||
|         }, | ||||
|         setViewFromOffsets(offsets) { | ||||
|             this.offsets.start = this.durationFormatter.format(Math.abs(offsets.start)); | ||||
| @@ -251,6 +305,15 @@ export default { | ||||
|                 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); | ||||
| @@ -262,37 +325,37 @@ export default { | ||||
|             input.setCustomValidity(''); | ||||
|             input.title = ''; | ||||
|         }, | ||||
|         validateAllBounds() { | ||||
|             return [this.$refs.startDate, this.$refs.endDate].every((input) => { | ||||
|                 let validationResult = true; | ||||
|                 let formattedDate; | ||||
|         validateAllBounds(ref) { | ||||
|             const input = this.$refs[ref]; | ||||
|             let validationResult = true; | ||||
|  | ||||
|                 if (input === this.$refs.startDate) { | ||||
|                     formattedDate = this.formattedBounds.start; | ||||
|                 } else { | ||||
|                     formattedDate = this.formattedBounds.end; | ||||
|                 } | ||||
|             const formattedDate = input === this.$refs.startDate | ||||
|                 ? this.formattedBounds.start | ||||
|                 : this.formattedBounds.end | ||||
|             ; | ||||
|  | ||||
|                 if (!this.timeFormatter.validate(formattedDate)) { | ||||
|                     validationResult = 'Invalid date'; | ||||
|             if (!this.timeFormatter.validate(formattedDate)) { | ||||
|                 validationResult = 'Invalid date'; | ||||
|             } else { | ||||
|                 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 | ||||
|                 ) { | ||||
|                     validationResult = "Start and end difference exceeds allowable limit"; | ||||
|                 } else { | ||||
|                     let boundsValues = { | ||||
|                         start: this.timeFormatter.parse(this.formattedBounds.start), | ||||
|                         end: this.timeFormatter.parse(this.formattedBounds.end) | ||||
|                     }; | ||||
|                     validationResult = this.openmct.time.validateBounds(boundsValues); | ||||
|                 } | ||||
|  | ||||
|                 if (validationResult !== true) { | ||||
|                     input.setCustomValidity(validationResult); | ||||
|                     input.title = validationResult; | ||||
|                     return false; | ||||
|                 } else { | ||||
|                     input.setCustomValidity(''); | ||||
|                     input.title = ''; | ||||
|                     return true; | ||||
|                 } | ||||
|             }); | ||||
|             } | ||||
|  | ||||
|             this.handleValidationResult(input, validationResult); | ||||
|         }, | ||||
|         validateAllOffsets(event) { | ||||
|             return [this.$refs.startOffset, this.$refs.endOffset].every((input) => { | ||||
| @@ -315,17 +378,20 @@ export default { | ||||
|                     validationResult = this.openmct.time.validateOffsets(offsetValues); | ||||
|                 } | ||||
|  | ||||
|                 if (validationResult !== true) { | ||||
|                     input.setCustomValidity(validationResult); | ||||
|                     input.title = validationResult; | ||||
|                     return false; | ||||
|                 } else { | ||||
|                     input.setCustomValidity(''); | ||||
|                     input.title = ''; | ||||
|                     return true; | ||||
|                 } | ||||
|                 this.handleValidationResult(input, validationResult); | ||||
|             }); | ||||
|         }, | ||||
|         handleValidationResult(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) | ||||
| @@ -338,12 +404,12 @@ export default { | ||||
|         }, | ||||
|         startDateSelected(date) { | ||||
|             this.formattedBounds.start = this.timeFormatter.format(date); | ||||
|             this.validateAllBounds(); | ||||
|             this.validateAllBounds('startDate'); | ||||
|             this.submitForm(); | ||||
|         }, | ||||
|         endDateSelected(date) { | ||||
|             this.formattedBounds.end = this.timeFormatter.format(date); | ||||
|             this.validateAllBounds(); | ||||
|             this.validateAllBounds('endDate'); | ||||
|             this.submitForm(); | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -24,7 +24,12 @@ | ||||
|     ref="axisHolder" | ||||
|     class="c-conductor-axis" | ||||
|     @mousedown="dragStart($event)" | ||||
| ></div> | ||||
| > | ||||
|     <div | ||||
|         class="c-conductor-axis__zoom-indicator" | ||||
|         :style="zoomStyle" | ||||
|     ></div> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| @@ -43,14 +48,35 @@ const PIXELS_PER_TICK_WIDE = 200; | ||||
| export default { | ||||
|     inject: ['openmct'], | ||||
|     props: { | ||||
|         bounds: { | ||||
|         viewBounds: { | ||||
|             type: Object, | ||||
|             required: true | ||||
|         }, | ||||
|         isFixed: { | ||||
|             type: Boolean, | ||||
|             required: true | ||||
|         }, | ||||
|         altPressed: { | ||||
|             type: Boolean, | ||||
|             required: true | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             inPanMode: false, | ||||
|             dragStartX: undefined, | ||||
|             dragX: undefined, | ||||
|             zoomStyle: {} | ||||
|         } | ||||
|     }, | ||||
|     computed: { | ||||
|         inZoomMode() { | ||||
|             return !this.inPanMode; | ||||
|         } | ||||
|     }, | ||||
|     watch: { | ||||
|         bounds: { | ||||
|             handler(bounds) { | ||||
|         viewBounds: { | ||||
|             handler() { | ||||
|                 this.setScale(); | ||||
|             }, | ||||
|             deep: true | ||||
| @@ -58,18 +84,23 @@ export default { | ||||
|     }, | ||||
|     mounted() { | ||||
|         let axisHolder = this.$refs.axisHolder; | ||||
|         let height = axisHolder.offsetHeight; | ||||
|         this.height = axisHolder.offsetHeight; | ||||
|         this.width = axisHolder.clientWidth; | ||||
|         const rect = axisHolder.getBoundingClientRect(); | ||||
|         this.left = Math.round(rect.left); | ||||
|  | ||||
|         let vis = d3Selection.select(axisHolder) | ||||
|             .append("svg:svg") | ||||
|             .attr("width", "100%") | ||||
|             .attr("height", height); | ||||
|             .attr("height", this.height); | ||||
|  | ||||
|  | ||||
|         this.width = this.$refs.axisHolder.clientWidth; | ||||
|         this.xAxis = d3Axis.axisTop(); | ||||
|         this.dragging = false; | ||||
|  | ||||
|         // draw x axis with labels. CSS is used to position them. | ||||
|         this.axisElement = vis.append("g"); | ||||
|         this.axisElement = vis.append("g") | ||||
|             .attr("class", "axis"); | ||||
|  | ||||
|         this.setViewFromTimeSystem(this.openmct.time.timeSystem()); | ||||
|         this.setScale(); | ||||
| @@ -83,12 +114,15 @@ export default { | ||||
|     methods: { | ||||
|         setScale() { | ||||
|             let timeSystem = this.openmct.time.timeSystem(); | ||||
|             let bounds = this.bounds; | ||||
|  | ||||
|             if (timeSystem.isUTCBased) { | ||||
|                 this.xScale.domain([new Date(bounds.start), new Date(bounds.end)]); | ||||
|                 this.xScale.domain( | ||||
|                     [new Date(this.viewBounds.start), new Date(this.viewBounds.end)] | ||||
|                 ); | ||||
|             } else { | ||||
|                 this.xScale.domain([bounds.start, bounds.end]); | ||||
|                 this.xScale.domain( | ||||
|                     [this.viewBounds.start, this.viewBounds.end] | ||||
|                 ); | ||||
|             } | ||||
|  | ||||
|             this.xAxis.scale(this.xScale); | ||||
| @@ -102,7 +136,7 @@ export default { | ||||
|                 this.xAxis.ticks(this.width / PIXELS_PER_TICK); | ||||
|             } | ||||
|  | ||||
|             this.msPerPixel = (bounds.end - bounds.start) / this.width; | ||||
|             this.msPerPixel = (this.viewBounds.end - this.viewBounds.start) / this.width; | ||||
|         }, | ||||
|         setViewFromTimeSystem(timeSystem) { | ||||
|             //The D3 scale used depends on the type of time system as d3 | ||||
| @@ -120,9 +154,8 @@ export default { | ||||
|         }, | ||||
|         getActiveFormatter() { | ||||
|             let timeSystem = this.openmct.time.timeSystem(); | ||||
|             let isFixed = this.openmct.time.clock() === undefined; | ||||
|  | ||||
|             if (isFixed) { | ||||
|             if (this.isFixed) { | ||||
|                 return this.getFormatter(timeSystem.timeFormat); | ||||
|             } else { | ||||
|                 return this.getFormatter(timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER); | ||||
| @@ -134,42 +167,128 @@ export default { | ||||
|             }).formatter; | ||||
|         }, | ||||
|         dragStart($event) { | ||||
|             let isFixed = this.openmct.time.clock() === undefined; | ||||
|             if (isFixed) { | ||||
|             if (this.isFixed) { | ||||
|                 this.dragStartX = $event.clientX; | ||||
|  | ||||
|                 if (this.altPressed) { | ||||
|                     this.inPanMode = true; | ||||
|                 } | ||||
|  | ||||
|                 document.addEventListener('mousemove', this.drag); | ||||
|                 document.addEventListener('mouseup', this.dragEnd, { | ||||
|                     once: true | ||||
|                 }); | ||||
|  | ||||
|                 if (this.inZoomMode) { | ||||
|                     this.startZoom(); | ||||
|                 } | ||||
|             } | ||||
|         }, | ||||
|         drag($event) { | ||||
|             if (!this.dragging) { | ||||
|                 this.dragging = true; | ||||
|                 requestAnimationFrame(()=>{ | ||||
|                     let deltaX = $event.clientX - this.dragStartX; | ||||
|                     let percX = deltaX / this.width; | ||||
|                     let bounds = this.openmct.time.bounds(); | ||||
|                     let deltaTime = bounds.end - bounds.start; | ||||
|                     let newStart = bounds.start - percX * deltaTime; | ||||
|                     this.$emit('panAxis',{ | ||||
|                         start: newStart, | ||||
|                         end: newStart + deltaTime | ||||
|                     }); | ||||
|  | ||||
|                 requestAnimationFrame(() => { | ||||
|                     this.dragX = $event.clientX; | ||||
|                     this.inPanMode ? this.pan() : this.zoom(); | ||||
|                     this.dragging = false; | ||||
|                 }) | ||||
|             } else { | ||||
|                 console.log('Rejected drag due to RAF cap'); | ||||
|                 }); | ||||
|             } | ||||
|         }, | ||||
|         dragEnd() { | ||||
|             this.inPanMode ? this.endPan() : this.endZoom(); | ||||
|  | ||||
|             document.removeEventListener('mousemove', this.drag); | ||||
|             this.openmct.time.bounds({ | ||||
|                 start: this.bounds.start, | ||||
|                 end: this.bounds.end | ||||
|             this.dragStartX = undefined; | ||||
|             this.dragX = undefined; | ||||
|         }, | ||||
|         pan() { | ||||
|             const panBounds = this.getPanBounds(); | ||||
|             this.$emit('panAxis', panBounds); | ||||
|         }, | ||||
|         endPan() { | ||||
|             const panBounds = this.dragStartX && this.dragX && this.dragStartX !== this.dragX | ||||
|                 ? this.getPanBounds() | ||||
|                 : undefined; | ||||
|             this.$emit('endPan', panBounds); | ||||
|             this.inPanMode = false; | ||||
|         }, | ||||
|         getPanBounds() { | ||||
|             const bounds = this.openmct.time.bounds(); | ||||
|             const deltaTime = bounds.end - bounds.start; | ||||
|             const deltaX = this.dragX - this.dragStartX; | ||||
|             const percX = deltaX / this.width; | ||||
|             const panStart = bounds.start - percX * deltaTime; | ||||
|  | ||||
|             return { | ||||
|                 start: panStart, | ||||
|                 end: panStart + deltaTime | ||||
|             }; | ||||
|         }, | ||||
|         startZoom() { | ||||
|             const x = this.scaleToBounds(this.dragStartX); | ||||
|  | ||||
|             this.zoomStyle = { | ||||
|                 left: `${this.dragStartX - this.left}px` | ||||
|             }; | ||||
|  | ||||
|             this.$emit('zoomAxis', { | ||||
|                 start: x, | ||||
|                 end: x | ||||
|             }); | ||||
|         }, | ||||
|         zoom() { | ||||
|             const zoomRange = this.getZoomRange(); | ||||
|  | ||||
|             this.zoomStyle = { | ||||
|                 left: `${zoomRange.start - this.left}px`, | ||||
|                 width: `${zoomRange.end - zoomRange.start}px` | ||||
|             }; | ||||
|  | ||||
|             this.$emit('zoomAxis', { | ||||
|                 start: this.scaleToBounds(zoomRange.start), | ||||
|                 end: this.scaleToBounds(zoomRange.end) | ||||
|             }); | ||||
|         }, | ||||
|         endZoom() { | ||||
|             const zoomRange = this.dragStartX && this.dragX && this.dragStartX !== this.dragX | ||||
|                 ? this.getZoomRange() | ||||
|                 : undefined; | ||||
|  | ||||
|             const zoomBounds = zoomRange | ||||
|                 ? { | ||||
|                     start: this.scaleToBounds(zoomRange.start), | ||||
|                     end: this.scaleToBounds(zoomRange.end) | ||||
|                 } | ||||
|                 : this.openmct.time.bounds(); | ||||
|  | ||||
|             this.zoomStyle = {}; | ||||
|             this.$emit('endZoom', zoomBounds); | ||||
|         }, | ||||
|         getZoomRange() { | ||||
|             const leftBound = this.left; | ||||
|             const rightBound = this.left + this.width; | ||||
|  | ||||
|             const zoomStart = this.dragX < leftBound | ||||
|                 ? leftBound | ||||
|                 : Math.min(this.dragX, this.dragStartX); | ||||
|  | ||||
|             const zoomEnd = this.dragX > rightBound | ||||
|                 ? rightBound | ||||
|                 : Math.max(this.dragX, this.dragStartX); | ||||
|  | ||||
|             return { | ||||
|                 start: zoomStart, | ||||
|                 end: zoomEnd | ||||
|             }; | ||||
|         }, | ||||
|         scaleToBounds(value) { | ||||
|             const bounds = this.openmct.time.bounds(); | ||||
|             const timeDelta = bounds.end - bounds.start; | ||||
|             const valueDelta = value - this.left; | ||||
|             const offset = valueDelta / this.width * timeDelta; | ||||
|             return bounds.start + offset; | ||||
|         }, | ||||
|         resize() { | ||||
|             if (this.$refs.axisHolder.clientWidth !== this.width) { | ||||
|                 this.width = this.$refs.axisHolder.clientWidth; | ||||
|   | ||||
							
								
								
									
										198
									
								
								src/plugins/timeConductor/ConductorHistory.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										198
									
								
								src/plugins/timeConductor/ConductorHistory.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,198 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT Web, Copyright (c) 2014-2018, 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-ctrl-wrapper c-ctrl-wrapper--menus-up"> | ||||
|     <button class="c-button--menu c-history-button icon-history" | ||||
|             @click.prevent="toggle" | ||||
|     > | ||||
|         <span class="c-button__label">History</span> | ||||
|     </button> | ||||
|     <div v-if="open" | ||||
|          class="c-menu c-conductor__history-menu" | ||||
|     > | ||||
|         <ul v-if="hasHistoryPresets"> | ||||
|             <li | ||||
|                 v-for="preset in presets" | ||||
|                 :key="preset.label" | ||||
|                 class="icon-clock" | ||||
|                 @click="selectTimespan(preset.bounds)" | ||||
|             > | ||||
|                 {{ preset.label }} | ||||
|             </li> | ||||
|         </ul> | ||||
|  | ||||
|         <div | ||||
|             v-if="hasHistoryPresets" | ||||
|             class="c-menu__section-separator" | ||||
|         ></div> | ||||
|  | ||||
|         <div class="c-menu__section-hint"> | ||||
|             Past timeframes, ordered by latest first | ||||
|         </div> | ||||
|  | ||||
|         <ul> | ||||
|             <li | ||||
|                 v-for="(timespan, index) in historyForCurrentTimeSystem" | ||||
|                 :key="index" | ||||
|                 class="icon-history" | ||||
|                 @click="selectTimespan(timespan)" | ||||
|             > | ||||
|                 {{ formatTime(timespan.start) }} - {{ formatTime(timespan.end) }} | ||||
|             </li> | ||||
|         </ul> | ||||
|     </div> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import toggleMixin from '../../ui/mixins/toggle-mixin'; | ||||
|  | ||||
| const LOCAL_STORAGE_HISTORY_KEY = 'tcHistory'; | ||||
| const DEFAULT_RECORDS = 10; | ||||
|  | ||||
| export default { | ||||
|     inject: ['openmct', 'configuration'], | ||||
|     mixins: [toggleMixin], | ||||
|     props: { | ||||
|         bounds: { | ||||
|             type: Object, | ||||
|             required: true | ||||
|         }, | ||||
|         timeSystem: { | ||||
|             type: Object, | ||||
|             required: true | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             history: {}, // contains arrays of timespans {start, end}, array key is time system key | ||||
|             presets: [] | ||||
|         } | ||||
|     }, | ||||
|     computed: { | ||||
|         hasHistoryPresets() { | ||||
|             return this.timeSystem.isUTCBased && this.presets.length; | ||||
|         }, | ||||
|         historyForCurrentTimeSystem() { | ||||
|             const history = this.history[this.timeSystem.key]; | ||||
|  | ||||
|             return history; | ||||
|         } | ||||
|     }, | ||||
|     watch: { | ||||
|         bounds: { | ||||
|             handler() { | ||||
|                 this.addTimespan(); | ||||
|             }, | ||||
|             deep: true | ||||
|         }, | ||||
|         timeSystem: { | ||||
|             handler() { | ||||
|                 this.loadConfiguration(); | ||||
|                 this.addTimespan(); | ||||
|             }, | ||||
|             deep: true | ||||
|         }, | ||||
|         history: { | ||||
|             handler() { | ||||
|                 this.persistHistoryToLocalStorage(); | ||||
|             }, | ||||
|             deep: true | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.getHistoryFromLocalStorage(); | ||||
|     }, | ||||
|     methods: { | ||||
|         getHistoryFromLocalStorage() { | ||||
|             if (localStorage.getItem(LOCAL_STORAGE_HISTORY_KEY)) { | ||||
|                 this.history = JSON.parse(localStorage.getItem(LOCAL_STORAGE_HISTORY_KEY)) | ||||
|             } else { | ||||
|                 this.history = {}; | ||||
|                 this.persistHistoryToLocalStorage(); | ||||
|             } | ||||
|         }, | ||||
|         persistHistoryToLocalStorage() { | ||||
|             localStorage.setItem(LOCAL_STORAGE_HISTORY_KEY, JSON.stringify(this.history)); | ||||
|         }, | ||||
|         addTimespan() { | ||||
|             const key = this.timeSystem.key; | ||||
|             let [...currentHistory] = this.history[key] || []; | ||||
|             const timespan = { | ||||
|                 start: this.bounds.start, | ||||
|                 end: this.bounds.end | ||||
|             }; | ||||
|  | ||||
|             const isNotEqual = function (entry) { | ||||
|                 const start = entry.start !== this.start; | ||||
|                 const end = entry.end !== this.end; | ||||
|  | ||||
|                 return start || end; | ||||
|             }; | ||||
|             currentHistory = currentHistory.filter(isNotEqual, timespan); | ||||
|  | ||||
|             while (currentHistory.length >= this.records) { | ||||
|                 currentHistory.pop(); | ||||
|             } | ||||
|  | ||||
|             currentHistory.unshift(timespan); | ||||
|             this.history[key] = currentHistory; | ||||
|         }, | ||||
|         selectTimespan(timespan) { | ||||
|             this.openmct.time.bounds(timespan); | ||||
|         }, | ||||
|         selectHours(hours) { | ||||
|             const now = Date.now(); | ||||
|             this.selectTimespan({ | ||||
|                 start: now - hours * 60 * 60 * 1000, | ||||
|                 end: now | ||||
|             }); | ||||
|         }, | ||||
|         loadConfiguration() { | ||||
|             const configurations = this.configuration.menuOptions | ||||
|                 .filter(option => option.timeSystem ===  this.timeSystem.key); | ||||
|  | ||||
|             this.presets = this.loadPresets(configurations); | ||||
|             this.records = this.loadRecords(configurations); | ||||
|         }, | ||||
|         loadPresets(configurations) { | ||||
|             const configuration = configurations.find(option => option.presets); | ||||
|             const presets = configuration ? configuration.presets : []; | ||||
|  | ||||
|             return presets; | ||||
|         }, | ||||
|         loadRecords(configurations) { | ||||
|             const configuration = configurations.find(option => option.records); | ||||
|             const records = configuration ? configuration.records : DEFAULT_RECORDS; | ||||
|  | ||||
|             return records; | ||||
|         }, | ||||
|         formatTime(time) { | ||||
|             const formatter = this.openmct.telemetry.getValueFormatter({ | ||||
|                 format: this.timeSystem.timeFormat | ||||
|             }).formatter; | ||||
|  | ||||
|             return formatter.format(time); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| </script> | ||||
| @@ -110,7 +110,7 @@ export default { | ||||
|             if (clock === undefined) { | ||||
|                 return { | ||||
|                     key: 'fixed', | ||||
|                     name: 'Fixed Timespan Mode', | ||||
|                     name: 'Fixed Timespan', | ||||
|                     description: 'Query and explore data that falls between two fixed datetimes.', | ||||
|                     cssClass: 'icon-tabular' | ||||
|                 } | ||||
|   | ||||
| @@ -13,7 +13,7 @@ | ||||
|         text-rendering: geometricPrecision; | ||||
|         width: 100%; | ||||
|         height: 100%; | ||||
|         > g { | ||||
|         > g.axis { | ||||
|             // Overall Tick holder | ||||
|             transform: translateY($tickYPos); | ||||
|             path { | ||||
| @@ -44,7 +44,6 @@ | ||||
|     } | ||||
|  | ||||
|     body.desktop .is-fixed-mode & { | ||||
|         @include cursorGrab(); | ||||
|         background-size: 3px 30%; | ||||
|         background-color: $colorBodyBgSubtle; | ||||
|         box-shadow: inset rgba(black, 0.4) 0 1px 1px; | ||||
| @@ -55,17 +54,6 @@ | ||||
|             stroke: $colorBodyBgSubtle; | ||||
|             transition: $transOut; | ||||
|         } | ||||
|  | ||||
|         &:hover, | ||||
|         &:active { | ||||
|             $c: $colorKeySubtle; | ||||
|             background-color: $c; | ||||
|             transition: $transIn; | ||||
|             svg text { | ||||
|                 stroke: $c; | ||||
|                 transition: $transIn; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     .is-realtime-mode & { | ||||
|   | ||||
| @@ -57,6 +57,65 @@ | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     &.is-fixed-mode { | ||||
|         .c-conductor-axis { | ||||
|             &__zoom-indicator { | ||||
|                 border: 1px solid transparent; | ||||
|                 display: none; // Hidden by default | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         &:not(.is-panning), | ||||
|         &:not(.is-zooming) { | ||||
|             .c-conductor-axis { | ||||
|                 &:hover, | ||||
|                 &:active { | ||||
|                     cursor: col-resize; | ||||
|                     filter: $timeConductorAxisHoverFilter; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         &.is-panning, | ||||
|         &.is-zooming { | ||||
|             .c-conductor-input input { | ||||
|                 // Styles for inputs while zooming or panning | ||||
|                 background: rgba($timeConductorActiveBg, 0.4); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         &.alt-pressed { | ||||
|             .c-conductor-axis:hover { | ||||
|                 // When alt is being pressed and user is hovering over the axis, set the cursor | ||||
|                 @include cursorGrab(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         &.is-panning { | ||||
|             .c-conductor-axis { | ||||
|                 @include cursorGrab(); | ||||
|                 background-color: $timeConductorActivePanBg; | ||||
|                 transition: $transIn; | ||||
|  | ||||
|                 svg text { | ||||
|                     stroke: $timeConductorActivePanBg; | ||||
|                     transition: $transIn; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         &.is-zooming { | ||||
|             .c-conductor-axis__zoom-indicator { | ||||
|                 display: block; | ||||
|                 position: absolute; | ||||
|                 background: rgba($timeConductorActiveBg, 0.4); | ||||
|                 border-left-color: $timeConductorActiveBg; | ||||
|                 border-right-color: $timeConductorActiveBg; | ||||
|                 top: 0; bottom: 0; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     &.is-realtime-mode { | ||||
|         .c-conductor__time-bounds { | ||||
|             grid-template-columns: 20px auto 1fr auto auto; | ||||
|   | ||||
| @@ -142,6 +142,9 @@ $colorTimeHov: pullForward($colorTime, 10%); | ||||
| $colorTimeSubtle: pushBack($colorTime, 20%); | ||||
| $colorTOI: $colorBodyFg; // was $timeControllerToiLineColor | ||||
| $colorTOIHov: $colorTime; // was $timeControllerToiLineColorHov | ||||
| $timeConductorAxisHoverFilter: brightness(1.2); | ||||
| $timeConductorActiveBg: $colorKey; | ||||
| $timeConductorActivePanBg: #226074; | ||||
|  | ||||
| /************************************************** BROWSING */ | ||||
| $browseFrameColor: pullForward($colorBodyBg, 10%); | ||||
|   | ||||
| @@ -146,6 +146,9 @@ $colorTimeHov: pullForward($colorTime, 10%); | ||||
| $colorTimeSubtle: pushBack($colorTime, 20%); | ||||
| $colorTOI: $colorBodyFg; // was $timeControllerToiLineColor | ||||
| $colorTOIHov: $colorTime; // was $timeControllerToiLineColorHov | ||||
| $timeConductorAxisHoverFilter: brightness(1.2); | ||||
| $timeConductorActiveBg: $colorKey; | ||||
| $timeConductorActivePanBg: #226074; | ||||
|  | ||||
| /************************************************** BROWSING */ | ||||
| $browseFrameColor: pullForward($colorBodyBg, 10%); | ||||
|   | ||||
| @@ -132,7 +132,7 @@ $colorPausedFg: #fff; | ||||
| // Base variations | ||||
| $colorBodyBgSubtle: pullForward($colorBodyBg, 5%); | ||||
| $colorBodyBgSubtleHov: pushBack($colorKey, 50%); | ||||
| $colorKeySubtle: pushBack($colorKey, 10%); | ||||
| $colorKeySubtle: pushBack($colorKey, 20%); | ||||
|  | ||||
| // Time Colors | ||||
| $colorTime: #618cff; | ||||
| @@ -142,6 +142,9 @@ $colorTimeHov: pushBack($colorTime, 5%); | ||||
| $colorTimeSubtle: pushBack($colorTime, 20%); | ||||
| $colorTOI: $colorBodyFg; // was $timeControllerToiLineColor | ||||
| $colorTOIHov: $colorTime; // was $timeControllerToiLineColorHov | ||||
| $timeConductorAxisHoverFilter: brightness(0.8); | ||||
| $timeConductorActiveBg: $colorKey; | ||||
| $timeConductorActivePanBg: #A0CDE1; | ||||
|  | ||||
| /************************************************** BROWSING */ | ||||
| $browseFrameColor: pullForward($colorBodyBg, 10%); | ||||
|   | ||||
| @@ -462,9 +462,17 @@ select { | ||||
|     text-shadow: $shdwMenuText; | ||||
|     padding: $interiorMarginSm; | ||||
|     box-shadow: $shdwMenu; | ||||
|     display: block; | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     position: absolute; | ||||
|     z-index: 100; | ||||
|  | ||||
|     > * { | ||||
|         flex: 0 0 auto; | ||||
|         //+ * { | ||||
|         //    margin-top: $interiorMarginSm; | ||||
|         //} | ||||
|     } | ||||
| } | ||||
|  | ||||
| @mixin menuInner() { | ||||
| @@ -502,6 +510,23 @@ select { | ||||
| .c-menu { | ||||
|     @include menuOuter(); | ||||
|     @include menuInner(); | ||||
|  | ||||
|     &__section-hint { | ||||
|         $m: $interiorMargin; | ||||
|         margin: $m 0; | ||||
|         padding: $m nth($menuItemPad, 2) 0 nth($menuItemPad, 2); | ||||
|  | ||||
|         opacity: 0.6; | ||||
|         font-size: 0.9em; | ||||
|         font-style: italic; | ||||
|     } | ||||
|  | ||||
|     &__section-separator { | ||||
|         $m: $interiorMargin; | ||||
|         border-top: 1px solid $colorInteriorBorder; | ||||
|         margin: $m 0; | ||||
|         padding: $m nth($menuItemPad, 2) 0 nth($menuItemPad, 2); | ||||
|     } | ||||
| } | ||||
|  | ||||
| .c-super-menu { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user