Compare commits
	
		
			52 Commits
		
	
	
		
			icon-telem
			...
			router-tes
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | ab371406c0 | ||
|   | c269e089da | ||
|   | 4873f40614 | ||
|   | abc0fa068f | ||
|   | da4825df8c | ||
|   | c5ecd5fb2e | ||
|   | 10bb9173ec | ||
|   | ea8c9c7cc8 | ||
|   | 4c9c084eec | ||
|   | b64ee10812 | ||
|   | ee1ecf43db | ||
|   | 4d8db8eb7c | ||
|   | 1b13965200 | ||
|   | 38db8f7fe5 | ||
|   | ff2de44df6 | ||
|   | 904e787de9 | ||
|   | 4ba8f893a6 | ||
|   | c4b9be18f1 | ||
|   | eabdf6cd04 | ||
|   | e56c673005 | ||
|   | dad9f12a5c | ||
|   | aa5edb0b83 | ||
|   | b315803180 | ||
|   | b27317631b | ||
|   | 953a9daafb | ||
|   | 63f9cd449f | ||
|   | 54220f547b | ||
|   | 93d967c2b3 | ||
|   | 1226459c6f | ||
|   | d7c9c9cb98 | ||
|   | 2131ef2397 | ||
|   | 48c22369a1 | ||
|   | 6506077f4d | ||
|   | b1b4266ff3 | ||
|   | 42b0148f93 | ||
|   | 5c2ec16e11 | ||
|   | 8dde143dd5 | ||
|   | 2f00794cf2 | ||
|   | f4674c18e1 | ||
|   | 407798bc3e | ||
|   | 3a90e0ed5e | ||
|   | 5847472b06 | ||
|   | 90491c1233 | ||
|   | 5f99a5a68b | ||
|   | 21289ff0fa | ||
|   | 39b0f7ad02 | ||
|   | ed815ec8bf | ||
|   | 3a197a2e64 | ||
|   | d8327b4180 | ||
|   | 450923476b | ||
|   | 19a4481151 | ||
|   | 8398b2ae59 | 
| @@ -42,6 +42,7 @@ jobs: | ||||
|             - ~/.npm | ||||
|             - ~/.cache | ||||
|             - node_modules | ||||
|       - run: npm run lint | ||||
|       - run: npm run test:coverage -- --browsers=<<parameters.browser>> || <<parameters.always-pass>> | ||||
|       - store_test_results: | ||||
|           path: dist/reports/tests/ | ||||
|   | ||||
							
								
								
									
										1
									
								
								.github/PULL_REQUEST_TEMPLATE.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/PULL_REQUEST_TEMPLATE.md
									
									
									
									
										vendored
									
									
								
							| @@ -2,6 +2,7 @@ | ||||
|  | ||||
| * [ ] Have you followed the guidelines in our [Contributing document](https://github.com/nasa/openmct/blob/master/CONTRIBUTING.md)? | ||||
| * [ ] Have you checked to ensure there aren't other open [Pull Requests](https://github.com/nasa/openmct/pulls) for the same update/change? | ||||
| * [ ] Is this change backwards compatible? For example, developers won't need to change how they are calling the API or how they've extended core plugins such as Tables or Plots. | ||||
|  | ||||
| ### Author Checklist | ||||
|  | ||||
|   | ||||
							
								
								
									
										33
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | ||||
|  | ||||
| name: "CodeQL" | ||||
|  | ||||
| on: | ||||
|   push: | ||||
|     branches: [ master ] | ||||
|   schedule: | ||||
|     - cron: '28 21 * * 3' | ||||
|  | ||||
| jobs: | ||||
|   analyze: | ||||
|     name: Analyze | ||||
|     runs-on: ubuntu-latest | ||||
|     permissions: | ||||
|       actions: read | ||||
|       contents: read | ||||
|       security-events: write | ||||
|  | ||||
|     steps: | ||||
|     - name: Checkout repository | ||||
|       uses: actions/checkout@v2 | ||||
|  | ||||
|     # Initializes the CodeQL tools for scanning. | ||||
|     - name: Initialize CodeQL | ||||
|       uses: github/codeql-action/init@v1 | ||||
|       with: | ||||
|         languages: javascript | ||||
|  | ||||
|     - name: Autobuild | ||||
|       uses: github/codeql-action/autobuild@v1 | ||||
|  | ||||
|     - name: Perform CodeQL Analysis | ||||
|       uses: github/codeql-action/analyze@v1 | ||||
| @@ -317,6 +317,7 @@ checklist). | ||||
| ### Reviewer Checklist | ||||
|  | ||||
| * [ ] Changes appear to address issue? | ||||
| * [ ] Changes appear not to be breaking changes? | ||||
| * [ ] Appropriate unit tests included? | ||||
| * [ ] Code style and in-line documentation are appropriate? | ||||
| * [ ] Commit messages meet standards? | ||||
|   | ||||
| @@ -28,6 +28,15 @@ define([ | ||||
|                         domain: 2 | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     key: "cos", | ||||
|                     name: "Cosine", | ||||
|                     unit: "deg", | ||||
|                     formatString: '%0.2f', | ||||
|                     hints: { | ||||
|                         domain: 3 | ||||
|                     } | ||||
|                 }, | ||||
|                 // Need to enable "LocalTimeSystem" plugin to make use of this | ||||
|                 // { | ||||
|                 //     key: "local", | ||||
| @@ -109,6 +118,100 @@ define([ | ||||
|                     } | ||||
|                 } | ||||
|             ] | ||||
|         }, | ||||
|         'example.spectral-generator': { | ||||
|             values: [ | ||||
|                 { | ||||
|                     key: "name", | ||||
|                     name: "Name", | ||||
|                     format: "string" | ||||
|                 }, | ||||
|                 { | ||||
|                     key: "utc", | ||||
|                     name: "Time", | ||||
|                     format: "utc", | ||||
|                     hints: { | ||||
|                         domain: 1 | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     key: "wavelength", | ||||
|                     name: "Wavelength", | ||||
|                     unit: "Hz", | ||||
|                     formatString: '%0.2f', | ||||
|                     hints: { | ||||
|                         domain: 2, | ||||
|                         spectralAttribute: true | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     key: "cos", | ||||
|                     name: "Cosine", | ||||
|                     unit: "deg", | ||||
|                     formatString: '%0.2f', | ||||
|                     hints: { | ||||
|                         range: 2, | ||||
|                         spectralAttribute: true | ||||
|                     } | ||||
|                 } | ||||
|             ] | ||||
|         }, | ||||
|         'example.spectral-aggregate-generator': { | ||||
|             values: [ | ||||
|                 { | ||||
|                     key: "name", | ||||
|                     name: "Name", | ||||
|                     format: "string" | ||||
|                 }, | ||||
|                 { | ||||
|                     key: "utc", | ||||
|                     name: "Time", | ||||
|                     format: "utc", | ||||
|                     hints: { | ||||
|                         domain: 1 | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     key: "ch1", | ||||
|                     name: "Channel 1", | ||||
|                     format: "string", | ||||
|                     hints: { | ||||
|                         range: 1 | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     key: "ch2", | ||||
|                     name: "Channel 2", | ||||
|                     format: "string", | ||||
|                     hints: { | ||||
|                         range: 2 | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     key: "ch3", | ||||
|                     name: "Channel 3", | ||||
|                     format: "string", | ||||
|                     hints: { | ||||
|                         range: 3 | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     key: "ch4", | ||||
|                     name: "Channel 4", | ||||
|                     format: "string", | ||||
|                     hints: { | ||||
|                         range: 4 | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     key: "ch5", | ||||
|                     name: "Channel 5", | ||||
|                     format: "string", | ||||
|                     hints: { | ||||
|                         range: 5 | ||||
|                     } | ||||
|                 } | ||||
|             ] | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|   | ||||
							
								
								
									
										86
									
								
								example/generator/SpectralAggregateGeneratorProvider.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								example/generator/SpectralAggregateGeneratorProvider.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,86 @@ | ||||
| /***************************************************************************** | ||||
|  * 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. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define([ | ||||
|  | ||||
| ], function ( | ||||
|  | ||||
| ) { | ||||
|  | ||||
|     function SpectralAggregateGeneratorProvider() { | ||||
|  | ||||
|     } | ||||
|  | ||||
|     function pointForTimestamp(timestamp, count, name) { | ||||
|         return { | ||||
|             name: name, | ||||
|             utc: String(Math.floor(timestamp / count) * count), | ||||
|             ch1: String(Math.floor(timestamp / count) % 1), | ||||
|             ch2: String(Math.floor(timestamp / count) % 2), | ||||
|             ch3: String(Math.floor(timestamp / count) % 3), | ||||
|             ch4: String(Math.floor(timestamp / count) % 4), | ||||
|             ch5: String(Math.floor(timestamp / count) % 5) | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     SpectralAggregateGeneratorProvider.prototype.supportsSubscribe = function (domainObject) { | ||||
|         return domainObject.type === 'example.spectral-aggregate-generator'; | ||||
|     }; | ||||
|  | ||||
|     SpectralAggregateGeneratorProvider.prototype.subscribe = function (domainObject, callback) { | ||||
|         var count = 5000; | ||||
|  | ||||
|         var interval = setInterval(function () { | ||||
|             var now = Date.now(); | ||||
|             var datum = pointForTimestamp(now, count, domainObject.name); | ||||
|             callback(datum); | ||||
|         }, count); | ||||
|  | ||||
|         return function () { | ||||
|             clearInterval(interval); | ||||
|         }; | ||||
|     }; | ||||
|  | ||||
|     SpectralAggregateGeneratorProvider.prototype.supportsRequest = function (domainObject, options) { | ||||
|         return domainObject.type === 'example.spectral-aggregate-generator'; | ||||
|     }; | ||||
|  | ||||
|     SpectralAggregateGeneratorProvider.prototype.request = function (domainObject, options) { | ||||
|         var start = options.start; | ||||
|         var end = Math.min(Date.now(), options.end); // no future values | ||||
|         var count = 5000; | ||||
|         if (options.strategy === 'latest' || options.size === 1) { | ||||
|             start = end; | ||||
|         } | ||||
|  | ||||
|         var data = []; | ||||
|         while (start <= end && data.length < 5000) { | ||||
|             data.push(pointForTimestamp(start, count, domainObject.name)); | ||||
|             start += count; | ||||
|         } | ||||
|  | ||||
|         return Promise.resolve(data); | ||||
|     }; | ||||
|  | ||||
|     return SpectralAggregateGeneratorProvider; | ||||
|  | ||||
| }); | ||||
							
								
								
									
										102
									
								
								example/generator/SpectralGeneratorProvider.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								example/generator/SpectralGeneratorProvider.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,102 @@ | ||||
| /***************************************************************************** | ||||
|  * 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. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define([ | ||||
|     './WorkerInterface' | ||||
| ], function ( | ||||
|     WorkerInterface | ||||
| ) { | ||||
|  | ||||
|     var REQUEST_DEFAULTS = { | ||||
|         amplitude: 1, | ||||
|         wavelength: 1, | ||||
|         period: 10, | ||||
|         offset: 0, | ||||
|         dataRateInHz: 1, | ||||
|         randomness: 0, | ||||
|         phase: 0 | ||||
|     }; | ||||
|  | ||||
|     function SpectralGeneratorProvider() { | ||||
|         this.workerInterface = new WorkerInterface(); | ||||
|     } | ||||
|  | ||||
|     SpectralGeneratorProvider.prototype.canProvideTelemetry = function (domainObject) { | ||||
|         return domainObject.type === 'example.spectral-generator'; | ||||
|     }; | ||||
|  | ||||
|     SpectralGeneratorProvider.prototype.supportsRequest = | ||||
|         SpectralGeneratorProvider.prototype.supportsSubscribe = | ||||
|             SpectralGeneratorProvider.prototype.canProvideTelemetry; | ||||
|  | ||||
|     SpectralGeneratorProvider.prototype.makeWorkerRequest = function (domainObject, request = {}) { | ||||
|         var props = [ | ||||
|             'amplitude', | ||||
|             'wavelength', | ||||
|             'period', | ||||
|             'offset', | ||||
|             'dataRateInHz', | ||||
|             'phase', | ||||
|             'randomness' | ||||
|         ]; | ||||
|  | ||||
|         var workerRequest = {}; | ||||
|  | ||||
|         props.forEach(function (prop) { | ||||
|             if (domainObject.telemetry && Object.prototype.hasOwnProperty.call(domainObject.telemetry, prop)) { | ||||
|                 workerRequest[prop] = domainObject.telemetry[prop]; | ||||
|             } | ||||
|  | ||||
|             if (request && Object.prototype.hasOwnProperty.call(request, prop)) { | ||||
|                 workerRequest[prop] = request[prop]; | ||||
|             } | ||||
|  | ||||
|             if (!Object.prototype.hasOwnProperty.call(workerRequest, prop)) { | ||||
|                 workerRequest[prop] = REQUEST_DEFAULTS[prop]; | ||||
|             } | ||||
|  | ||||
|             workerRequest[prop] = Number(workerRequest[prop]); | ||||
|         }); | ||||
|  | ||||
|         workerRequest.name = domainObject.name; | ||||
|  | ||||
|         return workerRequest; | ||||
|     }; | ||||
|  | ||||
|     SpectralGeneratorProvider.prototype.request = function (domainObject, request) { | ||||
|         var workerRequest = this.makeWorkerRequest(domainObject, request); | ||||
|         workerRequest.start = request.start; | ||||
|         workerRequest.end = request.end; | ||||
|         workerRequest.spectra = true; | ||||
|  | ||||
|         return this.workerInterface.request(workerRequest); | ||||
|     }; | ||||
|  | ||||
|     SpectralGeneratorProvider.prototype.subscribe = function (domainObject, callback) { | ||||
|         var workerRequest = this.makeWorkerRequest(domainObject, {}); | ||||
|         workerRequest.spectra = true; | ||||
|  | ||||
|         return this.workerInterface.subscribe(workerRequest, callback); | ||||
|     }; | ||||
|  | ||||
|     return SpectralGeneratorProvider; | ||||
| }); | ||||
| @@ -54,23 +54,38 @@ | ||||
|         var start = Date.now(); | ||||
|         var step = 1000 / data.dataRateInHz; | ||||
|         var nextStep = start - (start % step) + step; | ||||
|         let work; | ||||
|         if (data.spectra) { | ||||
|             work = function (now) { | ||||
|                 while (nextStep < now) { | ||||
|                     const messageCopy = Object.create(message); | ||||
|                     message.data.start = nextStep - (60 * 1000); | ||||
|                     message.data.end = nextStep; | ||||
|                     onRequest(messageCopy); | ||||
|                     nextStep += step; | ||||
|                 } | ||||
|  | ||||
|         function work(now) { | ||||
|             while (nextStep < now) { | ||||
|                 self.postMessage({ | ||||
|                     id: message.id, | ||||
|                     data: { | ||||
|                         name: data.name, | ||||
|                         utc: nextStep, | ||||
|                         yesterday: nextStep - 60 * 60 * 24 * 1000, | ||||
|                         sin: sin(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness), | ||||
|                         cos: cos(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness) | ||||
|                     } | ||||
|                 }); | ||||
|                 nextStep += step; | ||||
|             } | ||||
|                 return nextStep; | ||||
|             }; | ||||
|         } else { | ||||
|             work = function (now) { | ||||
|                 while (nextStep < now) { | ||||
|                     self.postMessage({ | ||||
|                         id: message.id, | ||||
|                         data: { | ||||
|                             name: data.name, | ||||
|                             utc: nextStep, | ||||
|                             yesterday: nextStep - 60 * 60 * 24 * 1000, | ||||
|                             sin: sin(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness), | ||||
|                             wavelength: wavelength(start, nextStep), | ||||
|                             cos: cos(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness) | ||||
|                         } | ||||
|                     }); | ||||
|                     nextStep += step; | ||||
|                 } | ||||
|  | ||||
|             return nextStep; | ||||
|                 return nextStep; | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         subscriptions[message.id] = work; | ||||
| @@ -111,13 +126,21 @@ | ||||
|                 utc: nextStep, | ||||
|                 yesterday: nextStep - 60 * 60 * 24 * 1000, | ||||
|                 sin: sin(nextStep, period, amplitude, offset, phase, randomness), | ||||
|                 wavelength: wavelength(start, nextStep), | ||||
|                 cos: cos(nextStep, period, amplitude, offset, phase, randomness) | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         self.postMessage({ | ||||
|             id: message.id, | ||||
|             data: data | ||||
|             data: request.spectra ? { | ||||
|                 wavelength: data.map((item) => { | ||||
|                     return item.wavelength; | ||||
|                 }), | ||||
|                 cos: data.map((item) => { | ||||
|                     return item.cos; | ||||
|                 }) | ||||
|             } : data | ||||
|         }); | ||||
|     } | ||||
|  | ||||
| @@ -131,6 +154,10 @@ | ||||
|             * Math.sin(phase + (timestamp / period / 1000 * Math.PI * 2)) + (amplitude * Math.random() * randomness) + offset; | ||||
|     } | ||||
|  | ||||
|     function wavelength(start, nextStep) { | ||||
|         return (nextStep - start) / 10; | ||||
|     } | ||||
|  | ||||
|     function sendError(error, message) { | ||||
|         self.postMessage({ | ||||
|             error: error.name + ': ' + error.message, | ||||
|   | ||||
| @@ -24,11 +24,15 @@ define([ | ||||
|     "./GeneratorProvider", | ||||
|     "./SinewaveLimitProvider", | ||||
|     "./StateGeneratorProvider", | ||||
|     "./SpectralGeneratorProvider", | ||||
|     "./SpectralAggregateGeneratorProvider", | ||||
|     "./GeneratorMetadataProvider" | ||||
| ], function ( | ||||
|     GeneratorProvider, | ||||
|     SinewaveLimitProvider, | ||||
|     StateGeneratorProvider, | ||||
|     SpectralGeneratorProvider, | ||||
|     SpectralAggregateGeneratorProvider, | ||||
|     GeneratorMetadataProvider | ||||
| ) { | ||||
|  | ||||
| @@ -61,6 +65,37 @@ define([ | ||||
|  | ||||
|         openmct.telemetry.addProvider(new StateGeneratorProvider()); | ||||
|  | ||||
|         openmct.types.addType("example.spectral-generator", { | ||||
|             name: "Spectral Generator", | ||||
|             description: "For development use. Generates example streaming telemetry data using a simple sine wave algorithm.", | ||||
|             cssClass: "icon-generator-telemetry", | ||||
|             creatable: true, | ||||
|             initialize: function (object) { | ||||
|                 object.telemetry = { | ||||
|                     period: 10, | ||||
|                     amplitude: 1, | ||||
|                     wavelength: 1, | ||||
|                     frequency: 1, | ||||
|                     offset: 0, | ||||
|                     dataRateInHz: 1, | ||||
|                     phase: 0, | ||||
|                     randomness: 0 | ||||
|                 }; | ||||
|             } | ||||
|         }); | ||||
|         openmct.telemetry.addProvider(new SpectralGeneratorProvider()); | ||||
|  | ||||
|         openmct.types.addType("example.spectral-aggregate-generator", { | ||||
|             name: "Spectral Aggregate Generator", | ||||
|             description: "For development use. Generates example streaming telemetry data using a simple state algorithm.", | ||||
|             cssClass: "icon-generator-telemetry", | ||||
|             creatable: true, | ||||
|             initialize: function (object) { | ||||
|                 object.telemetry = {}; | ||||
|             } | ||||
|         }); | ||||
|         openmct.telemetry.addProvider(new SpectralAggregateGeneratorProvider()); | ||||
|  | ||||
|         openmct.types.addType("generator", { | ||||
|             name: "Sine Wave Generator", | ||||
|             description: "For development use. Generates example streaming telemetry data using a simple sine wave algorithm.", | ||||
|   | ||||
| @@ -195,6 +195,7 @@ | ||||
|             ['table', 'telemetry.plot.overlay', 'telemetry.plot.stacked'], | ||||
|             {indicator: true} | ||||
|         )); | ||||
|         openmct.install(openmct.plugins.Clock({ enableClockIndicator: true })); | ||||
|         openmct.start(); | ||||
|     </script> | ||||
| </html> | ||||
|   | ||||
| @@ -25,7 +25,7 @@ | ||||
| const devMode = process.env.NODE_ENV !== 'production'; | ||||
| const browsers = [process.env.NODE_ENV === 'debug' ? 'ChromeDebugging' : 'ChromeHeadless']; | ||||
| const coverageEnabled = process.env.COVERAGE === 'true'; | ||||
| const reporters = ['progress', 'html', 'junit']; | ||||
| const reporters = ['spec', 'html', 'junit']; | ||||
|  | ||||
| if (coverageEnabled) { | ||||
|     reporters.push('coverage-istanbul'); | ||||
| @@ -60,7 +60,7 @@ module.exports = (config) => { | ||||
|         client: { | ||||
|             jasmine: { | ||||
|                 random: false, | ||||
|                 timeoutInterval: 30000 | ||||
|                 timeoutInterval: 5000 | ||||
|             } | ||||
|         }, | ||||
|         customLaunchers: { | ||||
| @@ -88,11 +88,6 @@ module.exports = (config) => { | ||||
|             outputFile: "test-results.xml", | ||||
|             useBrowserName: false | ||||
|         }, | ||||
|         browserConsoleLogOptions: { | ||||
|             level: "error", | ||||
|             format: "%b %T: %m", | ||||
|             terminal: true | ||||
|         }, | ||||
|         coverageIstanbulReporter: { | ||||
|             fixWebpackSourcePaths: true, | ||||
|             dir: process.env.CIRCLE_ARTIFACTS | ||||
| @@ -105,6 +100,15 @@ module.exports = (config) => { | ||||
|                 } | ||||
|             } | ||||
|         }, | ||||
|         specReporter: { | ||||
|             maxLogLines: 5, | ||||
|             suppressErrorSummary: true, | ||||
|             suppressFailed: false, | ||||
|             suppressPassed: false, | ||||
|             suppressSkipped: true, | ||||
|             showSpecTiming: true, | ||||
|             failFast: false | ||||
|         }, | ||||
|         preprocessors: { | ||||
|             'indexTest.js': ['webpack', 'sourcemap'] | ||||
|         }, | ||||
|   | ||||
							
								
								
									
										16
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								package.json
									
									
									
									
									
								
							| @@ -2,7 +2,6 @@ | ||||
|   "name": "openmct", | ||||
|   "version": "1.7.8-SNAPSHOT", | ||||
|   "description": "The Open MCT core platform", | ||||
|   "dependencies": {}, | ||||
|   "devDependencies": { | ||||
|     "angular": ">=1.8.0", | ||||
|     "angular-route": "1.4.14", | ||||
| @@ -12,16 +11,9 @@ | ||||
|     "copy-webpack-plugin": "^4.5.2", | ||||
|     "cross-env": "^6.0.3", | ||||
|     "css-loader": "^1.0.0", | ||||
|     "d3-array": "1.2.x", | ||||
|     "d3-axis": "1.0.x", | ||||
|     "d3-collection": "1.0.x", | ||||
|     "d3-color": "1.0.x", | ||||
|     "d3-format": "1.2.x", | ||||
|     "d3-interpolate": "1.1.x", | ||||
|     "d3-scale": "1.0.x", | ||||
|     "d3-selection": "1.3.x", | ||||
|     "d3-time": "1.0.x", | ||||
|     "d3-time-format": "2.1.x", | ||||
|     "eslint": "7.0.0", | ||||
|     "eslint-plugin-vue": "^7.5.0", | ||||
|     "eslint-plugin-you-dont-need-lodash-underscore": "^6.10.0", | ||||
| @@ -41,14 +33,15 @@ | ||||
|     "jsdoc": "^3.3.2", | ||||
|     "karma": "6.3.4", | ||||
|     "karma-chrome-launcher": "3.1.0", | ||||
|     "karma-firefox-launcher": "2.1.1", | ||||
|     "karma-cli": "2.0.0", | ||||
|     "karma-coverage": "2.0.3", | ||||
|     "karma-coverage-istanbul-reporter": "3.0.3", | ||||
|     "karma-junit-reporter": "2.0.1", | ||||
|     "karma-firefox-launcher": "2.1.1", | ||||
|     "karma-html-reporter": "0.2.7", | ||||
|     "karma-jasmine": "4.0.1", | ||||
|     "karma-junit-reporter": "2.0.1", | ||||
|     "karma-sourcemap-loader": "0.3.8", | ||||
|     "karma-spec-reporter": "0.0.32", | ||||
|     "karma-webpack": "4.0.2", | ||||
|     "location-bar": "^3.0.1", | ||||
|     "lodash": "^4.17.12", | ||||
| @@ -62,6 +55,8 @@ | ||||
|     "node-bourbon": "^4.2.3", | ||||
|     "node-sass": "^4.14.1", | ||||
|     "painterro": "^1.2.56", | ||||
|     "plotly.js-basic-dist": "^2.5.0", | ||||
|     "plotly.js-gl2d-dist": "^2.5.0", | ||||
|     "printj": "^1.2.1", | ||||
|     "raw-loader": "^0.5.1", | ||||
|     "request": "^2.69.0", | ||||
| @@ -70,6 +65,7 @@ | ||||
|     "uuid": "^3.3.3", | ||||
|     "v8-compile-cache": "^1.1.0", | ||||
|     "vue": "2.5.6", | ||||
|     "vue-eslint-parser": "7.11.0", | ||||
|     "vue-loader": "^15.2.6", | ||||
|     "vue-template-compiler": "2.5.6", | ||||
|     "webpack": "^4.16.2", | ||||
|   | ||||
| @@ -50,8 +50,6 @@ define( | ||||
|          * or finish() are called. | ||||
|          */ | ||||
|         EditorCapability.prototype.edit = function () { | ||||
|             console.warn('DEPRECATED: cannot edit via edit capability, use openmct.editor instead.'); | ||||
|  | ||||
|             if (!this.openmct.editor.isEditing()) { | ||||
|                 this.openmct.editor.edit(); | ||||
|                 this.domainObject.getCapability('status').set('editing', true); | ||||
| @@ -82,8 +80,6 @@ define( | ||||
|          * @returns {*} | ||||
|          */ | ||||
|         EditorCapability.prototype.save = function () { | ||||
|             console.warn('DEPRECATED: cannot save via edit capability, use openmct.editor instead.'); | ||||
|  | ||||
|             return Promise.resolve(); | ||||
|         }; | ||||
|  | ||||
| @@ -95,8 +91,6 @@ define( | ||||
|          * @returns {*} | ||||
|          */ | ||||
|         EditorCapability.prototype.finish = function () { | ||||
|             console.warn('DEPRECATED: cannot finish via edit capability, use openmct.editor instead.'); | ||||
|  | ||||
|             return Promise.resolve(); | ||||
|         }; | ||||
|  | ||||
|   | ||||
| @@ -25,15 +25,14 @@ define([ | ||||
| ], function ( | ||||
|     moment | ||||
| ) { | ||||
|  | ||||
|     var DATE_FORMAT = "YYYY-MM-DD HH:mm:ss.SSS", | ||||
|         DATE_FORMATS = [ | ||||
|             DATE_FORMAT, | ||||
|             DATE_FORMAT + "Z", | ||||
|             "YYYY-MM-DD HH:mm:ss", | ||||
|             "YYYY-MM-DD HH:mm", | ||||
|             "YYYY-MM-DD" | ||||
|         ]; | ||||
|     const DATE_FORMAT = "YYYY-MM-DD HH:mm:ss.SSS"; | ||||
|     const DATE_FORMATS = [ | ||||
|         DATE_FORMAT, | ||||
|         DATE_FORMAT + "Z", | ||||
|         "YYYY-MM-DD HH:mm:ss", | ||||
|         "YYYY-MM-DD HH:mm", | ||||
|         "YYYY-MM-DD" | ||||
|     ]; | ||||
|  | ||||
|     /** | ||||
|      * @typedef Scale | ||||
| @@ -53,15 +52,27 @@ define([ | ||||
|         this.key = "utc"; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @param {string} formatString | ||||
|      * @returns the value of formatString if the value is a string type and exists in the DATE_FORMATS array; otherwise the DATE_FORMAT value. | ||||
|      */ | ||||
|     function validateFormatString(formatString) { | ||||
|         return typeof formatString === 'string' && DATE_FORMATS.includes(formatString) ? formatString : DATE_FORMAT; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @param {number} value The value to format. | ||||
|      * @returns {string} the formatted date(s). If multiple values were requested, then an array of | ||||
|      * @param {string} formatString The string format to format. Default "YYYY-MM-DD HH:mm:ss.SSS" + "Z" | ||||
|      * @returns {string} the formatted date(s) according to the proper parameter of formatString or the default value of "YYYY-MM-DD HH:mm:ss.SSS" + "Z". | ||||
|      * If multiple values were requested, then an array of | ||||
|      * formatted values will be returned. Where a value could not be formatted, `undefined` will be returned at its position | ||||
|      * in the array. | ||||
|      */ | ||||
|     UTCTimeFormat.prototype.format = function (value) { | ||||
|     UTCTimeFormat.prototype.format = function (value, formatString) { | ||||
|         if (value !== undefined) { | ||||
|             return moment.utc(value).format(DATE_FORMAT) + "Z"; | ||||
|             const format = validateFormatString(formatString); | ||||
|  | ||||
|             return moment.utc(value).format(format) + (formatString ? '' : 'Z'); | ||||
|         } else { | ||||
|             return value; | ||||
|         } | ||||
|   | ||||
| @@ -21,32 +21,24 @@ | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define([ | ||||
|     "moment-timezone", | ||||
|     "./src/indicators/ClockIndicator", | ||||
|     "./src/services/TickerService", | ||||
|     "./src/services/TimerService", | ||||
|     "./src/controllers/ClockController", | ||||
|     "./src/controllers/TimerController", | ||||
|     "./src/controllers/RefreshingController", | ||||
|     "./src/actions/StartTimerAction", | ||||
|     "./src/actions/RestartTimerAction", | ||||
|     "./src/actions/StopTimerAction", | ||||
|     "./src/actions/PauseTimerAction", | ||||
|     "./res/templates/clock.html", | ||||
|     "./res/templates/timer.html" | ||||
| ], function ( | ||||
|     MomentTimezone, | ||||
|     ClockIndicator, | ||||
|     TickerService, | ||||
|     TimerService, | ||||
|     ClockController, | ||||
|     TimerController, | ||||
|     RefreshingController, | ||||
|     StartTimerAction, | ||||
|     RestartTimerAction, | ||||
|     StopTimerAction, | ||||
|     PauseTimerAction, | ||||
|     clockTemplate, | ||||
|     timerTemplate | ||||
| ) { | ||||
|     return { | ||||
| @@ -73,16 +65,6 @@ define([ | ||||
|                         "value": "YYYY/MM/DD HH:mm:ss" | ||||
|                     } | ||||
|                 ], | ||||
|                 "indicators": [ | ||||
|                     { | ||||
|                         "implementation": ClockIndicator, | ||||
|                         "depends": [ | ||||
|                             "tickerService", | ||||
|                             "CLOCK_INDICATOR_FORMAT" | ||||
|                         ], | ||||
|                         "priority": "preferred" | ||||
|                     } | ||||
|                 ], | ||||
|                 "services": [ | ||||
|                     { | ||||
|                         "key": "tickerService", | ||||
| @@ -99,14 +81,6 @@ define([ | ||||
|                     } | ||||
|                 ], | ||||
|                 "controllers": [ | ||||
|                     { | ||||
|                         "key": "ClockController", | ||||
|                         "implementation": ClockController, | ||||
|                         "depends": [ | ||||
|                             "$scope", | ||||
|                             "tickerService" | ||||
|                         ] | ||||
|                     }, | ||||
|                     { | ||||
|                         "key": "TimerController", | ||||
|                         "implementation": TimerController, | ||||
| @@ -126,12 +100,6 @@ define([ | ||||
|                     } | ||||
|                 ], | ||||
|                 "views": [ | ||||
|                     { | ||||
|                         "key": "clock", | ||||
|                         "type": "clock", | ||||
|                         "editable": false, | ||||
|                         "template": clockTemplate | ||||
|                     }, | ||||
|                     { | ||||
|                         "key": "timer", | ||||
|                         "type": "timer", | ||||
| @@ -186,70 +154,6 @@ define([ | ||||
|                     } | ||||
|                 ], | ||||
|                 "types": [ | ||||
|                     { | ||||
|                         "key": "clock", | ||||
|                         "name": "Clock", | ||||
|                         "cssClass": "icon-clock", | ||||
|                         "description": "A UTC-based clock that supports a variety of display formats. Clocks can be added to Display Layouts.", | ||||
|                         "priority": 101, | ||||
|                         "features": [ | ||||
|                             "creation" | ||||
|                         ], | ||||
|                         "properties": [ | ||||
|                             { | ||||
|                                 "key": "clockFormat", | ||||
|                                 "name": "Display Format", | ||||
|                                 "control": "composite", | ||||
|                                 "items": [ | ||||
|                                     { | ||||
|                                         "control": "select", | ||||
|                                         "options": [ | ||||
|                                             { | ||||
|                                                 "value": "YYYY/MM/DD hh:mm:ss", | ||||
|                                                 "name": "YYYY/MM/DD hh:mm:ss" | ||||
|                                             }, | ||||
|                                             { | ||||
|                                                 "value": "YYYY/DDD hh:mm:ss", | ||||
|                                                 "name": "YYYY/DDD hh:mm:ss" | ||||
|                                             }, | ||||
|                                             { | ||||
|                                                 "value": "hh:mm:ss", | ||||
|                                                 "name": "hh:mm:ss" | ||||
|                                             } | ||||
|                                         ], | ||||
|                                         "cssClass": "l-inline" | ||||
|                                     }, | ||||
|                                     { | ||||
|                                         "control": "select", | ||||
|                                         "options": [ | ||||
|                                             { | ||||
|                                                 "value": "clock12", | ||||
|                                                 "name": "12hr" | ||||
|                                             }, | ||||
|                                             { | ||||
|                                                 "value": "clock24", | ||||
|                                                 "name": "24hr" | ||||
|                                             } | ||||
|                                         ], | ||||
|                                         "cssClass": "l-inline" | ||||
|                                     } | ||||
|                                 ] | ||||
|                             }, | ||||
|                             { | ||||
|                                 "key": "timezone", | ||||
|                                 "name": "Timezone", | ||||
|                                 "control": "autocomplete", | ||||
|                                 "options": MomentTimezone.tz.names() | ||||
|                             } | ||||
|                         ], | ||||
|                         "model": { | ||||
|                             "clockFormat": [ | ||||
|                                 "YYYY/MM/DD hh:mm:ss", | ||||
|                                 "clock12" | ||||
|                             ], | ||||
|                             "timezone": "UTC" | ||||
|                         } | ||||
|                     }, | ||||
|                     { | ||||
|                         "key": "timer", | ||||
|                         "name": "Timer", | ||||
|   | ||||
| @@ -1,32 +0,0 @@ | ||||
| <!-- | ||||
|  Open MCT, Copyright (c) 2009-2016, 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. | ||||
| --> | ||||
| <div class="c-clock l-time-display u-style-receiver js-style-receiver" ng-controller="ClockController as clock"> | ||||
| 	<div class="c-clock__timezone"> | ||||
| 		{{clock.zone()}} | ||||
| 	</div> | ||||
| 	<div class="c-clock__value"> | ||||
| 		{{clock.text()}} | ||||
| 	</div> | ||||
| 	<div class="c-clock__ampm"> | ||||
| 		{{clock.ampm()}} | ||||
| 	</div> | ||||
| </div> | ||||
| @@ -1,110 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2009-2016, 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. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define([ | ||||
|     'moment', | ||||
|     'moment-timezone' | ||||
| ], | ||||
| function ( | ||||
|     moment, | ||||
|     momentTimezone | ||||
| ) { | ||||
|  | ||||
|     /** | ||||
|          * Controller for views of a Clock domain object. | ||||
|          * | ||||
|          * @constructor | ||||
|          * @memberof platform/features/clock | ||||
|          * @param {angular.Scope} $scope the Angular scope | ||||
|          * @param {platform/features/clock.TickerService} tickerService | ||||
|          *        a service used to align behavior with clock ticks | ||||
|          */ | ||||
|     function ClockController($scope, tickerService) { | ||||
|         var lastTimestamp, | ||||
|             unlisten, | ||||
|             timeFormat, | ||||
|             zoneName, | ||||
|             self = this; | ||||
|  | ||||
|         function update() { | ||||
|             var m = zoneName | ||||
|                 ? moment.utc(lastTimestamp).tz(zoneName) : moment.utc(lastTimestamp); | ||||
|             self.zoneAbbr = m.zoneAbbr(); | ||||
|             self.textValue = timeFormat && m.format(timeFormat); | ||||
|             self.ampmValue = m.format("A"); // Just the AM or PM part | ||||
|         } | ||||
|  | ||||
|         function tick(timestamp) { | ||||
|             lastTimestamp = timestamp; | ||||
|             update(); | ||||
|         } | ||||
|  | ||||
|         function updateModel(model) { | ||||
|             var baseFormat; | ||||
|             if (model !== undefined) { | ||||
|                 baseFormat = model.clockFormat[0]; | ||||
|  | ||||
|                 self.use24 = model.clockFormat[1] === 'clock24'; | ||||
|                 timeFormat = self.use24 | ||||
|                     ? baseFormat.replace('hh', "HH") : baseFormat; | ||||
|                 // If wrong timezone is provided, the UTC will be used | ||||
|                 zoneName = momentTimezone.tz.names().includes(model.timezone) | ||||
|                     ? model.timezone : "UTC"; | ||||
|                 update(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // Pull in the model (clockFormat and timezone) from the domain object model | ||||
|         $scope.$watch('model', updateModel); | ||||
|  | ||||
|         // Listen for clock ticks ... and stop listening on destroy | ||||
|         unlisten = tickerService.listen(tick); | ||||
|         $scope.$on('$destroy', unlisten); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|          * Get the clock's time zone, as displayable text. | ||||
|          * @returns {string} | ||||
|          */ | ||||
|     ClockController.prototype.zone = function () { | ||||
|         return this.zoneAbbr; | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|          * Get the current time, as displayable text. | ||||
|          * @returns {string} | ||||
|          */ | ||||
|     ClockController.prototype.text = function () { | ||||
|         return this.textValue; | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|          * Get the text to display to qualify a time as AM or PM. | ||||
|          * @returns {string} | ||||
|          */ | ||||
|     ClockController.prototype.ampm = function () { | ||||
|         return this.use24 ? '' : this.ampmValue; | ||||
|     }; | ||||
|  | ||||
|     return ClockController; | ||||
| } | ||||
| ); | ||||
| @@ -1,65 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2009-2016, 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. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define( | ||||
|     ['moment'], | ||||
|     function (moment) { | ||||
|  | ||||
|         /** | ||||
|          * Indicator that displays the current UTC time in the status area. | ||||
|          * @implements {Indicator} | ||||
|          * @memberof platform/features/clock | ||||
|          * @param {platform/features/clock.TickerService} tickerService | ||||
|          *        a service used to align behavior with clock ticks | ||||
|          * @param {string} indicatorFormat format string for timestamps | ||||
|          *        shown in this indicator | ||||
|          */ | ||||
|         function ClockIndicator(tickerService, indicatorFormat) { | ||||
|             var self = this; | ||||
|  | ||||
|             this.text = ""; | ||||
|  | ||||
|             tickerService.listen(function (timestamp) { | ||||
|                 self.text = moment.utc(timestamp) | ||||
|                     .format(indicatorFormat) + " UTC"; | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         ClockIndicator.prototype.getGlyphClass = function () { | ||||
|             return ""; | ||||
|         }; | ||||
|  | ||||
|         ClockIndicator.prototype.getCssClass = function () { | ||||
|             return "t-indicator-clock icon-clock no-minify c-indicator--not-clickable"; | ||||
|         }; | ||||
|  | ||||
|         ClockIndicator.prototype.getText = function () { | ||||
|             return this.text; | ||||
|         }; | ||||
|  | ||||
|         ClockIndicator.prototype.getDescription = function () { | ||||
|             return ""; | ||||
|         }; | ||||
|  | ||||
|         return ClockIndicator; | ||||
|     } | ||||
| ); | ||||
| @@ -1,107 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2009-2017, 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. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define( | ||||
|     ["../../src/controllers/ClockController"], | ||||
|     function (ClockController) { | ||||
|  | ||||
|         // Wed, 03 Jun 2015 17:56:14 GMT | ||||
|         var TEST_TIMESTAMP = 1433354174000; | ||||
|  | ||||
|         describe("A clock view's controller", function () { | ||||
|             var mockScope, | ||||
|                 mockTicker, | ||||
|                 mockUnticker, | ||||
|                 controller; | ||||
|  | ||||
|             beforeEach(function () { | ||||
|                 mockScope = jasmine.createSpyObj('$scope', ['$watch', '$on']); | ||||
|                 mockTicker = jasmine.createSpyObj('ticker', ['listen']); | ||||
|                 mockUnticker = jasmine.createSpy('unticker'); | ||||
|  | ||||
|                 mockTicker.listen.and.returnValue(mockUnticker); | ||||
|  | ||||
|                 controller = new ClockController(mockScope, mockTicker); | ||||
|             }); | ||||
|  | ||||
|             it("watches for model (clockFormat and timezone) from the domain object model", function () { | ||||
|                 expect(mockScope.$watch).toHaveBeenCalledWith( | ||||
|                     "model", | ||||
|                     jasmine.any(Function) | ||||
|                 ); | ||||
|             }); | ||||
|  | ||||
|             it("subscribes to clock ticks", function () { | ||||
|                 expect(mockTicker.listen) | ||||
|                     .toHaveBeenCalledWith(jasmine.any(Function)); | ||||
|             }); | ||||
|  | ||||
|             it("unsubscribes to ticks when destroyed", function () { | ||||
|                 // Make sure $destroy is being listened for... | ||||
|                 expect(mockScope.$on.calls.mostRecent().args[0]).toEqual('$destroy'); | ||||
|                 expect(mockUnticker).not.toHaveBeenCalled(); | ||||
|  | ||||
|                 // ...and makes sure that its listener unsubscribes from ticker | ||||
|                 mockScope.$on.calls.mostRecent().args[1](); | ||||
|                 expect(mockUnticker).toHaveBeenCalled(); | ||||
|             }); | ||||
|  | ||||
|             it("formats using the format string from the model", function () { | ||||
|                 mockTicker.listen.calls.mostRecent().args[0](TEST_TIMESTAMP); | ||||
|                 mockScope.$watch.calls.mostRecent().args[1]({ | ||||
|                     "clockFormat": [ | ||||
|                         "YYYY-DDD hh:mm:ss", | ||||
|                         "clock24" | ||||
|                     ], | ||||
|                     "timezone": "Canada/Eastern" | ||||
|                 }); | ||||
|  | ||||
|                 expect(controller.zone()).toEqual("EDT"); | ||||
|                 expect(controller.text()).toEqual("2015-154 13:56:14"); | ||||
|                 expect(controller.ampm()).toEqual(""); | ||||
|             }); | ||||
|  | ||||
|             it("formats 12-hour time", function () { | ||||
|                 mockTicker.listen.calls.mostRecent().args[0](TEST_TIMESTAMP); | ||||
|                 mockScope.$watch.calls.mostRecent().args[1]({ | ||||
|                     "clockFormat": [ | ||||
|                         "YYYY-DDD hh:mm:ss", | ||||
|                         "clock12" | ||||
|                     ], | ||||
|                     "timezone": "" | ||||
|                 }); | ||||
|  | ||||
|                 expect(controller.zone()).toEqual("UTC"); | ||||
|                 expect(controller.text()).toEqual("2015-154 05:56:14"); | ||||
|                 expect(controller.ampm()).toEqual("PM"); | ||||
|             }); | ||||
|  | ||||
|             it("does not throw exceptions when model is undefined", function () { | ||||
|                 mockTicker.listen.calls.mostRecent().args[0](TEST_TIMESTAMP); | ||||
|                 expect(function () { | ||||
|                     mockScope.$watch.calls.mostRecent().args[1](undefined); | ||||
|                 }).not.toThrow(); | ||||
|             }); | ||||
|  | ||||
|         }); | ||||
|     } | ||||
| ); | ||||
| @@ -1,58 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2009-2016, 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. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define( | ||||
|     ["../../src/indicators/ClockIndicator"], | ||||
|     function (ClockIndicator) { | ||||
|  | ||||
|         // Wed, 03 Jun 2015 17:56:14 GMT | ||||
|         var TEST_TIMESTAMP = 1433354174000, | ||||
|             TEST_FORMAT = "YYYY-DDD HH:mm:ss"; | ||||
|  | ||||
|         describe("The clock indicator", function () { | ||||
|             var mockTicker, | ||||
|                 mockUnticker, | ||||
|                 indicator; | ||||
|  | ||||
|             beforeEach(function () { | ||||
|                 mockTicker = jasmine.createSpyObj('ticker', ['listen']); | ||||
|                 mockUnticker = jasmine.createSpy('unticker'); | ||||
|  | ||||
|                 mockTicker.listen.and.returnValue(mockUnticker); | ||||
|  | ||||
|                 indicator = new ClockIndicator(mockTicker, TEST_FORMAT); | ||||
|             }); | ||||
|  | ||||
|             it("displays the current time", function () { | ||||
|                 mockTicker.listen.calls.mostRecent().args[0](TEST_TIMESTAMP); | ||||
|                 expect(indicator.getText()).toEqual("2015-154 17:56:14 UTC"); | ||||
|             }); | ||||
|  | ||||
|             it("implements the Indicator interface", function () { | ||||
|                 expect(indicator.getCssClass()).toEqual(jasmine.any(String)); | ||||
|                 expect(indicator.getText()).toEqual(jasmine.any(String)); | ||||
|                 expect(indicator.getDescription()).toEqual(jasmine.any(String)); | ||||
|             }); | ||||
|  | ||||
|         }); | ||||
|     } | ||||
| ); | ||||
| @@ -30,8 +30,8 @@ define([ | ||||
|  | ||||
|     return function ImportExportPlugin() { | ||||
|         return function (openmct) { | ||||
|             ExportAsJSONAction.appliesTo = function (context) { | ||||
|                 return openmct.$injector.get('policyService') | ||||
|             ExportAsJSONAction.prototype.appliesTo = function (context) { | ||||
|                 return this.openmct.$injector.get('policyService') | ||||
|                     .allow("creation", context.domainObject.getCapability("type") | ||||
|                     ); | ||||
|             }; | ||||
|   | ||||
| @@ -29,7 +29,7 @@ define( | ||||
|     ], | ||||
|     function (ExportAsJSONAction, domainObjectFactory, MCT, AdapterCapability) { | ||||
|  | ||||
|         xdescribe("The export JSON action", function () { | ||||
|         describe("The export JSON action", function () { | ||||
|  | ||||
|             var context, | ||||
|                 action, | ||||
| @@ -102,7 +102,7 @@ define( | ||||
|                 expect(action).toBeDefined(); | ||||
|             }); | ||||
|  | ||||
|             it("doesn't export non-creatable objects in tree", function () { | ||||
|             xit("doesn't export non-creatable objects in tree", function () { | ||||
|                 var nonCreatableType = { | ||||
|                     hasFeature: | ||||
|                         function (feature) { | ||||
| @@ -149,7 +149,7 @@ define( | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|             it("can export self-containing objects", function () { | ||||
|             xit("can export self-containing objects", function () { | ||||
|                 var parent = domainObjectFactory({ | ||||
|                     name: 'parent', | ||||
|                     model: { | ||||
| @@ -191,7 +191,7 @@ define( | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|             it("exports links to external objects as new objects", function () { | ||||
|             xit("exports links to external objects as new objects", function () { | ||||
|                 var parent = domainObjectFactory({ | ||||
|                     name: 'parent', | ||||
|                     model: { | ||||
|   | ||||
| @@ -27,7 +27,7 @@ define( | ||||
|     ], | ||||
|     function (ImportAsJSONAction, domainObjectFactory) { | ||||
|  | ||||
|         xdescribe("The import JSON action", function () { | ||||
|         describe("The import JSON action", function () { | ||||
|  | ||||
|             var context = {}; | ||||
|             var action, | ||||
| @@ -146,7 +146,7 @@ define( | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|             it("can import self-containing objects", function () { | ||||
|             xit("can import self-containing objects", function () { | ||||
|                 var compDomainObject = domainObjectFactory({ | ||||
|                     name: 'compObject', | ||||
|                     model: { name: 'compObject'}, | ||||
| @@ -198,7 +198,7 @@ define( | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|             it("assigns new ids to each imported object", function () { | ||||
|             xit("assigns new ids to each imported object", function () { | ||||
|                 dialogService.getUserInput.and.returnValue(Promise.resolve( | ||||
|                     { | ||||
|                         selectFile: { | ||||
|   | ||||
| @@ -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. | ||||
|   | ||||
| @@ -28,8 +28,6 @@ export default function LegacyActionAdapter(openmct, legacyActions) { | ||||
|             return true; | ||||
|         } | ||||
|  | ||||
|         console.warn(`DEPRECATION WARNING: Action ${action.definition.key} in bundle ${action.bundle.path} is non-contextual and should be migrated.`); | ||||
|  | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -29,7 +29,6 @@ define([ | ||||
|     './capabilities/APICapabilityDecorator', | ||||
|     './policies/AdaptedViewPolicy', | ||||
|     './runs/AlternateCompositionInitializer', | ||||
|     './runs/TypeDeprecationChecker', | ||||
|     './runs/LegacyTelemetryProvider', | ||||
|     './runs/RegisterLegacyTypes', | ||||
|     './services/LegacyObjectAPIInterceptor', | ||||
| @@ -46,7 +45,6 @@ define([ | ||||
|     APICapabilityDecorator, | ||||
|     AdaptedViewPolicy, | ||||
|     AlternateCompositionInitializer, | ||||
|     TypeDeprecationChecker, | ||||
|     LegacyTelemetryProvider, | ||||
|     RegisterLegacyTypes, | ||||
|     LegacyObjectAPIInterceptor, | ||||
| @@ -135,10 +133,6 @@ define([ | ||||
|                     } | ||||
|                 ], | ||||
|                 runs: [ | ||||
|                     { | ||||
|                         implementation: TypeDeprecationChecker, | ||||
|                         depends: ["types[]"] | ||||
|                     }, | ||||
|                     { | ||||
|                         implementation: AlternateCompositionInitializer, | ||||
|                         depends: ["openmct"] | ||||
|   | ||||
| @@ -4,12 +4,6 @@ define([ | ||||
|  | ||||
| ) { | ||||
|     function RegisterLegacyTypes(types, openmct) { | ||||
|         types.forEach(function (legacyDefinition) { | ||||
|             if (!openmct.types.get(legacyDefinition.key)) { | ||||
|                 console.warn(`DEPRECATION WARNING: Migrate type ${legacyDefinition.key} from ${legacyDefinition.bundle.path} to use the new Types API.  Legacy type support will be removed soon.`); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         openmct.types.importLegacyTypes(types); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -15,8 +15,6 @@ define([ | ||||
|     }; | ||||
|  | ||||
|     function LegacyViewProvider(legacyView, openmct, convertToLegacyObject) { | ||||
|         console.warn(`DEPRECATION WARNING: Migrate ${legacyView.key} from ${legacyView.bundle.path} to use the new View APIs.  Legacy view support will be removed soon.`); | ||||
|  | ||||
|         return { | ||||
|             key: legacyView.key, | ||||
|             name: legacyView.name, | ||||
|   | ||||
| @@ -4,7 +4,6 @@ define([ | ||||
|  | ||||
| ) { | ||||
|     function TypeInspectorViewProvider(typeDefinition, openmct, convertToLegacyObject) { | ||||
|         console.warn(`DEPRECATION WARNING: Migrate ${typeDefinition.key} from ${typeDefinition.bundle.path} to use the new Inspector View APIs.  Legacy Inspector view support will be removed soon.`); | ||||
|         let representation = openmct.$injector.get('representations[]') | ||||
|             .filter((r) => r.key === typeDefinition.inspector)[0]; | ||||
|  | ||||
|   | ||||
| @@ -46,7 +46,7 @@ define([ | ||||
|     StatusAPI | ||||
| ) { | ||||
|     return { | ||||
|         TimeAPI: TimeAPI, | ||||
|         TimeAPI: TimeAPI.default, | ||||
|         ObjectAPI: ObjectAPI, | ||||
|         CompositionAPI: CompositionAPI, | ||||
|         TypeRegistry: TypeRegistry, | ||||
|   | ||||
| @@ -182,6 +182,12 @@ ObjectAPI.prototype.get = function (identifier, abortSignal) { | ||||
|     let objectPromise = provider.get(identifier, abortSignal).then(result => { | ||||
|         delete this.cache[keystring]; | ||||
|         result = this.applyGetInterceptors(identifier, result); | ||||
|         if (result.isMutable) { | ||||
|             result.$refresh(result); | ||||
|         } else { | ||||
|             let mutableDomainObject = this._toMutable(result); | ||||
|             mutableDomainObject.$refresh(result); | ||||
|         } | ||||
|  | ||||
|         return result; | ||||
|     }); | ||||
| @@ -298,10 +304,15 @@ ObjectAPI.prototype.save = function (domainObject) { | ||||
|                 savedResolve = resolve; | ||||
|             }); | ||||
|             domainObject.persisted = persistedTime; | ||||
|             provider.create(domainObject).then((response) => { | ||||
|                 this.mutate(domainObject, 'persisted', persistedTime); | ||||
|                 savedResolve(response); | ||||
|             }); | ||||
|             const newObjectPromise = provider.create(domainObject); | ||||
|             if (newObjectPromise) { | ||||
|                 newObjectPromise.then(response => { | ||||
|                     this.mutate(domainObject, 'persisted', persistedTime); | ||||
|                     savedResolve(response); | ||||
|                 }); | ||||
|             } else { | ||||
|                 result = Promise.reject(`[ObjectAPI][save] Object provider returned ${newObjectPromise} when creating new object.`); | ||||
|             } | ||||
|         } else { | ||||
|             domainObject.persisted = persistedTime; | ||||
|             this.mutate(domainObject, 'persisted', persistedTime); | ||||
| @@ -358,6 +369,20 @@ ObjectAPI.prototype.applyGetInterceptors = function (identifier, domainObject) { | ||||
|     return domainObject; | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Return relative url path from a given object path | ||||
|  * eg: #/browse/mine/cb56f6bf-c900-43b7-b923-2e3b64b412db/6e89e858-77ce-46e4-a1ad-749240286497/.... | ||||
|  * @param {Array} objectPath | ||||
|  * @returns {string} relative url for object | ||||
|  */ | ||||
| ObjectAPI.prototype.getRelativePath = function (objectPath) { | ||||
|     return objectPath | ||||
|         .map(p => this.makeKeyString(p.identifier)) | ||||
|         .reverse() | ||||
|         .join('/') | ||||
|     ; | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Modify a domain object. | ||||
|  * @param {module:openmct.DomainObject} object the object to mutate | ||||
|   | ||||
| @@ -10,28 +10,37 @@ const cssClasses = { | ||||
| }; | ||||
|  | ||||
| class Overlay extends EventEmitter { | ||||
|     constructor(options) { | ||||
|     constructor({ | ||||
|         buttons, | ||||
|         autoHide = true, | ||||
|         dismissable = true, | ||||
|         element, | ||||
|         onDestroy, | ||||
|         size | ||||
|     } = {}) { | ||||
|         super(); | ||||
|  | ||||
|         this.dismissable = options.dismissable !== false; | ||||
|         this.container = document.createElement('div'); | ||||
|         this.container.classList.add('l-overlay-wrapper', cssClasses[options.size]); | ||||
|         this.container.classList.add('l-overlay-wrapper', cssClasses[size]); | ||||
|  | ||||
|         this.autoHide = autoHide; | ||||
|         this.dismissable = dismissable !== false; | ||||
|  | ||||
|         this.component = new Vue({ | ||||
|             provide: { | ||||
|                 dismiss: this.dismiss.bind(this), | ||||
|                 element: options.element, | ||||
|                 buttons: options.buttons, | ||||
|                 dismissable: this.dismissable | ||||
|             }, | ||||
|             components: { | ||||
|                 OverlayComponent: OverlayComponent | ||||
|             }, | ||||
|             provide: { | ||||
|                 dismiss: this.dismiss.bind(this), | ||||
|                 element, | ||||
|                 buttons, | ||||
|                 dismissable: this.dismissable | ||||
|             }, | ||||
|             template: '<overlay-component></overlay-component>' | ||||
|         }); | ||||
|  | ||||
|         if (options.onDestroy) { | ||||
|             this.once('destroy', options.onDestroy); | ||||
|         if (onDestroy) { | ||||
|             this.once('destroy', onDestroy); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -30,7 +30,10 @@ class OverlayAPI { | ||||
|      */ | ||||
|     showOverlay(overlay) { | ||||
|         if (this.activeOverlays.length) { | ||||
|             this.activeOverlays[this.activeOverlays.length - 1].container.classList.add('invisible'); | ||||
|             const previousOverlay = this.activeOverlays[this.activeOverlays.length - 1]; | ||||
|             if (previousOverlay.autoHide) { | ||||
|                 previousOverlay.container.classList.add('invisible'); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         this.activeOverlays.push(overlay); | ||||
|   | ||||
| @@ -180,12 +180,6 @@ define([ | ||||
|      * @memberof module:openmct.TelemetryAPI~TelemetryProvider# | ||||
|      */ | ||||
|     TelemetryAPI.prototype.canProvideTelemetry = function (domainObject) { | ||||
|         console.warn( | ||||
|             'DEPRECATION WARNING: openmct.telemetry.canProvideTelemetry ' | ||||
|             + 'will not be supported in future versions of Open MCT.  Please ' | ||||
|             + 'use openmct.telemetry.isTelemetryObject instead.' | ||||
|         ); | ||||
|  | ||||
|         return Boolean(this.findSubscriptionProvider(domainObject)) | ||||
|                || Boolean(this.findRequestProvider(domainObject)); | ||||
|     }; | ||||
|   | ||||
| @@ -115,6 +115,7 @@ export class TelemetryCollection extends EventEmitter { | ||||
|  | ||||
|         this._requestHistoricalTelemetry(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * If a historical provider exists, then historical requests will be made | ||||
|      * @private | ||||
| @@ -126,20 +127,25 @@ export class TelemetryCollection extends EventEmitter { | ||||
|  | ||||
|         let historicalData; | ||||
|  | ||||
|         this.options.onPartialResponse = this._processNewTelemetry.bind(this); | ||||
|  | ||||
|         try { | ||||
|             this.requestAbort = new AbortController(); | ||||
|             this.options.signal = this.requestAbort.signal; | ||||
|             historicalData = await this.historicalProvider.request(this.domainObject, this.options); | ||||
|             this.requestAbort = undefined; | ||||
|         } catch (error) { | ||||
|             console.error('Error requesting telemetry data...'); | ||||
|             this.requestAbort = undefined; | ||||
|             this._error(error); | ||||
|             if (error.name !== 'AbortError') { | ||||
|                 console.error('Error requesting telemetry data...'); | ||||
|                 this._error(error); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         this.requestAbort = undefined; | ||||
|  | ||||
|         this._processNewTelemetry(historicalData); | ||||
|  | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * This uses the built in subscription function from Telemetry API | ||||
|      * @private | ||||
| @@ -342,6 +348,8 @@ export class TelemetryCollection extends EventEmitter { | ||||
|         this.boundedTelemetry = []; | ||||
|         this.futureBuffer = []; | ||||
|  | ||||
|         this.emit('clear'); | ||||
|  | ||||
|         this._requestHistoricalTelemetry(); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -31,11 +31,6 @@ define([ | ||||
|         valueMetadata.hints = valueMetadata.hints || {}; | ||||
|  | ||||
|         if (Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'x')) { | ||||
|             console.warn( | ||||
|                 'DEPRECATION WARNING: `x` hints should be replaced with ' | ||||
|                 + '`domain` hints moving forward.  ' | ||||
|                 + 'https://github.com/nasa/openmct/issues/1546' | ||||
|             ); | ||||
|             if (!Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'domain')) { | ||||
|                 valueMetadata.hints.domain = valueMetadata.hints.x; | ||||
|             } | ||||
| @@ -44,11 +39,6 @@ define([ | ||||
|         } | ||||
|  | ||||
|         if (Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'y')) { | ||||
|             console.warn( | ||||
|                 'DEPRECATION WARNING: `y` hints should be replaced with ' | ||||
|                 + '`range` hints moving forward.  ' | ||||
|                 + 'https://github.com/nasa/openmct/issues/1546' | ||||
|             ); | ||||
|             if (!Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'range')) { | ||||
|                 valueMetadata.hints.range = valueMetadata.hints.y; | ||||
|             } | ||||
|   | ||||
							
								
								
									
										106
									
								
								src/api/time/GlobalTimeContext.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								src/api/time/GlobalTimeContext.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,106 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2021, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| import TimeContext from "./TimeContext"; | ||||
|  | ||||
| /** | ||||
|  * The GlobalContext handles getting and setting time of the openmct application in general. | ||||
|  * Views will use this context unless they specify an alternate/independent time context | ||||
|  */ | ||||
| class GlobalTimeContext extends TimeContext { | ||||
|     constructor() { | ||||
|         super(); | ||||
|  | ||||
|         //The Time Of Interest | ||||
|         this.toi = undefined; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get or set the start and end time of the time conductor. Basic validation | ||||
|      * of bounds is performed. | ||||
|      * | ||||
|      * @param {module:openmct.TimeAPI~TimeConductorBounds} newBounds | ||||
|      * @throws {Error} Validation error | ||||
|      * @fires module:openmct.TimeAPI~bounds | ||||
|      * @returns {module:openmct.TimeAPI~TimeConductorBounds} | ||||
|      * @memberof module:openmct.TimeAPI# | ||||
|      * @method bounds | ||||
|      */ | ||||
|     bounds(newBounds) { | ||||
|         if (arguments.length > 0) { | ||||
|             super.bounds.call(this, ...arguments); | ||||
|             // If a bounds change results in a TOI outside of the current | ||||
|             // bounds, unset it | ||||
|             if (this.toi < newBounds.start || this.toi > newBounds.end) { | ||||
|                 this.timeOfInterest(undefined); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         //Return a copy to prevent direct mutation of time conductor bounds. | ||||
|         return JSON.parse(JSON.stringify(this.boundsVal)); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Update bounds based on provided time and current offsets | ||||
|      * @private | ||||
|      * @param {number} timestamp A time from which bounds will be calculated | ||||
|      * using current offsets. | ||||
|      */ | ||||
|     tick(timestamp) { | ||||
|         super.tick.call(this, ...arguments); | ||||
|  | ||||
|         // If a bounds change results in a TOI outside of the current | ||||
|         // bounds, unset it | ||||
|         if (this.toi < this.boundsVal.start || this.toi > this.boundsVal.end) { | ||||
|             this.timeOfInterest(undefined); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get or set the Time of Interest. The Time of Interest is a single point | ||||
|      * in time, and constitutes the temporal focus of application views. It can | ||||
|      * be manipulated by the user from the time conductor or from other views. | ||||
|      * The time of interest can effectively be unset by assigning a value of | ||||
|      * 'undefined'. | ||||
|      * @fires module:openmct.TimeAPI~timeOfInterest | ||||
|      * @param newTOI | ||||
|      * @returns {number} the current time of interest | ||||
|      * @memberof module:openmct.TimeAPI# | ||||
|      * @method timeOfInterest | ||||
|      */ | ||||
|     timeOfInterest(newTOI) { | ||||
|         if (arguments.length > 0) { | ||||
|             this.toi = newTOI; | ||||
|             /** | ||||
|              * The Time of Interest has moved. | ||||
|              * @event timeOfInterest | ||||
|              * @memberof module:openmct.TimeAPI~ | ||||
|              * @property {number} Current time of interest | ||||
|              */ | ||||
|             this.emit('timeOfInterest', this.toi); | ||||
|         } | ||||
|  | ||||
|         return this.toi; | ||||
|     } | ||||
| } | ||||
|  | ||||
| export default GlobalTimeContext; | ||||
							
								
								
									
										94
									
								
								src/api/time/IndependentTimeContext.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								src/api/time/IndependentTimeContext.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,94 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2021, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| import TimeContext from "./TimeContext"; | ||||
|  | ||||
| /** | ||||
|  * The IndependentTimeContext handles getting and setting time of the openmct application in general. | ||||
|  * Views will use the GlobalTimeContext unless they specify an alternate/independent time context here. | ||||
|  */ | ||||
| class IndependentTimeContext extends TimeContext { | ||||
|     constructor(globalTimeContext, key) { | ||||
|         super(); | ||||
|         this.key = key; | ||||
|  | ||||
|         this.globalTimeContext = globalTimeContext; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Set the active clock. Tick source will be immediately subscribed to | ||||
|      * and ticking will begin. Offsets from 'now' must also be provided. A clock | ||||
|      * can be unset by calling {@link stopClock}. | ||||
|      * | ||||
|      * @param {Clock || string} keyOrClock The clock to activate, or its key | ||||
|      * @param {ClockOffsets} offsets on each tick these will be used to calculate | ||||
|      * the start and end bounds. This maintains a sliding time window of a fixed | ||||
|      * width that automatically updates. | ||||
|      * @fires module:openmct.TimeAPI~clock | ||||
|      * @return {Clock} the currently active clock; | ||||
|      */ | ||||
|     clock(keyOrClock, offsets) { | ||||
|         if (arguments.length === 2) { | ||||
|             let clock; | ||||
|  | ||||
|             if (typeof keyOrClock === 'string') { | ||||
|                 clock = this.globalTimeContext.clocks.get(keyOrClock); | ||||
|                 if (clock === undefined) { | ||||
|                     throw "Unknown clock '" + keyOrClock + "'. Has it been registered with 'addClock'?"; | ||||
|                 } | ||||
|             } else if (typeof keyOrClock === 'object') { | ||||
|                 clock = keyOrClock; | ||||
|                 if (!this.globalTimeContext.clocks.has(clock.key)) { | ||||
|                     throw "Unknown clock '" + keyOrClock.key + "'. Has it been registered with 'addClock'?"; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             const previousClock = this.activeClock; | ||||
|             if (previousClock !== undefined) { | ||||
|                 previousClock.off("tick", this.tick); | ||||
|             } | ||||
|  | ||||
|             this.activeClock = clock; | ||||
|  | ||||
|             /** | ||||
|              * The active clock has changed. Clock can be unset by calling {@link stopClock} | ||||
|              * @event clock | ||||
|              * @memberof module:openmct.TimeAPI~ | ||||
|              * @property {Clock} clock The newly activated clock, or undefined | ||||
|              * if the system is no longer following a clock source | ||||
|              */ | ||||
|             this.emit("clock", this.activeClock); | ||||
|  | ||||
|             if (this.activeClock !== undefined) { | ||||
|                 this.clockOffsets(offsets); | ||||
|                 this.activeClock.on("tick", this.tick); | ||||
|             } | ||||
|  | ||||
|         } else if (arguments.length === 1) { | ||||
|             throw "When setting the clock, clock offsets must also be provided"; | ||||
|         } | ||||
|  | ||||
|         return this.activeClock; | ||||
|     } | ||||
| } | ||||
|  | ||||
| export default IndependentTimeContext; | ||||
| @@ -20,51 +20,35 @@ | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define(['EventEmitter'], function (EventEmitter) { | ||||
|  | ||||
|     /** | ||||
|      * The public API for setting and querying the temporal state of the | ||||
|      * application. The concept of time is integral to Open MCT, and at least | ||||
|      * one {@link TimeSystem}, as well as some default time bounds must be | ||||
|      * registered and enabled via {@link TimeAPI.addTimeSystem} and | ||||
|      * {@link TimeAPI.timeSystem} respectively for Open MCT to work. | ||||
|      * | ||||
|      * Time-sensitive views will typically respond to changes to bounds or other | ||||
|      * properties of the time conductor and update the data displayed based on | ||||
|      * the temporal state of the application. The current time bounds are also | ||||
|      * used in queries for historical data. | ||||
|      * | ||||
|      * The TimeAPI extends the EventEmitter class. A number of events are | ||||
|      * fired when properties of the time conductor change, which are documented | ||||
|      * below. | ||||
|      * | ||||
|      * @interface | ||||
|      * @memberof module:openmct | ||||
|      */ | ||||
|     function TimeAPI() { | ||||
|         EventEmitter.call(this); | ||||
|  | ||||
|         //The Time System | ||||
|         this.system = undefined; | ||||
|         //The Time Of Interest | ||||
|         this.toi = undefined; | ||||
|  | ||||
|         this.boundsVal = { | ||||
|             start: undefined, | ||||
|             end: undefined | ||||
|         }; | ||||
|  | ||||
|         this.timeSystems = new Map(); | ||||
|         this.clocks = new Map(); | ||||
|         this.activeClock = undefined; | ||||
|         this.offsets = undefined; | ||||
|  | ||||
|         this.tick = this.tick.bind(this); | ||||
| import GlobalTimeContext from "./GlobalTimeContext"; | ||||
| import IndependentTimeContext from "@/api/time/IndependentTimeContext"; | ||||
|  | ||||
| /** | ||||
| * The public API for setting and querying the temporal state of the | ||||
| * application. The concept of time is integral to Open MCT, and at least | ||||
| * one {@link TimeSystem}, as well as some default time bounds must be | ||||
| * registered and enabled via {@link TimeAPI.addTimeSystem} and | ||||
| * {@link TimeAPI.timeSystem} respectively for Open MCT to work. | ||||
| * | ||||
| * Time-sensitive views will typically respond to changes to bounds or other | ||||
| * properties of the time conductor and update the data displayed based on | ||||
| * the temporal state of the application. The current time bounds are also | ||||
| * used in queries for historical data. | ||||
| * | ||||
| * The TimeAPI extends the GlobalTimeContext which in turn extends the TimeContext/EventEmitter class. A number of events are | ||||
| * fired when properties of the time conductor change, which are documented | ||||
| * below. | ||||
| * | ||||
| * @interface | ||||
| * @memberof module:openmct | ||||
| */ | ||||
| class TimeAPI extends GlobalTimeContext { | ||||
|     constructor(openmct) { | ||||
|         super(); | ||||
|         this.openmct = openmct; | ||||
|         this.independentContexts = new Map(); | ||||
|     } | ||||
|  | ||||
|     TimeAPI.prototype = Object.create(EventEmitter.prototype); | ||||
|  | ||||
|     /** | ||||
|      * A TimeSystem provides meaning to the values returned by the TimeAPI. Open | ||||
|      * MCT supports multiple different types of time values, although all are | ||||
| @@ -94,16 +78,16 @@ define(['EventEmitter'], function (EventEmitter) { | ||||
|      * @memberof module:openmct.TimeAPI# | ||||
|      * @param {TimeSystem} timeSystem A time system object. | ||||
|      */ | ||||
|     TimeAPI.prototype.addTimeSystem = function (timeSystem) { | ||||
|     addTimeSystem(timeSystem) { | ||||
|         this.timeSystems.set(timeSystem.key, timeSystem); | ||||
|     }; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @returns {TimeSystem[]} | ||||
|      */ | ||||
|     TimeAPI.prototype.getAllTimeSystems = function () { | ||||
|     getAllTimeSystems() { | ||||
|         return Array.from(this.timeSystems.values()); | ||||
|     }; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Clocks provide a timing source that is used to | ||||
| @@ -126,340 +110,81 @@ define(['EventEmitter'], function (EventEmitter) { | ||||
|      * @memberof module:openmct.TimeAPI# | ||||
|      * @param {Clock} clock | ||||
|      */ | ||||
|     TimeAPI.prototype.addClock = function (clock) { | ||||
|     addClock(clock) { | ||||
|         this.clocks.set(clock.key, clock); | ||||
|     }; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @memberof module:openmct.TimeAPI# | ||||
|      * @returns {Clock[]} | ||||
|      * @memberof module:openmct.TimeAPI# | ||||
|      */ | ||||
|     TimeAPI.prototype.getAllClocks = function () { | ||||
|     getAllClocks() { | ||||
|         return Array.from(this.clocks.values()); | ||||
|     }; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Validate the given bounds. This can be used for pre-validation of bounds, | ||||
|      * for example by views validating user inputs. | ||||
|      * @param {TimeBounds} bounds The start and end time of the conductor. | ||||
|      * @returns {string | true} A validation error, or true if valid | ||||
|      * Get or set an independent time context which follows the TimeAPI timeSystem, | ||||
|      * but with different offsets for a given domain object | ||||
|      * @param {key | string} key The identifier key of the domain object these offsets are set for | ||||
|      * @param {ClockOffsets | TimeBounds} value This maintains a sliding time window of a fixed width that automatically updates | ||||
|      * @param {key | string} clockKey the real time clock key currently in use | ||||
|      * @memberof module:openmct.TimeAPI# | ||||
|      * @method validateBounds | ||||
|      * @method addIndependentTimeContext | ||||
|      */ | ||||
|     TimeAPI.prototype.validateBounds = function (bounds) { | ||||
|         if ((bounds.start === undefined) | ||||
|             || (bounds.end === undefined) | ||||
|             || isNaN(bounds.start) | ||||
|             || isNaN(bounds.end) | ||||
|         ) { | ||||
|             return "Start and end must be specified as integer values"; | ||||
|         } else if (bounds.start > bounds.end) { | ||||
|             return "Specified start date exceeds end bound"; | ||||
|     addIndependentContext(key, value, clockKey) { | ||||
|         let timeContext = this.independentContexts.get(key); | ||||
|         if (!timeContext) { | ||||
|             timeContext = new IndependentTimeContext(this, key); | ||||
|             this.independentContexts.set(key, timeContext); | ||||
|         } | ||||
|  | ||||
|         return true; | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Validate the given offsets. This can be used for pre-validation of | ||||
|      * offsets, for example by views validating user inputs. | ||||
|      * @param {ClockOffsets} offsets The start and end offsets from a 'now' value. | ||||
|      * @returns {string | true} A validation error, or true if valid | ||||
|      * @memberof module:openmct.TimeAPI# | ||||
|      * @method validateBounds | ||||
|      */ | ||||
|     TimeAPI.prototype.validateOffsets = function (offsets) { | ||||
|         if ((offsets.start === undefined) | ||||
|             || (offsets.end === undefined) | ||||
|             || isNaN(offsets.start) | ||||
|             || isNaN(offsets.end) | ||||
|         ) { | ||||
|             return "Start and end offsets must be specified as integer values"; | ||||
|         } else if (offsets.start >= offsets.end) { | ||||
|             return "Specified start offset must be < end offset"; | ||||
|         if (clockKey) { | ||||
|             timeContext.clock(clockKey, value); | ||||
|         } else { | ||||
|             timeContext.stopClock(); | ||||
|             timeContext.bounds(value); | ||||
|         } | ||||
|  | ||||
|         return true; | ||||
|     }; | ||||
|         this.emit('timeContext', key); | ||||
|  | ||||
|     /** | ||||
|      * @typedef {Object} TimeBounds | ||||
|      * @property {number} start The start time displayed by the time conductor | ||||
|      * in ms since epoch. Epoch determined by currently active time system | ||||
|      * @property {number} end The end time displayed by the time conductor in ms | ||||
|      * since epoch. | ||||
|      * @memberof module:openmct.TimeAPI~ | ||||
|      */ | ||||
|  | ||||
|     /** | ||||
|      * Get or set the start and end time of the time conductor. Basic validation | ||||
|      * of bounds is performed. | ||||
|      * | ||||
|      * @param {module:openmct.TimeAPI~TimeConductorBounds} newBounds | ||||
|      * @throws {Error} Validation error | ||||
|      * @fires module:openmct.TimeAPI~bounds | ||||
|      * @returns {module:openmct.TimeAPI~TimeConductorBounds} | ||||
|      * @memberof module:openmct.TimeAPI# | ||||
|      * @method bounds | ||||
|      */ | ||||
|     TimeAPI.prototype.bounds = function (newBounds) { | ||||
|         if (arguments.length > 0) { | ||||
|             const validationResult = this.validateBounds(newBounds); | ||||
|             if (validationResult !== true) { | ||||
|                 throw new Error(validationResult); | ||||
|             } | ||||
|  | ||||
|             //Create a copy to avoid direct mutation of conductor bounds | ||||
|             this.boundsVal = JSON.parse(JSON.stringify(newBounds)); | ||||
|             /** | ||||
|              * The start time, end time, or both have been updated. | ||||
|              * @event bounds | ||||
|              * @memberof module:openmct.TimeAPI~ | ||||
|              * @property {TimeConductorBounds} bounds The newly updated bounds | ||||
|              * @property {boolean} [tick] `true` if the bounds update was due to | ||||
|              * a "tick" event (ie. was an automatic update), false otherwise. | ||||
|              */ | ||||
|             this.emit('bounds', this.boundsVal, false); | ||||
|  | ||||
|             // If a bounds change results in a TOI outside of the current | ||||
|             // bounds, unset it | ||||
|             if (this.toi < newBounds.start || this.toi > newBounds.end) { | ||||
|                 this.timeOfInterest(undefined); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         //Return a copy to prevent direct mutation of time conductor bounds. | ||||
|         return JSON.parse(JSON.stringify(this.boundsVal)); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Get or set the time system of the TimeAPI. | ||||
|      * @param {TimeSystem | string} timeSystem | ||||
|      * @param {module:openmct.TimeAPI~TimeConductorBounds} bounds | ||||
|      * @fires module:openmct.TimeAPI~timeSystem | ||||
|      * @returns {TimeSystem} The currently applied time system | ||||
|      * @memberof module:openmct.TimeAPI# | ||||
|      * @method timeSystem | ||||
|      */ | ||||
|     TimeAPI.prototype.timeSystem = function (timeSystemOrKey, bounds) { | ||||
|         if (arguments.length >= 1) { | ||||
|             if (arguments.length === 1 && !this.activeClock) { | ||||
|                 throw new Error( | ||||
|                     "Must specify bounds when changing time system without " | ||||
|                     + "an active clock." | ||||
|                 ); | ||||
|             } | ||||
|  | ||||
|             let timeSystem; | ||||
|  | ||||
|             if (timeSystemOrKey === undefined) { | ||||
|                 throw "Please provide a time system"; | ||||
|             } | ||||
|  | ||||
|             if (typeof timeSystemOrKey === 'string') { | ||||
|                 timeSystem = this.timeSystems.get(timeSystemOrKey); | ||||
|  | ||||
|                 if (timeSystem === undefined) { | ||||
|                     throw "Unknown time system " + timeSystemOrKey + ". Has it been registered with 'addTimeSystem'?"; | ||||
|                 } | ||||
|             } else if (typeof timeSystemOrKey === 'object') { | ||||
|                 timeSystem = timeSystemOrKey; | ||||
|  | ||||
|                 if (!this.timeSystems.has(timeSystem.key)) { | ||||
|                     throw "Unknown time system " + timeSystem.key + ". Has it been registered with 'addTimeSystem'?"; | ||||
|                 } | ||||
|             } else { | ||||
|                 throw "Attempt to set invalid time system in Time API. Please provide a previously registered time system object or key"; | ||||
|             } | ||||
|  | ||||
|             this.system = timeSystem; | ||||
|  | ||||
|             /** | ||||
|              * The time system used by the time | ||||
|              * conductor has changed. A change in Time System will always be | ||||
|              * followed by a bounds event specifying new query bounds. | ||||
|              * | ||||
|              * @event module:openmct.TimeAPI~timeSystem | ||||
|              * @property {TimeSystem} The value of the currently applied | ||||
|              * Time System | ||||
|              * */ | ||||
|             this.emit('timeSystem', this.system); | ||||
|             if (bounds) { | ||||
|                 this.bounds(bounds); | ||||
|             } | ||||
|  | ||||
|         } | ||||
|  | ||||
|         return this.system; | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Get or set the Time of Interest. The Time of Interest is a single point | ||||
|      * in time, and constitutes the temporal focus of application views. It can | ||||
|      * be manipulated by the user from the time conductor or from other views. | ||||
|      * The time of interest can effectively be unset by assigning a value of | ||||
|      * 'undefined'. | ||||
|      * @fires module:openmct.TimeAPI~timeOfInterest | ||||
|      * @param newTOI | ||||
|      * @returns {number} the current time of interest | ||||
|      * @memberof module:openmct.TimeAPI# | ||||
|      * @method timeOfInterest | ||||
|      */ | ||||
|     TimeAPI.prototype.timeOfInterest = function (newTOI) { | ||||
|         if (arguments.length > 0) { | ||||
|             this.toi = newTOI; | ||||
|             /** | ||||
|              * The Time of Interest has moved. | ||||
|              * @event timeOfInterest | ||||
|              * @memberof module:openmct.TimeAPI~ | ||||
|              * @property {number} Current time of interest | ||||
|              */ | ||||
|             this.emit('timeOfInterest', this.toi); | ||||
|         } | ||||
|  | ||||
|         return this.toi; | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Update bounds based on provided time and current offsets | ||||
|      * @private | ||||
|      * @param {number} timestamp A time from which boudns will be calculated | ||||
|      * using current offsets. | ||||
|      */ | ||||
|     TimeAPI.prototype.tick = function (timestamp) { | ||||
|         const newBounds = { | ||||
|             start: timestamp + this.offsets.start, | ||||
|             end: timestamp + this.offsets.end | ||||
|         return () => { | ||||
|             this.independentContexts.delete(key); | ||||
|             timeContext.emit('timeContext', key); | ||||
|         }; | ||||
|  | ||||
|         this.boundsVal = newBounds; | ||||
|         this.emit('bounds', this.boundsVal, true); | ||||
|  | ||||
|         // If a bounds change results in a TOI outside of the current | ||||
|         // bounds, unset it | ||||
|         if (this.toi < newBounds.start || this.toi > newBounds.end) { | ||||
|             this.timeOfInterest(undefined); | ||||
|         } | ||||
|     }; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Set the active clock. Tick source will be immediately subscribed to | ||||
|      * and ticking will begin. Offsets from 'now' must also be provided. A clock | ||||
|      * can be unset by calling {@link stopClock}. | ||||
|      * | ||||
|      * @param {Clock || string} The clock to activate, or its key | ||||
|      * @param {ClockOffsets} offsets on each tick these will be used to calculate | ||||
|      * the start and end bounds. This maintains a sliding time window of a fixed | ||||
|      * width that automatically updates. | ||||
|      * @fires module:openmct.TimeAPI~clock | ||||
|      * @return {Clock} the currently active clock; | ||||
|      * Get the independent time context which follows the TimeAPI timeSystem, | ||||
|      * but with different offsets. | ||||
|      * @param {key | string} key The identifier key of the domain object these offsets | ||||
|      * @memberof module:openmct.TimeAPI# | ||||
|      * @method getIndependentTimeContext | ||||
|      */ | ||||
|     TimeAPI.prototype.clock = function (keyOrClock, offsets) { | ||||
|         if (arguments.length === 2) { | ||||
|             let clock; | ||||
|  | ||||
|             if (typeof keyOrClock === 'string') { | ||||
|                 clock = this.clocks.get(keyOrClock); | ||||
|                 if (clock === undefined) { | ||||
|                     throw "Unknown clock '" + keyOrClock + "'. Has it been registered with 'addClock'?"; | ||||
|                 } | ||||
|             } else if (typeof keyOrClock === 'object') { | ||||
|                 clock = keyOrClock; | ||||
|                 if (!this.clocks.has(clock.key)) { | ||||
|                     throw "Unknown clock '" + keyOrClock.key + "'. Has it been registered with 'addClock'?"; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             const previousClock = this.activeClock; | ||||
|             if (previousClock !== undefined) { | ||||
|                 previousClock.off("tick", this.tick); | ||||
|             } | ||||
|  | ||||
|             this.activeClock = clock; | ||||
|  | ||||
|             /** | ||||
|              * The active clock has changed. Clock can be unset by calling {@link stopClock} | ||||
|              * @event clock | ||||
|              * @memberof module:openmct.TimeAPI~ | ||||
|              * @property {Clock} clock The newly activated clock, or undefined | ||||
|              * if the system is no longer following a clock source | ||||
|              */ | ||||
|             this.emit("clock", this.activeClock); | ||||
|  | ||||
|             if (this.activeClock !== undefined) { | ||||
|                 this.clockOffsets(offsets); | ||||
|                 this.activeClock.on("tick", this.tick); | ||||
|             } | ||||
|  | ||||
|         } else if (arguments.length === 1) { | ||||
|             throw "When setting the clock, clock offsets must also be provided"; | ||||
|         } | ||||
|  | ||||
|         return this.activeClock; | ||||
|     }; | ||||
|     getIndependentContext(key) { | ||||
|         return this.independentContexts.get(key); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Clock offsets are used to calculate temporal bounds when the system is | ||||
|      * ticking on a clock source. | ||||
|      * | ||||
|      * @typedef {object} ClockOffsets | ||||
|      * @property {number} start A time span relative to the current value of the | ||||
|      * ticking clock, from which start bounds will be calculated. This value must | ||||
|      * be < 0. When a clock is active, bounds will be calculated automatically | ||||
|      * based on the value provided by the clock, and the defined clock offsets. | ||||
|      * @property {number} end A time span relative to the current value of the | ||||
|      * ticking clock, from which end bounds will be calculated. This value must | ||||
|      * be >= 0. | ||||
|      * Get the a timeContext for a view based on it's objectPath. If there is any object in the objectPath with an independent time context, it will be returned. | ||||
|      * Otherwise, the global time context will be returned. | ||||
|      * @param { Array } objectPath The view's objectPath | ||||
|      * @memberof module:openmct.TimeAPI# | ||||
|      * @method getContextForView | ||||
|      */ | ||||
|     /** | ||||
|      * Get or set the currently applied clock offsets. If no parameter is provided, | ||||
|      * the current value will be returned. If provided, the new value will be | ||||
|      * used as the new clock offsets. | ||||
|      * @param {ClockOffsets} offsets | ||||
|      * @returns {ClockOffsets} | ||||
|      */ | ||||
|     TimeAPI.prototype.clockOffsets = function (offsets) { | ||||
|         if (arguments.length > 0) { | ||||
|     getContextForView(objectPath) { | ||||
|         let timeContext = this; | ||||
|  | ||||
|             const validationResult = this.validateOffsets(offsets); | ||||
|             if (validationResult !== true) { | ||||
|                 throw new Error(validationResult); | ||||
|         objectPath.forEach(item => { | ||||
|             const key = this.openmct.objects.makeKeyString(item.identifier); | ||||
|             if (this.independentContexts.get(key)) { | ||||
|                 timeContext = this.independentContexts.get(key); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|             this.offsets = offsets; | ||||
|         return timeContext; | ||||
|     } | ||||
|  | ||||
|             const currentValue = this.activeClock.currentValue(); | ||||
|             const newBounds = { | ||||
|                 start: currentValue + offsets.start, | ||||
|                 end: currentValue + offsets.end | ||||
|             }; | ||||
| } | ||||
|  | ||||
|             this.bounds(newBounds); | ||||
|  | ||||
|             /** | ||||
|              * Event that is triggered when clock offsets change. | ||||
|              * @event clockOffsets | ||||
|              * @memberof module:openmct.TimeAPI~ | ||||
|              * @property {ClockOffsets} clockOffsets The newly activated clock | ||||
|              * offsets. | ||||
|              */ | ||||
|             this.emit("clockOffsets", offsets); | ||||
|         } | ||||
|  | ||||
|         return this.offsets; | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Stop the currently active clock from ticking, and unset it. This will | ||||
|      * revert all views to showing a static time frame defined by the current | ||||
|      * bounds. | ||||
|      */ | ||||
|     TimeAPI.prototype.stopClock = function () { | ||||
|         if (this.activeClock) { | ||||
|             this.clock(undefined, undefined); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     return TimeAPI; | ||||
| }); | ||||
| export default TimeAPI; | ||||
|   | ||||
| @@ -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); | ||||
|     }); | ||||
| }); | ||||
|   | ||||
							
								
								
									
										360
									
								
								src/api/time/TimeContext.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										360
									
								
								src/api/time/TimeContext.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,360 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2021, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| import EventEmitter from 'EventEmitter'; | ||||
|  | ||||
| class TimeContext extends EventEmitter { | ||||
|     constructor() { | ||||
|         super(); | ||||
|  | ||||
|         //The Time System | ||||
|         this.timeSystems = new Map(); | ||||
|  | ||||
|         this.system = undefined; | ||||
|  | ||||
|         this.clocks = new Map(); | ||||
|  | ||||
|         this.boundsVal = { | ||||
|             start: undefined, | ||||
|             end: undefined | ||||
|         }; | ||||
|  | ||||
|         this.activeClock = undefined; | ||||
|         this.offsets = undefined; | ||||
|  | ||||
|         this.tick = this.tick.bind(this); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get or set the time system of the TimeAPI. | ||||
|      * @param {TimeSystem | string} timeSystem | ||||
|      * @param {module:openmct.TimeAPI~TimeConductorBounds} bounds | ||||
|      * @fires module:openmct.TimeAPI~timeSystem | ||||
|      * @returns {TimeSystem} The currently applied time system | ||||
|      * @memberof module:openmct.TimeAPI# | ||||
|      * @method timeSystem | ||||
|      */ | ||||
|     timeSystem(timeSystemOrKey, bounds) { | ||||
|         if (arguments.length >= 1) { | ||||
|             if (arguments.length === 1 && !this.activeClock) { | ||||
|                 throw new Error( | ||||
|                     "Must specify bounds when changing time system without " | ||||
|                     + "an active clock." | ||||
|                 ); | ||||
|             } | ||||
|  | ||||
|             let timeSystem; | ||||
|  | ||||
|             if (timeSystemOrKey === undefined) { | ||||
|                 throw "Please provide a time system"; | ||||
|             } | ||||
|  | ||||
|             if (typeof timeSystemOrKey === 'string') { | ||||
|                 timeSystem = this.timeSystems.get(timeSystemOrKey); | ||||
|  | ||||
|                 if (timeSystem === undefined) { | ||||
|                     throw "Unknown time system " + timeSystemOrKey + ". Has it been registered with 'addTimeSystem'?"; | ||||
|                 } | ||||
|             } else if (typeof timeSystemOrKey === 'object') { | ||||
|                 timeSystem = timeSystemOrKey; | ||||
|  | ||||
|                 if (!this.timeSystems.has(timeSystem.key)) { | ||||
|                     throw "Unknown time system " + timeSystem.key + ". Has it been registered with 'addTimeSystem'?"; | ||||
|                 } | ||||
|             } else { | ||||
|                 throw "Attempt to set invalid time system in Time API. Please provide a previously registered time system object or key"; | ||||
|             } | ||||
|  | ||||
|             this.system = timeSystem; | ||||
|  | ||||
|             /** | ||||
|              * The time system used by the time | ||||
|              * conductor has changed. A change in Time System will always be | ||||
|              * followed by a bounds event specifying new query bounds. | ||||
|              * | ||||
|              * @event module:openmct.TimeAPI~timeSystem | ||||
|              * @property {TimeSystem} The value of the currently applied | ||||
|              * Time System | ||||
|              * */ | ||||
|             this.emit('timeSystem', this.system); | ||||
|             if (bounds) { | ||||
|                 this.bounds(bounds); | ||||
|             } | ||||
|  | ||||
|         } | ||||
|  | ||||
|         return this.system; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Clock offsets are used to calculate temporal bounds when the system is | ||||
|      * ticking on a clock source. | ||||
|      * | ||||
|      * @typedef {object} ValidationResult | ||||
|      * @property {boolean} valid Result of the validation - true or false. | ||||
|      * @property {string} message An error message if valid is false. | ||||
|      */ | ||||
|     /** | ||||
|      * Validate the given bounds. This can be used for pre-validation of bounds, | ||||
|      * for example by views validating user inputs. | ||||
|      * @param {TimeBounds} bounds The start and end time of the conductor. | ||||
|      * @returns {ValidationResult} A validation error, or true if valid | ||||
|      * @memberof module:openmct.TimeAPI# | ||||
|      * @method validateBounds | ||||
|      */ | ||||
|     validateBounds(bounds) { | ||||
|         if ((bounds.start === undefined) | ||||
|             || (bounds.end === undefined) | ||||
|             || isNaN(bounds.start) | ||||
|             || isNaN(bounds.end) | ||||
|         ) { | ||||
|             return { | ||||
|                 valid: false, | ||||
|                 message: "Start and end must be specified as integer values" | ||||
|             }; | ||||
|         } else if (bounds.start > bounds.end) { | ||||
|             return { | ||||
|                 valid: false, | ||||
|                 message: "Specified start date exceeds end bound" | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         return { | ||||
|             valid: true, | ||||
|             message: '' | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get or set the start and end time of the time conductor. Basic validation | ||||
|      * of bounds is performed. | ||||
|      * | ||||
|      * @param {module:openmct.TimeAPI~TimeConductorBounds} newBounds | ||||
|      * @throws {Error} Validation error | ||||
|      * @fires module:openmct.TimeAPI~bounds | ||||
|      * @returns {module:openmct.TimeAPI~TimeConductorBounds} | ||||
|      * @memberof module:openmct.TimeAPI# | ||||
|      * @method bounds | ||||
|      */ | ||||
|     bounds(newBounds) { | ||||
|         if (arguments.length > 0) { | ||||
|             const validationResult = this.validateBounds(newBounds); | ||||
|             if (validationResult.valid !== true) { | ||||
|                 throw new Error(validationResult.message); | ||||
|             } | ||||
|  | ||||
|             //Create a copy to avoid direct mutation of conductor bounds | ||||
|             this.boundsVal = JSON.parse(JSON.stringify(newBounds)); | ||||
|             /** | ||||
|              * The start time, end time, or both have been updated. | ||||
|              * @event bounds | ||||
|              * @memberof module:openmct.TimeAPI~ | ||||
|              * @property {TimeConductorBounds} bounds The newly updated bounds | ||||
|              * @property {boolean} [tick] `true` if the bounds update was due to | ||||
|              * a "tick" event (ie. was an automatic update), false otherwise. | ||||
|              */ | ||||
|             this.emit('bounds', this.boundsVal, false); | ||||
|         } | ||||
|  | ||||
|         //Return a copy to prevent direct mutation of time conductor bounds. | ||||
|         return JSON.parse(JSON.stringify(this.boundsVal)); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Validate the given offsets. This can be used for pre-validation of | ||||
|      * offsets, for example by views validating user inputs. | ||||
|      * @param {ClockOffsets} offsets The start and end offsets from a 'now' value. | ||||
|      * @returns { ValidationResult } A validation error, and true/false if valid or not | ||||
|      * @memberof module:openmct.TimeAPI# | ||||
|      * @method validateOffsets | ||||
|      */ | ||||
|     validateOffsets(offsets) { | ||||
|         if ((offsets.start === undefined) | ||||
|             || (offsets.end === undefined) | ||||
|             || isNaN(offsets.start) | ||||
|             || isNaN(offsets.end) | ||||
|         ) { | ||||
|             return { | ||||
|                 valid: false, | ||||
|                 message: "Start and end offsets must be specified as integer values" | ||||
|             }; | ||||
|         } else if (offsets.start >= offsets.end) { | ||||
|             return { | ||||
|                 valid: false, | ||||
|                 message: "Specified start offset must be < end offset" | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         return { | ||||
|             valid: true, | ||||
|             message: '' | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @typedef {Object} TimeBounds | ||||
|      * @property {number} start The start time displayed by the time conductor | ||||
|      * in ms since epoch. Epoch determined by currently active time system | ||||
|      * @property {number} end The end time displayed by the time conductor in ms | ||||
|      * since epoch. | ||||
|      * @memberof module:openmct.TimeAPI~ | ||||
|      */ | ||||
|  | ||||
|     /** | ||||
|      * Clock offsets are used to calculate temporal bounds when the system is | ||||
|      * ticking on a clock source. | ||||
|      * | ||||
|      * @typedef {object} ClockOffsets | ||||
|      * @property {number} start A time span relative to the current value of the | ||||
|      * ticking clock, from which start bounds will be calculated. This value must | ||||
|      * be < 0. When a clock is active, bounds will be calculated automatically | ||||
|      * based on the value provided by the clock, and the defined clock offsets. | ||||
|      * @property {number} end A time span relative to the current value of the | ||||
|      * ticking clock, from which end bounds will be calculated. This value must | ||||
|      * be >= 0. | ||||
|      */ | ||||
|     /** | ||||
|      * Get or set the currently applied clock offsets. If no parameter is provided, | ||||
|      * the current value will be returned. If provided, the new value will be | ||||
|      * used as the new clock offsets. | ||||
|      * @param {ClockOffsets} offsets | ||||
|      * @returns {ClockOffsets} | ||||
|      */ | ||||
|     clockOffsets(offsets) { | ||||
|         if (arguments.length > 0) { | ||||
|  | ||||
|             const validationResult = this.validateOffsets(offsets); | ||||
|             if (validationResult.valid !== true) { | ||||
|                 throw new Error(validationResult.message); | ||||
|             } | ||||
|  | ||||
|             this.offsets = offsets; | ||||
|  | ||||
|             const currentValue = this.activeClock.currentValue(); | ||||
|             const newBounds = { | ||||
|                 start: currentValue + offsets.start, | ||||
|                 end: currentValue + offsets.end | ||||
|             }; | ||||
|  | ||||
|             this.bounds(newBounds); | ||||
|  | ||||
|             /** | ||||
|              * Event that is triggered when clock offsets change. | ||||
|              * @event clockOffsets | ||||
|              * @memberof module:openmct.TimeAPI~ | ||||
|              * @property {ClockOffsets} clockOffsets The newly activated clock | ||||
|              * offsets. | ||||
|              */ | ||||
|             this.emit("clockOffsets", offsets); | ||||
|         } | ||||
|  | ||||
|         return this.offsets; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Stop the currently active clock from ticking, and unset it. This will | ||||
|      * revert all views to showing a static time frame defined by the current | ||||
|      * bounds. | ||||
|      */ | ||||
|     stopClock() { | ||||
|         if (this.activeClock) { | ||||
|             this.clock(undefined, undefined); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Set the active clock. Tick source will be immediately subscribed to | ||||
|      * and ticking will begin. Offsets from 'now' must also be provided. A clock | ||||
|      * can be unset by calling {@link stopClock}. | ||||
|      * | ||||
|      * @param {Clock || string} keyOrClock The clock to activate, or its key | ||||
|      * @param {ClockOffsets} offsets on each tick these will be used to calculate | ||||
|      * the start and end bounds. This maintains a sliding time window of a fixed | ||||
|      * width that automatically updates. | ||||
|      * @fires module:openmct.TimeAPI~clock | ||||
|      * @return {Clock} the currently active clock; | ||||
|      */ | ||||
|     clock(keyOrClock, offsets) { | ||||
|         if (arguments.length === 2) { | ||||
|             let clock; | ||||
|  | ||||
|             if (typeof keyOrClock === 'string') { | ||||
|                 clock = this.clocks.get(keyOrClock); | ||||
|                 if (clock === undefined) { | ||||
|                     throw "Unknown clock '" + keyOrClock + "'. Has it been registered with 'addClock'?"; | ||||
|                 } | ||||
|             } else if (typeof keyOrClock === 'object') { | ||||
|                 clock = keyOrClock; | ||||
|                 if (!this.clocks.has(clock.key)) { | ||||
|                     throw "Unknown clock '" + keyOrClock.key + "'. Has it been registered with 'addClock'?"; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             const previousClock = this.activeClock; | ||||
|             if (previousClock !== undefined) { | ||||
|                 previousClock.off("tick", this.tick); | ||||
|             } | ||||
|  | ||||
|             this.activeClock = clock; | ||||
|  | ||||
|             /** | ||||
|              * The active clock has changed. Clock can be unset by calling {@link stopClock} | ||||
|              * @event clock | ||||
|              * @memberof module:openmct.TimeAPI~ | ||||
|              * @property {Clock} clock The newly activated clock, or undefined | ||||
|              * if the system is no longer following a clock source | ||||
|              */ | ||||
|             this.emit("clock", this.activeClock); | ||||
|  | ||||
|             if (this.activeClock !== undefined) { | ||||
|                 this.clockOffsets(offsets); | ||||
|                 this.activeClock.on("tick", this.tick); | ||||
|             } | ||||
|  | ||||
|         } else if (arguments.length === 1) { | ||||
|             throw "When setting the clock, clock offsets must also be provided"; | ||||
|         } | ||||
|  | ||||
|         return this.activeClock; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Update bounds based on provided time and current offsets | ||||
|      * @param {number} timestamp A time from which bounds will be calculated | ||||
|      * using current offsets. | ||||
|      */ | ||||
|     tick(timestamp) { | ||||
|         if (!this.activeClock) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         const newBounds = { | ||||
|             start: timestamp + this.offsets.start, | ||||
|             end: timestamp + this.offsets.end | ||||
|         }; | ||||
|  | ||||
|         this.boundsVal = newBounds; | ||||
|         this.emit('bounds', this.boundsVal, true); | ||||
|     } | ||||
| } | ||||
|  | ||||
| export default TimeContext; | ||||
							
								
								
									
										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(); | ||||
|         }); | ||||
|  | ||||
|     }); | ||||
| }); | ||||
| @@ -63,12 +63,6 @@ define(['./Type'], function (Type) { | ||||
|      */ | ||||
|     TypeRegistry.prototype.standardizeType = function (typeDef) { | ||||
|         if (Object.prototype.hasOwnProperty.call(typeDef, 'label')) { | ||||
|             console.warn( | ||||
|                 'DEPRECATION WARNING typeDef: ' + typeDef.label + '.  ' | ||||
|                 + '`label` is deprecated in type definitions.  Please use ' | ||||
|                 + '`name` instead.  This will cause errors in a future version ' | ||||
|                 + 'of Open MCT.  For more information, see ' | ||||
|                 + 'https://github.com/nasa/openmct/issues/1568'); | ||||
|             if (!typeDef.name) { | ||||
|                 typeDef.name = typeDef.label; | ||||
|             } | ||||
|   | ||||
| @@ -90,7 +90,7 @@ class ImageExporter { | ||||
|                 element.id = oldId; | ||||
|             }, | ||||
|             removeContainer: true // Set to false to debug what html2canvas renders | ||||
|         }).then(function (canvas) { | ||||
|         }).then(canvas => { | ||||
|             dialog.dismiss(); | ||||
|  | ||||
|             return new Promise(function (resolve, reject) { | ||||
| @@ -105,9 +105,10 @@ class ImageExporter { | ||||
|  | ||||
|                 return canvas.toBlob(blob => resolve({ blob }), mimeType); | ||||
|             }); | ||||
|         }, function (error) { | ||||
|             console.log('error capturing image', error); | ||||
|         }).catch(error => { | ||||
|             dialog.dismiss(); | ||||
|  | ||||
|             console.error('error capturing image', error); | ||||
|             const errorDialog = overlays.dialog({ | ||||
|                 iconClass: 'error', | ||||
|                 message: 'Image was not captured successfully!', | ||||
|   | ||||
							
								
								
									
										78
									
								
								src/plugins/clearData/ClearDataAction.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								src/plugins/clearData/ClearDataAction.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,78 @@ | ||||
| /***************************************************************************** | ||||
|  * 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. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| function inSelectionPath(openmct, domainObject) { | ||||
|     const domainObjectIdentifier = domainObject.identifier; | ||||
|  | ||||
|     return openmct.selection.get().some(selectionPath => { | ||||
|         return selectionPath.some(objectInPath => { | ||||
|             const objectInPathIdentifier = objectInPath.context.item.identifier; | ||||
|  | ||||
|             return openmct.objects.areIdsEqual(objectInPathIdentifier, domainObjectIdentifier); | ||||
|         }); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| export default class ClearDataAction { | ||||
|     constructor(openmct, appliesToObjects) { | ||||
|         this.name = 'Clear Data for Object'; | ||||
|         this.key = 'clear-data-action'; | ||||
|         this.description = 'Clears current data for object, unsubscribes and resubscribes to data'; | ||||
|         this.cssClass = 'icon-clear-data'; | ||||
|  | ||||
|         this._openmct = openmct; | ||||
|         this._appliesToObjects = appliesToObjects; | ||||
|     } | ||||
|     invoke(objectPath) { | ||||
|         let domainObject = null; | ||||
|         if (objectPath) { | ||||
|             domainObject = objectPath[0]; | ||||
|         } | ||||
|  | ||||
|         this._openmct.objectViews.emit('clearData', domainObject); | ||||
|     } | ||||
|     appliesTo(objectPath) { | ||||
|         if (!objectPath) { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         const contextualDomainObject = objectPath[0]; | ||||
|         // first check to see if this action applies to this sort of object at all | ||||
|         const appliesToThisObject = this._appliesToObjects.some(type => { | ||||
|             return contextualDomainObject.type === type; | ||||
|         }); | ||||
|         if (!appliesToThisObject) { | ||||
|             // we've selected something not applicable | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         const objectInSelectionPath = inSelectionPath(this._openmct, contextualDomainObject); | ||||
|         if (objectInSelectionPath) { | ||||
|             return true; | ||||
|         } else { | ||||
|             // if this it doesn't match up, check to see if we're in a composition (i.e., layout) | ||||
|             const routerPath = this._openmct.router.path[0]; | ||||
|  | ||||
|             return routerPath.type === 'layout'; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -22,7 +22,7 @@ | ||||
|  | ||||
| define([ | ||||
|     './components/globalClearIndicator.vue', | ||||
|     './clearDataAction', | ||||
|     './ClearDataAction', | ||||
|     'vue' | ||||
| ], function ( | ||||
|     GlobaClearIndicator, | ||||
|   | ||||
							
								
								
									
										140
									
								
								src/plugins/clearData/test/ClearDataActionSpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								src/plugins/clearData/test/ClearDataActionSpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,140 @@ | ||||
| /***************************************************************************** | ||||
|  * 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 ClearDataActionPlugin from '../plugin.js'; | ||||
| import ClearDataAction from '../ClearDataAction.js'; | ||||
|  | ||||
| describe('When the Clear Data Plugin is installed,', () => { | ||||
|     const mockObjectViews = jasmine.createSpyObj('objectViews', ['emit']); | ||||
|     const mockIndicatorProvider = jasmine.createSpyObj('indicators', ['add']); | ||||
|     const mockActionsProvider = jasmine.createSpyObj('actions', ['register']); | ||||
|     const goodMockSelectionPath = [[{ | ||||
|         context: { | ||||
|             item: { | ||||
|                 identifier: { | ||||
|                     key: 'apple', | ||||
|                     namespace: '' | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     }]]; | ||||
|  | ||||
|     const openmct = { | ||||
|         objectViews: mockObjectViews, | ||||
|         indicators: mockIndicatorProvider, | ||||
|         actions: mockActionsProvider, | ||||
|         install: function (plugin) { | ||||
|             plugin(this); | ||||
|         }, | ||||
|         selection: { | ||||
|             get: function () { | ||||
|                 return goodMockSelectionPath; | ||||
|             } | ||||
|         }, | ||||
|         objects: { | ||||
|             areIdsEqual: function (obj1, obj2) { | ||||
|                 return true; | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     const mockObjectPath = [ | ||||
|         { | ||||
|             name: 'mockObject1', | ||||
|             type: 'apple' | ||||
|         }, | ||||
|         { | ||||
|             name: 'mockObject2', | ||||
|             type: 'banana' | ||||
|         } | ||||
|     ]; | ||||
|  | ||||
|     it('Global Clear Indicator is installed', () => { | ||||
|         openmct.install(ClearDataActionPlugin([])); | ||||
|  | ||||
|         expect(mockIndicatorProvider.add).toHaveBeenCalled(); | ||||
|     }); | ||||
|  | ||||
|     it('Clear Data context menu action is installed', () => { | ||||
|         openmct.install(ClearDataActionPlugin([])); | ||||
|  | ||||
|         expect(mockActionsProvider.register).toHaveBeenCalled(); | ||||
|     }); | ||||
|  | ||||
|     it('clear data action emits a clearData event when invoked', () => { | ||||
|         const action = new ClearDataAction(openmct); | ||||
|  | ||||
|         action.invoke(mockObjectPath); | ||||
|  | ||||
|         expect(mockObjectViews.emit).toHaveBeenCalledWith('clearData', mockObjectPath[0]); | ||||
|     }); | ||||
|  | ||||
|     it('clears data on applicable objects', () => { | ||||
|         let action = new ClearDataAction(openmct, ['apple']); | ||||
|  | ||||
|         const actionApplies = action.appliesTo(mockObjectPath); | ||||
|  | ||||
|         expect(actionApplies).toBe(true); | ||||
|     }); | ||||
|  | ||||
|     it('does not clear data on inapplicable objects', () => { | ||||
|         let action = new ClearDataAction(openmct, ['pineapple']); | ||||
|  | ||||
|         const actionApplies = action.appliesTo(mockObjectPath); | ||||
|  | ||||
|         expect(actionApplies).toBe(false); | ||||
|     }); | ||||
|  | ||||
|     it('does not clear data if not in the selection path and not a layout', () => { | ||||
|         openmct.objects = { | ||||
|             areIdsEqual: function (obj1, obj2) { | ||||
|                 return false; | ||||
|             } | ||||
|         }; | ||||
|         openmct.router = { | ||||
|             path: [{type: 'not-a-layout'}] | ||||
|         }; | ||||
|  | ||||
|         let action = new ClearDataAction(openmct, ['apple']); | ||||
|  | ||||
|         const actionApplies = action.appliesTo(mockObjectPath); | ||||
|  | ||||
|         expect(actionApplies).toBe(false); | ||||
|     }); | ||||
|  | ||||
|     it('does clear data if not in the selection path and is a layout', () => { | ||||
|         openmct.objects = { | ||||
|             areIdsEqual: function (obj1, obj2) { | ||||
|                 return false; | ||||
|             } | ||||
|         }; | ||||
|         openmct.router = { | ||||
|             path: [{type: 'layout'}] | ||||
|         }; | ||||
|  | ||||
|         let action = new ClearDataAction(openmct, ['apple']); | ||||
|  | ||||
|         const actionApplies = action.appliesTo(mockObjectPath); | ||||
|  | ||||
|         expect(actionApplies).toBe(true); | ||||
|     }); | ||||
| }); | ||||
| @@ -1,64 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * 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 ClearDataActionPlugin from '../plugin.js'; | ||||
| import ClearDataAction from '../clearDataAction.js'; | ||||
|  | ||||
| describe('When the Clear Data Plugin is installed,', function () { | ||||
|     const mockObjectViews = jasmine.createSpyObj('objectViews', ['emit']); | ||||
|     const mockIndicatorProvider = jasmine.createSpyObj('indicators', ['add']); | ||||
|     const mockActionsProvider = jasmine.createSpyObj('actions', ['register']); | ||||
|  | ||||
|     const openmct = { | ||||
|         objectViews: mockObjectViews, | ||||
|         indicators: mockIndicatorProvider, | ||||
|         actions: mockActionsProvider, | ||||
|         install: function (plugin) { | ||||
|             plugin(this); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     const mockObjectPath = [ | ||||
|         {name: 'mockObject1'}, | ||||
|         {name: 'mockObject2'} | ||||
|     ]; | ||||
|  | ||||
|     it('Global Clear Indicator is installed', function () { | ||||
|         openmct.install(ClearDataActionPlugin([])); | ||||
|  | ||||
|         expect(mockIndicatorProvider.add).toHaveBeenCalled(); | ||||
|     }); | ||||
|  | ||||
|     it('Clear Data context menu action is installed', function () { | ||||
|         openmct.install(ClearDataActionPlugin([])); | ||||
|  | ||||
|         expect(mockActionsProvider.register).toHaveBeenCalled(); | ||||
|     }); | ||||
|  | ||||
|     it('clear data action emits a clearData event when invoked', function () { | ||||
|         let action = new ClearDataAction(openmct); | ||||
|  | ||||
|         action.invoke(mockObjectPath); | ||||
|  | ||||
|         expect(mockObjectViews.emit).toHaveBeenCalledWith('clearData', mockObjectPath[0]); | ||||
|     }); | ||||
| }); | ||||
							
								
								
									
										59
									
								
								src/plugins/clock/ClockViewProvider.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								src/plugins/clock/ClockViewProvider.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | ||||
| /***************************************************************************** | ||||
|  * 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 Clock from './components/Clock.vue'; | ||||
| import Vue from 'vue'; | ||||
|  | ||||
| export default function ClockViewProvider(openmct) { | ||||
|     return { | ||||
|         key: 'clock.view', | ||||
|         name: 'Clock', | ||||
|         cssClass: 'icon-clock', | ||||
|         canView(domainObject) { | ||||
|             return domainObject.type === 'clock'; | ||||
|         }, | ||||
|  | ||||
|         view: function (domainObject) { | ||||
|             let component; | ||||
|  | ||||
|             return { | ||||
|                 show: function (element) { | ||||
|                     component = new Vue({ | ||||
|                         el: element, | ||||
|                         components: { | ||||
|                             Clock | ||||
|                         }, | ||||
|                         provide: { | ||||
|                             openmct, | ||||
|                             domainObject | ||||
|                         }, | ||||
|                         template: '<clock />' | ||||
|                     }); | ||||
|                 }, | ||||
|                 destroy: function () { | ||||
|                     component.$destroy(); | ||||
|                     component = undefined; | ||||
|                 } | ||||
|             }; | ||||
|         } | ||||
|     }; | ||||
| } | ||||
							
								
								
									
										99
									
								
								src/plugins/clock/components/Clock.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								src/plugins/clock/components/Clock.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,99 @@ | ||||
| <!-- | ||||
|  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. | ||||
| --> | ||||
|  | ||||
| <template> | ||||
| <div class="l-angular-ov-wrapper"> | ||||
|     <div class="u-contents"> | ||||
|         <div class="c-clock l-time-display u-style-receiver js-style-receiver"> | ||||
|             <div class="c-clock__timezone"> | ||||
|                 {{ timeZoneAbbr }} | ||||
|             </div> | ||||
|             <div class="c-clock__value"> | ||||
|                 {{ timeTextValue }} | ||||
|             </div> | ||||
|             <div class="c-clock__ampm"> | ||||
|                 {{ timeAmPm }} | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import moment from 'moment'; | ||||
| import momentTimezone from 'moment-timezone'; | ||||
|  | ||||
| export default { | ||||
|     inject: ['openmct', 'domainObject'], | ||||
|     data() { | ||||
|         return { | ||||
|             lastTimestamp: null | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
|         configuration() { | ||||
|             return this.domainObject.configuration; | ||||
|         }, | ||||
|         baseFormat() { | ||||
|             return this.configuration.baseFormat; | ||||
|         }, | ||||
|         use24() { | ||||
|             return this.configuration.use24 === 'clock24'; | ||||
|         }, | ||||
|         timezone() { | ||||
|             return this.configuration.timezone; | ||||
|         }, | ||||
|         timeFormat() { | ||||
|             return this.use24 ? this.baseFormat.replace('hh', "HH") : this.baseFormat; | ||||
|         }, | ||||
|         zoneName() { | ||||
|             return momentTimezone.tz.names().includes(this.timezone) ? this.timezone : "UTC"; | ||||
|         }, | ||||
|         momentTime() { | ||||
|             return this.zoneName ? moment.utc(this.lastTimestamp).tz(this.zoneName) : moment.utc(this.lastTimestamp); | ||||
|         }, | ||||
|         timeZoneAbbr() { | ||||
|             return this.momentTime.zoneAbbr(); | ||||
|         }, | ||||
|         timeTextValue() { | ||||
|             return this.timeFormat && this.momentTime.format(this.timeFormat); | ||||
|         }, | ||||
|         timeAmPm() { | ||||
|             return this.use24 ? '' : this.momentTime.format("A"); | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|         const TickerService = this.openmct.$injector.get('tickerService'); | ||||
|         this.unlisten = TickerService.listen(this.tick); | ||||
|     }, | ||||
|     beforeDestroy() { | ||||
|         if (this.unlisten) { | ||||
|             this.unlisten(); | ||||
|         } | ||||
|     }, | ||||
|     methods: { | ||||
|         tick(timestamp) { | ||||
|             this.lastTimestamp = timestamp; | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| </script> | ||||
							
								
								
									
										64
									
								
								src/plugins/clock/components/ClockIndicator.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								src/plugins/clock/components/ClockIndicator.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,64 @@ | ||||
| <!-- | ||||
|  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. | ||||
| --> | ||||
|  | ||||
| <template> | ||||
| <div class="c-indicator t-indicator-clock icon-clock no-minify c-indicator--not-clickable"> | ||||
|     <span class="label c-indicator__label"> | ||||
|         {{ timeTextValue }} | ||||
|     </span> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import moment from 'moment'; | ||||
|  | ||||
| export default { | ||||
|     inject: ['openmct'], | ||||
|     props: { | ||||
|         indicatorFormat: { | ||||
|             type: String, | ||||
|             required: true | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             timeTextValue: null | ||||
|         }; | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.openmct.on('start', () => { | ||||
|             const TickerService = this.openmct.$injector.get('tickerService'); | ||||
|             this.unlisten = TickerService.listen(this.tick); | ||||
|         }); | ||||
|     }, | ||||
|     beforeDestroy() { | ||||
|         if (this.unlisten) { | ||||
|             this.unlisten(); | ||||
|         } | ||||
|     }, | ||||
|     methods: { | ||||
|         tick(timestamp) { | ||||
|             this.timeTextValue = `${moment.utc(timestamp).format(this.indicatorFormat)} UTC`; | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| </script> | ||||
							
								
								
									
										154
									
								
								src/plugins/clock/plugin.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										154
									
								
								src/plugins/clock/plugin.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,154 @@ | ||||
|  | ||||
| /***************************************************************************** | ||||
|  * 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 ClockViewProvider from './ClockViewProvider'; | ||||
| import ClockIndicator from './components/ClockIndicator.vue'; | ||||
|  | ||||
| import momentTimezone from 'moment-timezone'; | ||||
| import Vue from 'vue'; | ||||
|  | ||||
| export default function ClockPlugin(options) { | ||||
|     return function install(openmct) { | ||||
|         const CLOCK_INDICATOR_FORMAT = 'YYYY/MM/DD HH:mm:ss'; | ||||
|         openmct.types.addType('clock', { | ||||
|             name: 'Clock', | ||||
|             description: 'A UTC-based clock that supports a variety of display formats. Clocks can be added to Display Layouts.', | ||||
|             creatable: true, | ||||
|             cssClass: 'icon-clock', | ||||
|             initialize: function (domainObject) { | ||||
|                 domainObject.configuration = { | ||||
|                     baseFormat: 'YYYY/MM/DD hh:mm:ss', | ||||
|                     use24: 'clock12', | ||||
|                     timezone: 'UTC' | ||||
|                 }; | ||||
|             }, | ||||
|             "form": [ | ||||
|                 { | ||||
|                     "key": "displayFormat", | ||||
|                     "name": "Display Format", | ||||
|                     control: 'select', | ||||
|                     options: [ | ||||
|                         { | ||||
|                             value: 'YYYY/MM/DD hh:mm:ss', | ||||
|                             name: 'YYYY/MM/DD hh:mm:ss' | ||||
|                         }, | ||||
|                         { | ||||
|                             value: 'YYYY/DDD hh:mm:ss', | ||||
|                             name: 'YYYY/DDD hh:mm:ss' | ||||
|                         }, | ||||
|                         { | ||||
|                             value: 'hh:mm:ss', | ||||
|                             name: 'hh:mm:ss' | ||||
|                         } | ||||
|                     ], | ||||
|                     cssClass: 'l-inline', | ||||
|                     property: [ | ||||
|                         'configuration', | ||||
|                         'baseFormat' | ||||
|                     ] | ||||
|                 }, | ||||
|                 { | ||||
|                     control: 'select', | ||||
|                     options: [ | ||||
|                         { | ||||
|                             value: 'clock12', | ||||
|                             name: '12hr' | ||||
|                         }, | ||||
|                         { | ||||
|                             value: 'clock24', | ||||
|                             name: '24hr' | ||||
|                         } | ||||
|                     ], | ||||
|                     cssClass: 'l-inline', | ||||
|                     property: [ | ||||
|                         'configuration', | ||||
|                         'use24' | ||||
|                     ] | ||||
|                 }, | ||||
|                 { | ||||
|                     "key": "timezone", | ||||
|                     "name": "Timezone", | ||||
|                     "control": "autocomplete", | ||||
|                     "options": momentTimezone.tz.names(), | ||||
|                     property: [ | ||||
|                         'configuration', | ||||
|                         'timezone' | ||||
|                     ] | ||||
|                 } | ||||
|             ] | ||||
|         }); | ||||
|         openmct.objectViews.addProvider(new ClockViewProvider(openmct)); | ||||
|  | ||||
|         if (options && options.enableClockIndicator === true) { | ||||
|             const clockIndicator = new Vue ({ | ||||
|                 components: { | ||||
|                     ClockIndicator | ||||
|                 }, | ||||
|                 provide: { | ||||
|                     openmct | ||||
|                 }, | ||||
|                 data() { | ||||
|                     return { | ||||
|                         indicatorFormat: CLOCK_INDICATOR_FORMAT | ||||
|                     }; | ||||
|                 }, | ||||
|                 template: '<ClockIndicator :indicator-format="indicatorFormat" />' | ||||
|             }); | ||||
|             const indicator = { | ||||
|                 element: clockIndicator.$mount().$el, | ||||
|                 key: 'clock-indicator' | ||||
|             }; | ||||
|  | ||||
|             openmct.indicators.add(indicator); | ||||
|         } | ||||
|  | ||||
|         openmct.objects.addGetInterceptor({ | ||||
|             appliesTo: (identifier, domainObject) => { | ||||
|                 return domainObject && domainObject.type === 'clock'; | ||||
|             }, | ||||
|             invoke: (identifier, domainObject) => { | ||||
|                 if (domainObject.configuration) { | ||||
|                     return domainObject; | ||||
|                 } | ||||
|  | ||||
|                 if (domainObject.clockFormat | ||||
|                     && domainObject.timezone) { | ||||
|                     const baseFormat = domainObject.clockFormat[0]; | ||||
|                     const use24 = domainObject.clockFormat[1]; | ||||
|                     const timezone = domainObject.timezone; | ||||
|  | ||||
|                     domainObject.configuration = { | ||||
|                         baseFormat, | ||||
|                         use24, | ||||
|                         timezone | ||||
|                     }; | ||||
|  | ||||
|                     openmct.objects.mutate(domainObject, 'configuration', domainObject.configuration); | ||||
|                 } | ||||
|  | ||||
|                 return domainObject; | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|     }; | ||||
| } | ||||
							
								
								
									
										231
									
								
								src/plugins/clock/pluginSpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										231
									
								
								src/plugins/clock/pluginSpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,231 @@ | ||||
| /***************************************************************************** | ||||
|  * 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 { createOpenMct, resetApplicationState } from 'utils/testing'; | ||||
| import clockPlugin from './plugin'; | ||||
|  | ||||
| import Vue from 'vue'; | ||||
|  | ||||
| describe("Clock plugin:", () => { | ||||
|     let openmct; | ||||
|     let clockDefinition; | ||||
|     let element; | ||||
|     let child; | ||||
|     let appHolder; | ||||
|  | ||||
|     let clockDomainObject; | ||||
|  | ||||
|     function setupClock(enableClockIndicator) { | ||||
|         return new Promise((resolve, reject) => { | ||||
|             clockDomainObject = { | ||||
|                 identifier: { | ||||
|                     key: 'clock', | ||||
|                     namespace: 'test-namespace' | ||||
|                 }, | ||||
|                 type: 'clock' | ||||
|             }; | ||||
|  | ||||
|             appHolder = document.createElement('div'); | ||||
|             appHolder.style.width = '640px'; | ||||
|             appHolder.style.height = '480px'; | ||||
|             document.body.appendChild(appHolder); | ||||
|  | ||||
|             openmct = createOpenMct(); | ||||
|  | ||||
|             element = document.createElement('div'); | ||||
|             child = document.createElement('div'); | ||||
|             element.appendChild(child); | ||||
|  | ||||
|             openmct.install(clockPlugin({ enableClockIndicator })); | ||||
|  | ||||
|             clockDefinition = openmct.types.get('clock').definition; | ||||
|             clockDefinition.initialize(clockDomainObject); | ||||
|  | ||||
|             openmct.on('start', resolve); | ||||
|             openmct.start(appHolder); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     describe("Clock view:", () => { | ||||
|         let clockViewProvider; | ||||
|         let clockView; | ||||
|         let clockViewObject; | ||||
|         let mutableClockObject; | ||||
|  | ||||
|         beforeEach(async () => { | ||||
|             await setupClock(true); | ||||
|  | ||||
|             clockViewObject = { | ||||
|                 ...clockDomainObject, | ||||
|                 id: "test-object", | ||||
|                 name: 'Clock', | ||||
|                 configuration: { | ||||
|                     baseFormat: 'YYYY/MM/DD hh:mm:ss', | ||||
|                     use24: 'clock12', | ||||
|                     timezone: 'UTC' | ||||
|                 } | ||||
|             }; | ||||
|  | ||||
|             spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve(clockViewObject)); | ||||
|             spyOn(openmct.objects, 'save').and.returnValue(Promise.resolve(true)); | ||||
|  | ||||
|             const applicableViews = openmct.objectViews.get(clockViewObject, [clockViewObject]); | ||||
|             clockViewProvider = applicableViews.find(viewProvider => viewProvider.key === 'clock.view'); | ||||
|  | ||||
|             mutableClockObject = await openmct.objects.getMutable(clockViewObject.identifier); | ||||
|  | ||||
|             clockView = clockViewProvider.view(mutableClockObject); | ||||
|             clockView.show(child); | ||||
|  | ||||
|             await Vue.nextTick(); | ||||
|         }); | ||||
|  | ||||
|         afterEach(() => { | ||||
|             clockView.destroy(); | ||||
|             openmct.objects.destroyMutable(mutableClockObject); | ||||
|             if (appHolder) { | ||||
|                 appHolder.remove(); | ||||
|             } | ||||
|  | ||||
|             return resetApplicationState(openmct); | ||||
|         }); | ||||
|  | ||||
|         it("has name as Clock", () => { | ||||
|             expect(clockDefinition.name).toEqual('Clock'); | ||||
|         }); | ||||
|  | ||||
|         it("is creatable", () => { | ||||
|             expect(clockDefinition.creatable).toEqual(true); | ||||
|         }); | ||||
|  | ||||
|         it("provides clock view", () => { | ||||
|             expect(clockViewProvider).toBeDefined(); | ||||
|         }); | ||||
|  | ||||
|         it("renders clock element", () => { | ||||
|             const clockElement = element.querySelectorAll('.c-clock'); | ||||
|             expect(clockElement.length).toBe(1); | ||||
|         }); | ||||
|  | ||||
|         it("renders major elements", () => { | ||||
|             const clockElement = element.querySelector('.c-clock'); | ||||
|             const timezone = clockElement.querySelector('.c-clock__timezone'); | ||||
|             const time = clockElement.querySelector('.c-clock__value'); | ||||
|             const amPm = clockElement.querySelector('.c-clock__ampm'); | ||||
|             const hasMajorElements = Boolean(timezone && time && amPm); | ||||
|  | ||||
|             expect(hasMajorElements).toBe(true); | ||||
|         }); | ||||
|  | ||||
|         it("renders time in UTC", () => { | ||||
|             const clockElement = element.querySelector('.c-clock'); | ||||
|             const timezone = clockElement.querySelector('.c-clock__timezone').textContent.trim(); | ||||
|  | ||||
|             expect(timezone).toBe('UTC'); | ||||
|         }); | ||||
|  | ||||
|         it("updates the 24 hour option in the configuration", (done) => { | ||||
|             expect(clockDomainObject.configuration.use24).toBe('clock12'); | ||||
|             const new24Option = 'clock24'; | ||||
|  | ||||
|             openmct.objects.observe(clockViewObject, 'configuration', (changedDomainObject) => { | ||||
|                 expect(changedDomainObject.use24).toBe(new24Option); | ||||
|                 done(); | ||||
|             }); | ||||
|  | ||||
|             openmct.objects.mutate(clockViewObject, 'configuration.use24', new24Option); | ||||
|         }); | ||||
|  | ||||
|         it("updates the timezone option in the configuration", (done) => { | ||||
|             expect(clockDomainObject.configuration.timezone).toBe('UTC'); | ||||
|             const newZone = 'CST6CDT'; | ||||
|  | ||||
|             openmct.objects.observe(clockViewObject, 'configuration', (changedDomainObject) => { | ||||
|                 expect(changedDomainObject.timezone).toBe(newZone); | ||||
|                 done(); | ||||
|             }); | ||||
|  | ||||
|             openmct.objects.mutate(clockViewObject, 'configuration.timezone', newZone); | ||||
|         }); | ||||
|  | ||||
|         it("updates the time format option in the configuration", (done) => { | ||||
|             expect(clockDomainObject.configuration.baseFormat).toBe('YYYY/MM/DD hh:mm:ss'); | ||||
|             const newFormat = 'hh:mm:ss'; | ||||
|  | ||||
|             openmct.objects.observe(clockViewObject, 'configuration', (changedDomainObject) => { | ||||
|                 expect(changedDomainObject.baseFormat).toBe(newFormat); | ||||
|                 done(); | ||||
|             }); | ||||
|  | ||||
|             openmct.objects.mutate(clockViewObject, 'configuration.baseFormat', newFormat); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe("Clock Indicator view:", () => { | ||||
|         let clockIndicator; | ||||
|  | ||||
|         afterEach(() => { | ||||
|             if (clockIndicator) { | ||||
|                 clockIndicator.remove(); | ||||
|             } | ||||
|  | ||||
|             clockIndicator = undefined; | ||||
|             if (appHolder) { | ||||
|                 appHolder.remove(); | ||||
|             } | ||||
|  | ||||
|             return resetApplicationState(openmct); | ||||
|         }); | ||||
|  | ||||
|         it("doesn't exist", async () => { | ||||
|             await setupClock(false); | ||||
|  | ||||
|             clockIndicator = openmct.indicators.indicatorObjects | ||||
|                 .find(indicator => indicator.key === 'clock-indicator'); | ||||
|  | ||||
|             const clockIndicatorMissing = clockIndicator === null || clockIndicator === undefined; | ||||
|             expect(clockIndicatorMissing).toBe(true); | ||||
|         }); | ||||
|  | ||||
|         it("exists", async () => { | ||||
|             await setupClock(true); | ||||
|  | ||||
|             clockIndicator = openmct.indicators.indicatorObjects | ||||
|                 .find(indicator => indicator.key === 'clock-indicator').element; | ||||
|  | ||||
|             const hasClockIndicator = clockIndicator !== null && clockIndicator !== undefined; | ||||
|             expect(hasClockIndicator).toBe(true); | ||||
|         }); | ||||
|  | ||||
|         it("contains text", async () => { | ||||
|             await setupClock(true); | ||||
|  | ||||
|             clockIndicator = openmct.indicators.indicatorObjects | ||||
|                 .find(indicator => indicator.key === 'clock-indicator').element; | ||||
|  | ||||
|             const clockIndicatorText = clockIndicator.textContent.trim(); | ||||
|             const textIncludesUTC = clockIndicatorText.includes('UTC'); | ||||
|  | ||||
|             expect(textIncludesUTC).toBe(true); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
| @@ -30,10 +30,10 @@ | ||||
|             <div v-if="staticStyle" | ||||
|                  class="c-inspect-styles__style" | ||||
|             > | ||||
|                 <style-editor class="c-inspect-styles__editor" | ||||
|                               :style-item="staticStyle" | ||||
|                               :is-editing="isEditing" | ||||
|                               @persist="updateStaticStyle" | ||||
|                 <StyleEditor class="c-inspect-styles__editor" | ||||
|                              :style-item="staticStyle" | ||||
|                              :is-editing="isEditing" | ||||
|                              @persist="updateStaticStyle" | ||||
|                 /> | ||||
|             </div> | ||||
|             <button | ||||
| @@ -87,10 +87,10 @@ | ||||
|                 <condition-description :show-label="true" | ||||
|                                        :condition="getCondition(conditionStyle.conditionId)" | ||||
|                 /> | ||||
|                 <style-editor class="c-inspect-styles__editor" | ||||
|                               :style-item="conditionStyle" | ||||
|                               :is-editing="isEditing" | ||||
|                               @persist="updateConditionalStyle" | ||||
|                 <StyleEditor class="c-inspect-styles__editor" | ||||
|                              :style-item="conditionStyle" | ||||
|                              :is-editing="isEditing" | ||||
|                              @persist="updateConditionalStyle" | ||||
|                 /> | ||||
|             </div> | ||||
|         </div> | ||||
| @@ -240,10 +240,10 @@ export default { | ||||
|             } | ||||
|  | ||||
|             let vm = new Vue({ | ||||
|                 components: {ConditionSetSelectorDialog}, | ||||
|                 provide: { | ||||
|                     openmct: this.openmct | ||||
|                 }, | ||||
|                 components: {ConditionSetSelectorDialog}, | ||||
|                 data() { | ||||
|                     return { | ||||
|                         handleItemSelection | ||||
| @@ -273,10 +273,7 @@ export default { | ||||
|             this.openmct.objects.getOriginalPath(this.conditionSetDomainObject.identifier).then( | ||||
|                 (objectPath) => { | ||||
|                     this.objectPath = objectPath; | ||||
|                     this.navigateToPath = '#/browse/' + this.objectPath | ||||
|                         .map(o => o && this.openmct.objects.makeKeyString(o.identifier)) | ||||
|                         .reverse() | ||||
|                         .join('/'); | ||||
|                     this.navigateToPath = '#/browse/' + this.openmct.objects.getRelativePath(this.objectPath); | ||||
|                 } | ||||
|             ); | ||||
|         }, | ||||
|   | ||||
| @@ -40,13 +40,13 @@ | ||||
|             <div v-if="staticStyle" | ||||
|                  class="c-inspect-styles__style" | ||||
|             > | ||||
|                 <style-editor class="c-inspect-styles__editor" | ||||
|                               :style-item="staticStyle" | ||||
|                               :is-editing="allowEditing" | ||||
|                               :mixed-styles="mixedStyles" | ||||
|                               :non-specific-font-properties="nonSpecificFontProperties" | ||||
|                               @persist="updateStaticStyle" | ||||
|                               @save-style="saveStyle" | ||||
|                 <StyleEditor class="c-inspect-styles__editor" | ||||
|                              :style-item="staticStyle" | ||||
|                              :is-editing="allowEditing" | ||||
|                              :mixed-styles="mixedStyles" | ||||
|                              :non-specific-font-properties="nonSpecificFontProperties" | ||||
|                              @persist="updateStaticStyle" | ||||
|                              @save-style="saveStyle" | ||||
|                 /> | ||||
|             </div> | ||||
|             <button | ||||
| @@ -108,12 +108,12 @@ | ||||
|                 <condition-description :show-label="true" | ||||
|                                        :condition="getCondition(conditionStyle.conditionId)" | ||||
|                 /> | ||||
|                 <style-editor class="c-inspect-styles__editor" | ||||
|                               :style-item="conditionStyle" | ||||
|                               :non-specific-font-properties="nonSpecificFontProperties" | ||||
|                               :is-editing="allowEditing" | ||||
|                               @persist="updateConditionalStyle" | ||||
|                               @save-style="saveStyle" | ||||
|                 <StyleEditor class="c-inspect-styles__editor" | ||||
|                              :style-item="conditionStyle" | ||||
|                              :non-specific-font-properties="nonSpecificFontProperties" | ||||
|                              :is-editing="allowEditing" | ||||
|                              @persist="updateConditionalStyle" | ||||
|                              @save-style="saveStyle" | ||||
|                 /> | ||||
|             </div> | ||||
|         </div> | ||||
| @@ -297,10 +297,7 @@ export default { | ||||
|             this.openmct.objects.getOriginalPath(this.conditionSetDomainObject.identifier).then( | ||||
|                 (objectPath) => { | ||||
|                     this.objectPath = objectPath; | ||||
|                     this.navigateToPath = '#/browse/' + this.objectPath | ||||
|                         .map(o => o && this.openmct.objects.makeKeyString(o.identifier)) | ||||
|                         .reverse() | ||||
|                         .join('/'); | ||||
|                     this.navigateToPath = '#/browse/' + this.openmct.objects.getRelativePath(this.objectPath); | ||||
|                 } | ||||
|             ); | ||||
|         }, | ||||
| @@ -559,10 +556,10 @@ export default { | ||||
|             } | ||||
|  | ||||
|             let vm = new Vue({ | ||||
|                 components: {ConditionSetSelectorDialog}, | ||||
|                 provide: { | ||||
|                     openmct: this.openmct | ||||
|                 }, | ||||
|                 components: {ConditionSetSelectorDialog}, | ||||
|                 data() { | ||||
|                     return { | ||||
|                         handleItemSelection | ||||
|   | ||||
| @@ -38,12 +38,16 @@ a.c-condition-widget { | ||||
|  | ||||
| // Make Condition Widget expand when in a hidden frame Layout context | ||||
| // For both static and Flexible Layouts | ||||
| .c-so-view--no-frame > .c-so-view__object-view > .c-condition-widget { | ||||
|     @include abs(); | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     justify-content: center; | ||||
|     padding: 0; | ||||
| .c-so-view--conditionWidget.c-so-view--no-frame { | ||||
|     .c-condition-widget { | ||||
|         @include abs(); | ||||
|         display: flex; | ||||
|         align-items: center; | ||||
|         justify-content: center; | ||||
|         padding: 0; | ||||
|     } | ||||
|  | ||||
|     .c-so-view__frame-controls { display: none; } | ||||
| } | ||||
|  | ||||
| // Add some margin when a Condition Widget is in a Flexible Layout | ||||
|   | ||||
| @@ -47,8 +47,8 @@ | ||||
|  | ||||
| .is-editing { | ||||
|     .l-shell__main-container { | ||||
|         &[s-selected], | ||||
|         &[s-selected-parent] { | ||||
|         [s-selected], | ||||
|         [s-selected-parent] { | ||||
|             // Display grid and allow edit marquee to display in main layout holder when editing | ||||
|             > .l-layout { | ||||
|                 background: $editUIGridColorBg; | ||||
|   | ||||
							
								
								
									
										73
									
								
								src/plugins/imagery/ImageryTimestripViewProvider.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								src/plugins/imagery/ImageryTimestripViewProvider.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,73 @@ | ||||
| /***************************************************************************** | ||||
|  * 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 ImageryTimeView from './components/ImageryTimeView.vue'; | ||||
| import Vue from "vue"; | ||||
|  | ||||
| export default function ImageryTimestripViewProvider(openmct) { | ||||
|     const type = 'example.imagery.time-strip.view'; | ||||
|  | ||||
|     function hasImageTelemetry(domainObject) { | ||||
|         const metadata = openmct.telemetry.getMetadata(domainObject); | ||||
|         if (!metadata) { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         return metadata.valuesForHints(['image']).length > 0; | ||||
|     } | ||||
|  | ||||
|     return { | ||||
|         key: type, | ||||
|         name: 'Imagery Timestrip View', | ||||
|         cssClass: 'icon-image', | ||||
|         canView: function (domainObject, objectPath) { | ||||
|             let isChildOfTimeStrip = objectPath.find(object => object.type === 'time-strip'); | ||||
|  | ||||
|             return hasImageTelemetry(domainObject) && isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath); | ||||
|         }, | ||||
|         view: function (domainObject, objectPath) { | ||||
|             let component; | ||||
|  | ||||
|             return { | ||||
|                 show: function (element) { | ||||
|                     component = new Vue({ | ||||
|                         el: element, | ||||
|                         components: { | ||||
|                             ImageryTimeView | ||||
|                         }, | ||||
|                         provide: { | ||||
|                             openmct: openmct, | ||||
|                             domainObject: domainObject, | ||||
|                             objectPath: objectPath | ||||
|                         }, | ||||
|                         template: '<imagery-time-view></imagery-time-view>' | ||||
|  | ||||
|                     }); | ||||
|                 }, | ||||
|  | ||||
|                 destroy: function () { | ||||
|                     component.$destroy(); | ||||
|                     component = undefined; | ||||
|                 } | ||||
|             }; | ||||
|         } | ||||
|     }; | ||||
| } | ||||
| @@ -1,4 +1,4 @@ | ||||
| import ImageryViewLayout from './components/ImageryViewLayout.vue'; | ||||
| import ImageryViewComponent from './components/ImageryView.vue'; | ||||
|  | ||||
| import Vue from 'vue'; | ||||
|  | ||||
| @@ -14,7 +14,7 @@ export default class ImageryView { | ||||
|         this.component = new Vue({ | ||||
|             el: element, | ||||
|             components: { | ||||
|                 ImageryViewLayout | ||||
|                 'imagery-view': ImageryViewComponent | ||||
|             }, | ||||
|             provide: { | ||||
|                 openmct: this.openmct, | ||||
| @@ -22,7 +22,8 @@ export default class ImageryView { | ||||
|                 objectPath: this.objectPath, | ||||
|                 currentView: this | ||||
|             }, | ||||
|             template: '<imagery-view-layout ref="ImageryLayout"></imagery-view-layout>' | ||||
|             template: '<imagery-view ref="ImageryContainer"></imagery-view>' | ||||
|  | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -37,8 +37,10 @@ export default function ImageryViewProvider(openmct) { | ||||
|         key: type, | ||||
|         name: 'Imagery Layout', | ||||
|         cssClass: 'icon-image', | ||||
|         canView: function (domainObject) { | ||||
|             return hasImageTelemetry(domainObject); | ||||
|         canView: function (domainObject, objectPath) { | ||||
|             let isChildOfTimeStrip = objectPath.find(object => object.type === 'time-strip'); | ||||
|  | ||||
|             return hasImageTelemetry(domainObject) && (!isChildOfTimeStrip || openmct.router.isNavigatedObject(objectPath)); | ||||
|         }, | ||||
|         view: function (domainObject, objectPath) { | ||||
|             return new ImageryView(openmct, domainObject, objectPath); | ||||
|   | ||||
| @@ -39,10 +39,13 @@ describe("The Compass component", () => { | ||||
|             sunAngle: 30 | ||||
|         }; | ||||
|         let propsData = { | ||||
|             containerWidth: 600, | ||||
|             containerHeight: 600, | ||||
|             naturalAspectRatio: 0.9, | ||||
|             image: imageDatum | ||||
|             image: imageDatum, | ||||
|             sizedImageDimensions: { | ||||
|                 width: 100, | ||||
|                 height: 100 | ||||
|             }, | ||||
|             compassRoseSizingClasses: '--rose-small --rose-min' | ||||
|         }; | ||||
|  | ||||
|         app = new Vue({ | ||||
| @@ -51,13 +54,13 @@ describe("The Compass component", () => { | ||||
|                 return propsData; | ||||
|             }, | ||||
|             template: `<Compass | ||||
|                 :container-width="containerWidth" | ||||
|                 :container-height="containerHeight" | ||||
|                 :compass-rose-sizing-classes="compassRoseSizingClasses" | ||||
|                 :image="image" | ||||
|                 :natural-aspect-ratio="naturalAspectRatio" | ||||
|                 :image="image" />` | ||||
|                 :sized-image-dimensions="sizedImageDimensions" | ||||
|             />` | ||||
|         }); | ||||
|         instance = app.$mount(); | ||||
|  | ||||
|     }); | ||||
|  | ||||
|     afterAll(() => { | ||||
|   | ||||
							
								
								
									
										475
									
								
								src/plugins/imagery/components/ImageryTimeView.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										475
									
								
								src/plugins/imagery/components/ImageryTimeView.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,475 @@ | ||||
| <!-- | ||||
|  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. | ||||
| --> | ||||
|  | ||||
| <template> | ||||
| <div ref="imagery" | ||||
|      class="c-imagery-tsv c-timeline-holder" | ||||
| > | ||||
|     <div ref="imageryHolder" | ||||
|          class="c-imagery-tsv__contents u-contents" | ||||
|     > | ||||
|     </div> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import * as d3Scale from 'd3-scale'; | ||||
| import SwimLane from "@/ui/components/swim-lane/SwimLane.vue"; | ||||
| import Vue from "vue"; | ||||
| import imageryData from "../../imagery/mixins/imageryData"; | ||||
| import PreviewAction from "@/ui/preview/PreviewAction"; | ||||
| import _ from "lodash"; | ||||
|  | ||||
| const PADDING = 1; | ||||
| const ROW_HEIGHT = 100; | ||||
| const IMAGE_WIDTH_THRESHOLD = 40; | ||||
|  | ||||
| export default { | ||||
|     mixins: [imageryData], | ||||
|     inject: ['openmct', 'domainObject', 'objectPath'], | ||||
|     data() { | ||||
|         let timeSystem = this.openmct.time.timeSystem(); | ||||
|         this.metadata = {}; | ||||
|         this.requestCount = 0; | ||||
|  | ||||
|         return { | ||||
|             viewBounds: undefined, | ||||
|             height: 0, | ||||
|             durationFormatter: undefined, | ||||
|             imageHistory: [], | ||||
|             timeSystem: timeSystem, | ||||
|             keyString: undefined | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
|         imageHistorySize() { | ||||
|             return this.imageHistory.length; | ||||
|         } | ||||
|     }, | ||||
|     watch: { | ||||
|         imageHistorySize(newSize, oldSize) { | ||||
|             this.updatePlotImagery(); | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.previewAction = new PreviewAction(this.openmct); | ||||
|  | ||||
|         this.canvas = this.$refs.imagery.appendChild(document.createElement('canvas')); | ||||
|         this.canvas.height = 0; | ||||
|         this.canvasContext = this.canvas.getContext('2d'); | ||||
|         this.setDimensions(); | ||||
|  | ||||
|         this.updateViewBounds(); | ||||
|  | ||||
|         this.openmct.time.on("timeSystem", this.setScaleAndPlotImagery); | ||||
|         this.openmct.time.on("bounds", this.updateViewBounds); | ||||
|  | ||||
|         this.resize = _.debounce(this.resize, 400); | ||||
|         this.imageryStripResizeObserver = new ResizeObserver(this.resize); | ||||
|         this.imageryStripResizeObserver.observe(this.$refs.imagery); | ||||
|  | ||||
|         this.unlisten = this.openmct.objects.observe(this.domainObject, '*', this.observeForChanges); | ||||
|     }, | ||||
|     beforeDestroy() { | ||||
|         if (this.unsubscribe) { | ||||
|             this.unsubscribe(); | ||||
|             delete this.unsubscribe; | ||||
|         } | ||||
|  | ||||
|         if (this.imageryStripResizeObserver) { | ||||
|             this.imageryStripResizeObserver.disconnect(); | ||||
|         } | ||||
|  | ||||
|         this.openmct.time.off("timeSystem", this.setScaleAndPlotImagery); | ||||
|         this.openmct.time.off("bounds", this.updateViewBounds); | ||||
|         if (this.unlisten) { | ||||
|             this.unlisten(); | ||||
|         } | ||||
|     }, | ||||
|     methods: { | ||||
|         expand(index) { | ||||
|             const path = this.objectPath[0]; | ||||
|             this.previewAction.invoke([path]); | ||||
|         }, | ||||
|         observeForChanges(mutatedObject) { | ||||
|             this.updateViewBounds(); | ||||
|         }, | ||||
|         resize() { | ||||
|             let clientWidth = this.getClientWidth(); | ||||
|             if (clientWidth !== this.width) { | ||||
|                 this.setDimensions(); | ||||
|                 this.updateViewBounds(); | ||||
|             } | ||||
|         }, | ||||
|         getClientWidth() { | ||||
|             let clientWidth = this.$refs.imagery.clientWidth; | ||||
|  | ||||
|             if (!clientWidth) { | ||||
|                 //this is a hack - need a better way to find the parent of this component | ||||
|                 let parent = this.openmct.layout.$refs.browseObject.$el; | ||||
|                 if (parent) { | ||||
|                     clientWidth = parent.getBoundingClientRect().width; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             return clientWidth; | ||||
|         }, | ||||
|         updateViewBounds(bounds, isTick) { | ||||
|             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; | ||||
|  | ||||
|             if (this.timeSystem === undefined) { | ||||
|                 this.timeSystem = this.openmct.time.timeSystem(); | ||||
|             } | ||||
|  | ||||
|             this.setScaleAndPlotImagery(this.timeSystem, !isTick); | ||||
|  | ||||
|         }, | ||||
|         setScaleAndPlotImagery(timeSystem, clearAllImagery) { | ||||
|             if (timeSystem !== undefined) { | ||||
|                 this.timeSystem = timeSystem; | ||||
|                 this.timeFormatter = this.getFormatter(this.timeSystem.key); | ||||
|             } | ||||
|  | ||||
|             this.setScale(this.timeSystem); | ||||
|             this.updatePlotImagery(clearAllImagery); | ||||
|         }, | ||||
|         getFormatter(key) { | ||||
|             const metadata = this.openmct.telemetry.getMetadata(this.domainObject); | ||||
|  | ||||
|             let metadataValue = metadata.value(key) || { format: key }; | ||||
|             let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue); | ||||
|  | ||||
|             return valueFormatter; | ||||
|         }, | ||||
|         updatePlotImagery(clearAllImagery) { | ||||
|             this.clearPreviousImagery(clearAllImagery); | ||||
|             if (this.xScale) { | ||||
|                 this.drawImagery(); | ||||
|             } | ||||
|         }, | ||||
|         clearPreviousImagery(clearAllImagery) { | ||||
|             //TODO: Only clear items that are out of bounds | ||||
|             let noItemsEl = this.$el.querySelectorAll(".c-imagery-tsv__no-items"); | ||||
|             noItemsEl.forEach(item => { | ||||
|                 item.remove(); | ||||
|             }); | ||||
|             let imagery = this.$el.querySelectorAll(".c-imagery-tsv__image-wrapper"); | ||||
|             imagery.forEach(item => { | ||||
|                 if (clearAllImagery) { | ||||
|                     item.remove(); | ||||
|                 } else { | ||||
|                     const id = this.getNSAttributesForElement(item, 'id'); | ||||
|                     if (id) { | ||||
|                         const timestamp = id.replace('id-', ''); | ||||
|                         if (!this.isImageryInBounds({ | ||||
|                             time: timestamp | ||||
|                         })) { | ||||
|                             item.remove(); | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             }); | ||||
|         }, | ||||
|         setDimensions() { | ||||
|             const imageryHolder = this.$refs.imagery; | ||||
|             this.width = this.getClientWidth(); | ||||
|  | ||||
|             this.height = Math.round(imageryHolder.getBoundingClientRect().height); | ||||
|         }, | ||||
|         setScale(timeSystem) { | ||||
|             if (!this.width) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             if (timeSystem === undefined) { | ||||
|                 timeSystem = this.openmct.time.timeSystem(); | ||||
|             } | ||||
|  | ||||
|             if (timeSystem.isUTCBased) { | ||||
|                 this.xScale = d3Scale.scaleUtc(); | ||||
|                 this.xScale.domain( | ||||
|                     [new Date(this.viewBounds.start), new Date(this.viewBounds.end)] | ||||
|                 ); | ||||
|             } else { | ||||
|                 this.xScale = d3Scale.scaleLinear(); | ||||
|                 this.xScale.domain( | ||||
|                     [this.viewBounds.start, this.viewBounds.end] | ||||
|                 ); | ||||
|             } | ||||
|  | ||||
|             this.xScale.range([PADDING, this.width - PADDING * 2]); | ||||
|         }, | ||||
|         isImageryInBounds(imageObj) { | ||||
|             return (imageObj.time < this.viewBounds.end) && (imageObj.time > this.viewBounds.start); | ||||
|         }, | ||||
|         getImageryContainer() { | ||||
|             let svgHeight = 100; | ||||
|             let svgWidth = this.imageHistory.length ? this.width : 200; | ||||
|             let groupSVG; | ||||
|  | ||||
|             let existingSVG = this.$el.querySelector(".c-imagery-tsv__contents svg"); | ||||
|             if (existingSVG) { | ||||
|                 groupSVG = existingSVG; | ||||
|                 this.setNSAttributesForElement(groupSVG, { | ||||
|                     width: svgWidth | ||||
|                 }); | ||||
|             } else { | ||||
|                 let component = new Vue({ | ||||
|                     components: { | ||||
|                         SwimLane | ||||
|                     }, | ||||
|                     provide: { | ||||
|                         openmct: this.openmct | ||||
|                     }, | ||||
|                     data() { | ||||
|                         return { | ||||
|                             isNested: true, | ||||
|                             height: svgHeight, | ||||
|                             width: svgWidth | ||||
|                         }; | ||||
|                     }, | ||||
|                     template: `<swim-lane :is-nested="isNested" :hide-label="true"><template slot="object"><svg class="c-imagery-tsv-container" :height="height" :width="width"></svg></template></swim-lane>` | ||||
|                 }); | ||||
|  | ||||
|                 this.$refs.imageryHolder.appendChild(component.$mount().$el); | ||||
|  | ||||
|                 groupSVG = component.$el.querySelector('svg'); | ||||
|  | ||||
|                 groupSVG.addEventListener('mouseout', (event) => { | ||||
|                     if (event.target.nodeName === 'svg' || event.target.nodeName === 'use') { | ||||
|                         this.removeFromForeground(); | ||||
|                     } | ||||
|                 }); | ||||
|             } | ||||
|  | ||||
|             return groupSVG; | ||||
|         }, | ||||
|         isImageryWidthAcceptable() { | ||||
|             // We're calculating if there is enough space between images to show the thumbnails. | ||||
|             // This algorithm could probably be enhanced to check the x co-ordinate distance between 2 consecutive images, but | ||||
|             // we will go with this for now assuming imagery is not sorted by asc time so it's difficult to calculate. | ||||
|             // TODO: Use telemetry.requestCollection to get sorted telemetry | ||||
|             const currentStart = this.viewBounds.start; | ||||
|             const currentEnd = this.viewBounds.end; | ||||
|             const rectX = this.xScale(currentStart); | ||||
|             const rectY = this.xScale(currentEnd); | ||||
|             const imageContainerWidth = this.imageHistory.length ? (rectY - rectX) / this.imageHistory.length : 0; | ||||
|  | ||||
|             return imageContainerWidth < IMAGE_WIDTH_THRESHOLD; | ||||
|         }, | ||||
|         drawImagery() { | ||||
|             let groupSVG = this.getImageryContainer(); | ||||
|             const showImagePlaceholders = this.isImageryWidthAcceptable(); | ||||
|  | ||||
|             if (this.imageHistory.length) { | ||||
|                 this.imageHistory.forEach((currentImageObject, index) => { | ||||
|                     if (this.isImageryInBounds(currentImageObject)) { | ||||
|                         this.plotImagery(currentImageObject, showImagePlaceholders, groupSVG, index); | ||||
|                     } | ||||
|                 }); | ||||
|             } else { | ||||
|                 this.plotNoItems(groupSVG); | ||||
|             } | ||||
|         }, | ||||
|         plotNoItems(svgElement) { | ||||
|             let textElement = document.createElementNS('http://www.w3.org/2000/svg', 'text'); | ||||
|             this.setNSAttributesForElement(textElement, { | ||||
|                 x: "10", | ||||
|                 y: "20", | ||||
|                 class: "c-imagery-tsv__no-items" | ||||
|             }); | ||||
|             textElement.innerHTML = 'No images within timeframe'; | ||||
|  | ||||
|             svgElement.appendChild(textElement); | ||||
|         }, | ||||
|         setNSAttributesForElement(element, attributes) { | ||||
|             Object.keys(attributes).forEach((key) => { | ||||
|                 if (key === 'url') { | ||||
|                     element.setAttributeNS('http://www.w3.org/1999/xlink', 'href', attributes[key]); | ||||
|                 } else { | ||||
|                     element.setAttributeNS(null, key, attributes[key]); | ||||
|                 } | ||||
|             }); | ||||
|         }, | ||||
|         getNSAttributesForElement(element, attribute) { | ||||
|             return element.getAttributeNS(null, attribute); | ||||
|         }, | ||||
|         getImageWrapper(item) { | ||||
|             const id = `id-${item.time}`; | ||||
|  | ||||
|             return this.$el.querySelector(`.c-imagery-tsv__contents g[id=${id}]`); | ||||
|         }, | ||||
|         plotImagery(item, showImagePlaceholders, svgElement, index) { | ||||
|             //TODO: Placeholder image | ||||
|             let existingImageWrapper = this.getImageWrapper(item); | ||||
|             //imageWrapper wraps the vertical tick rect and the image | ||||
|             if (existingImageWrapper) { | ||||
|                 this.updateExistingImageWrapper(existingImageWrapper, item, showImagePlaceholders); | ||||
|             } else { | ||||
|                 let imageWrapper = this.createImageWrapper(index, item, showImagePlaceholders, svgElement); | ||||
|                 svgElement.appendChild(imageWrapper); | ||||
|             } | ||||
|         }, | ||||
|         updateExistingImageWrapper(existingImageWrapper, item, showImagePlaceholders) { | ||||
|             //Update the x co-ordinates of the handle and image elements and the url of image | ||||
|             //this is to avoid tearing down all elements completely and re-drawing them | ||||
|             this.setNSAttributesForElement(existingImageWrapper, { | ||||
|                 showImagePlaceholders | ||||
|             }); | ||||
|             let imageTickElement = existingImageWrapper.querySelector('rect.c-imagery-tsv__image-handle'); | ||||
|             this.setNSAttributesForElement(imageTickElement, { | ||||
|                 x: this.xScale(item.time) | ||||
|             }); | ||||
|  | ||||
|             let imageRect = existingImageWrapper.querySelector('rect.c-imagery-tsv__image-placeholder'); | ||||
|             this.setNSAttributesForElement(imageRect, { | ||||
|                 x: this.xScale(item.time) + 2 | ||||
|             }); | ||||
|  | ||||
|             let imageElement = existingImageWrapper.querySelector('image'); | ||||
|             const selector = `href*=${existingImageWrapper.id}`; | ||||
|             let hoverEl = this.$el.querySelector(`.c-imagery-tsv__contents use[${selector}]`); | ||||
|             const hideImageUrl = (showImagePlaceholders && !hoverEl); | ||||
|             this.setNSAttributesForElement(imageElement, { | ||||
|                 x: this.xScale(item.time) + 2, | ||||
|                 url: hideImageUrl ? '' : item.url | ||||
|             }); | ||||
|         }, | ||||
|         createImageWrapper(index, item, showImagePlaceholders, svgElement) { | ||||
|             const id = `id-${item.time}`; | ||||
|             const imgSize = String(ROW_HEIGHT - 15); | ||||
|             let imageWrapper = document.createElementNS('http://www.w3.org/2000/svg', 'g'); | ||||
|             this.setNSAttributesForElement(imageWrapper, { | ||||
|                 id, | ||||
|                 class: 'c-imagery-tsv__image-wrapper', | ||||
|                 showImagePlaceholders | ||||
|             }); | ||||
|             //create image tick indicator | ||||
|             let imageTickElement = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); | ||||
|             this.setNSAttributesForElement(imageTickElement, { | ||||
|                 class: 'c-imagery-tsv__image-handle', | ||||
|                 x: this.xScale(item.time), | ||||
|                 y: 5, | ||||
|                 rx: 0, | ||||
|                 width: 2, | ||||
|                 height: String(ROW_HEIGHT - 10) | ||||
|             }); | ||||
|             imageWrapper.appendChild(imageTickElement); | ||||
|  | ||||
|             let imageRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); | ||||
|             this.setNSAttributesForElement(imageRect, { | ||||
|                 class: 'c-imagery-tsv__image-placeholder', | ||||
|                 x: this.xScale(item.time) + 2, | ||||
|                 y: 10, | ||||
|                 rx: 0, | ||||
|                 width: imgSize, | ||||
|                 height: imgSize, | ||||
|                 mask: `#image-${item.time}` | ||||
|             }); | ||||
|             imageWrapper.appendChild(imageRect); | ||||
|  | ||||
|             let imageElement = document.createElementNS('http://www.w3.org/2000/svg', 'image'); | ||||
|             this.setNSAttributesForElement(imageElement, { | ||||
|                 id: `image-${item.time}`, | ||||
|                 x: this.xScale(item.time) + 2, | ||||
|                 y: 10, | ||||
|                 rx: 0, | ||||
|                 width: imgSize, | ||||
|                 height: imgSize, | ||||
|                 url: showImagePlaceholders ? '' : item.url | ||||
|             }); | ||||
|             imageWrapper.appendChild(imageElement); | ||||
|  | ||||
|             //TODO: Don't add the hover listener if the width is too small | ||||
|             imageWrapper.addEventListener('mouseover', this.bringToForeground.bind(this, svgElement, imageWrapper, index, item.url)); | ||||
|  | ||||
|             return imageWrapper; | ||||
|         }, | ||||
|         bringToForeground(svgElement, imageWrapper, index, url, event) { | ||||
|             const selector = `href*=${imageWrapper.id}`; | ||||
|             let hoverEls = this.$el.querySelectorAll(`.c-imagery-tsv__contents use:not([${selector}])`); | ||||
|             if (hoverEls.length > 0) { | ||||
|                 this.removeFromForeground(hoverEls); | ||||
|             } | ||||
|  | ||||
|             hoverEls = this.$el.querySelectorAll(`.c-imagery-tsv__contents use[${selector}]`); | ||||
|             if (hoverEls.length) { | ||||
|  | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             let imageElement = imageWrapper.querySelector('image'); | ||||
|             this.setNSAttributesForElement(imageElement, { | ||||
|                 url: url, | ||||
|                 fill: 'none' | ||||
|             }); | ||||
|             let hoverElement = document.createElementNS('http://www.w3.org/2000/svg', 'use'); | ||||
|             this.setNSAttributesForElement(hoverElement, { | ||||
|                 class: 'image-highlight', | ||||
|                 x: 0, | ||||
|                 href: `#${imageWrapper.id}` | ||||
|             }); | ||||
|             this.setNSAttributesForElement(imageWrapper, { | ||||
|                 class: 'c-imagery-tsv__image-wrapper is-hovered' | ||||
|             }); | ||||
|             // We're using mousedown here and not 'click' because 'click' doesn't seem to be triggered reliably | ||||
|             hoverElement.addEventListener('mousedown', (e) => { | ||||
|                 if (e.button === 0) { | ||||
|                     this.expand(index); | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|             svgElement.appendChild(hoverElement); | ||||
|  | ||||
|         }, | ||||
|         removeFromForeground(items) { | ||||
|             let hoverEls; | ||||
|             if (items) { | ||||
|                 hoverEls = items; | ||||
|             } else { | ||||
|                 hoverEls = this.$el.querySelectorAll(".c-imagery-tsv__contents use"); | ||||
|             } | ||||
|  | ||||
|             hoverEls.forEach(item => { | ||||
|                 let selector = `id*=${this.getNSAttributesForElement(item, 'href').replace('#', '')}`; | ||||
|                 let imageWrapper = this.$el.querySelector(`.c-imagery-tsv__contents g[${selector}]`); | ||||
|                 this.setNSAttributesForElement(imageWrapper, { | ||||
|                     class: 'c-imagery-tsv__image-wrapper' | ||||
|                 }); | ||||
|                 let showImagePlaceholders = this.getNSAttributesForElement(imageWrapper, 'showImagePlaceholders'); | ||||
|                 if (showImagePlaceholders === 'true') { | ||||
|                     let imageElement = imageWrapper.querySelector('image'); | ||||
|                     this.setNSAttributesForElement(imageElement, { | ||||
|                         url: '' | ||||
|                     }); | ||||
|                 } | ||||
|  | ||||
|                 item.remove(); | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| </script> | ||||
| @@ -84,18 +84,18 @@ | ||||
|                 /> | ||||
|             </div> | ||||
|         </div> | ||||
|         <div class="c-local-controls c-local-controls--show-on-hover c-imagery__prev-next-buttons"> | ||||
|             <button class="c-nav c-nav--prev" | ||||
|                     title="Previous image" | ||||
|                     :disabled="isPrevDisabled" | ||||
|                     @click="prevImage()" | ||||
|             ></button> | ||||
|             <button class="c-nav c-nav--next" | ||||
|                     title="Next image" | ||||
|                     :disabled="isNextDisabled" | ||||
|                     @click="nextImage()" | ||||
|             ></button> | ||||
|         </div> | ||||
| 
 | ||||
|         <button class="c-local-controls c-local-controls--show-on-hover c-imagery__prev-next-button c-nav c-nav--prev" | ||||
|                 title="Previous image" | ||||
|                 :disabled="isPrevDisabled" | ||||
|                 @click="prevImage()" | ||||
|         ></button> | ||||
| 
 | ||||
|         <button class="c-local-controls c-local-controls--show-on-hover c-imagery__prev-next-button c-nav c-nav--next" | ||||
|                 title="Next image" | ||||
|                 :disabled="isNextDisabled" | ||||
|                 @click="nextImage()" | ||||
|         ></button> | ||||
| 
 | ||||
|         <div class="c-imagery__control-bar"> | ||||
|             <div class="c-imagery__time"> | ||||
| @@ -129,12 +129,11 @@ | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
|     <div | ||||
|         class="c-imagery__thumbs-wrapper" | ||||
|         :class="[ | ||||
|             { 'is-paused': isPaused }, | ||||
|             { 'is-autoscroll-off': !resizingWindow && !autoScroll && !isPaused } | ||||
|         ]" | ||||
|     <div class="c-imagery__thumbs-wrapper" | ||||
|          :class="[ | ||||
|              { 'is-paused': isPaused }, | ||||
|              { 'is-autoscroll-off': !resizingWindow && !autoScroll && !isPaused } | ||||
|          ]" | ||||
|     > | ||||
|         <div | ||||
|             ref="thumbsWrapper" | ||||
| @@ -175,7 +174,8 @@ import moment from 'moment'; | ||||
| import RelatedTelemetry from './RelatedTelemetry/RelatedTelemetry'; | ||||
| import Compass from './Compass/Compass.vue'; | ||||
| 
 | ||||
| const DEFAULT_DURATION_FORMATTER = 'duration'; | ||||
| import imageryData from "../../imagery/mixins/imageryData"; | ||||
| 
 | ||||
| const REFRESH_CSS_MS = 500; | ||||
| const DURATION_TRACK_MS = 1000; | ||||
| const ARROW_DOWN_DELAY_CHECK_MS = 400; | ||||
| @@ -197,30 +197,29 @@ export default { | ||||
|     components: { | ||||
|         Compass | ||||
|     }, | ||||
|     mixins: [imageryData], | ||||
|     inject: ['openmct', 'domainObject', 'objectPath', 'currentView'], | ||||
|     data() { | ||||
|         let timeSystem = this.openmct.time.timeSystem(); | ||||
|         this.metadata = {}; | ||||
|         this.requestCount = 0; | ||||
| 
 | ||||
|         return { | ||||
|             autoScroll: true, | ||||
|             durationFormatter: undefined, | ||||
|             imageHistory: [], | ||||
|             timeSystem: timeSystem, | ||||
|             keyString: undefined, | ||||
|             autoScroll: true, | ||||
|             filters: { | ||||
|                 brightness: 100, | ||||
|                 contrast: 100 | ||||
|             }, | ||||
|             imageHistory: [], | ||||
|             thumbnailClick: THUMBNAIL_CLICKED, | ||||
|             isPaused: false, | ||||
|             metadata: {}, | ||||
|             requestCount: 0, | ||||
|             timeSystem: timeSystem, | ||||
|             timeFormatter: undefined, | ||||
|             refreshCSS: false, | ||||
|             keyString: undefined, | ||||
|             focusedImageIndex: undefined, | ||||
|             focusedImageRelatedTelemetry: {}, | ||||
|             numericDuration: undefined, | ||||
|             metadataEndpoints: {}, | ||||
|             relatedTelemetry: {}, | ||||
|             latestRelatedTelemetry: {}, | ||||
|             focusedImageNaturalAspectRatio: undefined, | ||||
| @@ -231,6 +230,9 @@ export default { | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
|         imageHistorySize() { | ||||
|             return this.imageHistory.length; | ||||
|         }, | ||||
|         compassRoseSizingClasses() { | ||||
|             let compassRoseSizingClasses = ''; | ||||
|             if (this.sizedImageDimensions.width < 300) { | ||||
| @@ -258,9 +260,6 @@ export default { | ||||
|         canTrackDuration() { | ||||
|             return this.openmct.time.clock() && this.timeSystem.isUTCBased; | ||||
|         }, | ||||
|         focusedImageDownloadName() { | ||||
|             return this.getImageDownloadName(this.focusedImage); | ||||
|         }, | ||||
|         isNextDisabled() { | ||||
|             let disabled = false; | ||||
| 
 | ||||
| @@ -383,6 +382,10 @@ export default { | ||||
|         } | ||||
|     }, | ||||
|     watch: { | ||||
|         imageHistorySize(newSize, oldSize) { | ||||
|             this.setFocusedImage(newSize - 1, false); | ||||
|             this.scrollToRight(); | ||||
|         }, | ||||
|         focusedImageIndex() { | ||||
|             this.trackDuration(); | ||||
|             this.resetAgeCSS(); | ||||
| @@ -391,18 +394,9 @@ export default { | ||||
|         } | ||||
|     }, | ||||
|     async mounted() { | ||||
|         // listen | ||||
|         this.openmct.time.on('bounds', this.boundsChange); | ||||
|         this.openmct.time.on('timeSystem', this.timeSystemChange); | ||||
|         this.openmct.time.on('clock', this.clockChange); | ||||
| 
 | ||||
|         // set | ||||
|         this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier); | ||||
|         this.metadata = this.openmct.telemetry.getMetadata(this.domainObject); | ||||
|         this.imageHints = { ...this.metadata.valuesForHints(['image'])[0] }; | ||||
|         this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER); | ||||
|         this.imageFormatter = this.openmct.telemetry.getValueFormatter(this.imageHints); | ||||
|         this.imageDownloadNameHints = { ...this.metadata.valuesForHints(['imageDownloadName'])[0]}; | ||||
|         //listen | ||||
|         this.openmct.time.on('timeSystem', this.trackDuration); | ||||
|         this.openmct.time.on('clock', this.trackDuration); | ||||
| 
 | ||||
|         // related telemetry keys | ||||
|         this.spacecraftPositionKeys = ['positionX', 'positionY', 'positionZ']; | ||||
| @@ -410,56 +404,49 @@ export default { | ||||
|         this.cameraKeys = ['cameraPan', 'cameraTilt']; | ||||
|         this.sunKeys = ['sunOrientation']; | ||||
| 
 | ||||
|         // initialize | ||||
|         this.timeKey = this.timeSystem.key; | ||||
|         this.timeFormatter = this.getFormatter(this.timeKey); | ||||
| 
 | ||||
|         // kickoff | ||||
|         this.subscribe(); | ||||
|         this.requestHistory(); | ||||
| 
 | ||||
|         // related telemetry | ||||
|         await this.initializeRelatedTelemetry(); | ||||
|         this.updateRelatedTelemetryForFocusedImage(); | ||||
|         await this.updateRelatedTelemetryForFocusedImage(); | ||||
|         this.trackLatestRelatedTelemetry(); | ||||
| 
 | ||||
|         // for scrolling through images quickly and resizing the object view | ||||
|         _.debounce(this.updateRelatedTelemetryForFocusedImage, 400); | ||||
|         _.debounce(this.resizeImageContainer, 400); | ||||
|         this.updateRelatedTelemetryForFocusedImage = _.debounce(this.updateRelatedTelemetryForFocusedImage, 400); | ||||
| 
 | ||||
|         this.imageContainerResizeObserver = new ResizeObserver(this.resizeImageContainer); | ||||
|         this.imageContainerResizeObserver.observe(this.$refs.imageBG); | ||||
|         // for resizing the object view | ||||
|         this.resizeImageContainer = _.debounce(this.resizeImageContainer, 400); | ||||
| 
 | ||||
|         if (this.$refs.imageBG) { | ||||
|             this.imageContainerResizeObserver = new ResizeObserver(this.resizeImageContainer); | ||||
|             this.imageContainerResizeObserver.observe(this.$refs.imageBG); | ||||
|         } | ||||
| 
 | ||||
|         // For adjusting scroll bar size and position when resizing thumbs wrapper | ||||
|         this.handleScroll = _.debounce(this.handleScroll, SCROLL_LATENCY); | ||||
|         this.handleThumbWindowResizeEnded = _.debounce(this.handleThumbWindowResizeEnded, SCROLL_LATENCY); | ||||
|         this.handleThumbWindowResizeStart = _.debounce(this.handleThumbWindowResizeStart, SCROLL_LATENCY); | ||||
| 
 | ||||
|         if (this.$refs.thumbsWrapper) { | ||||
|             this.thumbWrapperResizeObserver = new ResizeObserver(this.handleThumbWindowResizeStart); | ||||
|             this.thumbWrapperResizeObserver.observe(this.$refs.thumbsWrapper); | ||||
|         } | ||||
| 
 | ||||
|         this.thumbWrapperResizeObserver = new ResizeObserver(this.handleThumbWindowResizeStart); | ||||
|         this.thumbWrapperResizeObserver.observe(this.$refs.thumbsWrapper); | ||||
|     }, | ||||
|     beforeDestroy() { | ||||
|         if (this.unsubscribe) { | ||||
|             this.unsubscribe(); | ||||
|             delete this.unsubscribe; | ||||
|         this.openmct.time.off('timeSystem', this.trackDuration); | ||||
|         this.openmct.time.off('clock', this.trackDuration); | ||||
| 
 | ||||
|         if (this.thumbWrapperResizeObserver) { | ||||
|             this.thumbWrapperResizeObserver.disconnect(); | ||||
|         } | ||||
| 
 | ||||
|         if (this.imageContainerResizeObserver) { | ||||
|             this.imageContainerResizeObserver.disconnect(); | ||||
|         } | ||||
| 
 | ||||
|         if (this.thumbWrapperResizeObserver) { | ||||
|             this.thumbWrapperResizeObserver.disconnect(); | ||||
|         } | ||||
| 
 | ||||
|         if (this.relatedTelemetry.hasRelatedTelemetry) { | ||||
|             this.relatedTelemetry.destroy(); | ||||
|         } | ||||
| 
 | ||||
|         this.stopDurationTracking(); | ||||
|         this.openmct.time.off('bounds', this.boundsChange); | ||||
|         this.openmct.time.off('timeSystem', this.timeSystemChange); | ||||
|         this.openmct.time.off('clock', this.clockChange); | ||||
| 
 | ||||
|         // unsubscribe from related telemetry | ||||
|         if (this.relatedTelemetry.hasRelatedTelemetry) { | ||||
|             for (let key of this.relatedTelemetry.keys) { | ||||
| @@ -576,56 +563,6 @@ export default { | ||||
|         focusElement() { | ||||
|             this.$el.focus(); | ||||
|         }, | ||||
|         datumIsNotValid(datum) { | ||||
|             if (this.imageHistory.length === 0) { | ||||
|                 return false; | ||||
|             } | ||||
| 
 | ||||
|             const datumURL = this.formatImageUrl(datum); | ||||
|             const lastHistoryURL = this.formatImageUrl(this.imageHistory.slice(-1)[0]); | ||||
| 
 | ||||
|             // datum is not valid if it matches the last datum in history, | ||||
|             // or it is before the last datum in the history | ||||
|             const datumTimeCheck = this.parseTime(datum); | ||||
|             const historyTimeCheck = this.parseTime(this.imageHistory.slice(-1)[0]); | ||||
|             const matchesLast = (datumTimeCheck === historyTimeCheck) && (datumURL === lastHistoryURL); | ||||
|             const isStale = datumTimeCheck < historyTimeCheck; | ||||
| 
 | ||||
|             return matchesLast || isStale; | ||||
|         }, | ||||
|         formatImageUrl(datum) { | ||||
|             if (!datum) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             return this.imageFormatter.format(datum); | ||||
|         }, | ||||
|         formatTime(datum) { | ||||
|             if (!datum) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             let dateTimeStr = this.timeFormatter.format(datum); | ||||
| 
 | ||||
|             // Replace ISO "T" with a space to allow wrapping | ||||
|             return dateTimeStr.replace("T", " "); | ||||
|         }, | ||||
|         getImageDownloadName(datum) { | ||||
|             let imageDownloadName = ''; | ||||
|             if (datum) { | ||||
|                 const key = this.imageDownloadNameHints.key; | ||||
|                 imageDownloadName = datum[key]; | ||||
|             } | ||||
| 
 | ||||
|             return imageDownloadName; | ||||
|         }, | ||||
|         parseTime(datum) { | ||||
|             if (!datum) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             return this.timeFormatter.parse(datum); | ||||
|         }, | ||||
|         handleScroll() { | ||||
|             const thumbsWrapper = this.$refs.thumbsWrapper; | ||||
|             if (!thumbsWrapper || this.resizingWindow) { | ||||
| @@ -683,6 +620,10 @@ export default { | ||||
|         setFocusedImage(index, thumbnailClick = false) { | ||||
|             if (this.isPaused && !thumbnailClick) { | ||||
|                 this.nextImageIndex = index; | ||||
|                 //this could happen if bounds changes | ||||
|                 if (this.focusedImageIndex > this.imageHistory.length - 1) { | ||||
|                     this.focusedImageIndex = index; | ||||
|                 } | ||||
| 
 | ||||
|                 return; | ||||
|             } | ||||
| @@ -693,70 +634,6 @@ export default { | ||||
|                 this.paused(true); | ||||
|             } | ||||
|         }, | ||||
|         boundsChange(bounds, isTick) { | ||||
|             if (!isTick) { | ||||
|                 this.requestHistory(); | ||||
|             } | ||||
|         }, | ||||
|         async requestHistory() { | ||||
|             let bounds = this.openmct.time.bounds(); | ||||
|             this.requestCount++; | ||||
|             const requestId = this.requestCount; | ||||
|             this.imageHistory = []; | ||||
| 
 | ||||
|             let data = await this.openmct.telemetry | ||||
|                 .request(this.domainObject, bounds) || []; | ||||
| 
 | ||||
|             if (this.requestCount === requestId) { | ||||
|                 data.forEach((datum, index) => { | ||||
|                     this.updateHistory(datum, index === data.length - 1); | ||||
|                 }); | ||||
|             } | ||||
|         }, | ||||
|         timeSystemChange(system) { | ||||
|             this.timeSystem = this.openmct.time.timeSystem(); | ||||
|             this.timeKey = this.timeSystem.key; | ||||
|             this.timeFormatter = this.getFormatter(this.timeKey); | ||||
|             this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER); | ||||
|             this.trackDuration(); | ||||
|         }, | ||||
|         clockChange(clock) { | ||||
|             this.trackDuration(); | ||||
|         }, | ||||
|         subscribe() { | ||||
|             this.unsubscribe = this.openmct.telemetry | ||||
|                 .subscribe(this.domainObject, (datum) => { | ||||
|                     let parsedTimestamp = this.parseTime(datum); | ||||
|                     let bounds = this.openmct.time.bounds(); | ||||
| 
 | ||||
|                     if (parsedTimestamp >= bounds.start && parsedTimestamp <= bounds.end) { | ||||
|                         this.updateHistory(datum); | ||||
|                     } | ||||
|                 }); | ||||
|         }, | ||||
|         updateHistory(datum, setFocused = true) { | ||||
|             if (this.datumIsNotValid(datum)) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             let image = { ...datum }; | ||||
|             image.formattedTime = this.formatTime(datum); | ||||
|             image.url = this.formatImageUrl(datum); | ||||
|             image.time = datum[this.timeKey]; | ||||
|             image.imageDownloadName = this.getImageDownloadName(datum); | ||||
| 
 | ||||
|             this.imageHistory.push(image); | ||||
|             if (setFocused) { | ||||
|                 this.setFocusedImage(this.imageHistory.length - 1); | ||||
|                 this.scrollToRight(); | ||||
|             } | ||||
|         }, | ||||
|         getFormatter(key) { | ||||
|             let metadataValue = this.metadata.value(key) || { format: key }; | ||||
|             let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue); | ||||
| 
 | ||||
|             return valueFormatter; | ||||
|         }, | ||||
|         trackDuration() { | ||||
|             if (this.canTrackDuration) { | ||||
|                 this.stopDurationTracking(); | ||||
| @@ -876,6 +753,10 @@ export default { | ||||
|             }, { once: true }); | ||||
|         }, | ||||
|         resizeImageContainer() { | ||||
|             if (!this.$refs.imageBG) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             if (this.$refs.imageBG.clientWidth !== this.imageContainerWidth) { | ||||
|                 this.imageContainerWidth = this.$refs.imageBG.clientWidth; | ||||
|             } | ||||
| @@ -285,17 +285,17 @@ | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| .c-imagery__prev-next-buttons { | ||||
|     display: flex; | ||||
|     width: 100%; | ||||
|     justify-content: space-between; | ||||
|     pointer-events: none; | ||||
| .c-imagery__prev-next-button { | ||||
|     pointer-events: all; | ||||
|     position: absolute; | ||||
|     top: 50%; | ||||
|     transform: translateY(-75%); | ||||
|     transform: translateY(-75%); // 75% due to transform: rotation approach to the button | ||||
| 
 | ||||
|     .c-nav { | ||||
|         pointer-events: all; | ||||
|     &.c-nav { | ||||
|         position: absolute; | ||||
| 
 | ||||
|         &--prev { left: 0; } | ||||
|         &--next { right: 0; } | ||||
|     } | ||||
| 
 | ||||
|     .s-status-taking-snapshot & { | ||||
| @@ -312,3 +312,34 @@ | ||||
|         @include cArrowButtonSizing($dimOuter: 32px); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /*************************************** IMAGERY IN TIMESTRIP VIEWS */ | ||||
| .c-imagery-tsv { | ||||
|     g.c-imagery-tsv__image-wrapper { | ||||
|         cursor: pointer; | ||||
| 
 | ||||
|         &.is-hovered { | ||||
|             filter: brightness(1) contrast(1) !important; | ||||
|             [class*='__image-handle'] { | ||||
|                 fill: $colorBodyFg; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     &__no-items { | ||||
|         fill: $colorBodyFg !important; | ||||
|     } | ||||
| 
 | ||||
|     &__image-handle { | ||||
|         fill: rgba($colorBodyFg, 0.5); | ||||
|     } | ||||
| 
 | ||||
|     &__image-placeholder { | ||||
|         fill: pushBack($colorBodyBg, 0.3); | ||||
|     } | ||||
| 
 | ||||
|     &:hover g.c-imagery-tsv__image-wrapper { | ||||
|         // TODO CH: convert to theme constants | ||||
|         filter: brightness(0.5) contrast(0.7); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										174
									
								
								src/plugins/imagery/mixins/imageryData.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										174
									
								
								src/plugins/imagery/mixins/imageryData.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,174 @@ | ||||
| /***************************************************************************** | ||||
|  * 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. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| const DEFAULT_DURATION_FORMATTER = 'duration'; | ||||
|  | ||||
| export default { | ||||
|     inject: ['openmct', 'domainObject', 'objectPath'], | ||||
|     mounted() { | ||||
|         // listen | ||||
|         this.openmct.time.on('bounds', this.boundsChange); | ||||
|         this.openmct.time.on('timeSystem', this.timeSystemChange); | ||||
|  | ||||
|         // set | ||||
|         this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier); | ||||
|         this.metadata = this.openmct.telemetry.getMetadata(this.domainObject); | ||||
|         this.imageHints = { ...this.metadata.valuesForHints(['image'])[0] }; | ||||
|         this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER); | ||||
|         this.imageFormatter = this.openmct.telemetry.getValueFormatter(this.imageHints); | ||||
|         this.imageDownloadNameHints = { ...this.metadata.valuesForHints(['imageDownloadName'])[0]}; | ||||
|  | ||||
|         // initialize | ||||
|         this.timeKey = this.timeSystem.key; | ||||
|         this.timeFormatter = this.getFormatter(this.timeKey); | ||||
|  | ||||
|         // kickoff | ||||
|         this.subscribe(); | ||||
|         this.requestHistory(); | ||||
|     }, | ||||
|     beforeDestroy() { | ||||
|         if (this.unsubscribe) { | ||||
|             this.unsubscribe(); | ||||
|             delete this.unsubscribe; | ||||
|         } | ||||
|  | ||||
|         this.openmct.time.off('bounds', this.boundsChange); | ||||
|         this.openmct.time.off('timeSystem', this.timeSystemChange); | ||||
|     }, | ||||
|     methods: { | ||||
|         datumIsNotValid(datum) { | ||||
|             if (this.imageHistory.length === 0) { | ||||
|                 return false; | ||||
|             } | ||||
|  | ||||
|             const datumURL = this.formatImageUrl(datum); | ||||
|             const lastHistoryURL = this.formatImageUrl(this.imageHistory.slice(-1)[0]); | ||||
|  | ||||
|             // datum is not valid if it matches the last datum in history, | ||||
|             // or it is before the last datum in the history | ||||
|             const datumTimeCheck = this.parseTime(datum); | ||||
|             const historyTimeCheck = this.parseTime(this.imageHistory.slice(-1)[0]); | ||||
|             const matchesLast = (datumTimeCheck === historyTimeCheck) && (datumURL === lastHistoryURL); | ||||
|             const isStale = datumTimeCheck < historyTimeCheck; | ||||
|  | ||||
|             return matchesLast || isStale; | ||||
|         }, | ||||
|         formatImageUrl(datum) { | ||||
|             if (!datum) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             return this.imageFormatter.format(datum); | ||||
|         }, | ||||
|         formatTime(datum) { | ||||
|             if (!datum) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             let dateTimeStr = this.timeFormatter.format(datum); | ||||
|  | ||||
|             // Replace ISO "T" with a space to allow wrapping | ||||
|             return dateTimeStr.replace("T", " "); | ||||
|         }, | ||||
|         getImageDownloadName(datum) { | ||||
|             let imageDownloadName = ''; | ||||
|             if (datum) { | ||||
|                 const key = this.imageDownloadNameHints.key; | ||||
|                 imageDownloadName = datum[key]; | ||||
|             } | ||||
|  | ||||
|             return imageDownloadName; | ||||
|         }, | ||||
|         parseTime(datum) { | ||||
|             if (!datum) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             return this.timeFormatter.parse(datum); | ||||
|         }, | ||||
|         boundsChange(bounds, isTick) { | ||||
|             if (!isTick) { | ||||
|                 this.requestHistory(); | ||||
|             } | ||||
|         }, | ||||
|         async requestHistory() { | ||||
|             let bounds = this.openmct.time.bounds(); | ||||
|             this.requestCount++; | ||||
|             const requestId = this.requestCount; | ||||
|             this.imageHistory = []; | ||||
|  | ||||
|             let data = await this.openmct.telemetry | ||||
|                 .request(this.domainObject, bounds) || []; | ||||
|  | ||||
|             if (this.requestCount === requestId) { | ||||
|                 let imagery = []; | ||||
|                 data.forEach((datum) => { | ||||
|                     let image = this.normalizeDatum(datum); | ||||
|                     if (image) { | ||||
|                         imagery.push(image); | ||||
|                     } | ||||
|                 }); | ||||
|                 //this is to optimize anything that reacts to imageHistory length | ||||
|                 this.imageHistory = imagery; | ||||
|             } | ||||
|         }, | ||||
|         timeSystemChange() { | ||||
|             this.timeSystem = this.openmct.time.timeSystem(); | ||||
|             this.timeKey = this.timeSystem.key; | ||||
|             this.timeFormatter = this.getFormatter(this.timeKey); | ||||
|             this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER); | ||||
|         }, | ||||
|         subscribe() { | ||||
|             this.unsubscribe = this.openmct.telemetry | ||||
|                 .subscribe(this.domainObject, (datum) => { | ||||
|                     let parsedTimestamp = this.parseTime(datum); | ||||
|                     let bounds = this.openmct.time.bounds(); | ||||
|  | ||||
|                     if (parsedTimestamp >= bounds.start && parsedTimestamp <= bounds.end) { | ||||
|                         let image = this.normalizeDatum(datum); | ||||
|                         if (image) { | ||||
|                             this.imageHistory.push(image); | ||||
|                         } | ||||
|                     } | ||||
|                 }); | ||||
|         }, | ||||
|         normalizeDatum(datum) { | ||||
|             if (this.datumIsNotValid(datum)) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             let image = { ...datum }; | ||||
|             image.formattedTime = this.formatTime(datum); | ||||
|             image.url = this.formatImageUrl(datum); | ||||
|             image.time = datum[this.timeKey]; | ||||
|             image.imageDownloadName = this.getImageDownloadName(datum); | ||||
|  | ||||
|             return image; | ||||
|         }, | ||||
|         getFormatter(key) { | ||||
|             let metadataValue = this.metadata.value(key) || { format: key }; | ||||
|             let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue); | ||||
|  | ||||
|             return valueFormatter; | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| @@ -21,10 +21,12 @@ | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| import ImageryViewProvider from './ImageryViewProvider'; | ||||
| import ImageryTimestripViewProvider from './ImageryTimestripViewProvider'; | ||||
|  | ||||
| export default function () { | ||||
|     return function install(openmct) { | ||||
|         openmct.objectViews.addProvider(new ImageryViewProvider(openmct)); | ||||
|         openmct.objectViews.addProvider(new ImageryTimestripViewProvider(openmct)); | ||||
|     }; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -32,19 +32,19 @@ const TEN_MINUTES = ONE_MINUTE * 10; | ||||
| const MAIN_IMAGE_CLASS = '.js-imageryView-image'; | ||||
| const NEW_IMAGE_CLASS = '.c-imagery__age.c-imagery--new'; | ||||
| const REFRESH_CSS_MS = 500; | ||||
| const TOLERANCE = 0.50; | ||||
| // const TOLERANCE = 0.50; | ||||
|  | ||||
| function comparisonFunction(valueOne, valueTwo) { | ||||
|     let larger = valueOne; | ||||
|     let smaller = valueTwo; | ||||
|  | ||||
|     if (larger < smaller) { | ||||
|         larger = valueTwo; | ||||
|         smaller = valueOne; | ||||
|     } | ||||
|  | ||||
|     return (larger - smaller) < TOLERANCE; | ||||
| } | ||||
| // function comparisonFunction(valueOne, valueTwo) { | ||||
| //     let larger = valueOne; | ||||
| //     let smaller = valueTwo; | ||||
| // | ||||
| //     if (larger < smaller) { | ||||
| //         larger = valueTwo; | ||||
| //         smaller = valueOne; | ||||
| //     } | ||||
| // | ||||
| //     return (larger - smaller) < TOLERANCE; | ||||
| // } | ||||
|  | ||||
| function getImageInfo(doc) { | ||||
|     let imageElement = doc.querySelectorAll(MAIN_IMAGE_CLASS)[0]; | ||||
| @@ -84,12 +84,14 @@ function generateTelemetry(start, count) { | ||||
|     return telemetry; | ||||
| } | ||||
|  | ||||
| describe("The Imagery View Layout", () => { | ||||
| describe("The Imagery View Layouts", () => { | ||||
|     const imageryKey = 'example.imagery'; | ||||
|     const imageryForTimeStripKey = 'example.imagery.time-strip.view'; | ||||
|     const START = Date.now(); | ||||
|     const COUNT = 10; | ||||
|  | ||||
|     let resolveFunction; | ||||
|     let originalRouterPath; | ||||
|  | ||||
|     let openmct; | ||||
|     let appHolder; | ||||
| @@ -116,51 +118,51 @@ describe("The Imagery View Layout", () => { | ||||
|                         "image": 1, | ||||
|                         "priority": 3 | ||||
|                     }, | ||||
|                     "source": "url", | ||||
|                     "relatedTelemetry": { | ||||
|                         "heading": { | ||||
|                             "comparisonFunction": comparisonFunction, | ||||
|                             "historical": { | ||||
|                                 "telemetryObjectId": "heading", | ||||
|                                 "valueKey": "value" | ||||
|                             } | ||||
|                         }, | ||||
|                         "roll": { | ||||
|                             "comparisonFunction": comparisonFunction, | ||||
|                             "historical": { | ||||
|                                 "telemetryObjectId": "roll", | ||||
|                                 "valueKey": "value" | ||||
|                             } | ||||
|                         }, | ||||
|                         "pitch": { | ||||
|                             "comparisonFunction": comparisonFunction, | ||||
|                             "historical": { | ||||
|                                 "telemetryObjectId": "pitch", | ||||
|                                 "valueKey": "value" | ||||
|                             } | ||||
|                         }, | ||||
|                         "cameraPan": { | ||||
|                             "comparisonFunction": comparisonFunction, | ||||
|                             "historical": { | ||||
|                                 "telemetryObjectId": "cameraPan", | ||||
|                                 "valueKey": "value" | ||||
|                             } | ||||
|                         }, | ||||
|                         "cameraTilt": { | ||||
|                             "comparisonFunction": comparisonFunction, | ||||
|                             "historical": { | ||||
|                                 "telemetryObjectId": "cameraTilt", | ||||
|                                 "valueKey": "value" | ||||
|                             } | ||||
|                         }, | ||||
|                         "sunOrientation": { | ||||
|                             "comparisonFunction": comparisonFunction, | ||||
|                             "historical": { | ||||
|                                 "telemetryObjectId": "sunOrientation", | ||||
|                                 "valueKey": "value" | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                     "source": "url" | ||||
|                     // "relatedTelemetry": { | ||||
|                     //     "heading": { | ||||
|                     //         "comparisonFunction": comparisonFunction, | ||||
|                     //         "historical": { | ||||
|                     //             "telemetryObjectId": "heading", | ||||
|                     //             "valueKey": "value" | ||||
|                     //         } | ||||
|                     //     }, | ||||
|                     //     "roll": { | ||||
|                     //         "comparisonFunction": comparisonFunction, | ||||
|                     //         "historical": { | ||||
|                     //             "telemetryObjectId": "roll", | ||||
|                     //             "valueKey": "value" | ||||
|                     //         } | ||||
|                     //     }, | ||||
|                     //     "pitch": { | ||||
|                     //         "comparisonFunction": comparisonFunction, | ||||
|                     //         "historical": { | ||||
|                     //             "telemetryObjectId": "pitch", | ||||
|                     //             "valueKey": "value" | ||||
|                     //         } | ||||
|                     //     }, | ||||
|                     //     "cameraPan": { | ||||
|                     //         "comparisonFunction": comparisonFunction, | ||||
|                     //         "historical": { | ||||
|                     //             "telemetryObjectId": "cameraPan", | ||||
|                     //             "valueKey": "value" | ||||
|                     //         } | ||||
|                     //     }, | ||||
|                     //     "cameraTilt": { | ||||
|                     //         "comparisonFunction": comparisonFunction, | ||||
|                     //         "historical": { | ||||
|                     //             "telemetryObjectId": "cameraTilt", | ||||
|                     //             "valueKey": "value" | ||||
|                     //         } | ||||
|                     //     }, | ||||
|                     //     "sunOrientation": { | ||||
|                     //         "comparisonFunction": comparisonFunction, | ||||
|                     //         "historical": { | ||||
|                     //             "telemetryObjectId": "sunOrientation", | ||||
|                     //             "valueKey": "value" | ||||
|                     //         } | ||||
|                     //     } | ||||
|                     // } | ||||
|                 }, | ||||
|                 { | ||||
|                     "name": "Name", | ||||
| @@ -218,7 +220,9 @@ describe("The Imagery View Layout", () => { | ||||
|         }); | ||||
|  | ||||
|         spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([])); | ||||
|         spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({})); | ||||
|         spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve(imageryObject)); | ||||
|  | ||||
|         originalRouterPath = openmct.router.path; | ||||
|  | ||||
|         openmct.on('start', done); | ||||
|         openmct.start(appHolder); | ||||
| @@ -229,10 +233,34 @@ describe("The Imagery View Layout", () => { | ||||
|             start: 0, | ||||
|             end: 1 | ||||
|         }); | ||||
|         openmct.router.path = originalRouterPath; | ||||
|  | ||||
|         return resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|     it("should provide an imagery time strip view when in a time strip", () => { | ||||
|         openmct.router.path = [{ | ||||
|             identifier: { | ||||
|                 key: 'test-timestrip', | ||||
|                 namespace: '' | ||||
|             }, | ||||
|             type: 'time-strip' | ||||
|         }]; | ||||
|  | ||||
|         let applicableViews = openmct.objectViews.get(imageryObject, [imageryObject, { | ||||
|             identifier: { | ||||
|                 key: 'test-timestrip', | ||||
|                 namespace: '' | ||||
|             }, | ||||
|             type: 'time-strip' | ||||
|         }]); | ||||
|         let imageryView = applicableViews.find( | ||||
|             viewProvider => viewProvider.key === imageryForTimeStripKey | ||||
|         ); | ||||
|  | ||||
|         expect(imageryView).toBeDefined(); | ||||
|     }); | ||||
|  | ||||
|     it("should provide an imagery view only for imagery producing objects", () => { | ||||
|         let applicableViews = openmct.objectViews.get(imageryObject, []); | ||||
|         let imageryView = applicableViews.find( | ||||
| @@ -242,6 +270,46 @@ describe("The Imagery View Layout", () => { | ||||
|         expect(imageryView).toBeDefined(); | ||||
|     }); | ||||
|  | ||||
|     it("should not provide an imagery view when in a time strip", () => { | ||||
|         openmct.router.path = [{ | ||||
|             identifier: { | ||||
|                 key: 'test-timestrip', | ||||
|                 namespace: '' | ||||
|             }, | ||||
|             type: 'time-strip' | ||||
|         }]; | ||||
|  | ||||
|         let applicableViews = openmct.objectViews.get(imageryObject, [imageryObject, { | ||||
|             identifier: { | ||||
|                 key: 'test-timestrip', | ||||
|                 namespace: '' | ||||
|             }, | ||||
|             type: 'time-strip' | ||||
|         }]); | ||||
|         let imageryView = applicableViews.find( | ||||
|             viewProvider => viewProvider.key === imageryKey | ||||
|         ); | ||||
|  | ||||
|         expect(imageryView).toBeUndefined(); | ||||
|     }); | ||||
|  | ||||
|     it("should provide an imagery view when navigated to in the composition of a time strip", () => { | ||||
|         openmct.router.path = [imageryObject]; | ||||
|  | ||||
|         let applicableViews = openmct.objectViews.get(imageryObject, [imageryObject, { | ||||
|             identifier: { | ||||
|                 key: 'test-timestrip', | ||||
|                 namespace: '' | ||||
|             }, | ||||
|             type: 'time-strip' | ||||
|         }]); | ||||
|         let imageryView = applicableViews.find( | ||||
|             viewProvider => viewProvider.key === imageryKey | ||||
|         ); | ||||
|  | ||||
|         expect(imageryView).toBeDefined(); | ||||
|     }); | ||||
|  | ||||
|     describe("imagery view", () => { | ||||
|         let applicableViews; | ||||
|         let imageryViewProvider; | ||||
| @@ -302,18 +370,15 @@ describe("The Imagery View Layout", () => { | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         xit("should show that an image is not new", (done) => { | ||||
|         it("should show that an image is not new", (done) => { | ||||
|             const target = imageTelemetry[2].url; | ||||
|             parent.querySelectorAll(`img[src='${target}']`)[0].click(); | ||||
|  | ||||
|             Vue.nextTick(() => { | ||||
|                 // used in code, need to wait to the 500ms here too | ||||
|                 setTimeout(() => { | ||||
|                     const imageIsNew = isNew(parent); | ||||
|                 const imageIsNew = isNew(parent); | ||||
|  | ||||
|                     expect(imageIsNew).toBeFalse(); | ||||
|                     done(); | ||||
|                 }, REFRESH_CSS_MS); | ||||
|                 expect(imageIsNew).toBeFalse(); | ||||
|                 done(); | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
| @@ -367,18 +432,18 @@ describe("The Imagery View Layout", () => { | ||||
|         }); | ||||
|         it ('shows an auto scroll button when scroll to left', async () => { | ||||
|             // to mock what a scroll would do | ||||
|             imageryView._getInstance().$refs.ImageryLayout.autoScroll = false; | ||||
|             imageryView._getInstance().$refs.ImageryContainer.autoScroll = false; | ||||
|             await Vue.nextTick(); | ||||
|             let autoScrollButton = parent.querySelector('.c-imagery__auto-scroll-resume-button'); | ||||
|             expect(autoScrollButton).toBeTruthy(); | ||||
|         }); | ||||
|         it ('scrollToRight is called when clicking on auto scroll button', async () => { | ||||
|             // use spyon to spy the scroll function | ||||
|             spyOn(imageryView._getInstance().$refs.ImageryLayout, 'scrollToRight'); | ||||
|             imageryView._getInstance().$refs.ImageryLayout.autoScroll = false; | ||||
|             spyOn(imageryView._getInstance().$refs.ImageryContainer, 'scrollToRight'); | ||||
|             imageryView._getInstance().$refs.ImageryContainer.autoScroll = false; | ||||
|             await Vue.nextTick(); | ||||
|             parent.querySelector('.c-imagery__auto-scroll-resume-button').click(); | ||||
|             expect(imageryView._getInstance().$refs.ImageryLayout.scrollToRight).toHaveBeenCalledWith('reset'); | ||||
|             expect(imageryView._getInstance().$refs.ImageryContainer.scrollToRight).toHaveBeenCalledWith('reset'); | ||||
|  | ||||
|         }); | ||||
|     }); | ||||
|   | ||||
| @@ -45,8 +45,7 @@ | ||||
|     </div> | ||||
| </div> | ||||
| </template> | ||||
| <style lang="sass"> | ||||
| </style> | ||||
|  | ||||
| <script> | ||||
| import packages from './third-party-licenses.json'; | ||||
|  | ||||
|   | ||||
| @@ -140,6 +140,7 @@ import SearchResults from './SearchResults.vue'; | ||||
| import Sidebar from './Sidebar.vue'; | ||||
| import { clearDefaultNotebook, getDefaultNotebook, setDefaultNotebook, setDefaultNotebookSectionId, setDefaultNotebookPageId } from '../utils/notebook-storage'; | ||||
| import { addNotebookEntry, createNewEmbed, getEntryPosById, getNotebookEntries, mutateObject } from '../utils/notebook-entries'; | ||||
| import { saveNotebookImageDomainObject, updateNamespaceOfDomainObject } from '../utils/notebook-image'; | ||||
| import { NOTEBOOK_VIEW_TYPE } from '../notebook-constants'; | ||||
| import objectUtils from 'objectUtils'; | ||||
|  | ||||
| @@ -385,9 +386,13 @@ export default { | ||||
|             const snapshotId = event.dataTransfer.getData('openmct/snapshot/id'); | ||||
|             if (snapshotId.length) { | ||||
|                 const snapshot = this.snapshotContainer.getSnapshot(snapshotId); | ||||
|                 this.newEntry(snapshot); | ||||
|                 this.newEntry(snapshot.embedObject); | ||||
|                 this.snapshotContainer.removeSnapshot(snapshotId); | ||||
|  | ||||
|                 const namespace = this.domainObject.identifier.namespace; | ||||
|                 const notebookImageDomainObject = updateNamespaceOfDomainObject(snapshot.notebookImageDomainObject, namespace); | ||||
|                 saveNotebookImageDomainObject(this.openmct, notebookImageDomainObject); | ||||
|  | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
| @@ -451,12 +456,9 @@ export default { | ||||
|                 : undefined; | ||||
|         }, | ||||
|         getDefaultNotebookObject() { | ||||
|             const oldNotebookStorage = getDefaultNotebook(); | ||||
|             if (!oldNotebookStorage) { | ||||
|                 return null; | ||||
|             } | ||||
|             const defaultNotebook = getDefaultNotebook(); | ||||
|  | ||||
|             return this.openmct.objects.get(oldNotebookStorage.identifier); | ||||
|             return defaultNotebook && this.openmct.objects.get(defaultNotebook.identifier); | ||||
|         }, | ||||
|         getLinktoNotebook() { | ||||
|             const objectPath = this.openmct.router.path; | ||||
|   | ||||
| @@ -40,7 +40,7 @@ export default { | ||||
|     components: { | ||||
|         PopupMenu | ||||
|     }, | ||||
|     inject: ['openmct'], | ||||
|     inject: ['openmct', 'snapshotContainer'], | ||||
|     props: { | ||||
|         embed: { | ||||
|             type: Object, | ||||
| @@ -48,6 +48,12 @@ export default { | ||||
|                 return {}; | ||||
|             } | ||||
|         }, | ||||
|         isSnapshotContainer: { | ||||
|             type: Boolean, | ||||
|             default() { | ||||
|                 return false; | ||||
|             } | ||||
|         }, | ||||
|         removeActionString: { | ||||
|             type: String, | ||||
|             default() { | ||||
| @@ -135,6 +141,14 @@ export default { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             if (this.isSnapshotContainer) { | ||||
|                 const snapshot = this.snapshotContainer.getSnapshot(this.embed.id); | ||||
|                 const fullSizeImageURL = snapshot.notebookImageDomainObject.configuration.fullSizeImageURL; | ||||
|                 painterroInstance.show(fullSizeImageURL); | ||||
|  | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             this.openmct.objects.get(fullSizeImageObjectIdentifier) | ||||
|                 .then(object => { | ||||
|                     painterroInstance.show(object.configuration.fullSizeImageURL); | ||||
| @@ -190,6 +204,14 @@ export default { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             if (this.isSnapshotContainer) { | ||||
|                 const snapshot = this.snapshotContainer.getSnapshot(this.embed.id); | ||||
|                 const fullSizeImageURL = snapshot.notebookImageDomainObject.configuration.fullSizeImageURL; | ||||
|                 this.openSnapshotOverlay(fullSizeImageURL); | ||||
|  | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             this.openmct.objects.get(fullSizeImageObjectIdentifier) | ||||
|                 .then(object => { | ||||
|                     this.openSnapshotOverlay(object.configuration.fullSizeImageURL); | ||||
| @@ -259,8 +281,20 @@ export default { | ||||
|         updateSnapshot(snapshotObject) { | ||||
|             this.embed.snapshot.thumbnailImage = snapshotObject.thumbnailImage; | ||||
|  | ||||
|             updateNotebookImageDomainObject(this.openmct, this.embed.snapshot.fullSizeImageObjectIdentifier, snapshotObject.fullSizeImage); | ||||
|             this.updateNotebookImageDomainObjectSnapshot(snapshotObject); | ||||
|             this.updateEmbed(this.embed); | ||||
|         }, | ||||
|         updateNotebookImageDomainObjectSnapshot(snapshotObject) { | ||||
|             if (this.isSnapshotContainer) { | ||||
|                 const snapshot = this.snapshotContainer.getSnapshot(this.embed.id); | ||||
|  | ||||
|                 snapshot.embedObject.snapshot.thumbnailImage = snapshotObject.thumbnailImage; | ||||
|                 snapshot.notebookImageDomainObject.configuration.fullSizeImageURL = snapshotObject.fullSizeImage.src; | ||||
|  | ||||
|                 this.snapshotContainer.updateSnapshot(snapshot); | ||||
|             } else { | ||||
|                 updateNotebookImageDomainObject(this.openmct, this.embed.snapshot.fullSizeImageObjectIdentifier, snapshotObject.fullSizeImage); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|   | ||||
| @@ -102,9 +102,11 @@ | ||||
|  | ||||
| <script> | ||||
| import NotebookEmbed from './NotebookEmbed.vue'; | ||||
| import { createNewEmbed } from '../utils/notebook-entries'; | ||||
| import Moment from 'moment'; | ||||
| import TextHighlight from '../../../utils/textHighlight/TextHighlight.vue'; | ||||
| import { createNewEmbed } from '../utils/notebook-entries'; | ||||
| import { saveNotebookImageDomainObject, updateNamespaceOfDomainObject } from '../utils/notebook-image'; | ||||
|  | ||||
| import Moment from 'moment'; | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
| @@ -210,8 +212,12 @@ export default { | ||||
|             const snapshotId = $event.dataTransfer.getData('openmct/snapshot/id'); | ||||
|             if (snapshotId.length) { | ||||
|                 const snapshot = this.snapshotContainer.getSnapshot(snapshotId); | ||||
|                 this.entry.embeds.push(snapshot.embedObject); | ||||
|                 this.snapshotContainer.removeSnapshot(snapshotId); | ||||
|                 this.entry.embeds.push(snapshot); | ||||
|  | ||||
|                 const namespace = this.domainObject.identifier.namespace; | ||||
|                 const notebookImageDomainObject = updateNamespaceOfDomainObject(snapshot.notebookImageDomainObject, namespace); | ||||
|                 saveNotebookImageDomainObject(this.openmct, notebookImageDomainObject); | ||||
|             } else { | ||||
|                 const data = $event.dataTransfer.getData('openmct/domain-object-path'); | ||||
|                 const objectPath = JSON.parse(data); | ||||
|   | ||||
| @@ -17,19 +17,26 @@ | ||||
|  | ||||
| <script> | ||||
| import Snapshot from '../snapshot'; | ||||
| import { getDefaultNotebook, getNotebookSectionAndPage, validateNotebookStorageObject } from '../utils/notebook-storage'; | ||||
| import { getDefaultNotebook, validateNotebookStorageObject } from '../utils/notebook-storage'; | ||||
| import { NOTEBOOK_DEFAULT, NOTEBOOK_SNAPSHOT } from '../notebook-constants'; | ||||
| import { getMenuItems } from '../utils/notebook-snapshot-menu'; | ||||
|  | ||||
| export default { | ||||
|     inject: ['openmct'], | ||||
|     props: { | ||||
|         currentView: { | ||||
|             type: Object, | ||||
|             default() { | ||||
|                 return {}; | ||||
|             } | ||||
|         }, | ||||
|         domainObject: { | ||||
|             type: Object, | ||||
|             default() { | ||||
|                 return {}; | ||||
|             } | ||||
|         }, | ||||
|         ignoreLink: { | ||||
|         isPreview: { | ||||
|             type: Boolean, | ||||
|             default() { | ||||
|                 return false; | ||||
| @@ -50,51 +57,40 @@ export default { | ||||
|     }, | ||||
|     mounted() { | ||||
|         validateNotebookStorageObject(); | ||||
|         this.getDefaultNotebookObject(); | ||||
|  | ||||
|         this.notebookSnapshot = new Snapshot(this.openmct); | ||||
|         this.setDefaultNotebookStatus(); | ||||
|     }, | ||||
|     methods: { | ||||
|         getDefaultNotebookObject() { | ||||
|             const defaultNotebook = getDefaultNotebook(); | ||||
|         getPreviewObjectLink() { | ||||
|             const relativePath = this.openmct.objects.getRelativePath(this.objectPath); | ||||
|             const urlParams = this.openmct.router.getParams(); | ||||
|             urlParams.view = this.currentView.key; | ||||
|  | ||||
|             return defaultNotebook && this.openmct.objects.get(defaultNotebook.identifier); | ||||
|             const urlParamsString = Object.entries(urlParams) | ||||
|                 .map(([key, value]) => `${key}=${value}`) | ||||
|                 .join('&'); | ||||
|  | ||||
|             return `#/browse/${relativePath}?${urlParamsString}`; | ||||
|         }, | ||||
|         async showMenu(event) { | ||||
|             const notebookTypes = []; | ||||
|             const menuItemOptions = { | ||||
|                 default: { | ||||
|                     cssClass: 'icon-notebook', | ||||
|                     name: `Save to Notebook`, | ||||
|                     onItemClicked: () => this.snapshot(NOTEBOOK_DEFAULT, event.target) | ||||
|                 }, | ||||
|                 snapshot: { | ||||
|                     cssClass: 'icon-camera', | ||||
|                     name: 'Save to Notebook Snapshots', | ||||
|                     onItemClicked: () => this.snapshot(NOTEBOOK_SNAPSHOT, event.target) | ||||
|                 } | ||||
|             }; | ||||
|  | ||||
|             const notebookTypes = await getMenuItems(this.openmct, menuItemOptions); | ||||
|             const elementBoundingClientRect = this.$el.getBoundingClientRect(); | ||||
|             const x = elementBoundingClientRect.x; | ||||
|             const y = elementBoundingClientRect.y + elementBoundingClientRect.height; | ||||
|  | ||||
|             const defaultNotebookObject = await this.getDefaultNotebookObject(); | ||||
|             if (defaultNotebookObject) { | ||||
|                 const defaultNotebook = getDefaultNotebook(); | ||||
|                 const { section, page } = getNotebookSectionAndPage(defaultNotebookObject, defaultNotebook.defaultSectionId, defaultNotebook.defaultPageId); | ||||
|                 if (section && page) { | ||||
|                     const name = defaultNotebookObject.name; | ||||
|                     const sectionName = section.name; | ||||
|                     const pageName = page.name; | ||||
|                     const defaultPath = `${name} - ${sectionName} - ${pageName}`; | ||||
|  | ||||
|                     notebookTypes.push({ | ||||
|                         cssClass: 'icon-notebook', | ||||
|                         name: `Save to Notebook ${defaultPath}`, | ||||
|                         onItemClicked: () => { | ||||
|                             return this.snapshot(NOTEBOOK_DEFAULT, event.target); | ||||
|                         } | ||||
|                     }); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             notebookTypes.push({ | ||||
|                 cssClass: 'icon-camera', | ||||
|                 name: 'Save to Notebook Snapshots', | ||||
|                 onItemClicked: () => { | ||||
|                     return this.snapshot(NOTEBOOK_SNAPSHOT, event.target); | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|             this.openmct.menus.showMenu(x, y, notebookTypes); | ||||
|         }, | ||||
|         snapshot(notebookType, target) { | ||||
| @@ -102,15 +98,12 @@ export default { | ||||
|                 const wrapper = target && target.closest('.js-notebook-snapshot-item-wrapper') | ||||
|                     || document; | ||||
|                 const element = wrapper.querySelector('.js-notebook-snapshot-item'); | ||||
|  | ||||
|                 const bounds = this.openmct.time.bounds(); | ||||
|                 const link = !this.ignoreLink | ||||
|                     ? window.location.hash | ||||
|                     : null; | ||||
|  | ||||
|                 const objectPath = this.objectPath || this.openmct.router.path; | ||||
|                 const link = this.isPreview | ||||
|                     ? this.getPreviewObjectLink() | ||||
|                     : window.location.hash; | ||||
|                 const snapshotMeta = { | ||||
|                     bounds, | ||||
|                     bounds: this.openmct.time.bounds(), | ||||
|                     link, | ||||
|                     objectPath, | ||||
|                     openmct: this.openmct | ||||
|   | ||||
| @@ -27,15 +27,15 @@ | ||||
|     </div><!-- closes l-browse-bar --> | ||||
|     <div class="c-snapshots"> | ||||
|         <span v-for="snapshot in snapshots" | ||||
|               :key="snapshot.id" | ||||
|               :key="snapshot.embedObject.id" | ||||
|               draggable="true" | ||||
|               @dragstart="startEmbedDrag(snapshot, $event)" | ||||
|         > | ||||
|             <NotebookEmbed ref="notebookEmbed" | ||||
|                            :key="snapshot.id" | ||||
|                            :embed="snapshot" | ||||
|                            :key="snapshot.embedObject.id" | ||||
|                            :embed="snapshot.embedObject" | ||||
|                            :is-snapshot-container="true" | ||||
|                            :remove-action-string="'Delete Snapshot'" | ||||
|                            @updateEmbed="updateSnapshot" | ||||
|                            @removeEmbed="removeSnapshot" | ||||
|             /> | ||||
|         </span> | ||||
| @@ -119,11 +119,8 @@ export default { | ||||
|             this.snapshots = this.snapshotContainer.getSnapshots(); | ||||
|         }, | ||||
|         startEmbedDrag(snapshot, event) { | ||||
|             event.dataTransfer.setData('text/plain', snapshot.id); | ||||
|             event.dataTransfer.setData('openmct/snapshot/id', snapshot.id); | ||||
|         }, | ||||
|         updateSnapshot(snapshot) { | ||||
|             this.snapshotContainer.updateSnapshot(snapshot); | ||||
|             event.dataTransfer.setData('text/plain', snapshot.embedObject.id); | ||||
|             event.dataTransfer.setData('openmct/snapshot/id', snapshot.embedObject.id); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|   | ||||
| @@ -18,13 +18,18 @@ export default class SnapshotContainer extends EventEmitter { | ||||
|         return SnapshotContainer.instance; | ||||
|     } | ||||
|  | ||||
|     addSnapshot(embedObject) { | ||||
|     addSnapshot(notebookImageDomainObject, embedObject) { | ||||
|         const snapshots = this.getSnapshots(); | ||||
|         if (snapshots.length >= NOTEBOOK_SNAPSHOT_MAX_COUNT) { | ||||
|             snapshots.pop(); | ||||
|         } | ||||
|  | ||||
|         snapshots.unshift(embedObject); | ||||
|         const snapshotObject = { | ||||
|             notebookImageDomainObject, | ||||
|             embedObject | ||||
|         }; | ||||
|  | ||||
|         snapshots.unshift(snapshotObject); | ||||
|  | ||||
|         return this.saveSnapshots(snapshots); | ||||
|     } | ||||
| @@ -32,7 +37,7 @@ export default class SnapshotContainer extends EventEmitter { | ||||
|     getSnapshot(id) { | ||||
|         const snapshots = this.getSnapshots(); | ||||
|  | ||||
|         return snapshots.find(s => s.id === id); | ||||
|         return snapshots.find(s => s.embedObject.id === id); | ||||
|     } | ||||
|  | ||||
|     getSnapshots() { | ||||
| @@ -47,7 +52,7 @@ export default class SnapshotContainer extends EventEmitter { | ||||
|         } | ||||
|  | ||||
|         const snapshots = this.getSnapshots(); | ||||
|         const filteredsnapshots = snapshots.filter(snapshot => snapshot.id !== id); | ||||
|         const filteredsnapshots = snapshots.filter(snapshot => snapshot.embedObject.id !== id); | ||||
|  | ||||
|         return this.saveSnapshots(filteredsnapshots); | ||||
|     } | ||||
| @@ -73,7 +78,7 @@ export default class SnapshotContainer extends EventEmitter { | ||||
|     updateSnapshot(snapshot) { | ||||
|         const snapshots = this.getSnapshots(); | ||||
|         const updatedSnapshots = snapshots.map(s => { | ||||
|             return s.id === snapshot.id | ||||
|             return s.embedObject.id === snapshot.embedObject.id | ||||
|                 ? snapshot | ||||
|                 : s; | ||||
|         }); | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import { addNotebookEntry, createNewEmbed } from './utils/notebook-entries'; | ||||
| import { getDefaultNotebook, getNotebookSectionAndPage, getDefaultNotebookLink, setDefaultNotebook } from './utils/notebook-storage'; | ||||
| import { NOTEBOOK_DEFAULT } from '@/plugins/notebook/notebook-constants'; | ||||
| import { createNotebookImageDomainObject, DEFAULT_SIZE } from './utils/notebook-image'; | ||||
| import { createNotebookImageDomainObject, saveNotebookImageDomainObject, updateNamespaceOfDomainObject, DEFAULT_SIZE } from './utils/notebook-image'; | ||||
|  | ||||
| import SnapshotContainer from './snapshot-container'; | ||||
| import ImageExporter from '../../exporters/ImageExporter'; | ||||
| @@ -35,29 +35,28 @@ export default class Snapshot { | ||||
|      * @private | ||||
|      */ | ||||
|     _saveSnapShot(notebookType, fullSizeImageURL, thumbnailImageURL, snapshotMeta) { | ||||
|         createNotebookImageDomainObject(this.openmct, fullSizeImageURL) | ||||
|             .then(object => { | ||||
|                 const thumbnailImage = { src: thumbnailImageURL || '' }; | ||||
|                 const snapshot = { | ||||
|                     fullSizeImageObjectIdentifier: object.identifier, | ||||
|                     thumbnailImage | ||||
|                 }; | ||||
|                 const embed = createNewEmbed(snapshotMeta, snapshot); | ||||
|                 if (notebookType === NOTEBOOK_DEFAULT) { | ||||
|                     this._saveToDefaultNoteBook(embed); | ||||
|         const object = createNotebookImageDomainObject(fullSizeImageURL); | ||||
|         const thumbnailImage = { src: thumbnailImageURL || '' }; | ||||
|         const snapshot = { | ||||
|             fullSizeImageObjectIdentifier: object.identifier, | ||||
|             thumbnailImage | ||||
|         }; | ||||
|         const embed = createNewEmbed(snapshotMeta, snapshot); | ||||
|         if (notebookType === NOTEBOOK_DEFAULT) { | ||||
|             const notebookStorage = getDefaultNotebook(); | ||||
|  | ||||
|                     return; | ||||
|                 } | ||||
|  | ||||
|                 this._saveToNotebookSnapshots(embed); | ||||
|             }); | ||||
|             this._saveToDefaultNoteBook(notebookStorage, embed); | ||||
|             const notebookImageDomainObject = updateNamespaceOfDomainObject(object, notebookStorage.identifier.namespace); | ||||
|             saveNotebookImageDomainObject(this.openmct, notebookImageDomainObject); | ||||
|         } else { | ||||
|             this._saveToNotebookSnapshots(object, embed); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @private | ||||
|      */ | ||||
|     _saveToDefaultNoteBook(embed) { | ||||
|         const notebookStorage = getDefaultNotebook(); | ||||
|     _saveToDefaultNoteBook(notebookStorage, embed) { | ||||
|         this.openmct.objects.get(notebookStorage.identifier) | ||||
|             .then(async (domainObject) => { | ||||
|                 addNotebookEntry(this.openmct, domainObject, notebookStorage, embed); | ||||
| @@ -85,19 +84,22 @@ export default class Snapshot { | ||||
|     /** | ||||
|      * @private | ||||
|      */ | ||||
|     _saveToNotebookSnapshots(embed) { | ||||
|         this.snapshotContainer.addSnapshot(embed); | ||||
|     _saveToNotebookSnapshots(notebookImageDomainObject, embed) { | ||||
|         this.snapshotContainer.addSnapshot(notebookImageDomainObject, embed); | ||||
|     } | ||||
|  | ||||
|     _showNotification(msg, url) { | ||||
|         const options = { | ||||
|             autoDismissTimeout: 30000, | ||||
|             link: { | ||||
|             autoDismissTimeout: 30000 | ||||
|         }; | ||||
|  | ||||
|         if (!this.openmct.editor.isEditing()) { | ||||
|             options.link = { | ||||
|                 cssClass: '', | ||||
|                 text: 'click to view', | ||||
|                 onClick: this._navigateToNotebook(url) | ||||
|             } | ||||
|         }; | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         this.openmct.notifications.info(msg, options); | ||||
|     } | ||||
| @@ -108,7 +110,8 @@ export default class Snapshot { | ||||
|         } | ||||
|  | ||||
|         return () => { | ||||
|             window.location.href = window.location.origin + url; | ||||
|             const path = window.location.href.split('#'); | ||||
|             window.location.href = path[0] + url; | ||||
|         }; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -5,14 +5,14 @@ export const DEFAULT_SIZE = { | ||||
|     height: 30 | ||||
| }; | ||||
|  | ||||
| export function createNotebookImageDomainObject(openmct, fullSizeImageURL) { | ||||
| export function createNotebookImageDomainObject(fullSizeImageURL) { | ||||
|     const identifier = { | ||||
|         key: uuid(), | ||||
|         namespace: '' | ||||
|     }; | ||||
|     const viewType = 'notebookSnapshotImage'; | ||||
|  | ||||
|     const object = { | ||||
|     return { | ||||
|         name: 'Notebook Snapshot Image', | ||||
|         type: viewType, | ||||
|         identifier, | ||||
| @@ -20,21 +20,6 @@ export function createNotebookImageDomainObject(openmct, fullSizeImageURL) { | ||||
|             fullSizeImageURL | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     return new Promise((resolve, reject) => { | ||||
|         openmct.objects.save(object) | ||||
|             .then(result => { | ||||
|                 if (result) { | ||||
|                     resolve(object); | ||||
|                 } | ||||
|  | ||||
|                 reject(); | ||||
|             }) | ||||
|             .catch(e => { | ||||
|                 console.error(e); | ||||
|                 reject(); | ||||
|             }); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| export function getThumbnailURLFromCanvas(canvas, size = DEFAULT_SIZE) { | ||||
| @@ -67,6 +52,23 @@ export function getThumbnailURLFromimageUrl(imageUrl, size = DEFAULT_SIZE) { | ||||
|     }); | ||||
| } | ||||
|  | ||||
| export function saveNotebookImageDomainObject(openmct, object) { | ||||
|     return new Promise((resolve, reject) => { | ||||
|         openmct.objects.save(object) | ||||
|             .then(result => { | ||||
|                 if (result) { | ||||
|                     resolve(object); | ||||
|                 } else { | ||||
|                     reject(); | ||||
|                 } | ||||
|             }) | ||||
|             .catch(e => { | ||||
|                 console.error(e); | ||||
|                 reject(); | ||||
|             }); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| export function updateNotebookImageDomainObject(openmct, identifier, fullSizeImage) { | ||||
|     openmct.objects.get(identifier) | ||||
|         .then(domainObject => { | ||||
| @@ -76,3 +78,9 @@ export function updateNotebookImageDomainObject(openmct, identifier, fullSizeIma | ||||
|             openmct.objects.mutate(domainObject, 'configuration', configuration); | ||||
|         }); | ||||
| } | ||||
|  | ||||
| export function updateNamespaceOfDomainObject(object, namespace) { | ||||
|     object.identifier.namespace = namespace; | ||||
|  | ||||
|     return object; | ||||
| } | ||||
|   | ||||
| @@ -1,16 +1,18 @@ | ||||
| import { createNotebookImageDomainObject, getThumbnailURLFromimageUrl } from './notebook-image'; | ||||
| import { createNotebookImageDomainObject, getThumbnailURLFromimageUrl, saveNotebookImageDomainObject, updateNamespaceOfDomainObject } from './notebook-image'; | ||||
| import { mutateObject } from './notebook-entries'; | ||||
|  | ||||
| const IMAGE_MIGRATION_VER = "v1"; | ||||
|  | ||||
| export function notebookImageMigration(openmct, domainObject) { | ||||
|     const configuration = domainObject.configuration; | ||||
|     const notebookEntries = configuration.entries; | ||||
|  | ||||
|     const imageMigrationVer = configuration.imageMigrationVer; | ||||
|     if (imageMigrationVer && imageMigrationVer === 'v1') { | ||||
|     if (imageMigrationVer && imageMigrationVer === IMAGE_MIGRATION_VER) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     configuration.imageMigrationVer = 'v1'; | ||||
|     configuration.imageMigrationVer = IMAGE_MIGRATION_VER; | ||||
|  | ||||
|     // to avoid muliple notebookImageMigration calls updating images. | ||||
|     mutateObject(openmct, domainObject, 'configuration', configuration); | ||||
| @@ -27,14 +29,16 @@ export function notebookImageMigration(openmct, domainObject) { | ||||
|                     const fullSizeImageURL = snapshot.src; | ||||
|                     if (fullSizeImageURL) { | ||||
|                         const thumbnailImageURL = await getThumbnailURLFromimageUrl(fullSizeImageURL); | ||||
|                         const notebookImageDomainObject = await createNotebookImageDomainObject(openmct, fullSizeImageURL); | ||||
|  | ||||
|                         const object = createNotebookImageDomainObject(fullSizeImageURL); | ||||
|                         const notebookImageDomainObject = updateNamespaceOfDomainObject(object, domainObject.identifier.namespace); | ||||
|                         embed.snapshot = { | ||||
|                             fullSizeImageObjectIdentifier: notebookImageDomainObject.identifier, | ||||
|                             thumbnailImage: { src: thumbnailImageURL || '' } | ||||
|                         }; | ||||
|  | ||||
|                         mutateObject(openmct, domainObject, 'configuration.entries', notebookEntries); | ||||
|  | ||||
|                         saveNotebookImageDomainObject(openmct, notebookImageDomainObject); | ||||
|                     } | ||||
|                 }); | ||||
|             }); | ||||
|   | ||||
							
								
								
									
										31
									
								
								src/plugins/notebook/utils/notebook-snapshot-menu.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/plugins/notebook/utils/notebook-snapshot-menu.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| import { getDefaultNotebook, getNotebookSectionAndPage } from './notebook-storage'; | ||||
|  | ||||
| export async function getMenuItems(openmct, menuItemOptions) { | ||||
|     const notebookTypes = []; | ||||
|  | ||||
|     const defaultNotebook = getDefaultNotebook(); | ||||
|     const defaultNotebookObject = defaultNotebook && await openmct.objects.get(defaultNotebook.identifier); | ||||
|     if (defaultNotebookObject) { | ||||
|         const { section, page } = getNotebookSectionAndPage(defaultNotebookObject, defaultNotebook.defaultSectionId, defaultNotebook.defaultPageId); | ||||
|         if (section && page) { | ||||
|             const name = defaultNotebookObject.name; | ||||
|             const sectionName = section.name; | ||||
|             const pageName = page.name; | ||||
|             const defaultPath = `${name} - ${sectionName} - ${pageName}`; | ||||
|  | ||||
|             notebookTypes.push({ | ||||
|                 cssClass: menuItemOptions.default.cssClass, | ||||
|                 name: `${menuItemOptions.default.name} ${defaultPath}`, | ||||
|                 onItemClicked: menuItemOptions.default.onItemClicked | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     notebookTypes.push({ | ||||
|         cssClass: menuItemOptions.snapshot.cssClass, | ||||
|         name: menuItemOptions.snapshot.name, | ||||
|         onItemClicked: menuItemOptions.snapshot.onItemClicked | ||||
|     }); | ||||
|  | ||||
|     return notebookTypes; | ||||
| } | ||||
| @@ -71,11 +71,7 @@ export async function getDefaultNotebookLink(openmct, domainObject = null) { | ||||
|     } | ||||
|  | ||||
|     const path = await openmct.objects.getOriginalPath(domainObject.identifier) | ||||
|         .then(objectPath => objectPath | ||||
|             .map(o => o && openmct.objects.makeKeyString(o.identifier)) | ||||
|             .reverse() | ||||
|             .join('/') | ||||
|         ); | ||||
|         .then(openmct.objects.getRelativePath); | ||||
|     const { defaultPageId, defaultSectionId } = getDefaultNotebook(); | ||||
|  | ||||
|     return `#/browse/${path}?sectionId=${defaultSectionId}&pageId=${defaultPageId}`; | ||||
| @@ -100,6 +96,9 @@ export function setDefaultNotebookPageId(pageId) { | ||||
|  | ||||
| export function validateNotebookStorageObject() { | ||||
|     const notebookStorage = getDefaultNotebook(); | ||||
|     if (!notebookStorage) { | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     let valid = false; | ||||
|     if (notebookStorage) { | ||||
|   | ||||
| @@ -34,7 +34,7 @@ describe('the plugin', () => { | ||||
|     let countFramesPromise; | ||||
|  | ||||
|     beforeEach((done) => { | ||||
|         openmct = createOpenMct(false); | ||||
|         openmct = createOpenMct(); | ||||
|  | ||||
|         element = document.createElement('div'); | ||||
|         child = document.createElement('div'); | ||||
|   | ||||
| @@ -49,7 +49,7 @@ describe('the plugin', () => { | ||||
|             filter: {}, | ||||
|             disableObserve: true | ||||
|         }; | ||||
|         openmct = createOpenMct(false); | ||||
|         openmct = createOpenMct(); | ||||
|  | ||||
|         openmct.$injector = jasmine.createSpyObj('$injector', ['get']); | ||||
|         mockIdentifierService = jasmine.createSpyObj( | ||||
|   | ||||
| @@ -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,10 @@ 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.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 { | ||||
|   | ||||
| @@ -36,7 +36,15 @@ describe('the plugin', function () { | ||||
|         appHolder.style.width = '640px'; | ||||
|         appHolder.style.height = '480px'; | ||||
|  | ||||
|         openmct = createOpenMct(); | ||||
|         const timeSystemOptions = { | ||||
|             timeSystemKey: 'utc', | ||||
|             bounds: { | ||||
|                 start: 1597160002854, | ||||
|                 end: 1597181232854 | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         openmct = createOpenMct(timeSystemOptions); | ||||
|         openmct.install(new PlanPlugin()); | ||||
|  | ||||
|         planDefinition = openmct.types.get('plan').definition; | ||||
| @@ -48,7 +56,6 @@ describe('the plugin', function () { | ||||
|         child.style.width = '640px'; | ||||
|         child.style.height = '480px'; | ||||
|         element.appendChild(child); | ||||
|  | ||||
|         openmct.on('start', done); | ||||
|         openmct.start(appHolder); | ||||
|     }); | ||||
| @@ -72,7 +79,6 @@ describe('the plugin', function () { | ||||
|     }); | ||||
|  | ||||
|     describe('the plan view', () => { | ||||
|  | ||||
|         it('provides a plan view', () => { | ||||
|             const testViewObject = { | ||||
|                 id: "test-object", | ||||
| @@ -83,7 +89,6 @@ describe('the plugin', function () { | ||||
|             let planView = applicableViews.find((viewProvider) => viewProvider.key === 'plan.view'); | ||||
|             expect(planView).toBeDefined(); | ||||
|         }); | ||||
|  | ||||
|     }); | ||||
|  | ||||
|     describe('the plan view displays activities', () => { | ||||
| @@ -155,12 +160,22 @@ describe('the plugin', function () { | ||||
|             expect(labelEl.innerHTML).toEqual('TEST-GROUP'); | ||||
|         }); | ||||
|  | ||||
|         it('displays the activities and their labels', () => { | ||||
|             const rectEls = element.querySelectorAll('.c-plan__contents rect'); | ||||
|             expect(rectEls.length).toEqual(2); | ||||
|             const textEls = element.querySelectorAll('.c-plan__contents text'); | ||||
|             expect(textEls.length).toEqual(3); | ||||
|         it('displays the activities and their labels', (done) => { | ||||
|             const bounds = { | ||||
|                 start: 1597160002854, | ||||
|                 end: 1597181232854 | ||||
|             }; | ||||
|  | ||||
|             openmct.time.bounds(bounds); | ||||
|  | ||||
|             Vue.nextTick(() => { | ||||
|                 const rectEls = element.querySelectorAll('.c-plan__contents rect'); | ||||
|                 expect(rectEls.length).toEqual(2); | ||||
|                 const textEls = element.querySelectorAll('.c-plan__contents text'); | ||||
|                 expect(textEls.length).toEqual(3); | ||||
|  | ||||
|                 done(); | ||||
|             }); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
| }); | ||||
|   | ||||
							
								
								
									
										170
									
								
								src/plugins/plot/ColorSwatch.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										170
									
								
								src/plugins/plot/ColorSwatch.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,170 @@ | ||||
| <!-- | ||||
|  Open MCT, Copyright (c) 2014-2020, United States Government | ||||
|  as represented by the Administrator of the National Aeronautics and Space | ||||
|  Administration. All rights reserved. | ||||
|  | ||||
|  Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  "License"); you may not use this file except in compliance with the License. | ||||
|  You may obtain a copy of the License at | ||||
|  http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  | ||||
|  Unless required by applicable law or agreed to in writing, software | ||||
|  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  License for the specific language governing permissions and limitations | ||||
|  under the License. | ||||
|  | ||||
|  Open MCT includes source code licensed under additional open source | ||||
|  licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  this source code distribution or the Licensing information page available | ||||
|  at runtime from the About dialog for additional information. | ||||
| --> | ||||
| <template> | ||||
| <div class="u-contents"> | ||||
|     <ul v-if="canEdit" | ||||
|         class="l-inspector-part" | ||||
|     > | ||||
|         <h2 v-if="heading" | ||||
|             :title="heading" | ||||
|         >{{ heading }}</h2> | ||||
|         <li class="grid-row"> | ||||
|             <div class="grid-cell label" | ||||
|                  :title="editTitle" | ||||
|             >{{ shortLabel }}</div> | ||||
|             <div class="grid-cell value"> | ||||
|                 <div class="c-click-swatch c-click-swatch--menu" | ||||
|                      @click="toggleSwatch()" | ||||
|                 > | ||||
|                     <span class="c-color-swatch" | ||||
|                           :style="{ background: currentColor }" | ||||
|                     > | ||||
|                     </span> | ||||
|                 </div> | ||||
|                 <div class="c-palette c-palette--color"> | ||||
|                     <div v-show="swatchActive" | ||||
|                          class="c-palette__items" | ||||
|                     > | ||||
|                         <div v-for="group in colorPaletteGroups" | ||||
|                              :key="group.id" | ||||
|                              class="u-contents" | ||||
|                         > | ||||
|                             <div v-for="color in group" | ||||
|                                  :key="color.id" | ||||
|                                  class="c-palette__item" | ||||
|                                  :class="{ 'selected': currentColor === color.hexString }" | ||||
|                                  :style="{ background: color.hexString }" | ||||
|                                  @click="setColor(color)" | ||||
|                             > | ||||
|                             </div> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </li> | ||||
|     </ul> | ||||
|     <ul v-else | ||||
|         class="l-inspector-part" | ||||
|     > | ||||
|         <h2 v-if="heading" | ||||
|             :title="heading" | ||||
|         >{{ heading }}</h2> | ||||
|         <li class="grid-row"> | ||||
|             <div class="grid-cell label" | ||||
|                  :title="viewTitle" | ||||
|             >{{ shortLabel }}</div> | ||||
|             <div class="grid-cell value"> | ||||
|                 <span class="c-color-swatch" | ||||
|                       :style="{ | ||||
|                           'background': currentColor | ||||
|                       }" | ||||
|                 > | ||||
|                 </span> | ||||
|             </div> | ||||
|         </li> | ||||
|     </ul> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import ColorPalette from './lib/ColorPalette'; | ||||
|  | ||||
| export default { | ||||
|     inject: ['openmct', 'domainObject'], | ||||
|     props: { | ||||
|         currentColor: { | ||||
|             type: String, | ||||
|             default() { | ||||
|                 return ''; | ||||
|             } | ||||
|         }, | ||||
|         editTitle: { | ||||
|             type: String, | ||||
|             default() { | ||||
|                 return 'Set the color.'; | ||||
|             } | ||||
|         }, | ||||
|         viewTitle: { | ||||
|             type: String, | ||||
|             default() { | ||||
|                 return 'The current color.'; | ||||
|             } | ||||
|         }, | ||||
|         shortLabel: { | ||||
|             type: String, | ||||
|             default() { | ||||
|                 return 'Color'; | ||||
|             } | ||||
|         }, | ||||
|         heading: { | ||||
|             type: String, | ||||
|             default() { | ||||
|                 return ''; | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             swatchActive: false, | ||||
|             colorPaletteGroups: [], | ||||
|             isEditing: this.openmct.editor.isEditing() | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
|         canEdit() { | ||||
|             return this.isEditing && !this.domainObject.locked; | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.colorPalette = new ColorPalette(); | ||||
|         this.openmct.editor.on('isEditing', this.setEditState); | ||||
|         this.initialize(); | ||||
|     }, | ||||
|     beforeDestroy() { | ||||
|         this.openmct.editor.off('isEditing', this.setEditState); | ||||
|     }, | ||||
|     methods: { | ||||
|         initialize() { | ||||
|             const colorPaletteGroups = this.colorPalette.groups(); | ||||
|             colorPaletteGroups.forEach((group, index) => { | ||||
|                 let groupId = []; | ||||
|                 group.forEach(color => { | ||||
|                     color.hexString = color.asHexString(); | ||||
|                     color.id = `${color.hexString}-${index}`; | ||||
|                     groupId.push(color.id); | ||||
|                 }); | ||||
|                 group.id = groupId.join('-'); | ||||
|             }); | ||||
|             this.colorPaletteGroups = colorPaletteGroups; | ||||
|         }, | ||||
|         setEditState(isEditing) { | ||||
|             this.isEditing = isEditing; | ||||
|         }, | ||||
|         setColor(chosenColor) { | ||||
|             this.$emit('colorSet', chosenColor); | ||||
|         }, | ||||
|         toggleSwatch() { | ||||
|             this.swatchActive = !this.swatchActive; | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| </script> | ||||
| @@ -156,7 +156,7 @@ | ||||
| import eventHelpers from './lib/eventHelpers'; | ||||
| import LinearScale from "./LinearScale"; | ||||
| import PlotConfigurationModel from './configuration/PlotConfigurationModel'; | ||||
| import configStore from './configuration/configStore'; | ||||
| import configStore from './configuration/ConfigStore'; | ||||
|  | ||||
| import PlotLegend from "./legend/PlotLegend.vue"; | ||||
| import MctTicks from "./MctTicks.vue"; | ||||
| @@ -173,7 +173,7 @@ export default { | ||||
|         MctTicks, | ||||
|         MctChart | ||||
|     }, | ||||
|     inject: ['openmct', 'domainObject'], | ||||
|     inject: ['openmct', 'domainObject', 'path'], | ||||
|     props: { | ||||
|         options: { | ||||
|             type: Object, | ||||
| @@ -244,6 +244,9 @@ export default { | ||||
|     }, | ||||
|     mounted() { | ||||
|         eventHelpers.extend(this); | ||||
|         this.updateRealTime = this.updateRealTime.bind(this); | ||||
|         this.updateDisplayBounds = this.updateDisplayBounds.bind(this); | ||||
|         this.setTimeContext = this.setTimeContext.bind(this); | ||||
|  | ||||
|         this.config = this.getConfig(); | ||||
|         this.legend = this.config.legend; | ||||
| @@ -261,7 +264,7 @@ export default { | ||||
|         this.removeStatusListener = this.openmct.status.observe(this.domainObject.identifier, this.updateStatus); | ||||
|  | ||||
|         this.openmct.objectViews.on('clearData', this.clearData); | ||||
|         this.followTimeConductor(); | ||||
|         this.setTimeContext(); | ||||
|  | ||||
|         this.loaded = true; | ||||
|  | ||||
| @@ -274,11 +277,27 @@ export default { | ||||
|         this.destroy(); | ||||
|     }, | ||||
|     methods: { | ||||
|         followTimeConductor() { | ||||
|             this.openmct.time.on('clock', this.updateRealTime); | ||||
|             this.openmct.time.on('bounds', this.updateDisplayBounds); | ||||
|         setTimeContext() { | ||||
|             this.stopFollowingTimeContext(); | ||||
|  | ||||
|             this.timeContext = this.openmct.time.getContextForView(this.path); | ||||
|             this.timeContext.on('timeContext', this.setTimeContext); | ||||
|             this.followTimeContext(); | ||||
|  | ||||
|         }, | ||||
|         followTimeContext() { | ||||
|             this.updateDisplayBounds(this.timeContext.bounds()); | ||||
|             this.timeContext.on('clock', this.updateRealTime); | ||||
|             this.timeContext.on('bounds', this.updateDisplayBounds); | ||||
|             this.synchronized(true); | ||||
|         }, | ||||
|         stopFollowingTimeContext() { | ||||
|             if (this.timeContext) { | ||||
|                 this.timeContext.off("clock", this.updateRealTime); | ||||
|                 this.timeContext.off("bounds", this.updateDisplayBounds); | ||||
|                 this.timeContext.off("timeContext", this.setTimeContext); | ||||
|             } | ||||
|         }, | ||||
|         getConfig() { | ||||
|             const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier); | ||||
|             let config = configStore.get(configId); | ||||
| @@ -393,12 +412,31 @@ export default { | ||||
|             }); | ||||
|         }, | ||||
|  | ||||
|         clearData() { | ||||
|         clearSeries() { | ||||
|             this.config.series.forEach(function (series) { | ||||
|                 series.reset(); | ||||
|             }); | ||||
|         }, | ||||
|  | ||||
|         compositionPathContainsId(domainObjectToClear) { | ||||
|             return domainObjectToClear.composition.some((compositionIdentifier) => { | ||||
|                 return this.openmct.objects.areIdsEqual(compositionIdentifier, this.domainObject.identifier); | ||||
|             }); | ||||
|         }, | ||||
|  | ||||
|         clearData(domainObjectToClear) { | ||||
|             // If we don't have an object to clear (global), or the IDs are equal, just clear the data. | ||||
|             // If we have an object to clear, but the IDs don't match, we need to check the composition | ||||
|             // of the object we've been asked to clear to see if it contains the id we're looking for. | ||||
|             // This happens with stacked plots for example. | ||||
|             // If we find the ID, clear the plot. | ||||
|             if (!domainObjectToClear | ||||
|             || this.openmct.objects.areIdsEqual(domainObjectToClear.identifier, this.domainObject.identifier) | ||||
|             || this.compositionPathContainsId(domainObjectToClear)) { | ||||
|                 this.clearSeries(); | ||||
|             } | ||||
|         }, | ||||
|  | ||||
|         setDisplayRange(series, xKey) { | ||||
|             if (this.config.series.length !== 1) { | ||||
|                 return; | ||||
| @@ -466,7 +504,7 @@ export default { | ||||
|        * displays can update accordingly. | ||||
|        */ | ||||
|         synchronized(value) { | ||||
|             const isLocalClock = this.openmct.time.clock(); | ||||
|             const isLocalClock = this.timeContext.clock(); | ||||
|  | ||||
|             if (typeof value !== 'undefined') { | ||||
|                 this._synchronized = value; | ||||
| @@ -939,7 +977,7 @@ export default { | ||||
|         }, | ||||
|  | ||||
|         showSynchronizeDialog() { | ||||
|             const isLocalClock = this.openmct.time.clock(); | ||||
|             const isLocalClock = this.timeContext.clock(); | ||||
|             if (isLocalClock !== undefined) { | ||||
|                 const message = ` | ||||
|                 This action will change the Time Conductor to Fixed Timespan mode with this plot view's current time bounds. | ||||
| @@ -974,9 +1012,9 @@ export default { | ||||
|         }, | ||||
|  | ||||
|         synchronizeTimeConductor() { | ||||
|             this.openmct.time.stopClock(); | ||||
|             this.timeContext.stopClock(); | ||||
|             const range = this.config.xAxis.get('displayRange'); | ||||
|             this.openmct.time.bounds({ | ||||
|             this.timeContext.bounds({ | ||||
|                 start: range.min, | ||||
|                 end: range.max | ||||
|             }); | ||||
| @@ -987,6 +1025,7 @@ export default { | ||||
|             configStore.deleteStore(this.config.id); | ||||
|  | ||||
|             this.stopListening(); | ||||
|  | ||||
|             if (this.checkForSize) { | ||||
|                 clearInterval(this.checkForSize); | ||||
|                 delete this.checkForSize; | ||||
| @@ -1002,15 +1041,15 @@ 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) { | ||||
|             this.$emit('statusUpdated', status); | ||||
|         }, | ||||
|         handleWindowResize() { | ||||
|             if (this.offsetWidth !== this.$parent.$refs.plotWrapper.offsetWidth) { | ||||
|             if (this.$parent.$refs.plotWrapper | ||||
|                 && (this.offsetWidth !== this.$parent.$refs.plotWrapper.offsetWidth)) { | ||||
|                 this.offsetWidth = this.$parent.$refs.plotWrapper.offsetWidth; | ||||
|                 this.config.series.models.forEach(this.loadSeriesData, this); | ||||
|             } | ||||
|   | ||||
| @@ -77,7 +77,7 @@ | ||||
| <script> | ||||
| import eventHelpers from "./lib/eventHelpers"; | ||||
| import { ticks, getFormattedTicks } from "./tickUtils"; | ||||
| import configStore from "./configuration/configStore"; | ||||
| import configStore from "./configuration/ConfigStore"; | ||||
|  | ||||
| export default { | ||||
|     inject: ['openmct', 'domainObject'], | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -54,7 +54,7 @@ | ||||
| <script> | ||||
| import MctTicks from "../MctTicks.vue"; | ||||
| import eventHelpers from '../lib/eventHelpers'; | ||||
| import configStore from "../configuration/configStore"; | ||||
| import configStore from "../configuration/ConfigStore"; | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
|   | ||||
| @@ -57,7 +57,7 @@ | ||||
|  | ||||
| <script> | ||||
| import MctTicks from "../MctTicks.vue"; | ||||
| import configStore from "../configuration/configStore"; | ||||
| import configStore from "../configuration/ConfigStore"; | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
|   | ||||
							
								
								
									
										53
									
								
								src/plugins/plot/barGraph/BarGraphCompositionPolicy.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								src/plugins/plot/barGraph/BarGraphCompositionPolicy.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | ||||
| /***************************************************************************** | ||||
|  * 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 { BAR_GRAPH_KEY } from './BarGraphConstants'; | ||||
|  | ||||
| export default function BarGraphCompositionPolicy(openmct) { | ||||
|     function hasAggregateDomainAndRange(metadata) { | ||||
|         const rangeValues = metadata.valuesForHints(['range']); | ||||
|  | ||||
|         return rangeValues.length > 0; | ||||
|     } | ||||
|  | ||||
|     function hasBarGraphTelemetry(domainObject) { | ||||
|         if (!Object.prototype.hasOwnProperty.call(domainObject, 'telemetry')) { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         let metadata = openmct.telemetry.getMetadata(domainObject); | ||||
|  | ||||
|         return metadata.values().length > 0 && hasAggregateDomainAndRange(metadata); | ||||
|     } | ||||
|  | ||||
|     return { | ||||
|         allow: function (parent, child) { | ||||
|             if ((parent.type === BAR_GRAPH_KEY) | ||||
|                 && ((child.type !== 'telemetry.plot.overlay') && (hasBarGraphTelemetry(child) === false)) | ||||
|             ) { | ||||
|                 return false; | ||||
|             } | ||||
|  | ||||
|             return true; | ||||
|         } | ||||
|     }; | ||||
| } | ||||
							
								
								
									
										346
									
								
								src/plugins/plot/barGraph/BarGraphCompositionPolicySpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										346
									
								
								src/plugins/plot/barGraph/BarGraphCompositionPolicySpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,346 @@ | ||||
| /***************************************************************************** | ||||
|  * 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 BarGraphCompositionPolicy from "./BarGraphCompositionPolicy"; | ||||
| import { createOpenMct } from "utils/testing"; | ||||
|  | ||||
| describe("The bar graph composition policy", () => { | ||||
|     let openmct; | ||||
|     const mockMetaDataWithNoRangeHints = { | ||||
|         "period": 10, | ||||
|         "amplitude": 1, | ||||
|         "offset": 0, | ||||
|         "dataRateInHz": 1, | ||||
|         "phase": 0, | ||||
|         "randomness": 0, | ||||
|         valuesForHints: () => { | ||||
|             return []; | ||||
|         }, | ||||
|         values: [ | ||||
|             { | ||||
|                 "key": "name", | ||||
|                 "name": "Name", | ||||
|                 "format": "string" | ||||
|             }, | ||||
|             { | ||||
|                 "key": "utc", | ||||
|                 "name": "Time", | ||||
|                 "format": "utc", | ||||
|                 "hints": { | ||||
|                     "domain": 1, | ||||
|                     "priority": 1 | ||||
|                 }, | ||||
|                 "source": "utc" | ||||
|             } | ||||
|         ] | ||||
|     }; | ||||
|     const mockMetaDataWithRangeHints = { | ||||
|         "period": 10, | ||||
|         "amplitude": 1, | ||||
|         "offset": 0, | ||||
|         "dataRateInHz": 1, | ||||
|         "phase": 0, | ||||
|         "randomness": 0, | ||||
|         "wavelength": 0, | ||||
|         valuesForHints: () => { | ||||
|             return [ | ||||
|                 { | ||||
|                     "key": "sin", | ||||
|                     "name": "Sine", | ||||
|                     "unit": "Hz", | ||||
|                     "formatString": "%0.2f", | ||||
|                     "hints": { | ||||
|                         "range": 1, | ||||
|                         "priority": 4 | ||||
|                     }, | ||||
|                     "source": "sin" | ||||
|                 }, | ||||
|                 { | ||||
|                     "key": "cos", | ||||
|                     "name": "Cosine", | ||||
|                     "unit": "deg", | ||||
|                     "formatString": "%0.2f", | ||||
|                     "hints": { | ||||
|                         "range": 2, | ||||
|                         "priority": 5 | ||||
|                     }, | ||||
|                     "source": "cos" | ||||
|                 } | ||||
|             ]; | ||||
|         }, | ||||
|         values: [ | ||||
|             { | ||||
|                 "key": "name", | ||||
|                 "name": "Name", | ||||
|                 "format": "string", | ||||
|                 "source": "name", | ||||
|                 "hints": { | ||||
|                     "priority": 0 | ||||
|                 } | ||||
|             }, | ||||
|             { | ||||
|                 "key": "utc", | ||||
|                 "name": "Time", | ||||
|                 "format": "utc", | ||||
|                 "hints": { | ||||
|                     "domain": 1, | ||||
|                     "priority": 1 | ||||
|                 }, | ||||
|                 "source": "utc" | ||||
|             }, | ||||
|             { | ||||
|                 "key": "yesterday", | ||||
|                 "name": "Yesterday", | ||||
|                 "format": "utc", | ||||
|                 "hints": { | ||||
|                     "domain": 2, | ||||
|                     "priority": 2 | ||||
|                 }, | ||||
|                 "source": "yesterday" | ||||
|             }, | ||||
|             { | ||||
|                 "key": "sin", | ||||
|                 "name": "Sine", | ||||
|                 "unit": "Hz", | ||||
|                 "formatString": "%0.2f", | ||||
|                 "hints": { | ||||
|                     "range": 1, | ||||
|                     "spectralAttribute": true | ||||
|                 }, | ||||
|                 "source": "sin" | ||||
|             }, | ||||
|             { | ||||
|                 "key": "cos", | ||||
|                 "name": "Cosine", | ||||
|                 "unit": "deg", | ||||
|                 "formatString": "%0.2f", | ||||
|                 "hints": { | ||||
|                     "range": 2, | ||||
|                     "priority": 5 | ||||
|                 }, | ||||
|                 "source": "cos" | ||||
|             } | ||||
|         ] | ||||
|     }; | ||||
|  | ||||
|     beforeEach(() => { | ||||
|         openmct = createOpenMct(); | ||||
|         const mockTypeDef = { | ||||
|             telemetry: mockMetaDataWithRangeHints | ||||
|         }; | ||||
|         const mockTypeService = { | ||||
|             getType: () => { | ||||
|                 return { | ||||
|                     typeDef: mockTypeDef | ||||
|                 }; | ||||
|             } | ||||
|         }; | ||||
|         openmct.$injector = { | ||||
|             get: () => { | ||||
|                 return mockTypeService; | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         openmct.telemetry.isTelemetryObject = function (domainObject) { | ||||
|             return true; | ||||
|         }; | ||||
|     }); | ||||
|  | ||||
|     it("exists", () => { | ||||
|         expect(BarGraphCompositionPolicy(openmct).allow).toBeDefined(); | ||||
|     }); | ||||
|  | ||||
|     xit("allow composition for telemetry that provides/supports bar graph meta data", () => { | ||||
|         const parent = { | ||||
|             "composition": [], | ||||
|             "configuration": {}, | ||||
|             "name": "Some Bar Graph", | ||||
|             "type": "telemetry.plot.bar-graph", | ||||
|             "location": "mine", | ||||
|             "modified": 1631005183584, | ||||
|             "persisted": 1631005183502, | ||||
|             "identifier": { | ||||
|                 "namespace": "", | ||||
|                 "key": "b78e7e23-f2b8-4776-b1f0-3ff778f5c8a9" | ||||
|             } | ||||
|         }; | ||||
|         const child = { | ||||
|             "telemetry": { | ||||
|                 "period": 10, | ||||
|                 "amplitude": 1, | ||||
|                 "offset": 0, | ||||
|                 "dataRateInHz": 1, | ||||
|                 "phase": 0, | ||||
|                 "randomness": 0 | ||||
|             }, | ||||
|             "name": "Unnamed Sine Wave Generator", | ||||
|             "type": "generator", | ||||
|             "location": "mine", | ||||
|             "modified": 1630399715531, | ||||
|             "persisted": 1630399715531, | ||||
|             "identifier": { | ||||
|                 "namespace": "", | ||||
|                 "key": "21d61f2d-6d2d-4bea-8b0a-7f59fd504c6c" | ||||
|             } | ||||
|         }; | ||||
|         expect(BarGraphCompositionPolicy(openmct).allow(parent, child)).toEqual(true); | ||||
|     }); | ||||
|  | ||||
|     it("allows composition for telemetry that contain at least one range", () => { | ||||
|         const mockTypeDef = { | ||||
|             telemetry: mockMetaDataWithRangeHints | ||||
|         }; | ||||
|         const mockTypeService = { | ||||
|             getType: () => { | ||||
|                 return { | ||||
|                     typeDef: mockTypeDef | ||||
|                 }; | ||||
|             } | ||||
|         }; | ||||
|         openmct.$injector = { | ||||
|             get: () => { | ||||
|                 return mockTypeService; | ||||
|             } | ||||
|         }; | ||||
|         const parent = { | ||||
|             "composition": [], | ||||
|             "configuration": {}, | ||||
|             "name": "Some Bar Graph", | ||||
|             "type": "telemetry.plot.bar-graph", | ||||
|             "location": "mine", | ||||
|             "modified": 1631005183584, | ||||
|             "persisted": 1631005183502, | ||||
|             "identifier": { | ||||
|                 "namespace": "", | ||||
|                 "key": "b78e7e23-f2b8-4776-b1f0-3ff778f5c8a9" | ||||
|             } | ||||
|         }; | ||||
|         const child = { | ||||
|             "telemetry": { | ||||
|                 "period": 10, | ||||
|                 "amplitude": 1, | ||||
|                 "offset": 0, | ||||
|                 "dataRateInHz": 1, | ||||
|                 "phase": 0, | ||||
|                 "randomness": 0 | ||||
|             }, | ||||
|             "name": "Unnamed Sine Wave Generator", | ||||
|             "type": "generator", | ||||
|             "location": "mine", | ||||
|             "modified": 1630399715531, | ||||
|             "persisted": 1630399715531, | ||||
|             "identifier": { | ||||
|                 "namespace": "", | ||||
|                 "key": "21d61f2d-6d2d-4bea-8b0a-7f59fd504c6c" | ||||
|             } | ||||
|         }; | ||||
|         expect(BarGraphCompositionPolicy(openmct).allow(parent, child)).toEqual(true); | ||||
|     }); | ||||
|  | ||||
|     it("disallows composition for telemetry that don't contain any range hints", () => { | ||||
|         const mockTypeDef = { | ||||
|             telemetry: mockMetaDataWithNoRangeHints | ||||
|         }; | ||||
|         const mockTypeService = { | ||||
|             getType: () => { | ||||
|                 return { | ||||
|                     typeDef: mockTypeDef | ||||
|                 }; | ||||
|             } | ||||
|         }; | ||||
|         openmct.$injector = { | ||||
|             get: () => { | ||||
|                 return mockTypeService; | ||||
|             } | ||||
|         }; | ||||
|         const parent = { | ||||
|             "composition": [], | ||||
|             "configuration": {}, | ||||
|             "name": "Some Bar Graph", | ||||
|             "type": "telemetry.plot.bar-graph", | ||||
|             "location": "mine", | ||||
|             "modified": 1631005183584, | ||||
|             "persisted": 1631005183502, | ||||
|             "identifier": { | ||||
|                 "namespace": "", | ||||
|                 "key": "b78e7e23-f2b8-4776-b1f0-3ff778f5c8a9" | ||||
|             } | ||||
|         }; | ||||
|         const child = { | ||||
|             "telemetry": { | ||||
|                 "period": 10, | ||||
|                 "amplitude": 1, | ||||
|                 "offset": 0, | ||||
|                 "dataRateInHz": 1, | ||||
|                 "phase": 0, | ||||
|                 "randomness": 0 | ||||
|             }, | ||||
|             "name": "Unnamed Sine Wave Generator", | ||||
|             "type": "generator", | ||||
|             "location": "mine", | ||||
|             "modified": 1630399715531, | ||||
|             "persisted": 1630399715531, | ||||
|             "identifier": { | ||||
|                 "namespace": "", | ||||
|                 "key": "21d61f2d-6d2d-4bea-8b0a-7f59fd504c6c" | ||||
|             } | ||||
|         }; | ||||
|         expect(BarGraphCompositionPolicy(openmct).allow(parent, child)).toEqual(false); | ||||
|     }); | ||||
|  | ||||
|     it("passthrough for composition for non bar graph plots", () => { | ||||
|         const parent = { | ||||
|             "composition": [], | ||||
|             "configuration": {}, | ||||
|             "name": "Some Stacked Plot", | ||||
|             "type": "telemetry.plot.stacked", | ||||
|             "location": "mine", | ||||
|             "modified": 1631005183584, | ||||
|             "persisted": 1631005183502, | ||||
|             "identifier": { | ||||
|                 "namespace": "", | ||||
|                 "key": "b78e7e23-f2b8-4776-b1f0-3ff778f5c8a9" | ||||
|             } | ||||
|         }; | ||||
|         const child = { | ||||
|             "telemetry": { | ||||
|                 "period": 10, | ||||
|                 "amplitude": 1, | ||||
|                 "offset": 0, | ||||
|                 "dataRateInHz": 1, | ||||
|                 "phase": 0, | ||||
|                 "randomness": 0 | ||||
|             }, | ||||
|             "name": "Unnamed Sine Wave Generator", | ||||
|             "type": "generator", | ||||
|             "location": "mine", | ||||
|             "modified": 1630399715531, | ||||
|             "persisted": 1630399715531, | ||||
|             "identifier": { | ||||
|                 "namespace": "", | ||||
|                 "key": "21d61f2d-6d2d-4bea-8b0a-7f59fd504c6c" | ||||
|             } | ||||
|         }; | ||||
|         expect(BarGraphCompositionPolicy(openmct).allow(parent, child)).toEqual(true); | ||||
|     }); | ||||
| }); | ||||
|  | ||||
							
								
								
									
										5
									
								
								src/plugins/plot/barGraph/BarGraphConstants.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src/plugins/plot/barGraph/BarGraphConstants.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| export const BAR_GRAPH_VIEW = 'bar-graph.view'; | ||||
| export const BAR_GRAPH_KEY = 'telemetry.plot.bar-graph'; | ||||
| export const BAR_GRAPH_INSPECTOR_KEY = 'telemetry.plot.bar-graph.inspector'; | ||||
| export const SUBSCRIBE = 'subscribe'; | ||||
| export const UNSUBSCRIBE = 'unsubscribe'; | ||||
							
								
								
									
										293
									
								
								src/plugins/plot/barGraph/BarGraphPlot.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										293
									
								
								src/plugins/plot/barGraph/BarGraphPlot.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,293 @@ | ||||
| <template> | ||||
| <div ref="plotWrapper" | ||||
|      class="has-local-controls" | ||||
|      :class="{ 's-unsynced' : isZoomed }" | ||||
| > | ||||
|     <div v-if="isZoomed" | ||||
|          class="l-state-indicators" | ||||
|     > | ||||
|         <span class="l-state-indicators__alert-no-lad t-object-alert t-alert-unsynced icon-alert-triangle" | ||||
|               title="This plot is not currently displaying the latest data. Reset pan/zoom to view latest data." | ||||
|         ></span> | ||||
|     </div> | ||||
|     <div ref="plot" | ||||
|          class="c-bar-chart" | ||||
|     ></div> | ||||
|     <div v-if="false" | ||||
|          ref="localControl" | ||||
|          class="gl-plot__local-controls h-local-controls h-local-controls--overlay-content c-local-controls--show-on-hover" | ||||
|     > | ||||
|         <button v-if="data.length" | ||||
|                 class="c-button icon-reset" | ||||
|                 :disabled="!isZoomed" | ||||
|                 title="Reset pan/zoom" | ||||
|                 @click="reset()" | ||||
|         > | ||||
|         </button> | ||||
|     </div> | ||||
| </div> | ||||
| </template> | ||||
| <script> | ||||
| import Plotly from 'plotly.js-basic-dist'; | ||||
| import { SUBSCRIBE, UNSUBSCRIBE } from './BarGraphConstants'; | ||||
|  | ||||
| const MULTI_AXES_X_PADDING_PERCENT = { | ||||
|     LEFT: 8, | ||||
|     RIGHT: 94 | ||||
| }; | ||||
|  | ||||
| export default { | ||||
|     inject: ['openmct', 'domainObject'], | ||||
|     props: { | ||||
|         data: { | ||||
|             type: Array, | ||||
|             default() { | ||||
|                 return []; | ||||
|             } | ||||
|         }, | ||||
|         plotAxisTitle: { | ||||
|             type: Object, | ||||
|             default() { | ||||
|                 return {}; | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             isZoomed: false, | ||||
|             primaryYAxisRange: { | ||||
|                 min: '', | ||||
|                 max: '' | ||||
|             }, | ||||
|             xAxisRange: { | ||||
|                 min: '', | ||||
|                 max: '' | ||||
|             } | ||||
|         }; | ||||
|     }, | ||||
|     watch: { | ||||
|         data: { | ||||
|             immediate: false, | ||||
|             handler: 'updateData' | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|         Plotly.newPlot(this.$refs.plot, Array.from(this.data), this.getLayout(), { | ||||
|             responsive: true, | ||||
|             displayModeBar: false | ||||
|         }); | ||||
|         this.registerListeners(); | ||||
|     }, | ||||
|     beforeDestroy() { | ||||
|         this.$refs.plot.removeAllListeners(); | ||||
|  | ||||
|         if (this.plotResizeObserver) { | ||||
|             this.plotResizeObserver.unobserve(this.$refs.plotWrapper); | ||||
|             clearTimeout(this.resizeTimer); | ||||
|         } | ||||
|  | ||||
|         if (this.removeBarColorListener) { | ||||
|             this.removeBarColorListener(); | ||||
|         } | ||||
|     }, | ||||
|     methods: { | ||||
|         getAxisMinMax(axis) { | ||||
|             const min = axis.autoSize | ||||
|                 ? '' | ||||
|                 : axis.min; | ||||
|             const max = axis.autoSize | ||||
|                 ? '' | ||||
|                 : axis.max; | ||||
|  | ||||
|             return { | ||||
|                 min, | ||||
|                 max | ||||
|             }; | ||||
|         }, | ||||
|         getLayout() { | ||||
|             const yAxesMeta = this.getYAxisMeta(); | ||||
|             const primaryYaxis = this.getYaxisLayout(yAxesMeta['1']); | ||||
|             const xAxisDomain = this.getXAxisDomain(yAxesMeta); | ||||
|  | ||||
|             return { | ||||
|                 autosize: true, | ||||
|                 showlegend: false, | ||||
|                 textposition: 'auto', | ||||
|                 font: { | ||||
|                     family: 'Helvetica Neue, Helvetica, Arial, sans-serif', | ||||
|                     size: '12px', | ||||
|                     color: '#666' | ||||
|                 }, | ||||
|                 xaxis: { | ||||
|                     domain: xAxisDomain, | ||||
|                     range: [this.xAxisRange.min, this.xAxisRange.max], | ||||
|                     title: this.plotAxisTitle.xAxisTitle, | ||||
|                     automargin: true, | ||||
|                     fixedrange: true | ||||
|                 }, | ||||
|                 yaxis: primaryYaxis, | ||||
|                 margin: { | ||||
|                     l: 5, | ||||
|                     r: 5, | ||||
|                     t: 5, | ||||
|                     b: 0 | ||||
|                 }, | ||||
|                 paper_bgcolor: 'transparent', | ||||
|                 plot_bgcolor: 'transparent' | ||||
|             }; | ||||
|         }, | ||||
|         getYAxisMeta() { | ||||
|             const yAxisMeta = {}; | ||||
|  | ||||
|             this.data.forEach(d => { | ||||
|                 const yAxisMetadata = d.yAxisMetadata; | ||||
|                 const range = '1'; | ||||
|                 const side = 'left'; | ||||
|                 const name = ''; | ||||
|                 const unit = yAxisMetadata.units; | ||||
|  | ||||
|                 yAxisMeta[range] = { | ||||
|                     range, | ||||
|                     side, | ||||
|                     name, | ||||
|                     unit | ||||
|                 }; | ||||
|             }); | ||||
|  | ||||
|             return yAxisMeta; | ||||
|         }, | ||||
|         getXAxisDomain(yAxisMeta) { | ||||
|             let leftPaddingPerc = 0; | ||||
|             let rightPaddingPerc = 100; | ||||
|             let rightSide = yAxisMeta && Object.values(yAxisMeta).filter((axisMeta => axisMeta.side === 'right')); | ||||
|             let leftSide = yAxisMeta && Object.values(yAxisMeta).filter((axisMeta => axisMeta.side === 'left')); | ||||
|             if (yAxisMeta && rightSide.length > 1) { | ||||
|                 rightPaddingPerc = MULTI_AXES_X_PADDING_PERCENT.RIGHT; | ||||
|             } | ||||
|  | ||||
|             if (yAxisMeta && leftSide.length > 1) { | ||||
|                 leftPaddingPerc = MULTI_AXES_X_PADDING_PERCENT.LEFT; | ||||
|             } | ||||
|  | ||||
|             return [leftPaddingPerc / 100, rightPaddingPerc / 100]; | ||||
|         }, | ||||
|         getYaxisLayout(yAxisMeta) { | ||||
|             if (!yAxisMeta) { | ||||
|                 return {}; | ||||
|             } | ||||
|  | ||||
|             const { name, range, side = 'left', unit } = yAxisMeta; | ||||
|             const title = `${name} ${unit ? '(' + unit + ')' : ''}`; | ||||
|             const yaxis = { | ||||
|                 automargin: true, | ||||
|                 fixedrange: true, | ||||
|                 title | ||||
|             }; | ||||
|  | ||||
|             let yAxistype = this.primaryYAxisRange; | ||||
|             if (range === '1') { | ||||
|                 yaxis.range = [yAxistype.min, yAxistype.max]; | ||||
|  | ||||
|                 return yaxis; | ||||
|             } | ||||
|  | ||||
|             yaxis.range = [yAxistype.min, yAxistype.max]; | ||||
|             yaxis.anchor = side.toLowerCase() === 'left' | ||||
|                 ? 'free' | ||||
|                 : 'x'; | ||||
|             yaxis.showline = side.toLowerCase() === 'left'; | ||||
|             yaxis.side = side.toLowerCase(); | ||||
|             yaxis.overlaying = 'y'; | ||||
|             yaxis.position = 0.01; | ||||
|  | ||||
|             return yaxis; | ||||
|         }, | ||||
|         registerListeners() { | ||||
|             this.$refs.plot.on('plotly_relayout', this.zoom); | ||||
|  | ||||
|             this.removeBarColorListener = this.openmct.objects.observe( | ||||
|                 this.domainObject, | ||||
|                 'configuration.barStyles', | ||||
|                 this.barColorChanged | ||||
|             ); | ||||
|             this.resizeTimer = false; | ||||
|             if (window.ResizeObserver) { | ||||
|                 this.plotResizeObserver = new ResizeObserver(() => { | ||||
|                     // debounce and trigger window resize so that plotly can resize the plot | ||||
|                     clearTimeout(this.resizeTimer); | ||||
|                     this.resizeTimer = setTimeout(() => { | ||||
|                         window.dispatchEvent(new Event('resize')); | ||||
|                     }, 250); | ||||
|                 }); | ||||
|                 this.plotResizeObserver.observe(this.$refs.plotWrapper); | ||||
|             } | ||||
|         }, | ||||
|         reset() { | ||||
|             this.updatePlot(); | ||||
|  | ||||
|             this.isZoomed = false; | ||||
|             this.$emit(SUBSCRIBE); | ||||
|         }, | ||||
|         barColorChanged() { | ||||
|             const colors = []; | ||||
|             const indices = []; | ||||
|             this.data.forEach((item, index) => { | ||||
|                 const key = item.key; | ||||
|                 const color = this.domainObject.configuration.barStyles[key] && this.domainObject.configuration.barStyles[key].color; | ||||
|                 indices.push(index); | ||||
|                 if (color) { | ||||
|                     colors.push(); | ||||
|                 } else { | ||||
|                     colors.push(item.marker.color); | ||||
|                 } | ||||
|             }); | ||||
|             const plotUpdate = { | ||||
|                 'marker.color': colors | ||||
|             }; | ||||
|             Plotly.restyle(this.$refs.plot, plotUpdate, indices); | ||||
|         }, | ||||
|         updateData() { | ||||
|             this.updatePlot(); | ||||
|         }, | ||||
|         updateLocalControlPosition() { | ||||
|             const localControl = this.$refs.localControl; | ||||
|             localControl.style.display = 'none'; | ||||
|  | ||||
|             const plot = this.$refs.plot; | ||||
|             const bgLayer = this.$el.querySelector('.bglayer'); | ||||
|  | ||||
|             const plotBoundingRect = plot.getBoundingClientRect(); | ||||
|             const bgLayerBoundingRect = bgLayer.getBoundingClientRect(); | ||||
|  | ||||
|             const top = bgLayerBoundingRect.top - plotBoundingRect.top + 5; | ||||
|             const left = bgLayerBoundingRect.left - plotBoundingRect.left + 5; | ||||
|  | ||||
|             localControl.style.top = `${top}px`; | ||||
|             localControl.style.left = `${left}px`; | ||||
|             localControl.style.display = 'block'; | ||||
|         }, | ||||
|         updatePlot() { | ||||
|             if (!this.$refs || !this.$refs.plot) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             Plotly.react(this.$refs.plot, Array.from(this.data), this.getLayout()); | ||||
|         }, | ||||
|         zoom(eventData) { | ||||
|             const autorange = eventData['xaxis.autorange']; | ||||
|             const { autosize } = eventData; | ||||
|  | ||||
|             if (autosize || autorange) { | ||||
|                 this.isZoomed = false; | ||||
|                 this.reset(); | ||||
|  | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             this.isZoomed = true; | ||||
|             this.$emit(UNSUBSCRIBE); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| </script> | ||||
|  | ||||
							
								
								
									
										286
									
								
								src/plugins/plot/barGraph/BarGraphView.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										286
									
								
								src/plugins/plot/barGraph/BarGraphView.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,286 @@ | ||||
| <!-- | ||||
|  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. | ||||
| --> | ||||
|  | ||||
| <template> | ||||
| <BarGraph ref="barGraph" | ||||
|           class="c-plot c-bar-chart-view" | ||||
|           :data="trace" | ||||
|           :plot-axis-title="plotAxisTitle" | ||||
| /> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import * as SPECTRAL_AGGREGATE from './BarGraphConstants'; | ||||
| import ColorPalette from '../lib/ColorPalette'; | ||||
| import BarGraph from './BarGraphPlot.vue'; | ||||
| import Color from "@/plugins/plot/lib/Color"; | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
|         BarGraph | ||||
|     }, | ||||
|     inject: ['openmct', 'domainObject'], | ||||
|     data() { | ||||
|         return { | ||||
|             composition: {}, | ||||
|             currentDomainObject: this.domainObject, | ||||
|             subscriptions: [], | ||||
|             telemetryObjects: {}, | ||||
|             trace: [] | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
|         activeClock() { | ||||
|             return this.openmct.time.activeClock; | ||||
|         }, | ||||
|         plotAxisTitle() { | ||||
|             const { xAxisMetadata = {}, yAxisMetadata = {} } = this.trace[0] || {}; | ||||
|             const xAxisUnit = xAxisMetadata.units ? `(${xAxisMetadata.units})` : ''; | ||||
|             const yAxisUnit = yAxisMetadata.units ? `(${yAxisMetadata.units})` : ''; | ||||
|  | ||||
|             return { | ||||
|                 xAxisTitle: `${xAxisMetadata.name || ''} ${xAxisUnit}`, | ||||
|                 yAxisTitle: `${yAxisMetadata.name || ''} ${yAxisUnit}` | ||||
|             }; | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.colorPalette = new ColorPalette(); | ||||
|         this.loadComposition(); | ||||
|  | ||||
|         this.openmct.time.on('bounds', this.refreshData); | ||||
|         this.openmct.time.on('clock', this.clockChanged); | ||||
|  | ||||
|         this.$refs.barGraph.$on(SPECTRAL_AGGREGATE.SUBSCRIBE, this.subscribeToAll); | ||||
|         this.$refs.barGraph.$on(SPECTRAL_AGGREGATE.UNSUBSCRIBE, this.removeAllSubscriptions); | ||||
|  | ||||
|         this.unobserve = this.openmct.objects.observe(this.currentDomainObject, '*', this.updateDomainObject); | ||||
|     }, | ||||
|     beforeDestroy() { | ||||
|         this.$refs.barGraph.$off(); | ||||
|         this.openmct.time.off('bounds', this.refreshData); | ||||
|         this.openmct.time.off('clock', this.clockChanged); | ||||
|  | ||||
|         this.removeAllSubscriptions(); | ||||
|         this.unobserve(); | ||||
|  | ||||
|         if (!this.composition) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         this.composition.off('add', this.addTelemetryObject); | ||||
|         this.composition.off('remove', this.removeTelemetryObject); | ||||
|     }, | ||||
|     methods: { | ||||
|         addTelemetryObject(telemetryObject) { | ||||
|             const key = this.openmct.objects.makeKeyString(telemetryObject.identifier); | ||||
|  | ||||
|             if (!this.domainObject.configuration.barStyles) { | ||||
|                 this.domainObject.configuration.barStyles = {}; | ||||
|             } | ||||
|  | ||||
|             // check to see if we've set a bar color | ||||
|             if (!this.domainObject.configuration.barStyles[key] || !this.domainObject.configuration.barStyles[key].color) { | ||||
|                 const color = this.colorPalette.getNextColor().asHexString(); | ||||
|                 this.domainObject.configuration.barStyles[key] = { | ||||
|                     name: telemetryObject.name, | ||||
|                     color | ||||
|                 }; | ||||
|                 this.openmct.objects.mutate( | ||||
|                     this.domainObject, | ||||
|                     `configuration.barStyles[${this.key}]`, | ||||
|                     this.domainObject.configuration.barStyles[key] | ||||
|                 ); | ||||
|             } else { | ||||
|                 let color = this.domainObject.configuration.barStyles[key].color; | ||||
|                 if (!(color instanceof Color)) { | ||||
|                     color = Color.fromHexString(color); | ||||
|                 } | ||||
|  | ||||
|                 this.colorPalette.remove(color); | ||||
|             } | ||||
|  | ||||
|             this.telemetryObjects[key] = telemetryObject; | ||||
|  | ||||
|             this.requestDataFor(telemetryObject); | ||||
|             this.subscribeToObject(telemetryObject); | ||||
|         }, | ||||
|         addTrace(trace, key) { | ||||
|             if (!this.trace.length) { | ||||
|                 this.trace = this.trace.concat([trace]); | ||||
|  | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             let isInTrace = false; | ||||
|             const newTrace = this.trace.map((currentTrace, index) => { | ||||
|                 if (currentTrace.key !== key) { | ||||
|                     return currentTrace; | ||||
|                 } | ||||
|  | ||||
|                 isInTrace = true; | ||||
|  | ||||
|                 return trace; | ||||
|             }); | ||||
|  | ||||
|             this.trace = isInTrace ? newTrace : newTrace.concat([trace]); | ||||
|         }, | ||||
|         clockChanged() { | ||||
|             this.removeAllSubscriptions(); | ||||
|             this.subscribeToAll(); | ||||
|         }, | ||||
|         getAxisMetadata(telemetryObject) { | ||||
|             const metadata = this.openmct.telemetry.getMetadata(telemetryObject); | ||||
|             const yAxisMetadata = metadata.valuesForHints(['range'])[0]; | ||||
|             //Exclude 'name' and 'time' based metadata specifically, from the x-Axis values by using range hints only | ||||
|             const xAxisMetadata = metadata.valuesForHints(['range']); | ||||
|  | ||||
|             return { | ||||
|                 xAxisMetadata, | ||||
|                 yAxisMetadata | ||||
|             }; | ||||
|         }, | ||||
|         getOptions(telemetryObject) { | ||||
|             const { start, end } = this.openmct.time.bounds(); | ||||
|  | ||||
|             return { | ||||
|                 end, | ||||
|                 start, | ||||
|                 startTime: null, | ||||
|                 spectra: true | ||||
|             }; | ||||
|         }, | ||||
|         loadComposition() { | ||||
|             this.composition = this.openmct.composition.get(this.currentDomainObject); | ||||
|  | ||||
|             if (!this.composition) { | ||||
|                 this.addTelemetryObject(this.currentDomainObject); | ||||
|  | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             this.composition.on('add', this.addTelemetryObject); | ||||
|             this.composition.on('remove', this.removeTelemetryObject); | ||||
|             this.composition.load(); | ||||
|         }, | ||||
|         refreshData(bounds, isTick) { | ||||
|             if (!isTick) { | ||||
|                 const telemetryObjects = Object.values(this.telemetryObjects); | ||||
|                 telemetryObjects.forEach(this.requestDataFor); | ||||
|             } | ||||
|         }, | ||||
|         removeAllSubscriptions() { | ||||
|             this.subscriptions.forEach(subscription => subscription.unsubscribe()); | ||||
|             this.subscriptions = []; | ||||
|         }, | ||||
|         removeSubscription(key) { | ||||
|             const found = this.subscriptions.findIndex(subscription => subscription.key === key); | ||||
|             if (found > -1) { | ||||
|                 this.subscriptions[found].unsubscribe(); | ||||
|                 this.subscriptions.splice(found, 1); | ||||
|             } | ||||
|         }, | ||||
|         removeTelemetryObject(identifier) { | ||||
|             const key = this.openmct.objects.makeKeyString(identifier); | ||||
|             delete this.telemetryObjects[key]; | ||||
|             if (this.domainObject.configuration.barStyles[key]) { | ||||
|                 delete this.domainObject.configuration.barStyles[key]; | ||||
|             } | ||||
|  | ||||
|             this.removeSubscription(key); | ||||
|  | ||||
|             this.trace = this.trace.filter(t => t.key !== key); | ||||
|         }, | ||||
|         processData(telemetryObject, data, axisMetadata) { | ||||
|             const key = this.openmct.objects.makeKeyString(telemetryObject.identifier); | ||||
|  | ||||
|             if (data.message) { | ||||
|                 this.openmct.notifications.alert(data.message); | ||||
|             } | ||||
|  | ||||
|             let xValues = []; | ||||
|             let yValues = []; | ||||
|  | ||||
|             //populate X and Y values for plotly | ||||
|             axisMetadata.xAxisMetadata.forEach((metadata) => { | ||||
|                 xValues.push(metadata.name); | ||||
|                 if (data[metadata.key]) { | ||||
|                     //TODO: Format the data? | ||||
|                     yValues.push(data[metadata.key]); | ||||
|                 } else { | ||||
|                     yValues.push(''); | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|             const trace = { | ||||
|                 key, | ||||
|                 name: telemetryObject.name, | ||||
|                 x: xValues, | ||||
|                 y: yValues, | ||||
|                 text: yValues.map(String), | ||||
|                 xAxisMetadata: axisMetadata.xAxisMetadata, | ||||
|                 yAxisMetadata: axisMetadata.yAxisMetadata, | ||||
|                 type: 'bar', | ||||
|                 marker: { | ||||
|                     color: this.domainObject.configuration.barStyles[key].color | ||||
|                 }, | ||||
|                 hoverinfo: 'skip' | ||||
|             }; | ||||
|  | ||||
|             this.addTrace(trace, key); | ||||
|         }, | ||||
|         requestDataFor(telemetryObject) { | ||||
|             const axisMetadata = this.getAxisMetadata(telemetryObject); | ||||
|             this.openmct.telemetry.request(telemetryObject, this.getOptions(telemetryObject)) | ||||
|                 .then(data => { | ||||
|                     data.forEach((datum) => { | ||||
|                         this.processData(telemetryObject, datum, axisMetadata); | ||||
|                     }); | ||||
|                 }); | ||||
|         }, | ||||
|         subscribeToObject(telemetryObject) { | ||||
|             const key = this.openmct.objects.makeKeyString(telemetryObject.identifier); | ||||
|  | ||||
|             this.removeSubscription(key); | ||||
|  | ||||
|             const options = this.getOptions(telemetryObject); | ||||
|             const axisMetadata = this.getAxisMetadata(telemetryObject); | ||||
|             const unsubscribe = this.openmct.telemetry.subscribe(telemetryObject, | ||||
|                 data => this.processData(telemetryObject, data, axisMetadata) | ||||
|                 , options); | ||||
|  | ||||
|             this.subscriptions.push({ | ||||
|                 key, | ||||
|                 unsubscribe | ||||
|             }); | ||||
|         }, | ||||
|         subscribeToAll() { | ||||
|             const telemetryObjects = Object.values(this.telemetryObjects); | ||||
|             telemetryObjects.forEach(this.subscribeToObject); | ||||
|         }, | ||||
|         updateDomainObject(newDomainObject) { | ||||
|             this.currentDomainObject = newDomainObject; | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|  | ||||
| </script> | ||||
							
								
								
									
										76
									
								
								src/plugins/plot/barGraph/BarGraphViewProvider.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								src/plugins/plot/barGraph/BarGraphViewProvider.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,76 @@ | ||||
| /***************************************************************************** | ||||
|  * 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 BarGraphView from './BarGraphView.vue'; | ||||
| import { BAR_GRAPH_KEY, BAR_GRAPH_VIEW } from './BarGraphConstants'; | ||||
| import Vue from 'vue'; | ||||
|  | ||||
| export default function BarGraphViewProvider(openmct) { | ||||
|     function isCompactView(objectPath) { | ||||
|         return objectPath.find(object => object.type === 'time-strip'); | ||||
|     } | ||||
|  | ||||
|     return { | ||||
|         key: BAR_GRAPH_VIEW, | ||||
|         name: 'Spectral Aggregate Plot', | ||||
|         cssClass: 'icon-telemetry', | ||||
|         canView(domainObject, objectPath) { | ||||
|             return domainObject && domainObject.type === BAR_GRAPH_KEY; | ||||
|         }, | ||||
|  | ||||
|         canEdit(domainObject, objectPath) { | ||||
|             return domainObject && domainObject.type === BAR_GRAPH_KEY; | ||||
|         }, | ||||
|  | ||||
|         view: function (domainObject, objectPath) { | ||||
|             let component; | ||||
|  | ||||
|             return { | ||||
|                 show: function (element) { | ||||
|                     let isCompact = isCompactView(objectPath); | ||||
|                     component = new Vue({ | ||||
|                         el: element, | ||||
|                         components: { | ||||
|                             BarGraphView | ||||
|                         }, | ||||
|                         provide: { | ||||
|                             openmct, | ||||
|                             domainObject | ||||
|                         }, | ||||
|                         data() { | ||||
|                             return { | ||||
|                                 options: { | ||||
|                                     compact: isCompact | ||||
|                                 } | ||||
|                             }; | ||||
|                         }, | ||||
|                         template: '<bar-graph-view :options="options"></bar-graph-view>' | ||||
|                     }); | ||||
|                 }, | ||||
|                 destroy: function () { | ||||
|                     component.$destroy(); | ||||
|                     component = undefined; | ||||
|                 } | ||||
|             }; | ||||
|         } | ||||
|     }; | ||||
| } | ||||
| @@ -0,0 +1,48 @@ | ||||
| import { BAR_GRAPH_INSPECTOR_KEY, BAR_GRAPH_KEY } from '../BarGraphConstants'; | ||||
| import Vue from 'vue'; | ||||
| import Options from "./Options.vue"; | ||||
|  | ||||
| export default function BarGraphInspectorViewProvider(openmct) { | ||||
|     return { | ||||
|         key: BAR_GRAPH_INSPECTOR_KEY, | ||||
|         name: 'Bar Graph Inspector View', | ||||
|         canView: function (selection) { | ||||
|             if (selection.length === 0 || selection[0].length === 0) { | ||||
|                 return false; | ||||
|             } | ||||
|  | ||||
|             let object = selection[0][0].context.item; | ||||
|  | ||||
|             return object | ||||
|                 && object.type === BAR_GRAPH_KEY; | ||||
|         }, | ||||
|         view: function (selection) { | ||||
|             let component; | ||||
|  | ||||
|             return { | ||||
|                 show: function (element) { | ||||
|                     component = new Vue({ | ||||
|                         el: element, | ||||
|                         components: { | ||||
|                             Options | ||||
|                         }, | ||||
|                         provide: { | ||||
|                             openmct, | ||||
|                             domainObject: selection[0][0].context.item | ||||
|                         }, | ||||
|                         template: '<options></options>' | ||||
|                     }); | ||||
|                 }, | ||||
|                 destroy: function () { | ||||
|                     if (component) { | ||||
|                         component.$destroy(); | ||||
|                         component = undefined; | ||||
|                     } | ||||
|                 } | ||||
|             }; | ||||
|         }, | ||||
|         priority: function () { | ||||
|             return 1; | ||||
|         } | ||||
|     }; | ||||
| } | ||||
							
								
								
									
										107
									
								
								src/plugins/plot/barGraph/inspector/BarGraphOptions.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								src/plugins/plot/barGraph/inspector/BarGraphOptions.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,107 @@ | ||||
| <!-- | ||||
|  Open MCT, Copyright (c) 2014-2020, United States Government | ||||
|  as represented by the Administrator of the National Aeronautics and Space | ||||
|  Administration. All rights reserved. | ||||
|  | ||||
|  Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  "License"); you may not use this file except in compliance with the License. | ||||
|  You may obtain a copy of the License at | ||||
|  http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  | ||||
|  Unless required by applicable law or agreed to in writing, software | ||||
|  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  License for the specific language governing permissions and limitations | ||||
|  under the License. | ||||
|  | ||||
|  Open MCT includes source code licensed under additional open source | ||||
|  licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  this source code distribution or the Licensing information page available | ||||
|  at runtime from the About dialog for additional information. | ||||
| --> | ||||
| <template> | ||||
| <ul> | ||||
|     <li class="c-tree__item menus-to-left"> | ||||
|         <span class="c-disclosure-triangle is-enabled flex-elem" | ||||
|               :class="expandedCssClass" | ||||
|               @click="expanded = !expanded" | ||||
|         > | ||||
|         </span> | ||||
|         <div> | ||||
|             <div class="c-object-label__name">{{ name }}</div> | ||||
|         </div> | ||||
|     </li> | ||||
|     <ColorSwatch v-if="expanded" | ||||
|                  :current-color="currentColor" | ||||
|                  title="Manually set the color for this bar graph." | ||||
|                  edit-title="Manually set the color for this bar graph" | ||||
|                  view-title="The color for this bar graph." | ||||
|                  short-label="Color" | ||||
|                  class="grid-properties" | ||||
|                  @colorSet="setColor" | ||||
|     /> | ||||
| </ul> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import ColorSwatch from '../../ColorSwatch.vue'; | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
|         ColorSwatch | ||||
|     }, | ||||
|     inject: ['openmct', 'domainObject'], | ||||
|     props: { | ||||
|         item: { | ||||
|             type: Object, | ||||
|             required: true | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             currentColor: undefined, | ||||
|             name: '', | ||||
|             expanded: false | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
|         expandedCssClass() { | ||||
|             return this.expanded ? 'c-disclosure-triangle--expanded' : ''; | ||||
|         } | ||||
|     }, | ||||
|     watch: { | ||||
|         item: { | ||||
|             handler() { | ||||
|                 this.initColor(); | ||||
|             }, | ||||
|             deep: true | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.key = this.openmct.objects.makeKeyString(this.item); | ||||
|         this.initColor(); | ||||
|         this.unObserve = this.openmct.objects.observe(this.domainObject, `this.domainObject.configuration.barStyles[${this.key}]`, this.initColor); | ||||
|     }, | ||||
|     beforeDestroy() { | ||||
|         if (this.unObserve) { | ||||
|             this.unObserve(); | ||||
|         } | ||||
|     }, | ||||
|     methods: { | ||||
|         initColor() { | ||||
|             if (this.domainObject.configuration.barStyles && this.domainObject.configuration.barStyles[this.key]) { | ||||
|                 this.currentColor = this.domainObject.configuration.barStyles[this.key].color; | ||||
|                 this.name = this.domainObject.configuration.barStyles[this.key].name; | ||||
|             } | ||||
|         }, | ||||
|         setColor(chosenColor) { | ||||
|             this.currentColor = chosenColor.asHexString(); | ||||
|             this.openmct.objects.mutate( | ||||
|                 this.domainObject, | ||||
|                 `configuration.barStyles[${this.key}].color`, | ||||
|                 this.currentColor | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| </script> | ||||
							
								
								
									
										63
									
								
								src/plugins/plot/barGraph/inspector/Options.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								src/plugins/plot/barGraph/inspector/Options.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,63 @@ | ||||
| <!-- | ||||
|  Open MCT, Copyright (c) 2014-2020, United States Government | ||||
|  as represented by the Administrator of the National Aeronautics and Space | ||||
|  Administration. All rights reserved. | ||||
|  | ||||
|  Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  "License"); you may not use this file except in compliance with the License. | ||||
|  You may obtain a copy of the License at | ||||
|  http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  | ||||
|  Unless required by applicable law or agreed to in writing, software | ||||
|  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  License for the specific language governing permissions and limitations | ||||
|  under the License. | ||||
|  | ||||
|  Open MCT includes source code licensed under additional open source | ||||
|  licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  this source code distribution or the Licensing information page available | ||||
|  at runtime from the About dialog for additional information. | ||||
| --> | ||||
| <template> | ||||
| <div> | ||||
|     <ul class="c-tree"> | ||||
|         <li v-for="series in domainObject.composition" | ||||
|             :key="series.key" | ||||
|         > | ||||
|             <bar-graph-options :item="series" /> | ||||
|         </li> | ||||
|     </ul> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import BarGraphOptions from "./BarGraphOptions.vue"; | ||||
| export default { | ||||
|     components: { | ||||
|         BarGraphOptions | ||||
|     }, | ||||
|     inject: ['openmct', 'domainObject'], | ||||
|     data() { | ||||
|         return { | ||||
|             isEditing: this.openmct.editor.isEditing() | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
|         canEdit() { | ||||
|             return this.isEditing && !this.domainObject.locked; | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.openmct.editor.on('isEditing', this.setEditState); | ||||
|     }, | ||||
|     beforeDestroy() { | ||||
|         this.openmct.editor.off('isEditing', this.setEditState); | ||||
|     }, | ||||
|     methods: { | ||||
|         setEditState(isEditing) { | ||||
|             this.isEditing = isEditing; | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| </script> | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user