Compare commits
	
		
			38 Commits
		
	
	
		
			testing/st
			...
			memory-lea
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 7ac37a481c | ||
|   | d6474045c3 | ||
|   | 23c10d40a8 | ||
|   | cc37a5f899 | ||
|   | 177dbec0c5 | ||
|   | 38952b8eb3 | ||
|   | f19c5699a6 | ||
|   | 5c15e53abb | ||
|   | f58b3881f2 | ||
|   | 071a13b219 | ||
|   | ca66898e51 | ||
|   | 94c7b2343a | ||
|   | c397c336ab | ||
|   | eea23f2caf | ||
|   | 6665641c02 | ||
|   | cc017e879c | ||
|   | 93172eb180 | ||
|   | c3ebf52dd2 | ||
|   | f8f2e7da9b | ||
|   | 240f58b2d0 | ||
|   | 7d3baee7b5 | ||
|   | 1f5cb7ca42 | ||
|   | 4a7ebe326c | ||
|   | 10da314a4a | ||
|   | b3ceccd7fb | ||
|   | 1bde4c9a0c | ||
|   | 4b85360446 | ||
|   | 41b860a547 | ||
|   | 254b3db966 | ||
|   | cbb3f32d1e | ||
|   | e3bf72e77f | ||
|   | 0b63b782cf | ||
|   | da39fd0c70 | ||
|   | 96dd581a67 | ||
|   | 2a1e322230 | ||
|   | 300b98bd54 | ||
|   | c946609d13 | ||
|   | 7ca559fbe4 | 
| @@ -1,36 +1,69 @@ | ||||
| version: 2 | ||||
| jobs: | ||||
|   build: | ||||
| version: 2.1 | ||||
| executors: | ||||
|   linux: | ||||
|     docker: | ||||
|         - image: circleci/node:13-browsers | ||||
|           environment: | ||||
|             CHROME_BIN: "/usr/bin/google-chrome" | ||||
|     steps: | ||||
|         - checkout | ||||
|         - run: | ||||
|             name: Update npm | ||||
|             command: 'sudo npm install -g npm@latest' | ||||
|         - restore_cache: | ||||
|             key: dependency-cache-{{ checksum "package.json" }} | ||||
|         - run: | ||||
|             name: Installing dependencies (npm install) | ||||
|             command: npm install | ||||
|         - save_cache: | ||||
|             key: dependency-cache-{{ checksum "package.json" }} | ||||
|             paths: | ||||
|               - node_modules | ||||
|         - run: | ||||
|             name: npm run test:coverage | ||||
|             command: npm run test:coverage | ||||
|         - run: | ||||
|             name: npm run lint | ||||
|             command: npm run lint | ||||
|         - store_artifacts: | ||||
|             path: dist | ||||
|             prefix: dist | ||||
|  | ||||
| workflows: | ||||
|   version: 2 | ||||
|       - image: cimg/base:stable | ||||
| orbs: | ||||
|   node: circleci/node@4.5.1 | ||||
|   browser-tools: circleci/browser-tools@1.1.3 | ||||
| jobs: | ||||
|   test: | ||||
|     parameters: | ||||
|       node-version: | ||||
|         type: string | ||||
|       browser: | ||||
|         type: string | ||||
|       always-pass: | ||||
|         type: boolean   | ||||
|     executor: linux | ||||
|     steps: | ||||
|       - checkout | ||||
|       - restore_cache: | ||||
|           key: deps-{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }} | ||||
|       - node/install: | ||||
|           node-version: << parameters.node-version >> | ||||
|       - node/install-packages: | ||||
|           override-ci-command: npm install | ||||
|       - when: # Just to save time until caching saves the browser bin | ||||
|           condition: | ||||
|             equal: [ "FirefoxESR", <<parameters.browser>> ] | ||||
|           steps: | ||||
|             - browser-tools/install-firefox: | ||||
|                 version: "78.11.0esr" #https://archive.mozilla.org/pub/firefox/releases/           | ||||
|       - when: # Just to save time until caching saves the browser bin | ||||
|           condition: | ||||
|             equal: [ "ChromeHeadless", <<parameters.browser>> ] | ||||
|           steps: | ||||
|             - browser-tools/install-chrome: | ||||
|                 replace-existing: false | ||||
|       - save_cache: | ||||
|           key: deps-{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }} | ||||
|           paths: | ||||
|             - ~/.npm | ||||
|             - ~/.cache | ||||
|             - node_modules | ||||
|       - run: npm run test:coverage -- --browsers=<<parameters.browser>> || <<parameters.always-pass>> | ||||
|       - store_test_results: | ||||
|           path: dist/reports/tests/ | ||||
|       - store_artifacts: | ||||
|           path: dist/reports/ | ||||
| workflows: | ||||
|   matrix-tests: | ||||
|     jobs: | ||||
|       - build | ||||
|       - test: | ||||
|           name: node10-chrome | ||||
|           node-version: lts/dubnium | ||||
|           browser: ChromeHeadless | ||||
|           always-pass: false | ||||
|       - test: | ||||
|           name: node12-firefoxESR | ||||
|           node-version: lts/erbium | ||||
|           browser: FirefoxESR | ||||
|           always-pass: true | ||||
|       - test: | ||||
|           name: node14-chrome | ||||
|           node-version: lts/fermium | ||||
|           browser: ChromeHeadless | ||||
|           always-pass: true | ||||
|  | ||||
|  | ||||
|   | ||||
							
								
								
									
										11
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										11
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
								
							| @@ -1,11 +1,12 @@ | ||||
| <!--- This is for filing bugs. If you have a general question, please --> | ||||
| <!--- visit https://github.com/nasa/openmct/discussions --> | ||||
|  | ||||
| --- | ||||
| name: Bug Report | ||||
| name: Bug report | ||||
| about: File a Bug ! | ||||
| title: '' | ||||
| labels: type:bug | ||||
| assignees: '' | ||||
|  | ||||
| --- | ||||
|  | ||||
| <!--- Focus on user impact in the title. Use the Summary Field to --> | ||||
| <!--- describe the problem technically. --> | ||||
|  | ||||
| @@ -35,7 +36,7 @@ about: File a Bug ! | ||||
|  | ||||
| #### Environment | ||||
| * Open MCT Version: <!--- date of build, version, or SHA --> | ||||
| * Deployment Type: <!--- npm dev? VIPER Dev? openmct-yams? --> | ||||
| * Deployment Type: <!--- npm dev? VIPER Dev? openmct-yamcs? --> | ||||
| * OS: | ||||
| * Browser: | ||||
|  | ||||
|   | ||||
							
								
								
									
										6
									
								
								.github/ISSUE_TEMPLATE/config.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/ISSUE_TEMPLATE/config.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1 +1,5 @@ | ||||
| blank_issues_enabled: false | ||||
| blank_issues_enabled: true | ||||
| contact_links: | ||||
|   - name: Discussions | ||||
|     url: https://github.com/nasa/openmct/discussions | ||||
|     about: Got a question? | ||||
|   | ||||
							
								
								
									
										20
									
								
								.github/ISSUE_TEMPLATE/enhancement-request.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								.github/ISSUE_TEMPLATE/enhancement-request.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| --- | ||||
| name: Enhancement request | ||||
| about: Suggest an enhancement or new improvement for this project | ||||
| title: '' | ||||
| labels: type:enhancement | ||||
| assignees: '' | ||||
|  | ||||
| --- | ||||
|  | ||||
| **Is your feature request related to a problem? Please describe.** | ||||
| A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] | ||||
|  | ||||
| **Describe the solution you'd like** | ||||
| A clear and concise description of what you want to happen. | ||||
|  | ||||
| **Describe alternatives you've considered** | ||||
| A clear and concise description of any alternative solutions or features you've considered. | ||||
|  | ||||
| **Additional context** | ||||
| Add any other context or screenshots about the feature request here. | ||||
							
								
								
									
										4
									
								
								.github/workflows/lighthouse.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/lighthouse.yml
									
									
									
									
										vendored
									
									
								
							| @@ -13,6 +13,8 @@ jobs: | ||||
|       - uses: actions/checkout@v2 | ||||
|         with: | ||||
|           ref: ${{ github.event.inputs.version }} | ||||
|       - uses: actions/setup-node@v1 | ||||
|       - uses: actions/setup-node@v2 | ||||
|         with: | ||||
|           node-version: '14' | ||||
|       - run: npm install && npm install -g @lhci/cli #Don't want to include this in our deps | ||||
|       - run: lhci autorun | ||||
| @@ -1,9 +1,11 @@ | ||||
| # Open MCT [](http://www.apache.org/licenses/LICENSE-2.0) | ||||
| # Open MCT [](http://www.apache.org/licenses/LICENSE-2.0) [](https://lgtm.com/projects/g/nasa/openmct/context:javascript) | ||||
|  | ||||
| Open MCT (Open Mission Control Technologies) is a next-generation mission control framework for visualization of data on desktop and mobile devices. It is developed at NASA's Ames Research Center, and is being used by NASA for data analysis of spacecraft missions, as well as planning and operation of experimental rover systems. As a generalizable and open source framework, Open MCT could be used as the basis for building applications for planning, operation, and analysis of any systems producing telemetry data. | ||||
|  | ||||
| Please visit our [Official Site](https://nasa.github.io/openmct/) and [Getting Started Guide](https://nasa.github.io/openmct/getting-started/) | ||||
|  | ||||
| Once you've created something amazing with Open MCT, showcase your work in our GitHub Discussions [Show and Tell](https://github.com/nasa/openmct/discussions/categories/show-and-tell) section. We love seeing unique and wonderful implementations of Open MCT! | ||||
|  | ||||
| ## See Open MCT in Action | ||||
|  | ||||
| Try Open MCT now with our [live demo](https://openmct-demo.herokuapp.com/). | ||||
|   | ||||
							
								
								
									
										10
									
								
								app.js
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								app.js
									
									
									
									
									
								
							| @@ -42,11 +42,11 @@ app.use('/proxyUrl', function proxyRequest(req, res, next) { | ||||
|  | ||||
| const webpack = require('webpack'); | ||||
| const webpackConfig = require('./webpack.config.js'); | ||||
| webpackConfig.plugins.push(new webpack.HotModuleReplacementPlugin()); | ||||
| webpackConfig.plugins.push(function() { this.plugin('watch-run', function(watching, callback) { console.log('Begin compile at ' + new Date()); callback(); }) }); | ||||
| //webpackConfig.plugins.push(new webpack.HotModuleReplacementPlugin()); | ||||
| //webpackConfig.plugins.push(function() { this.plugin('watch-run', function(watching, callback) { console.log('Begin compile at ' + new Date()); callback(); }) }); | ||||
|  | ||||
| webpackConfig.entry.openmct = [ | ||||
|     'webpack-hot-middleware/client?reload=true', | ||||
| //    'webpack-hot-middleware/client?reload=true', | ||||
|     webpackConfig.entry.openmct | ||||
| ]; | ||||
|  | ||||
| @@ -60,12 +60,12 @@ app.use(require('webpack-dev-middleware')( | ||||
|     } | ||||
| )); | ||||
|  | ||||
| app.use(require('webpack-hot-middleware')( | ||||
| /*app.use(require('webpack-hot-middleware')( | ||||
|     compiler, | ||||
|     { | ||||
|  | ||||
|     } | ||||
| )); | ||||
| ));*/ | ||||
|  | ||||
| // Expose index.html for development users. | ||||
| app.get('/', function (req, res) { | ||||
|   | ||||
| @@ -41,11 +41,6 @@ define([ | ||||
|                             "$scope" | ||||
|                         ] | ||||
|                     } | ||||
|                 ], | ||||
|                 "routes": [ | ||||
|                     { | ||||
|                         "templateUrl": "templates/exampleForm.html" | ||||
|                     } | ||||
|                 ] | ||||
|             } | ||||
|         } | ||||
|   | ||||
| @@ -49,6 +49,10 @@ define([ | ||||
|         ]; | ||||
|         const IMAGE_DELAY = 20000; | ||||
|  | ||||
|         function getCompassValues(min, max) { | ||||
|             return min + Math.random() * (max - min); | ||||
|         } | ||||
|  | ||||
|         function pointForTimestamp(timestamp, name) { | ||||
|             const url = IMAGE_SAMPLES[Math.floor(timestamp / IMAGE_DELAY) % IMAGE_SAMPLES.length]; | ||||
|             const urlItems = url.split('/'); | ||||
| @@ -59,6 +63,9 @@ define([ | ||||
|                 utc: Math.floor(timestamp / IMAGE_DELAY) * IMAGE_DELAY, | ||||
|                 local: Math.floor(timestamp / IMAGE_DELAY) * IMAGE_DELAY, | ||||
|                 url, | ||||
|                 sunOrientation: getCompassValues(0, 360), | ||||
|                 cameraPan: getCompassValues(0, 360), | ||||
|                 heading: getCompassValues(0, 360), | ||||
|                 imageDownloadName | ||||
|             }; | ||||
|         } | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import Vue from 'Vue'; | ||||
| import Vue from 'vue'; | ||||
| import HelloWorld from './HelloWorld.vue'; | ||||
|  | ||||
| function SimpleVuePlugin() { | ||||
|   | ||||
							
								
								
									
										257
									
								
								index.html
									
									
									
									
									
								
							
							
						
						
									
										257
									
								
								index.html
									
									
									
									
									
								
							| @@ -66,134 +66,143 @@ | ||||
|     <body> | ||||
|     </body> | ||||
|     <script> | ||||
|         const THIRTY_SECONDS = 30 * 1000; | ||||
|         const ONE_MINUTE = THIRTY_SECONDS * 2; | ||||
|         const FIVE_MINUTES = ONE_MINUTE * 5; | ||||
|         const FIFTEEN_MINUTES = FIVE_MINUTES * 3; | ||||
|         const THIRTY_MINUTES = FIFTEEN_MINUTES * 2; | ||||
|         const ONE_HOUR = THIRTY_MINUTES * 2; | ||||
|         const TWO_HOURS = ONE_HOUR * 2; | ||||
|         const ONE_DAY = ONE_HOUR * 24; | ||||
|         (function() { | ||||
|             const THIRTY_SECONDS = 30 * 1000; | ||||
|             const ONE_MINUTE = THIRTY_SECONDS * 2; | ||||
|             const FIVE_MINUTES = ONE_MINUTE * 5; | ||||
|             const FIFTEEN_MINUTES = FIVE_MINUTES * 3; | ||||
|             const THIRTY_MINUTES = FIFTEEN_MINUTES * 2; | ||||
|             const ONE_HOUR = THIRTY_MINUTES * 2; | ||||
|             const TWO_HOURS = ONE_HOUR * 2; | ||||
|             const ONE_DAY = ONE_HOUR * 24; | ||||
|  | ||||
|         [ | ||||
|             'example/eventGenerator' | ||||
|         ].forEach( | ||||
|             openmct.legacyRegistry.enable.bind(openmct.legacyRegistry) | ||||
|         ); | ||||
|             let app = new openmct(); | ||||
|  | ||||
|         openmct.install(openmct.plugins.LocalStorage()); | ||||
|         openmct.install(openmct.plugins.Espresso()); | ||||
|         openmct.install(openmct.plugins.MyItems()); | ||||
|         openmct.install(openmct.plugins.Generator()); | ||||
|         openmct.install(openmct.plugins.ExampleImagery()); | ||||
|         openmct.install(openmct.plugins.PlanLayout()); | ||||
|         openmct.install(openmct.plugins.Timeline()); | ||||
|         openmct.install(openmct.plugins.UTCTimeSystem()); | ||||
|         openmct.install(openmct.plugins.AutoflowView({ | ||||
|             type: "telemetry.panel" | ||||
|         })); | ||||
|         openmct.install(openmct.plugins.DisplayLayout({ | ||||
|             showAsView: ['summary-widget', 'example.imagery'] | ||||
|         })); | ||||
|         openmct.install(openmct.plugins.Conductor({ | ||||
|             menuOptions: [ | ||||
|                 { | ||||
|                     name: "Fixed", | ||||
|                     timeSystem: 'utc', | ||||
|                     bounds: { | ||||
|                         start: Date.now() - THIRTY_MINUTES, | ||||
|                         end: Date.now() | ||||
|             [ | ||||
|                 'example/eventGenerator' | ||||
|             ].forEach( | ||||
|                 app.legacyRegistry.enable.bind(app.legacyRegistry) | ||||
|             ); | ||||
|  | ||||
|             app.install(app.plugins.LocalStorage()); | ||||
|             app.install(app.plugins.Espresso()); | ||||
|             app.install(app.plugins.MyItems()); | ||||
|             //app.install(app.plugins.Generator()); | ||||
|             app.install(app.plugins.ExampleImagery()); | ||||
|             app.install(app.plugins.PlanLayout()); | ||||
|             app.install(app.plugins.Timeline()); | ||||
|             //app.install(app.plugins.Hyperlink()); | ||||
|             app.install(app.plugins.UTCTimeSystem()); | ||||
|             app.install(app.plugins.AutoflowView({ | ||||
|                 type: "telemetry.panel" | ||||
|             })); | ||||
|             app.install(app.plugins.DisplayLayout({ | ||||
|                 showAsView: ['summary-widget', 'example.imagery'] | ||||
|             })); | ||||
|             app.install(app.plugins.Conductor({ | ||||
|                 menuOptions: [ | ||||
|                     { | ||||
|                         name: "Fixed", | ||||
|                         timeSystem: 'utc', | ||||
|                         bounds: { | ||||
|                             start: Date.now() - THIRTY_MINUTES, | ||||
|                             end: Date.now() | ||||
|                         }, | ||||
|                         // commonly used bounds can be stored in history | ||||
|                         // bounds (start and end) can accept either a milliseconds number | ||||
|                         // or a callback function returning a milliseconds number | ||||
|                         // a function is useful for invoking Date.now() at exact moment of preset selection | ||||
|                         presets: [ | ||||
|                             { | ||||
|                                 label: 'Last Day', | ||||
|                                 bounds: { | ||||
|                                     start: () => Date.now() - ONE_DAY, | ||||
|                                     end: () => Date.now() | ||||
|                                 } | ||||
|                             }, | ||||
|                             { | ||||
|                                 label: 'Last 2 hours', | ||||
|                                 bounds: { | ||||
|                                     start: () => Date.now() - TWO_HOURS, | ||||
|                                     end: () => Date.now() | ||||
|                                 } | ||||
|                             }, | ||||
|                             { | ||||
|                                 label: 'Last hour', | ||||
|                                 bounds: { | ||||
|                                     start: () => Date.now() - ONE_HOUR, | ||||
|                                     end: () => Date.now() | ||||
|                                 } | ||||
|                             } | ||||
|                         ], | ||||
|                         // maximum recent bounds to retain in conductor history | ||||
|                         records: 10 | ||||
|                         // maximum duration between start and end bounds | ||||
|                         // for utc-based time systems this is in milliseconds | ||||
|                         // limit: ONE_DAY | ||||
|                     }, | ||||
|                     // commonly used bounds can be stored in history | ||||
|                     // bounds (start and end) can accept either a milliseconds number | ||||
|                     // or a callback function returning a milliseconds number | ||||
|                     // a function is useful for invoking Date.now() at exact moment of preset selection | ||||
|                     presets: [ | ||||
|                         { | ||||
|                             label: 'Last Day', | ||||
|                             bounds: { | ||||
|                                 start: () => Date.now() - ONE_DAY, | ||||
|                                 end: () => Date.now() | ||||
|                             } | ||||
|                     { | ||||
|                         name: "Realtime", | ||||
|                         timeSystem: 'utc', | ||||
|                         clock: 'local', | ||||
|                         clockOffsets: { | ||||
|                             start: - THIRTY_MINUTES, | ||||
|                             end: THIRTY_SECONDS | ||||
|                         }, | ||||
|                         { | ||||
|                             label: 'Last 2 hours', | ||||
|                             bounds: { | ||||
|                                 start: () => Date.now() - TWO_HOURS, | ||||
|                                 end: () => Date.now() | ||||
|                         presets: [ | ||||
|                             { | ||||
|                                 label: '1 Hour', | ||||
|                                 bounds: { | ||||
|                                     start: - ONE_HOUR, | ||||
|                                     end: THIRTY_SECONDS | ||||
|                                 } | ||||
|                             }, | ||||
|                             { | ||||
|                                 label: '30 Minutes', | ||||
|                                 bounds: { | ||||
|                                     start: - THIRTY_MINUTES, | ||||
|                                     end: THIRTY_SECONDS | ||||
|                                 } | ||||
|                             }, | ||||
|                             { | ||||
|                                 label: '15 Minutes', | ||||
|                                 bounds: { | ||||
|                                     start: - FIFTEEN_MINUTES, | ||||
|                                     end: THIRTY_SECONDS | ||||
|                                 } | ||||
|                             }, | ||||
|                             { | ||||
|                                 label: '5 Minutes', | ||||
|                                 bounds: { | ||||
|                                     start: - FIVE_MINUTES, | ||||
|                                     end: THIRTY_SECONDS | ||||
|                                 } | ||||
|                             }, | ||||
|                             { | ||||
|                                 label: '1 Minute', | ||||
|                                 bounds: { | ||||
|                                     start: - ONE_MINUTE, | ||||
|                                     end: THIRTY_SECONDS | ||||
|                                 } | ||||
|                             } | ||||
|                         }, | ||||
|                         { | ||||
|                             label: 'Last hour', | ||||
|                             bounds: { | ||||
|                                 start: () => Date.now() - ONE_HOUR, | ||||
|                                 end: () => Date.now() | ||||
|                             } | ||||
|                         } | ||||
|                     ], | ||||
|                     // maximum recent bounds to retain in conductor history | ||||
|                     records: 10 | ||||
|                     // maximum duration between start and end bounds | ||||
|                     // for utc-based time systems this is in milliseconds | ||||
|                     // limit: ONE_DAY | ||||
|                 }, | ||||
|                 { | ||||
|                     name: "Realtime", | ||||
|                     timeSystem: 'utc', | ||||
|                     clock: 'local', | ||||
|                     clockOffsets: { | ||||
|                         start: - THIRTY_MINUTES, | ||||
|                         end: THIRTY_SECONDS | ||||
|                     }, | ||||
|                     presets: [ | ||||
|                         { | ||||
|                             label: '1 Hour', | ||||
|                             bounds: { | ||||
|                                 start: - ONE_HOUR, | ||||
|                                 end: THIRTY_SECONDS | ||||
|                             } | ||||
|                         }, | ||||
|                         { | ||||
|                             label: '30 Minutes', | ||||
|                             bounds: { | ||||
|                                 start: - THIRTY_MINUTES, | ||||
|                                 end: THIRTY_SECONDS | ||||
|                             } | ||||
|                         }, | ||||
|                         { | ||||
|                             label: '15 Minutes', | ||||
|                             bounds: { | ||||
|                                 start: - FIFTEEN_MINUTES, | ||||
|                                 end: THIRTY_SECONDS | ||||
|                             } | ||||
|                         }, | ||||
|                         { | ||||
|                             label: '5 Minutes', | ||||
|                             bounds: { | ||||
|                                 start: - FIVE_MINUTES, | ||||
|                                 end: THIRTY_SECONDS | ||||
|                             } | ||||
|                         }, | ||||
|                         { | ||||
|                             label: '1 Minute', | ||||
|                             bounds: { | ||||
|                                 start: - ONE_MINUTE, | ||||
|                                 end: THIRTY_SECONDS | ||||
|                             } | ||||
|                         } | ||||
|                     ] | ||||
|                 } | ||||
|             ] | ||||
|         })); | ||||
|         openmct.install(openmct.plugins.SummaryWidget()); | ||||
|         openmct.install(openmct.plugins.Notebook()); | ||||
|         openmct.install(openmct.plugins.LADTable()); | ||||
|         openmct.install(openmct.plugins.Filters(['table', 'telemetry.plot.overlay'])); | ||||
|         openmct.install(openmct.plugins.ObjectMigration()); | ||||
|         openmct.install(openmct.plugins.ClearData( | ||||
|             ['table', 'telemetry.plot.overlay', 'telemetry.plot.stacked'], | ||||
|             {indicator: true} | ||||
|         )); | ||||
|         openmct.start(); | ||||
|                         ] | ||||
|                     } | ||||
|                 ] | ||||
|             })); | ||||
|             app.install(app.plugins.SummaryWidget()); | ||||
|             app.install(app.plugins.Notebook()); | ||||
|             app.install(app.plugins.LADTable()); | ||||
|             app.install(app.plugins.Filters(['table', 'telemetry.plot.overlay'])); | ||||
|             app.install(app.plugins.ObjectMigration()); | ||||
|             app.install(app.plugins.ClearData( | ||||
|                 ['table', 'telemetry.plot.overlay', 'telemetry.plot.stacked'], | ||||
|                 {indicator: true} | ||||
|             )); | ||||
|             app.start(); | ||||
|  | ||||
|             setTimeout(() => { | ||||
|                 app.destroy(); | ||||
|             }, 5000) | ||||
|         })(); | ||||
|     </script> | ||||
| </html> | ||||
|   | ||||
| @@ -1,3 +1,7 @@ | ||||
| const testsContext = require.context('.', true, /\/(src|platform)\/.*Spec.js$/); | ||||
| import MemoryLeaksReporter from "./src/utils/testing/MemoryLeaksReporter"; | ||||
|  | ||||
| jasmine.getEnv().addReporter(new MemoryLeaksReporter()); | ||||
|  | ||||
| const testsContext = require.context('.', true, /\/(src|platform)\/.*Spec.js$/); | ||||
| testsContext.keys().forEach(testsContext); | ||||
| console.error("Done running tests?"); | ||||
|   | ||||
| @@ -23,9 +23,9 @@ | ||||
| /*global module,process*/ | ||||
|  | ||||
| const devMode = process.env.NODE_ENV !== 'production'; | ||||
| const browsers = [process.env.NODE_ENV === 'debug' ? 'ChromeDebugging' : 'FirefoxHeadless']; | ||||
| const browsers = [process.env.NODE_ENV === 'debug' ? 'ChromeDebugging' : 'ChromeMemory']; | ||||
| const coverageEnabled = process.env.COVERAGE === 'true'; | ||||
| const reporters = ['progress', 'html']; | ||||
| const reporters = ['progress', 'html', 'junit']; | ||||
|  | ||||
| if (coverageEnabled) { | ||||
|     reporters.push('coverage-istanbul'); | ||||
| @@ -59,15 +59,24 @@ module.exports = (config) => { | ||||
|         browsers: browsers, | ||||
|         client: { | ||||
|             jasmine: { | ||||
|                 random: false | ||||
|                 random: false, | ||||
|                 timeoutInterval: 30000 | ||||
|             } | ||||
|         }, | ||||
|         customLaunchers: { | ||||
|             ChromeDebugging: { | ||||
|             ChromeMemory: { | ||||
|                 base: 'ChromeHeadless', | ||||
|                 flags: ['--enable-precise-memory-info', '--js-flags="--expose-gc --nocrankshaft --noopt"'] | ||||
|             } | ||||
| /*            ChromeDebugging: { | ||||
|                 base: 'Chrome', | ||||
|                 flags: ['--remote-debugging-port=9222'], | ||||
|                 debug: true | ||||
|             } | ||||
|             }, | ||||
|             FirefoxESR: { | ||||
|                 base: 'FirefoxHeadless', | ||||
|                 name: 'FirefoxESR' | ||||
|             }*/ | ||||
|         }, | ||||
|         colors: true, | ||||
|         logLevel: config.LOG_INFO, | ||||
| @@ -78,12 +87,21 @@ module.exports = (config) => { | ||||
|             preserveDescribeNesting: true, | ||||
|             foldAll: false | ||||
|         }, | ||||
|         browserConsoleLogOptions: { level: "error",  format: "%b %T: %m",  terminal: true }, | ||||
|         junitReporter: { | ||||
|             outputDir: "dist/reports/tests", | ||||
|             outputFile: "test-results.xml", | ||||
|             useBrowserName: false | ||||
|         }, | ||||
|         browserConsoleLogOptions: { | ||||
|             level: "error", | ||||
|             format: "%b %T: %m", | ||||
|             terminal: true | ||||
|         }, | ||||
|         coverageIstanbulReporter: { | ||||
|             fixWebpackSourcePaths: true, | ||||
|             dir: process.env.CIRCLE_ARTIFACTS ? | ||||
|                 process.env.CIRCLE_ARTIFACTS + '/coverage' : | ||||
|                 "dist/reports/coverage", | ||||
|             dir: process.env.CIRCLE_ARTIFACTS | ||||
|                 ? process.env.CIRCLE_ARTIFACTS + '/coverage' | ||||
|                 : "dist/reports/coverage", | ||||
|             reports: ['html', 'lcovonly', 'text-summary'], | ||||
|             thresholds: { | ||||
|                 global: { | ||||
|   | ||||
| @@ -32,6 +32,6 @@ if (document.currentScript) { | ||||
|  | ||||
| const MCT = require('./src/MCT'); | ||||
|  | ||||
| const openmct = new MCT(); | ||||
| //const openmct = new MCT(); | ||||
|  | ||||
| module.exports = openmct; | ||||
| module.exports = MCT; | ||||
|   | ||||
							
								
								
									
										25
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										25
									
								
								package.json
									
									
									
									
									
								
							| @@ -1,9 +1,12 @@ | ||||
| { | ||||
|   "name": "openmct", | ||||
|   "version": "1.7.4-SNAPSHOT", | ||||
|   "version": "1.7.6-SNAPSHOT", | ||||
|   "description": "The Open MCT core platform", | ||||
|   "dependencies": {}, | ||||
|   "dependencies": { | ||||
|     "http-server": "^0.12.3" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@webpack-cli/serve": "^1.5.1", | ||||
|     "angular": ">=1.8.0", | ||||
|     "angular-route": "1.4.14", | ||||
|     "babel-eslint": "10.0.3", | ||||
| @@ -34,20 +37,21 @@ | ||||
|     "git-rev-sync": "^1.4.0", | ||||
|     "glob": ">= 3.0.0", | ||||
|     "html-loader": "^0.5.5", | ||||
|     "html2canvas": "^1.0.0-alpha.12", | ||||
|     "html2canvas": "^1.0.0-rc.7", | ||||
|     "imports-loader": "^0.8.0", | ||||
|     "istanbul-instrumenter-loader": "^3.0.1", | ||||
|     "jasmine-core": "^3.1.0", | ||||
|     "jasmine-core": "^3.7.1", | ||||
|     "jsdoc": "^3.3.2", | ||||
|     "karma": "5.1.1", | ||||
|     "karma": "6.3.4", | ||||
|     "karma-chrome-launcher": "3.1.0", | ||||
|     "karma-cli": "2.0.0", | ||||
|     "karma-coverage": "2.0.3", | ||||
|     "karma-coverage-istanbul-reporter": "3.0.3", | ||||
|     "karma-firefox-launcher": "1.3.0", | ||||
|     "karma-firefox-launcher": "2.1.0", | ||||
|     "karma-html-reporter": "0.2.7", | ||||
|     "karma-jasmine": "3.3.1", | ||||
|     "karma-sourcemap-loader": "0.3.7", | ||||
|     "karma-jasmine": "4.0.1", | ||||
|     "karma-junit-reporter": "2.0.1", | ||||
|     "karma-sourcemap-loader": "0.3.8", | ||||
|     "karma-webpack": "4.0.2", | ||||
|     "location-bar": "^3.0.1", | ||||
|     "lodash": "^4.17.12", | ||||
| @@ -74,6 +78,7 @@ | ||||
|     "webpack": "^4.16.2", | ||||
|     "webpack-cli": "^3.1.0", | ||||
|     "webpack-dev-middleware": "^3.1.3", | ||||
|     "webpack-dev-server": "^3.11.2", | ||||
|     "webpack-hot-middleware": "^2.22.3", | ||||
|     "zepto": "^1.2.0" | ||||
|   }, | ||||
| @@ -89,6 +94,7 @@ | ||||
|     "test": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run", | ||||
|     "test:debug": "cross-env NODE_ENV=debug karma start --no-single-run", | ||||
|     "test:coverage": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" COVERAGE=true karma start --single-run", | ||||
|     "test:coverage:firefox": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run --browsers=FirefoxHeadless", | ||||
|     "test:watch": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --no-single-run", | ||||
|     "verify": "concurrently 'npm:test' 'npm:lint'", | ||||
|     "jsdoc": "jsdoc -c jsdoc.json -R API.md -r -d dist/docs/api", | ||||
| @@ -100,6 +106,9 @@ | ||||
|     "type": "git", | ||||
|     "url": "https://github.com/nasa/openmct.git" | ||||
|   }, | ||||
|   "engines": { | ||||
|     "node": ">=10.10.2 <16.0.0" | ||||
|   }, | ||||
|   "author": "", | ||||
|   "license": "Apache-2.0", | ||||
|   "private": true | ||||
|   | ||||
| @@ -164,16 +164,6 @@ define([ | ||||
|                         "license": "license-apache", | ||||
|                         "link": "http://logging.apache.org/log4net/license.html" | ||||
|                     } | ||||
|                 ], | ||||
|                 "routes": [ | ||||
|                     { | ||||
|                         "when": "/licenses", | ||||
|                         "template": licensesTemplate | ||||
|                     }, | ||||
|                     { | ||||
|                         "when": "/licenses-md", | ||||
|                         "template": licensesExportMdTemplate | ||||
|                     } | ||||
|                 ] | ||||
|             } | ||||
|         } | ||||
|   | ||||
| @@ -50,8 +50,6 @@ define([ | ||||
|         name: "platform/commonUI/browse", | ||||
|         definition: { | ||||
|             "extensions": { | ||||
|                 "routes": [ | ||||
|                 ], | ||||
|                 "constants": [ | ||||
|                     { | ||||
|                         "key": "DEFAULT_PATH", | ||||
|   | ||||
| @@ -40,8 +40,8 @@ define( | ||||
|             this.checks = []; | ||||
|             this.$window = $window; | ||||
|  | ||||
|             this.oldUnload = $window.onbeforeunload; | ||||
|             $window.onbeforeunload = this.onBeforeUnload.bind(this); | ||||
| /*            this.oldUnload = $window.onbeforeunload; | ||||
|             $window.onbeforeunload = this.onBeforeUnload.bind(this);*/ | ||||
|         } | ||||
|  | ||||
|         /** | ||||
|   | ||||
| @@ -89,7 +89,8 @@ define([ | ||||
|                         "implementation": TickerService, | ||||
|                         "depends": [ | ||||
|                             "$timeout", | ||||
|                             "now" | ||||
|                             "now", | ||||
|                             "$rootScope" | ||||
|                         ] | ||||
|                     }, | ||||
|                     { | ||||
| @@ -181,7 +182,7 @@ define([ | ||||
|                         ], | ||||
|                         "category": "contextual", | ||||
|                         "name": "Stop", | ||||
|                         "cssClass": "icon-box", | ||||
|                         "cssClass": "icon-box-round-corners", | ||||
|                         "priority": "preferred" | ||||
|                     } | ||||
|                 ], | ||||
|   | ||||
| @@ -32,8 +32,13 @@ define( | ||||
|          * @param $timeout Angular's $timeout | ||||
|          * @param {Function} now function to provide the current time in ms | ||||
|          */ | ||||
|         function TickerService($timeout, now) { | ||||
|         function TickerService($timeout, now, $rootScope) { | ||||
|             var self = this; | ||||
|             var timeoutId; | ||||
|  | ||||
|             $rootScope.$on('$destroy', function () { | ||||
|                 $timeout.cancel(timeoutId); | ||||
|             }); | ||||
|  | ||||
|             function tick() { | ||||
|                 var timestamp = now(), | ||||
| @@ -48,7 +53,7 @@ define( | ||||
|                 } | ||||
|  | ||||
|                 // Try to update at exactly the next second | ||||
|                 $timeout(tick, 1000 - millis, true); | ||||
|                 timeoutId = $timeout(tick, 1000 - millis, true); | ||||
|             } | ||||
|  | ||||
|             tick(); | ||||
|   | ||||
| @@ -101,7 +101,7 @@ define( | ||||
|                     name: "Pause" | ||||
|                 }); | ||||
|                 mockStop.getMetadata.and.returnValue({ | ||||
|                     cssClass: "icon-box", | ||||
|                     cssClass: "icon-box-round-corners", | ||||
|                     name: "Stop" | ||||
|                 }); | ||||
|                 mockScope.domainObject = mockDomainObject; | ||||
|   | ||||
| @@ -1,70 +0,0 @@ | ||||
| This bundle provides the Timeline domain object type, as well | ||||
| as other associated domain object types and relevant views. | ||||
|  | ||||
| # Implementation notes | ||||
|  | ||||
| ## Model Properties | ||||
|  | ||||
| The properties below record properties relevant to using and | ||||
| understanding timelines based on their JSON representation. | ||||
| Additional common properties, such as `modified` | ||||
| or `persisted` timestamps, may also be present. | ||||
|  | ||||
| ### Timeline Model | ||||
|  | ||||
| A timeline's model looks like: | ||||
|  | ||||
| ``` | ||||
| { | ||||
|     "type": "timeline", | ||||
|     "start": { | ||||
|         "timestamp": <number> (milliseconds since epoch), | ||||
|         "epoch": <string> (currently, always "SET") | ||||
|     }, | ||||
|     "capacity": <number> (optional; battery capacity in watt-hours) | ||||
|     "composition": <string[]> (array of identifiers for contained objects) | ||||
| } | ||||
| ``` | ||||
|  | ||||
| The identifiers in a timeline's `composition` field should refer to | ||||
| other Timeline objects, or to Activity objects. | ||||
|  | ||||
| ### Activity Model | ||||
|  | ||||
| An activity's model looks like: | ||||
|  | ||||
| ``` | ||||
| { | ||||
|     "type": "activity", | ||||
|     "start": { | ||||
|         "timestamp": <number> (milliseconds since epoch), | ||||
|         "epoch": <string> (currently, always "SET") | ||||
|     }, | ||||
|     "duration": { | ||||
|         "timestamp": <number> (duration of this activity, in milliseconds) | ||||
|         "epoch": "SET" (this is ignored) | ||||
|     }, | ||||
|     "relationships": { | ||||
|         "modes": <string[]> (array of applicable Activity Mode ids) | ||||
|     }, | ||||
|     "link": <string> (optional; URL linking to associated external resource) | ||||
|     "composition": <string[]> (array of identifiers for contained objects) | ||||
| } | ||||
| ``` | ||||
|  | ||||
| The identifiers in a timeline's `composition` field should only refer to | ||||
| other Activity objects. | ||||
|  | ||||
| ### Activity Mode Model | ||||
|  | ||||
| An activity mode's model looks like: | ||||
|  | ||||
| ``` | ||||
| { | ||||
|     "type": "mode", | ||||
|     "resources": { | ||||
|         "comms": <number> (communications utilization, in Kbps) | ||||
|         "power": <number> (power utilization, in watts) | ||||
|     } | ||||
| } | ||||
| ``` | ||||
| @@ -1,10 +0,0 @@ | ||||
| <div> | ||||
|     Timeline, Activity and Activity Mode objects have been deprecated and will no longer be supported. | ||||
| </div> | ||||
| <div> | ||||
|     Please open an issue in the | ||||
|     <a href="https://github.com/nasa/openmct/issues" target="_blank"> | ||||
|         Open MCT Issue tracker | ||||
|     </a> | ||||
|     if you have any questions about the timeline plugin. | ||||
| </div> | ||||
| @@ -58,7 +58,7 @@ define([ | ||||
|     ) { | ||||
|         var $http = this.$http, | ||||
|             $log = this.$log, | ||||
|             app = angular.module(Constants.MODULE_NAME, ["ngRoute"]), | ||||
|             app = angular.module(Constants.MODULE_NAME, []), | ||||
|             loader = new BundleLoader($http, $log, openmct.legacyRegistry), | ||||
|             resolver = new BundleResolver( | ||||
|                 new ExtensionResolver( | ||||
|   | ||||
| @@ -28,8 +28,7 @@ | ||||
| define( | ||||
|     [ | ||||
|         './FrameworkLayer', | ||||
|         'angular', | ||||
|         'angular-route' | ||||
|         'angular' | ||||
|     ], | ||||
|     function ( | ||||
|         FrameworkLayer, | ||||
|   | ||||
| @@ -57,7 +57,6 @@ define( | ||||
|                 $log = this.$log; | ||||
|  | ||||
|             return new Promise(function (resolve, reject) { | ||||
|                 $log.info("Bootstrapping application " + (app || {}).name); | ||||
|                 angular.element(document).ready(function () { | ||||
|                     angular.bootstrap(document, [app.name], { strictDi: true }); | ||||
|                     resolve(angular); | ||||
|   | ||||
| @@ -138,34 +138,6 @@ define( | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // Custom registration function for extensions of category "route" | ||||
|         function registerRoute(extension) { | ||||
|             var app = this.app, | ||||
|                 $log = this.$log, | ||||
|                 route = Object.create(extension); | ||||
|  | ||||
|             // Adjust path for bundle | ||||
|             if (route.templateUrl) { | ||||
|                 route.templateUrl = [ | ||||
|                     route.bundle.path, | ||||
|                     route.bundle.resources, | ||||
|                     route.templateUrl | ||||
|                 ].join(Constants.SEPARATOR); | ||||
|             } | ||||
|  | ||||
|             // Log the registration | ||||
|             $log.info("Registering route: " + (route.key || route.when)); | ||||
|  | ||||
|             // Register the route with Angular | ||||
|             app.config(['$routeProvider', function ($routeProvider) { | ||||
|                 if (route.when) { | ||||
|                     $routeProvider.when(route.when, route); | ||||
|                 } else { | ||||
|                     $routeProvider.otherwise(route); | ||||
|                 } | ||||
|             }]); | ||||
|         } | ||||
|  | ||||
|         // Handle service compositing | ||||
|         function registerComponents(components) { | ||||
|             var app = this.app, | ||||
| @@ -194,13 +166,6 @@ define( | ||||
|         CustomRegistrars.prototype.constants = | ||||
|             mapUpon(registerConstant); | ||||
|  | ||||
|         /** | ||||
|          * Register Angular routes. | ||||
|          * @param {Array} extensions the resolved extensions | ||||
|          */ | ||||
|         CustomRegistrars.prototype.routes = | ||||
|             mapUpon(registerRoute); | ||||
|  | ||||
|         /** | ||||
|          * Register Angular directives. | ||||
|          * @param {Array} extensions the resolved extensions | ||||
|   | ||||
| @@ -57,7 +57,6 @@ define( | ||||
|                 expect(customRegistrars.directives).toBeTruthy(); | ||||
|                 expect(customRegistrars.controllers).toBeTruthy(); | ||||
|                 expect(customRegistrars.services).toBeTruthy(); | ||||
|                 expect(customRegistrars.routes).toBeTruthy(); | ||||
|                 expect(customRegistrars.constants).toBeTruthy(); | ||||
|                 expect(customRegistrars.runs).toBeTruthy(); | ||||
|             }); | ||||
| @@ -139,47 +138,6 @@ define( | ||||
|                 expect(mockLog.warn.calls.count()).toEqual(0); | ||||
|             }); | ||||
|  | ||||
|             it("allows routes to be registered", function () { | ||||
|                 var mockRouteProvider = jasmine.createSpyObj( | ||||
|                         "$routeProvider", | ||||
|                         ["when", "otherwise"] | ||||
|                     ), | ||||
|                     bundle = { | ||||
|                         path: "test/bundle", | ||||
|                         resources: "res" | ||||
|                     }, | ||||
|                     routes = [ | ||||
|                         { | ||||
|                             when: "foo", | ||||
|                             templateUrl: "templates/test.html", | ||||
|                             bundle: bundle | ||||
|                         }, | ||||
|                         { | ||||
|                             templateUrl: "templates/default.html", | ||||
|                             bundle: bundle | ||||
|                         } | ||||
|                     ]; | ||||
|  | ||||
|                 customRegistrars.routes(routes); | ||||
|  | ||||
|                 // Give it the route provider based on its config call | ||||
|                 mockApp.config.calls.all().forEach(function (call) { | ||||
|                     // Invoke the provided callback | ||||
|                     call.args[0][1](mockRouteProvider); | ||||
|                 }); | ||||
|  | ||||
|                 // The "when" clause should have been mapped to the when method... | ||||
|                 expect(mockRouteProvider.when).toHaveBeenCalled(); | ||||
|                 expect(mockRouteProvider.when.calls.mostRecent().args[0]).toEqual("foo"); | ||||
|                 expect(mockRouteProvider.when.calls.mostRecent().args[1].templateUrl) | ||||
|                     .toEqual("test/bundle/res/templates/test.html"); | ||||
|  | ||||
|                 // ...while the other should have been treated as a default route | ||||
|                 expect(mockRouteProvider.otherwise).toHaveBeenCalled(); | ||||
|                 expect(mockRouteProvider.otherwise.calls.mostRecent().args[0].templateUrl) | ||||
|                     .toEqual("test/bundle/res/templates/default.html"); | ||||
|             }); | ||||
|  | ||||
|             it("accepts components for service compositing", function () { | ||||
|                 // Most relevant code will be exercised in service compositor spec | ||||
|                 expect(customRegistrars.components).toBeTruthy(); | ||||
|   | ||||
| @@ -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") | ||||
|                     ); | ||||
|             }; | ||||
|   | ||||
| @@ -110,8 +110,15 @@ define([ | ||||
|             worker = workerService.run('bareBonesSearchWorker'); | ||||
|         } | ||||
|  | ||||
|         worker.addEventListener('message', function (messageEvent) { | ||||
|         function handleWorkerMessage(messageEvent) { | ||||
|             provider.onWorkerMessage(messageEvent); | ||||
|         } | ||||
|  | ||||
|         worker.addEventListener('message', handleWorkerMessage); | ||||
|  | ||||
|         this.openmct.once('destroy', () => { | ||||
|             worker.removeEventListener('message', handleWorkerMessage); | ||||
|             worker.terminate(); | ||||
|         }); | ||||
|  | ||||
|         return worker; | ||||
|   | ||||
							
								
								
									
										37
									
								
								src/MCT.js
									
									
									
									
									
								
							
							
						
						
									
										37
									
								
								src/MCT.js
									
									
									
									
									
								
							| @@ -31,7 +31,6 @@ define([ | ||||
|     'objectUtils', | ||||
|     './plugins/plugins', | ||||
|     './adapter/indicators/legacy-indicators-plugin', | ||||
|     './plugins/buildInfo/plugin', | ||||
|     './ui/registries/ViewRegistry', | ||||
|     './plugins/imagery/plugin', | ||||
|     './ui/registries/InspectorViewRegistry', | ||||
| @@ -40,6 +39,7 @@ define([ | ||||
|     './ui/router/Browse', | ||||
|     '../platform/framework/src/Main', | ||||
|     './ui/layout/Layout.vue', | ||||
|     './ui/inspector/styles/StylesManager', | ||||
|     '../platform/core/src/objects/DomainObjectImpl', | ||||
|     '../platform/core/src/capabilities/ContextualDomainObject', | ||||
|     './ui/preview/plugin', | ||||
| @@ -60,7 +60,6 @@ define([ | ||||
|     objectUtils, | ||||
|     plugins, | ||||
|     LegacyIndicatorsPlugin, | ||||
|     buildInfoPlugin, | ||||
|     ViewRegistry, | ||||
|     ImageryPlugin, | ||||
|     InspectorViewRegistry, | ||||
| @@ -69,6 +68,7 @@ define([ | ||||
|     Browse, | ||||
|     Main, | ||||
|     Layout, | ||||
|     stylesManager, | ||||
|     DomainObjectImpl, | ||||
|     ContextualDomainObject, | ||||
|     PreviewPlugin, | ||||
| @@ -262,7 +262,7 @@ define([ | ||||
|         // Plugins that are installed by default | ||||
|  | ||||
|         this.install(this.plugins.Plot()); | ||||
|         this.install(this.plugins.TelemetryTable()); | ||||
|         this.install(this.plugins.TelemetryTable.default()); | ||||
|         this.install(PreviewPlugin.default()); | ||||
|         this.install(LegacyIndicatorsPlugin()); | ||||
|         this.install(LicensesPlugin.default()); | ||||
| @@ -283,6 +283,7 @@ define([ | ||||
|         this.install(this.plugins.NotificationIndicator()); | ||||
|         this.install(this.plugins.NewFolderAction()); | ||||
|         this.install(this.plugins.ViewDatumAction()); | ||||
|         this.install(this.plugins.ViewLargeAction()); | ||||
|         this.install(this.plugins.ObjectInterceptors()); | ||||
|         this.install(this.plugins.NonEditableFolder()); | ||||
|     } | ||||
| @@ -373,6 +374,7 @@ define([ | ||||
|      *        MCT; if undefined, MCT will be run in the body of the document | ||||
|      */ | ||||
|     MCT.prototype.start = function (domElement = document.body, isHeadlessMode = false) { | ||||
|  | ||||
|         if (this.types.get('layout') === undefined) { | ||||
|             this.install(this.plugins.DisplayLayout({ | ||||
|                 showAsView: ['summary-widget'] | ||||
| @@ -431,6 +433,9 @@ define([ | ||||
|                     domElement.appendChild(appLayout.$mount().$el); | ||||
|  | ||||
|                     this.layout = appLayout.$refs.layout; | ||||
|                     this.once('destroy', () => { | ||||
|                         this.layout.$destroy(); | ||||
|                     }); | ||||
|                     Browse(this); | ||||
|                 } | ||||
|  | ||||
| @@ -458,7 +463,31 @@ define([ | ||||
|  | ||||
|     MCT.prototype.destroy = function () { | ||||
|         this.emit('destroy'); | ||||
|         this.router.destroy(); | ||||
|         this.removeAllListeners(); | ||||
|  | ||||
|         if (this.$injector) { | ||||
|             this.$injector.get('$rootScope').$destroy(); | ||||
|             this.$injector = null; | ||||
|         } | ||||
|  | ||||
|         if (this.$angular) { | ||||
|             this.$angular.element(this.element).off().removeData(); | ||||
|             this.$angular.element(this.element).empty(); | ||||
|             this.$angular = null; | ||||
|         } | ||||
|  | ||||
|         this.overlays.destroy(); | ||||
|  | ||||
|         if (this.element) { | ||||
|             this.element.remove(); | ||||
|         } | ||||
|  | ||||
|         stylesManager.default.removeAllListeners(); | ||||
|  | ||||
|         window.angular = null; | ||||
|         window.openmct = null; | ||||
|  | ||||
|         Object.keys(require.cache).forEach(key => delete require.cache[key]); | ||||
|     }; | ||||
|  | ||||
|     MCT.prototype.plugins = plugins; | ||||
|   | ||||
| @@ -26,80 +26,93 @@ define([ | ||||
|     'utils/testing' | ||||
| ], function (plugins, legacyRegistry, testUtils) { | ||||
|     describe("MCT", function () { | ||||
|         let openmct; | ||||
|         let mockPlugin; | ||||
|         let mockPlugin2; | ||||
|         let mockListener; | ||||
|         let oldBundles; | ||||
|  | ||||
|         beforeEach(function () { | ||||
|             mockPlugin = jasmine.createSpy('plugin'); | ||||
|             mockPlugin2 = jasmine.createSpy('plugin2'); | ||||
|             mockListener = jasmine.createSpy('listener'); | ||||
|             oldBundles = legacyRegistry.list(); | ||||
|             this.mockPlugin = jasmine.createSpy('plugin'); | ||||
|             this.mockPlugin2 = jasmine.createSpy('plugin2'); | ||||
|             this.mockListener = jasmine.createSpy('listener'); | ||||
|             this.oldBundles = legacyRegistry.list(); | ||||
|  | ||||
|             openmct = testUtils.createOpenMct(); | ||||
|             this.openmct = testUtils.createOpenMct(); | ||||
|  | ||||
|             openmct.install(mockPlugin); | ||||
|             openmct.install(mockPlugin2); | ||||
|             openmct.on('start', mockListener); | ||||
|             this.openmct.install(this.mockPlugin); | ||||
|             this.openmct.install(this.mockPlugin2); | ||||
|             this.openmct.once('start', this.mockListener); | ||||
|         }); | ||||
|  | ||||
|         // Clean up the dirty singleton. | ||||
|         afterEach(function () { | ||||
|             legacyRegistry.list().forEach(function (bundle) { | ||||
|                 if (oldBundles.indexOf(bundle) === -1) { | ||||
|                 if (this.oldBundles.indexOf(bundle) === -1) { | ||||
|                     legacyRegistry.delete(bundle); | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|             return testUtils.resetApplicationState(openmct); | ||||
|             return testUtils.resetApplicationState(this.openmct) | ||||
|                 .then(() => { | ||||
|                     this.openmct = null; | ||||
|                     this.mockPlugin = null; | ||||
|                     this.mockPlugin2 = null; | ||||
|                     this.mockListener = null; | ||||
|                     this.oldBundles = null; | ||||
|                     console.error('Done clearing test variables'); | ||||
|                 }); | ||||
|         }); | ||||
|  | ||||
|         it("exposes plugins", function () { | ||||
|             expect(openmct.plugins).toEqual(plugins); | ||||
|             expect(this.openmct.plugins).toEqual(plugins); | ||||
|         }); | ||||
|  | ||||
|         it("does not issue a start event before started", function () { | ||||
|             expect(mockListener).not.toHaveBeenCalled(); | ||||
|             expect(this.mockListener).not.toHaveBeenCalled(); | ||||
|         }); | ||||
|  | ||||
|         describe("start", function () { | ||||
|             let appHolder; | ||||
|             beforeEach(function (done) { | ||||
|                 appHolder = document.createElement("div"); | ||||
|                 openmct.on('start', done); | ||||
|                 openmct.start(appHolder); | ||||
|             beforeEach(function () { | ||||
|                 this.appHolder = document.createElement("div"); | ||||
|  | ||||
|                 const startPromise = new Promise(resolve => { | ||||
|                     this.openmct.once('start', resolve); | ||||
|                 }); | ||||
|  | ||||
|                 this.openmct.startHeadless(); | ||||
|  | ||||
|                 return startPromise; | ||||
|             }); | ||||
|  | ||||
|             afterEach(() => { | ||||
|                 //this.appHolder.remove(); | ||||
|                 this.appHolder = null; | ||||
|             }); | ||||
|  | ||||
|             it("calls plugins for configuration", function () { | ||||
|                 expect(mockPlugin).toHaveBeenCalledWith(openmct); | ||||
|                 expect(mockPlugin2).toHaveBeenCalledWith(openmct); | ||||
|                 expect(this.mockPlugin).toHaveBeenCalledWith(this.openmct); | ||||
|                 expect(this.mockPlugin2).toHaveBeenCalledWith(this.openmct); | ||||
|             }); | ||||
|  | ||||
|             it("emits a start event", function () { | ||||
|                 expect(mockListener).toHaveBeenCalled(); | ||||
|                 expect(this.mockListener).toHaveBeenCalled(); | ||||
|             }); | ||||
|  | ||||
|             it("Renders the application into the provided container element", function () { | ||||
|                 let openMctShellElements = appHolder.querySelectorAll('div.l-shell'); | ||||
|                 let openMctShellElements = this.appHolder.querySelectorAll('div.l-shell'); | ||||
|                 expect(openMctShellElements.length).toBe(1); | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         describe("startHeadless", function () { | ||||
|             beforeEach(function (done) { | ||||
|                 openmct.on('start', done); | ||||
|                 openmct.startHeadless(); | ||||
|                 console.error('HERE'); | ||||
|                 this.openmct.on('start', done); | ||||
|                 this.openmct.startHeadless(); | ||||
|             }); | ||||
|  | ||||
|             it("calls plugins for configuration", function () { | ||||
|                 expect(mockPlugin).toHaveBeenCalledWith(openmct); | ||||
|                 expect(mockPlugin2).toHaveBeenCalledWith(openmct); | ||||
|                 expect(this.mockPlugin).toHaveBeenCalledWith(this.openmct); | ||||
|                 expect(this.mockPlugin2).toHaveBeenCalledWith(this.openmct); | ||||
|             }); | ||||
|  | ||||
|             it("emits a start event", function () { | ||||
|                 expect(mockListener).toHaveBeenCalled(); | ||||
|                 expect(this.mockListener).toHaveBeenCalled(); | ||||
|             }); | ||||
|  | ||||
|             it("Does not render Open MCT", function () { | ||||
| @@ -112,19 +125,19 @@ define([ | ||||
|             let testAssetPath; | ||||
|  | ||||
|             beforeEach(function () { | ||||
|                 openmct.legacyExtension = jasmine.createSpy('legacyExtension'); | ||||
|                 this.openmct.legacyExtension = jasmine.createSpy('legacyExtension'); | ||||
|             }); | ||||
|  | ||||
|             it("configures the path for assets", function () { | ||||
|                 testAssetPath = "some/path/"; | ||||
|                 openmct.setAssetPath(testAssetPath); | ||||
|                 expect(openmct.getAssetPath()).toBe(testAssetPath); | ||||
|                 this.openmct.setAssetPath(testAssetPath); | ||||
|                 expect(this.openmct.getAssetPath()).toBe(testAssetPath); | ||||
|             }); | ||||
|  | ||||
|             it("adds a trailing /", function () { | ||||
|                 testAssetPath = "some/path"; | ||||
|                 openmct.setAssetPath(testAssetPath); | ||||
|                 expect(openmct.getAssetPath()).toBe(testAssetPath + "/"); | ||||
|                 this.openmct.setAssetPath(testAssetPath); | ||||
|                 expect(this.openmct.getAssetPath()).toBe(testAssetPath + "/"); | ||||
|             }); | ||||
|         }); | ||||
|     }); | ||||
|   | ||||
| @@ -36,8 +36,7 @@ define([ | ||||
|     './views/installLegacyViews', | ||||
|     './policies/LegacyCompositionPolicyAdapter', | ||||
|     './actions/LegacyActionAdapter', | ||||
|     './services/LegacyPersistenceAdapter', | ||||
|     './services/ExportImageService' | ||||
|     './services/LegacyPersistenceAdapter' | ||||
| ], function ( | ||||
|     ActionDialogDecorator, | ||||
|     AdapterCapability, | ||||
| @@ -54,8 +53,7 @@ define([ | ||||
|     installLegacyViews, | ||||
|     legacyCompositionPolicyAdapter, | ||||
|     LegacyActionAdapter, | ||||
|     LegacyPersistenceAdapter, | ||||
|     ExportImageService | ||||
|     LegacyPersistenceAdapter | ||||
| ) { | ||||
|     return { | ||||
|         name: 'src/adapter', | ||||
| @@ -84,13 +82,6 @@ define([ | ||||
|                             "identifierService", | ||||
|                             "cacheService" | ||||
|                         ] | ||||
|                     }, | ||||
|                     { | ||||
|                         "key": "exportImageService", | ||||
|                         "implementation": ExportImageService, | ||||
|                         "depends": [ | ||||
|                             "dialogService" | ||||
|                         ] | ||||
|                     } | ||||
|                 ], | ||||
|                 components: [ | ||||
|   | ||||
| @@ -32,6 +32,10 @@ define([ | ||||
|     // cannot be injected. | ||||
|     function AlternateCompositionInitializer(openmct) { | ||||
|         AlternateCompositionCapability.appliesTo = function (model, id) { | ||||
|             openmct.once('destroy', () => { | ||||
|                 delete AlternateCompositionCapability.appliesTo; | ||||
|             }); | ||||
|  | ||||
|             model = objectUtils.toNewFormat(model, id || ''); | ||||
|  | ||||
|             return Boolean(openmct.composition.get(model)); | ||||
|   | ||||
| @@ -173,10 +173,11 @@ define([ | ||||
|         const limitEvaluator = oldObject.getCapability("limit"); | ||||
|  | ||||
|         return { | ||||
|             limits: function () { | ||||
|                 return limitEvaluator.limits(); | ||||
|             limits: () => { | ||||
|                 return limitEvaluator.limits.then !== undefined | ||||
|                     ? limitEvaluator.limits() | ||||
|                     : Promise.resolve(limitEvaluator.limits()); | ||||
|             } | ||||
|  | ||||
|         }; | ||||
|     }; | ||||
|  | ||||
|   | ||||
| @@ -1,218 +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. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /** | ||||
|  * Module defining ExportImageService. Created by hudsonfoo on 09/02/16 | ||||
|  */ | ||||
| define( | ||||
|     [ | ||||
|         "html2canvas", | ||||
|         "saveAs" | ||||
|     ], | ||||
|     function ( | ||||
|         html2canvas, | ||||
|         { saveAs } | ||||
|     ) { | ||||
|  | ||||
|         /** | ||||
|          * The export image service will export any HTML node to | ||||
|          * JPG, or PNG. | ||||
|          * @param {object} dialogService | ||||
|          * @constructor | ||||
|          */ | ||||
|         function ExportImageService(dialogService) { | ||||
|             this.dialogService = dialogService; | ||||
|             this.exportCount = 0; | ||||
|         } | ||||
|  | ||||
|         /** | ||||
|          * Converts an HTML element into a PNG or JPG Blob. | ||||
|          * @private | ||||
|          * @param {node} element that will be converted to an image | ||||
|          * @param {object} options Image options. | ||||
|          * @returns {promise} | ||||
|          */ | ||||
|         ExportImageService.prototype.renderElement = function (element, {imageType, className, thumbnailSize}) { | ||||
|             const self = this; | ||||
|             const dialogService = this.dialogService; | ||||
|             const dialog = dialogService.showBlockingMessage({ | ||||
|                 title: "Capturing...", | ||||
|                 hint: "Capturing an image", | ||||
|                 unknownProgress: true, | ||||
|                 severity: "info", | ||||
|                 delay: true | ||||
|             }); | ||||
|  | ||||
|             let mimeType = "image/png"; | ||||
|             if (imageType === "jpg") { | ||||
|                 mimeType = "image/jpeg"; | ||||
|             } | ||||
|  | ||||
|             let exportId = undefined; | ||||
|             let oldId = undefined; | ||||
|             if (className) { | ||||
|                 exportId = 'export-element-' + this.exportCount; | ||||
|                 this.exportCount++; | ||||
|                 oldId = element.id; | ||||
|                 element.id = exportId; | ||||
|             } | ||||
|  | ||||
|             return html2canvas(element, { | ||||
|                 onclone: function (document) { | ||||
|                     if (className) { | ||||
|                         const clonedElement = document.getElementById(exportId); | ||||
|                         clonedElement.classList.add(className); | ||||
|                     } | ||||
|  | ||||
|                     element.id = oldId; | ||||
|                 }, | ||||
|                 removeContainer: true // Set to false to debug what html2canvas renders | ||||
|             }).then(function (canvas) { | ||||
|                 dialog.dismiss(); | ||||
|  | ||||
|                 return new Promise(function (resolve, reject) { | ||||
|                     if (thumbnailSize) { | ||||
|                         const thumbnail = self.getThumbnail(canvas, mimeType, thumbnailSize); | ||||
|  | ||||
|                         return canvas.toBlob(blob => resolve({ | ||||
|                             blob, | ||||
|                             thumbnail | ||||
|                         }), mimeType); | ||||
|                     } | ||||
|  | ||||
|                     return canvas.toBlob(blob => resolve({ blob }), mimeType); | ||||
|                 }); | ||||
|             }, function (error) { | ||||
|                 console.log('error capturing image', error); | ||||
|                 dialog.dismiss(); | ||||
|                 const errorDialog = dialogService.showBlockingMessage({ | ||||
|                     title: "Error capturing image", | ||||
|                     severity: "error", | ||||
|                     hint: "Image was not captured successfully!", | ||||
|                     options: [{ | ||||
|                         label: "OK", | ||||
|                         callback: function () { | ||||
|                             errorDialog.dismiss(); | ||||
|                         } | ||||
|                     }] | ||||
|                 }); | ||||
|             }); | ||||
|         }; | ||||
|  | ||||
|         ExportImageService.prototype.getThumbnail = function (canvas, mimeType, size) { | ||||
|             const thumbnailCanvas = document.createElement('canvas'); | ||||
|             thumbnailCanvas.setAttribute('width', size.width); | ||||
|             thumbnailCanvas.setAttribute('height', size.height); | ||||
|             const ctx = thumbnailCanvas.getContext('2d'); | ||||
|             ctx.globalCompositeOperation = "copy"; | ||||
|             ctx.drawImage(canvas, 0, 0, canvas.width, canvas.height, 0, 0, size.width, size.height); | ||||
|  | ||||
|             return thumbnailCanvas.toDataURL(mimeType); | ||||
|         }; | ||||
|  | ||||
|         /** | ||||
|          * Takes a screenshot of a DOM node and exports to JPG. | ||||
|          * @param {node} element to be exported | ||||
|          * @param {string} filename the exported image | ||||
|          * @param {string} className to be added to element before capturing (optional) | ||||
|          * @returns {promise} | ||||
|          */ | ||||
|         ExportImageService.prototype.exportJPG = function (element, filename, className) { | ||||
|             const processedFilename = replaceDotsWithUnderscores(filename); | ||||
|  | ||||
|             return this.renderElement(element, { | ||||
|                 imageType: 'jpg', | ||||
|                 className | ||||
|             }) | ||||
|                 .then(function (img) { | ||||
|                     saveAs(img.blob, processedFilename); | ||||
|                 }); | ||||
|         }; | ||||
|  | ||||
|         /** | ||||
|          * Takes a screenshot of a DOM node and exports to PNG. | ||||
|          * @param {node} element to be exported | ||||
|          * @param {string} filename the exported image | ||||
|          * @param {string} className to be added to element before capturing (optional) | ||||
|          * @returns {promise} | ||||
|          */ | ||||
|         ExportImageService.prototype.exportPNG = function (element, filename, className) { | ||||
|             const processedFilename = replaceDotsWithUnderscores(filename); | ||||
|  | ||||
|             return this.renderElement(element, { | ||||
|                 imageType: 'png', | ||||
|                 className | ||||
|             }) | ||||
|                 .then(function (img) { | ||||
|                     saveAs(img.blob, processedFilename); | ||||
|                 }); | ||||
|         }; | ||||
|  | ||||
|         /** | ||||
|          * Takes a screenshot of a DOM node in PNG format. | ||||
|          * @param {node} element to be exported | ||||
|          * @param {string} filename the exported image | ||||
|          * @returns {promise} | ||||
|          */ | ||||
|  | ||||
|         ExportImageService.prototype.exportPNGtoSRC = function (element, options) { | ||||
|  | ||||
|             return this.renderElement(element, { | ||||
|                 imageType: 'png', | ||||
|                 ...options | ||||
|             }); | ||||
|         }; | ||||
|  | ||||
|         function replaceDotsWithUnderscores(filename) { | ||||
|             const regex = /\./gi; | ||||
|  | ||||
|             return filename.replace(regex, '_'); | ||||
|         } | ||||
|  | ||||
|         /** | ||||
|          * canvas.toBlob() not supported in IE < 10, Opera, and Safari. This polyfill | ||||
|          * implements the method in browsers that would not otherwise support it. | ||||
|          * https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob | ||||
|          */ | ||||
|         function polyfillToBlob() { | ||||
|             if (!HTMLCanvasElement.prototype.toBlob) { | ||||
|                 Object.defineProperty(HTMLCanvasElement.prototype, "toBlob", { | ||||
|                     value: function (callback, mimeType, quality) { | ||||
|                         const binStr = atob(this.toDataURL(mimeType, quality).split(',')[1]); | ||||
|                         const len = binStr.length; | ||||
|                         const arr = new Uint8Array(len); | ||||
|  | ||||
|                         for (let i = 0; i < len; i++) { | ||||
|                             arr[i] = binStr.charCodeAt(i); | ||||
|                         } | ||||
|  | ||||
|                         callback(new Blob([arr], {type: mimeType || "image/png"})); | ||||
|                     } | ||||
|                 }); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         polyfillToBlob(); | ||||
|  | ||||
|         return ExportImageService; | ||||
|     } | ||||
| ); | ||||
| @@ -28,6 +28,10 @@ export default class Editor extends EventEmitter { | ||||
|         super(); | ||||
|         this.editing = false; | ||||
|         this.openmct = openmct; | ||||
|  | ||||
|         openmct.once('destroy', () => { | ||||
|             this.removeAllListeners(); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|   | ||||
| @@ -46,8 +46,6 @@ class ActionCollection extends EventEmitter { | ||||
|             this._observeObjectPath(); | ||||
|             this.openmct.editor.on('isEditing', this._updateActions); | ||||
|         } | ||||
|  | ||||
|         this._initializeActions(); | ||||
|     } | ||||
|  | ||||
|     disable(actionKeys) { | ||||
| @@ -156,19 +154,10 @@ class ActionCollection extends EventEmitter { | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     _initializeActions() { | ||||
|         Object.keys(this.applicableActions).forEach(key => { | ||||
|             this.applicableActions[key].callBack = () => { | ||||
|                 return this.applicableActions[key].invoke(this.objectPath, this.view); | ||||
|             }; | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     _updateActions() { | ||||
|         let newApplicableActions = this.openmct.actions._applicableActions(this.objectPath, this.view); | ||||
|  | ||||
|         this.applicableActions = this._mergeOldAndNewActions(this.applicableActions, newApplicableActions); | ||||
|         this._initializeActions(); | ||||
|         this._update(); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -34,7 +34,7 @@ class ActionsAPI extends EventEmitter { | ||||
|         this._groupOrder = ['windowing', 'undefined', 'view', 'action', 'json']; | ||||
|  | ||||
|         this.register = this.register.bind(this); | ||||
|         this.get = this.get.bind(this); | ||||
|         this.getActionsCollection = this.getActionsCollection.bind(this); | ||||
|         this._applicableActions = this._applicableActions.bind(this); | ||||
|         this._updateCachedActionCollections = this._updateCachedActionCollections.bind(this); | ||||
|     } | ||||
| @@ -43,12 +43,14 @@ class ActionsAPI extends EventEmitter { | ||||
|         this._allActions[actionDefinition.key] = actionDefinition; | ||||
|     } | ||||
|  | ||||
|     get(objectPath, view) { | ||||
|         if (view) { | ||||
|     getAction(key) { | ||||
|         return this._allActions[key]; | ||||
|     } | ||||
|  | ||||
|     getActionsCollection(objectPath, view) { | ||||
|         if (view) { | ||||
|             return this._getCachedActionCollection(objectPath, view) || this._newActionCollection(objectPath, view, true); | ||||
|         } else { | ||||
|  | ||||
|             return this._newActionCollection(objectPath, view, true); | ||||
|         } | ||||
|     } | ||||
| @@ -57,15 +59,6 @@ class ActionsAPI extends EventEmitter { | ||||
|         this._groupOrder = groupArray; | ||||
|     } | ||||
|  | ||||
|     _get(objectPath, view) { | ||||
|         let actionCollection = this._newActionCollection(objectPath, view); | ||||
|  | ||||
|         this._actionCollections.set(view, actionCollection); | ||||
|         actionCollection.on('destroy', this._updateCachedActionCollections); | ||||
|  | ||||
|         return actionCollection; | ||||
|     } | ||||
|  | ||||
|     _getCachedActionCollection(objectPath, view) { | ||||
|         let cachedActionCollection = this._actionCollections.get(view); | ||||
|  | ||||
| @@ -75,7 +68,17 @@ class ActionsAPI extends EventEmitter { | ||||
|     _newActionCollection(objectPath, view, skipEnvironmentObservers) { | ||||
|         let applicableActions = this._applicableActions(objectPath, view); | ||||
|  | ||||
|         return new ActionCollection(applicableActions, objectPath, view, this._openmct, skipEnvironmentObservers); | ||||
|         const actionCollection = new ActionCollection(applicableActions, objectPath, view, this._openmct, skipEnvironmentObservers); | ||||
|         if (view) { | ||||
|             this._cacheActionCollection(view, actionCollection); | ||||
|         } | ||||
|  | ||||
|         return actionCollection; | ||||
|     } | ||||
|  | ||||
|     _cacheActionCollection(view, actionCollection) { | ||||
|         this._actionCollections.set(view, actionCollection); | ||||
|         actionCollection.on('destroy', this._updateCachedActionCollections); | ||||
|     } | ||||
|  | ||||
|     _updateCachedActionCollections(key) { | ||||
|   | ||||
| @@ -106,7 +106,7 @@ describe('The Actions API', () => { | ||||
|         it("adds action to ActionsAPI", () => { | ||||
|             actionsAPI.register(mockAction); | ||||
|  | ||||
|             let actionCollection = actionsAPI.get(mockObjectPath, mockViewContext1); | ||||
|             let actionCollection = actionsAPI.getActionsCollection(mockObjectPath, mockViewContext1); | ||||
|             let action = actionCollection.getActionsObject()[mockAction.key]; | ||||
|  | ||||
|             expect(action.key).toEqual(mockAction.key); | ||||
| @@ -121,21 +121,21 @@ describe('The Actions API', () => { | ||||
|         }); | ||||
|  | ||||
|         it("returns an ActionCollection when invoked with an objectPath only", () => { | ||||
|             let actionCollection = actionsAPI.get(mockObjectPath); | ||||
|             let actionCollection = actionsAPI.getActionsCollection(mockObjectPath); | ||||
|             let instanceOfActionCollection = actionCollection instanceof ActionCollection; | ||||
|  | ||||
|             expect(instanceOfActionCollection).toBeTrue(); | ||||
|         }); | ||||
|  | ||||
|         it("returns an ActionCollection when invoked with an objectPath and view", () => { | ||||
|             let actionCollection = actionsAPI.get(mockObjectPath, mockViewContext1); | ||||
|             let actionCollection = actionsAPI.getActionsCollection(mockObjectPath, mockViewContext1); | ||||
|             let instanceOfActionCollection = actionCollection instanceof ActionCollection; | ||||
|  | ||||
|             expect(instanceOfActionCollection).toBeTrue(); | ||||
|         }); | ||||
|  | ||||
|         it("returns relevant actions when invoked with objectPath only", () => { | ||||
|             let actionCollection = actionsAPI.get(mockObjectPath); | ||||
|             let actionCollection = actionsAPI.getActionsCollection(mockObjectPath); | ||||
|             let action = actionCollection.getActionsObject()[mockObjectPathAction.key]; | ||||
|  | ||||
|             expect(action.key).toEqual(mockObjectPathAction.key); | ||||
| @@ -143,7 +143,7 @@ describe('The Actions API', () => { | ||||
|         }); | ||||
|  | ||||
|         it("returns relevant actions when invoked with objectPath and view", () => { | ||||
|             let actionCollection = actionsAPI.get(mockObjectPath, mockViewContext1); | ||||
|             let actionCollection = actionsAPI.getActionsCollection(mockObjectPath, mockViewContext1); | ||||
|             let action = actionCollection.getActionsObject()[mockAction.key]; | ||||
|  | ||||
|             expect(action.key).toEqual(mockAction.key); | ||||
|   | ||||
| @@ -37,7 +37,7 @@ import Menu, { MENU_PLACEMENT } from './menu.js'; | ||||
|  * @property {Boolean} isDisabled adds disable class if true | ||||
|  * @property {String} name Menu item text | ||||
|  * @property {String} description Menu item description | ||||
|  * @property {Function} callBack callback function: invoked when item is clicked | ||||
|  * @property {Function} onItemClicked callback function: invoked when item is clicked | ||||
|  */ | ||||
|  | ||||
| /** | ||||
| @@ -66,12 +66,27 @@ class MenuAPI { | ||||
|      * @param {Array.<Action>|Array.<Array.<Action>>} actions collection of actions{@link Action} or collection of groups of actions {@link Action} | ||||
|      * @param {MenuOptions} [menuOptions] [Optional] The {@link MenuOptions} options for Menu | ||||
|      */ | ||||
|     showMenu(x, y, actions, menuOptions) { | ||||
|         this._createMenuComponent(x, y, actions, menuOptions); | ||||
|     showMenu(x, y, items, menuOptions) { | ||||
|         this._createMenuComponent(x, y, items, menuOptions); | ||||
|  | ||||
|         this.menuComponent.showMenu(); | ||||
|     } | ||||
|  | ||||
|     actionsToMenuItems(actions, objectPath, view) { | ||||
|         return actions.map(action => { | ||||
|             const isActionGroup = Array.isArray(action); | ||||
|             if (isActionGroup) { | ||||
|                 action = this.actionsToMenuItems(action, objectPath, view); | ||||
|             } else { | ||||
|                 action.onItemClicked = () => { | ||||
|                     action.invoke(objectPath, view); | ||||
|                 }; | ||||
|             } | ||||
|  | ||||
|             return action; | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Show popup menu with description of item on hover | ||||
|      * @param {number} x x-coordinates for popup | ||||
|   | ||||
| @@ -57,7 +57,7 @@ describe ('The Menu API', () => { | ||||
|                 name: 'Test Action 1', | ||||
|                 cssClass: 'icon-clock', | ||||
|                 description: 'This is a test action', | ||||
|                 callBack: () => { | ||||
|                 onItemClicked: () => { | ||||
|                     result = 'Test Action 1 Invoked'; | ||||
|                 } | ||||
|             }, | ||||
| @@ -66,7 +66,7 @@ describe ('The Menu API', () => { | ||||
|                 name: 'Test Action 2', | ||||
|                 cssClass: 'icon-clock', | ||||
|                 description: 'This is a test action', | ||||
|                 callBack: () => { | ||||
|                 onItemClicked: () => { | ||||
|                     result = 'Test Action 2 Invoked'; | ||||
|                 } | ||||
|             } | ||||
|   | ||||
| @@ -11,7 +11,7 @@ | ||||
|                 :key="action.name" | ||||
|                 :class="[action.cssClass, action.isDisabled ? 'disabled' : '']" | ||||
|                 :title="action.description" | ||||
|                 @click="action.callBack" | ||||
|                 @click="action.onItemClicked" | ||||
|             > | ||||
|                 {{ action.name }} | ||||
|             </li> | ||||
| @@ -36,7 +36,7 @@ | ||||
|             :key="action.name" | ||||
|             :class="action.cssClass" | ||||
|             :title="action.description" | ||||
|             @click="action.callBack" | ||||
|             @click="action.onItemClicked" | ||||
|         > | ||||
|             {{ action.name }} | ||||
|         </li> | ||||
|   | ||||
| @@ -13,7 +13,7 @@ | ||||
|                 :key="action.name" | ||||
|                 :class="[action.cssClass, action.isDisabled ? 'disabled' : '']" | ||||
|                 :title="action.description" | ||||
|                 @click="action.callBack" | ||||
|                 @click="action.onItemClicked" | ||||
|                 @mouseover="toggleItemDescription(action)" | ||||
|                 @mouseleave="toggleItemDescription()" | ||||
|             > | ||||
| @@ -42,7 +42,7 @@ | ||||
|             :key="action.name" | ||||
|             :class="action.cssClass" | ||||
|             :title="action.description" | ||||
|             @click="action.callBack" | ||||
|             @click="action.onItemClicked" | ||||
|             @mouseover="toggleItemDescription(action)" | ||||
|             @mouseleave="toggleItemDescription()" | ||||
|         > | ||||
|   | ||||
| @@ -71,12 +71,12 @@ class Menu extends EventEmitter { | ||||
|  | ||||
|     showMenu() { | ||||
|         this.component = new Vue({ | ||||
|             provide: { | ||||
|                 options: this.options | ||||
|             }, | ||||
|             components: { | ||||
|                 MenuComponent | ||||
|             }, | ||||
|             provide: { | ||||
|                 options: this.options | ||||
|             }, | ||||
|             template: '<menu-component />' | ||||
|         }); | ||||
|  | ||||
| @@ -85,12 +85,12 @@ class Menu extends EventEmitter { | ||||
|  | ||||
|     showSuperMenu() { | ||||
|         this.component = new Vue({ | ||||
|             provide: { | ||||
|                 options: this.options | ||||
|             }, | ||||
|             components: { | ||||
|                 SuperMenuComponent | ||||
|             }, | ||||
|             provide: { | ||||
|                 options: this.options | ||||
|             }, | ||||
|             template: '<super-menu-component />' | ||||
|         }); | ||||
|  | ||||
|   | ||||
| @@ -399,25 +399,25 @@ ObjectAPI.prototype._toMutable = function (object) { | ||||
|         mutableObject = object; | ||||
|     } else { | ||||
|         mutableObject = MutableDomainObject.createMutable(object, this.eventEmitter); | ||||
|     } | ||||
|  | ||||
|     // Check if provider supports realtime updates | ||||
|     let identifier = utils.parseKeyString(mutableObject.identifier); | ||||
|     let provider = this.getProvider(identifier); | ||||
|         // Check if provider supports realtime updates | ||||
|         let identifier = utils.parseKeyString(mutableObject.identifier); | ||||
|         let provider = this.getProvider(identifier); | ||||
|  | ||||
|     if (provider !== undefined | ||||
|         && provider.observe !== undefined | ||||
|         && this.SYNCHRONIZED_OBJECT_TYPES.includes(object.type)) { | ||||
|         let unobserve = provider.observe(identifier, (updatedModel) => { | ||||
|             if (updatedModel.persisted > mutableObject.modified) { | ||||
|                 //Don't replace with a stale model. This can happen on slow connections when multiple mutations happen | ||||
|                 //in rapid succession and intermediate persistence states are returned by the observe function. | ||||
|                 mutableObject.$refresh(updatedModel); | ||||
|             } | ||||
|         }); | ||||
|         mutableObject.$on('$_destroy', () => { | ||||
|             unobserve(); | ||||
|         }); | ||||
|         if (provider !== undefined | ||||
|             && provider.observe !== undefined | ||||
|             && this.SYNCHRONIZED_OBJECT_TYPES.includes(object.type)) { | ||||
|             let unobserve = provider.observe(identifier, (updatedModel) => { | ||||
|                 if (updatedModel.persisted > mutableObject.modified) { | ||||
|                     //Don't replace with a stale model. This can happen on slow connections when multiple mutations happen | ||||
|                     //in rapid succession and intermediate persistence states are returned by the observe function. | ||||
|                     mutableObject.$refresh(updatedModel); | ||||
|                 } | ||||
|             }); | ||||
|             mutableObject.$on('$_destroy', () => { | ||||
|                 unobserve(); | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     return mutableObject; | ||||
|   | ||||
| @@ -17,11 +17,7 @@ class OverlayAPI { | ||||
|  | ||||
|         this.dismissLastOverlay = this.dismissLastOverlay.bind(this); | ||||
|  | ||||
|         document.addEventListener('keyup', (event) => { | ||||
|             if (event.key === 'Escape') { | ||||
|                 this.dismissLastOverlay(); | ||||
|             } | ||||
|         }); | ||||
|         document.addEventListener('keyup', this.dismissLastOverlay); | ||||
|  | ||||
|     } | ||||
|  | ||||
| @@ -49,10 +45,12 @@ class OverlayAPI { | ||||
|     /** | ||||
|      * private | ||||
|      */ | ||||
|     dismissLastOverlay() { | ||||
|         let lastOverlay = this.activeOverlays[this.activeOverlays.length - 1]; | ||||
|         if (lastOverlay && lastOverlay.dismissable) { | ||||
|             lastOverlay.dismiss(); | ||||
|     dismissLastOverlay(event) { | ||||
|         if (event.key === 'Escape') { | ||||
|             let lastOverlay = this.activeOverlays[this.activeOverlays.length - 1]; | ||||
|             if (lastOverlay && lastOverlay.dismissable) { | ||||
|                 lastOverlay.dismiss(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -129,6 +127,10 @@ class OverlayAPI { | ||||
|         return progressDialog; | ||||
|     } | ||||
|  | ||||
|     destroy() { | ||||
|         document.removeEventListener('keyup', this.dismissLastOverlay); | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| export default OverlayAPI; | ||||
|   | ||||
| @@ -21,8 +21,7 @@ | ||||
|  | ||||
|     &__outer { | ||||
|         @include abs(); | ||||
|         background: $overlayColorBg; | ||||
|         color: $overlayColorFg; | ||||
|         background: $colorBodyBg; | ||||
|         display: flex; | ||||
|         flex-direction: column; | ||||
|         padding: $overlayInnerMargin; | ||||
| @@ -30,7 +29,6 @@ | ||||
|  | ||||
|     &__close-button { | ||||
|         $p: $interiorMargin + 2px; | ||||
|         color: $overlayColorFg; | ||||
|         font-size: 1.5em; | ||||
|         position: absolute; | ||||
|         top: $p; right: $p; | ||||
| @@ -82,11 +80,6 @@ | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     .c-button, | ||||
|     .c-click-icon { | ||||
|         filter: $overlayBrightnessAdjust; | ||||
|     } | ||||
|  | ||||
|     .c-object-label__name { | ||||
|         filter: $objectLabelNameFilter; | ||||
|     } | ||||
| @@ -103,6 +96,7 @@ body.desktop { | ||||
|     } | ||||
|  | ||||
|     // Overlay types, styling for desktop. Appended to .l-overlay-wrapper element. | ||||
|     .l-overlay-large, | ||||
|     .l-overlay-small, | ||||
|     .l-overlay-fit { | ||||
|         .c-overlay__outer { | ||||
| @@ -124,12 +118,8 @@ body.desktop { | ||||
|         $tbPad: floor($pad * 0.8); | ||||
|         $lrPad: $pad; | ||||
|         .c-overlay { | ||||
|             &__blocker { | ||||
|                 display: none; | ||||
|             } | ||||
|  | ||||
|             &__outer { | ||||
|                 @include overlaySizing($overlayOuterMarginFullscreen); | ||||
|                 @include overlaySizing($overlayOuterMarginLarge); | ||||
|                 padding: $tbPad $lrPad; | ||||
|             } | ||||
|  | ||||
|   | ||||
| @@ -32,6 +32,10 @@ export default class StatusAPI extends EventEmitter { | ||||
|         this.get = this.get.bind(this); | ||||
|         this.set = this.set.bind(this); | ||||
|         this.observe = this.observe.bind(this); | ||||
|  | ||||
|         openmct.once('destroy', () => { | ||||
|             this.removeAllListeners(); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     get(identifier) { | ||||
|   | ||||
							
								
								
									
										185
									
								
								src/exporters/ImageExporter.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										185
									
								
								src/exporters/ImageExporter.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,185 @@ | ||||
| /***************************************************************************** | ||||
|  * 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. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /** | ||||
|  * Class defining an image exporter for JPG/PNG output. | ||||
|  * Originally created by hudsonfoo on 09/02/16 | ||||
|  */ | ||||
|  | ||||
| function replaceDotsWithUnderscores(filename) { | ||||
|     const regex = /\./gi; | ||||
|  | ||||
|     return filename.replace(regex, '_'); | ||||
| } | ||||
|  | ||||
| import {saveAs} from 'file-saver/FileSaver'; | ||||
| import html2canvas from 'html2canvas'; | ||||
| import uuid from 'uuid'; | ||||
|  | ||||
| class ImageExporter { | ||||
|     constructor(openmct) { | ||||
|         this.openmct = openmct; | ||||
|     } | ||||
|     /** | ||||
|         * Converts an HTML element into a PNG or JPG Blob. | ||||
|         * @private | ||||
|         * @param {node} element that will be converted to an image | ||||
|         * @param {object} options Image options. | ||||
|         * @returns {promise} | ||||
|         */ | ||||
|     renderElement(element, { imageType, className, thumbnailSize }) { | ||||
|         const self = this; | ||||
|         const overlays = this.openmct.overlays; | ||||
|         const dialog = overlays.dialog({ | ||||
|             iconClass: 'info', | ||||
|             message: 'Caputuring an image', | ||||
|             buttons: [ | ||||
|                 { | ||||
|                     label: 'Cancel', | ||||
|                     emphasis: true, | ||||
|                     callback: function () { | ||||
|                         dialog.dismiss(); | ||||
|                     } | ||||
|                 } | ||||
|             ] | ||||
|         }); | ||||
|  | ||||
|         let mimeType = 'image/png'; | ||||
|         if (imageType === 'jpg') { | ||||
|             mimeType = 'image/jpeg'; | ||||
|         } | ||||
|  | ||||
|         let exportId = undefined; | ||||
|         let oldId = undefined; | ||||
|         if (className) { | ||||
|             const newUUID = uuid(); | ||||
|             exportId = `$export-element-${newUUID}`; | ||||
|             oldId = element.id; | ||||
|             element.id = exportId; | ||||
|         } | ||||
|  | ||||
|         return html2canvas(element, { | ||||
|             onclone: function (document) { | ||||
|                 if (className) { | ||||
|                     const clonedElement = document.getElementById(exportId); | ||||
|                     clonedElement.classList.add(className); | ||||
|                 } | ||||
|  | ||||
|                 element.id = oldId; | ||||
|             }, | ||||
|             removeContainer: true // Set to false to debug what html2canvas renders | ||||
|         }).then(function (canvas) { | ||||
|             dialog.dismiss(); | ||||
|  | ||||
|             return new Promise(function (resolve, reject) { | ||||
|                 if (thumbnailSize) { | ||||
|                     const thumbnail = self.getThumbnail(canvas, mimeType, thumbnailSize); | ||||
|  | ||||
|                     return canvas.toBlob(blob => resolve({ | ||||
|                         blob, | ||||
|                         thumbnail | ||||
|                     }), mimeType); | ||||
|                 } | ||||
|  | ||||
|                 return canvas.toBlob(blob => resolve({ blob }), mimeType); | ||||
|             }); | ||||
|         }, function (error) { | ||||
|             console.log('error capturing image', error); | ||||
|             dialog.dismiss(); | ||||
|             const errorDialog = overlays.dialog({ | ||||
|                 iconClass: 'error', | ||||
|                 message: 'Image was not captured successfully!', | ||||
|                 buttons: [ | ||||
|                     { | ||||
|                         label: "OK", | ||||
|                         emphasis: true, | ||||
|                         callback: function () { | ||||
|                             errorDialog.dismiss(); | ||||
|                         } | ||||
|                     } | ||||
|                 ] | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     getThumbnail(canvas, mimeType, size) { | ||||
|         const thumbnailCanvas = document.createElement('canvas'); | ||||
|         thumbnailCanvas.setAttribute('width', size.width); | ||||
|         thumbnailCanvas.setAttribute('height', size.height); | ||||
|         const ctx = thumbnailCanvas.getContext('2d'); | ||||
|         ctx.globalCompositeOperation = "copy"; | ||||
|         ctx.drawImage(canvas, 0, 0, canvas.width, canvas.height, 0, 0, size.width, size.height); | ||||
|  | ||||
|         return thumbnailCanvas.toDataURL(mimeType); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Takes a screenshot of a DOM node and exports to JPG. | ||||
|      * @param {node} element to be exported | ||||
|      * @param {string} filename the exported image | ||||
|      * @param {string} className to be added to element before capturing (optional) | ||||
|      * @returns {promise} | ||||
|      */ | ||||
|     async exportJPG(element, filename, className) { | ||||
|         const processedFilename = replaceDotsWithUnderscores(filename); | ||||
|  | ||||
|         const img = await this.renderElement(element, { | ||||
|             imageType: 'jpg', | ||||
|             className | ||||
|         }); | ||||
|         saveAs(img.blob, processedFilename); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Takes a screenshot of a DOM node and exports to PNG. | ||||
|      * @param {node} element to be exported | ||||
|      * @param {string} filename the exported image | ||||
|      * @param {string} className to be added to element before capturing (optional) | ||||
|      * @returns {promise} | ||||
|      */ | ||||
|     async exportPNG(element, filename, className) { | ||||
|         const processedFilename = replaceDotsWithUnderscores(filename); | ||||
|  | ||||
|         const img = await this.renderElement(element, { | ||||
|             imageType: 'png', | ||||
|             className | ||||
|         }); | ||||
|         saveAs(img.blob, processedFilename); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Takes a screenshot of a DOM node in PNG format. | ||||
|      * @param {node} element to be exported | ||||
|      * @param {string} filename the exported image | ||||
|      * @returns {promise} | ||||
|      */ | ||||
|  | ||||
|     exportPNGtoSRC(element, options) { | ||||
|         return this.renderElement(element, { | ||||
|             imageType: 'png', | ||||
|             ...options | ||||
|         }); | ||||
|     } | ||||
| } | ||||
|  | ||||
| export default ImageExporter; | ||||
|  | ||||
							
								
								
									
										58
									
								
								src/exporters/ImageExporterSpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								src/exporters/ImageExporterSpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,58 @@ | ||||
| /***************************************************************************** | ||||
|  * 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 ImageExporter from './ImageExporter'; | ||||
| import { createOpenMct, resetApplicationState } from '../utils/testing'; | ||||
|  | ||||
| describe('The Image Exporter', () => { | ||||
|     let openmct; | ||||
|     let imageExporter; | ||||
|  | ||||
|     beforeEach(() => { | ||||
|         openmct = createOpenMct(); | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         return resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|     describe("basic instatation", () => { | ||||
|         it("can be instatiated", () => { | ||||
|             imageExporter = new ImageExporter(openmct); | ||||
|  | ||||
|             expect(imageExporter).not.toEqual(null); | ||||
|         }); | ||||
|         it("can render an element to a blob", async () => { | ||||
|             const mockHeadElement = document.createElement("h1"); | ||||
|             const mockTextNode = document.createTextNode('foo bar'); | ||||
|             mockHeadElement.appendChild(mockTextNode); | ||||
|             document.body.appendChild(mockHeadElement); | ||||
|             imageExporter = new ImageExporter(openmct); | ||||
|             const returnedBlob = await imageExporter.renderElement(document.body, { | ||||
|                 imageType: 'png' | ||||
|             }); | ||||
|             expect(returnedBlob).not.toEqual(null); | ||||
|             expect(returnedBlob.blob).not.toEqual(null); | ||||
|             expect(returnedBlob.blob).toBeInstanceOf(Blob); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
| @@ -39,11 +39,10 @@ const DEFAULTS = [ | ||||
|     'platform/telemetry', | ||||
|     'platform/features/clock', | ||||
|     'platform/features/hyperlink', | ||||
|     'platform/features/timeline', | ||||
|     'platform/forms', | ||||
|     'platform/identity', | ||||
|     'platform/persistence/aggregator', | ||||
|     'platform/persistence/queue', | ||||
|     //'platform/persistence/queue', | ||||
|     'platform/policy', | ||||
|     'platform/entanglement', | ||||
|     'platform/search', | ||||
| @@ -84,7 +83,6 @@ define([ | ||||
|     '../platform/features/my-items/bundle', | ||||
|     '../platform/features/hyperlink/bundle', | ||||
|     '../platform/features/static-markup/bundle', | ||||
|     '../platform/features/timeline/bundle', | ||||
|     '../platform/forms/bundle', | ||||
|     '../platform/framework/bundle', | ||||
|     '../platform/framework/src/load/Bundle', | ||||
| @@ -92,7 +90,7 @@ define([ | ||||
|     '../platform/persistence/aggregator/bundle', | ||||
|     '../platform/persistence/elastic/bundle', | ||||
|     '../platform/persistence/local/bundle', | ||||
|     '../platform/persistence/queue/bundle', | ||||
|     //'../platform/persistence/queue/bundle', | ||||
|     '../platform/policy/bundle', | ||||
|     '../platform/representation/bundle', | ||||
|     '../platform/search/bundle', | ||||
|   | ||||
| @@ -19,8 +19,8 @@ | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
| import LadTableSet from './components/LadTableSet.vue'; | ||||
| import Vue from 'vue'; | ||||
|  | ||||
| import LadTableSetView from './LadTableSetView'; | ||||
|  | ||||
| export default function LADTableSetViewProvider(openmct) { | ||||
|     return { | ||||
| @@ -34,32 +34,7 @@ export default function LADTableSetViewProvider(openmct) { | ||||
|             return domainObject.type === 'LadTableSet'; | ||||
|         }, | ||||
|         view: function (domainObject, objectPath) { | ||||
|             let component; | ||||
|  | ||||
|             return { | ||||
|                 show: function (element) { | ||||
|                     component = new Vue({ | ||||
|                         el: element, | ||||
|                         components: { | ||||
|                             LadTableSet: LadTableSet | ||||
|                         }, | ||||
|                         provide: { | ||||
|                             openmct, | ||||
|                             objectPath | ||||
|                         }, | ||||
|                         data() { | ||||
|                             return { | ||||
|                                 domainObject | ||||
|                             }; | ||||
|                         }, | ||||
|                         template: '<lad-table-set :domain-object="domainObject"></lad-table-set>' | ||||
|                     }); | ||||
|                 }, | ||||
|                 destroy: function (element) { | ||||
|                     component.$destroy(); | ||||
|                     component = undefined; | ||||
|                 } | ||||
|             }; | ||||
|             return new LadTableSetView(openmct, domainObject, objectPath); | ||||
|         }, | ||||
|         priority: function () { | ||||
|             return 1; | ||||
|   | ||||
							
								
								
									
										45
									
								
								src/plugins/LADTable/LADTableView.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								src/plugins/LADTable/LADTableView.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | ||||
| import LadTable from './components/LADTable.vue'; | ||||
|  | ||||
| import Vue from 'vue'; | ||||
|  | ||||
| export default class LADTableView { | ||||
|     constructor(openmct, domainObject, objectPath) { | ||||
|         this.openmct = openmct; | ||||
|         this.domainObject = domainObject; | ||||
|         this.objectPath = objectPath; | ||||
|         this.component = undefined; | ||||
|     } | ||||
|  | ||||
|     show(element) { | ||||
|         this.component = new Vue({ | ||||
|             el: element, | ||||
|             components: { | ||||
|                 LadTable | ||||
|             }, | ||||
|             provide: { | ||||
|                 openmct: this.openmct, | ||||
|                 currentView: this | ||||
|             }, | ||||
|             data: () => { | ||||
|                 return { | ||||
|                     domainObject: this.domainObject, | ||||
|                     objectPath: this.objectPath | ||||
|                 }; | ||||
|             }, | ||||
|             template: '<lad-table ref="ladTable" :domain-object="domainObject" :object-path="objectPath"></lad-table>' | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     getViewContext() { | ||||
|         if (!this.component) { | ||||
|             return {}; | ||||
|         } | ||||
|  | ||||
|         return this.component.$refs.ladTable.getViewContext(); | ||||
|     } | ||||
|  | ||||
|     destroy(element) { | ||||
|         this.component.$destroy(); | ||||
|         this.component = undefined; | ||||
|     } | ||||
| } | ||||
| @@ -19,50 +19,30 @@ | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
| import LadTable from './components/LADTable.vue'; | ||||
| import Vue from 'vue'; | ||||
|  | ||||
| export default function LADTableViewProvider(openmct) { | ||||
|     return { | ||||
|         key: 'LadTable', | ||||
|         name: 'LAD Table', | ||||
|         cssClass: 'icon-tabular-lad', | ||||
|         canView: function (domainObject) { | ||||
|             return domainObject.type === 'LadTable'; | ||||
|         }, | ||||
|         canEdit: function (domainObject) { | ||||
|             return domainObject.type === 'LadTable'; | ||||
|         }, | ||||
|         view: function (domainObject, objectPath) { | ||||
|             let component; | ||||
| import LADTableView from './LADTableView'; | ||||
|  | ||||
|             return { | ||||
|                 show: function (element) { | ||||
|                     component = new Vue({ | ||||
|                         el: element, | ||||
|                         components: { | ||||
|                             LadTableComponent: LadTable | ||||
|                         }, | ||||
|                         provide: { | ||||
|                             openmct | ||||
|                         }, | ||||
|                         data: () => { | ||||
|                             return { | ||||
|                                 domainObject, | ||||
|                                 objectPath | ||||
|                             }; | ||||
|                         }, | ||||
|                         template: '<lad-table-component :domain-object="domainObject" :object-path="objectPath"></lad-table-component>' | ||||
|                     }); | ||||
|                 }, | ||||
|                 destroy: function (element) { | ||||
|                     component.$destroy(); | ||||
|                     component = undefined; | ||||
|                 } | ||||
|             }; | ||||
|         }, | ||||
|         priority: function () { | ||||
|             return 1; | ||||
|         } | ||||
|     }; | ||||
| export default class LADTableViewProvider { | ||||
|     constructor(openmct) { | ||||
|         this.openmct = openmct; | ||||
|         this.name = 'LAD Table'; | ||||
|         this.key = 'LadTable'; | ||||
|         this.cssClass = 'icon-tabular-lad'; | ||||
|     } | ||||
|  | ||||
|     canView(domainObject) { | ||||
|         return domainObject.type === 'LadTable'; | ||||
|     } | ||||
|  | ||||
|     canEdit(domainObject) { | ||||
|         return domainObject.type === 'LadTable'; | ||||
|     } | ||||
|  | ||||
|     view(domainObject, objectPath) { | ||||
|         return new LADTableView(this.openmct, domainObject, objectPath); | ||||
|     } | ||||
|  | ||||
|     priority(domainObject) { | ||||
|         return 1; | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										45
									
								
								src/plugins/LADTable/LadTableSetView.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								src/plugins/LADTable/LadTableSetView.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | ||||
| import LadTableSet from './components/LadTableSet.vue'; | ||||
|  | ||||
| import Vue from 'vue'; | ||||
|  | ||||
| export default class LadTableSetView { | ||||
|     constructor(openmct, domainObject, objectPath) { | ||||
|         this.openmct = openmct; | ||||
|         this.domainObject = domainObject; | ||||
|         this.objectPath = objectPath; | ||||
|         this.component = undefined; | ||||
|     } | ||||
|  | ||||
|     show(element) { | ||||
|         this.component = new Vue({ | ||||
|             el: element, | ||||
|             components: { | ||||
|                 LadTableSet | ||||
|             }, | ||||
|             provide: { | ||||
|                 openmct: this.openmct, | ||||
|                 objectPath: this.objectPath, | ||||
|                 currentView: this | ||||
|             }, | ||||
|             data: () => { | ||||
|                 return { | ||||
|                     domainObject: this.domainObject | ||||
|                 }; | ||||
|             }, | ||||
|             template: '<lad-table-set ref="ladTableSet" :domain-object="domainObject"></lad-table-set>' | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     getViewContext() { | ||||
|         if (!this.component) { | ||||
|             return {}; | ||||
|         } | ||||
|  | ||||
|         return this.component.$refs.ladTableSet.getViewContext(); | ||||
|     } | ||||
|  | ||||
|     destroy(element) { | ||||
|         this.component.$destroy(); | ||||
|         this.component = undefined; | ||||
|     } | ||||
| } | ||||
| @@ -50,7 +50,7 @@ const CONTEXT_MENU_ACTIONS = [ | ||||
| ]; | ||||
|  | ||||
| export default { | ||||
|     inject: ['openmct'], | ||||
|     inject: ['openmct', 'currentView'], | ||||
|     props: { | ||||
|         domainObject: { | ||||
|             type: Object, | ||||
| @@ -167,25 +167,23 @@ export default { | ||||
|             this.resetValues(); | ||||
|             this.timestampKey = timeSystem.key; | ||||
|         }, | ||||
|         getView() { | ||||
|             return { | ||||
|                 getViewContext: () => { | ||||
|                     return { | ||||
|                         viewHistoricalData: true, | ||||
|                         viewDatumAction: true, | ||||
|                         getDatum: () => { | ||||
|                             return this.datum; | ||||
|                         } | ||||
|                     }; | ||||
|         updateViewContext() { | ||||
|             this.$emit('rowContextClick', { | ||||
|                 viewHistoricalData: true, | ||||
|                 viewDatumAction: true, | ||||
|                 getDatum: () => { | ||||
|                     return this.datum; | ||||
|                 } | ||||
|             }; | ||||
|             }); | ||||
|         }, | ||||
|         showContextMenu(event) { | ||||
|             let actionCollection = this.openmct.actions.get(this.objectPath, this.getView()); | ||||
|             let allActions = actionCollection.getActionsObject(); | ||||
|             let applicableActions = CONTEXT_MENU_ACTIONS.map(key => allActions[key]); | ||||
|             this.updateViewContext(); | ||||
|  | ||||
|             this.openmct.menus.showMenu(event.x, event.y, applicableActions); | ||||
|             const actions = CONTEXT_MENU_ACTIONS.map(key => this.openmct.actions.getAction(key)); | ||||
|             const menuItems = this.openmct.menus.actionsToMenuItems(actions, this.objectPath, this.currentView); | ||||
|             if (menuItems.length) { | ||||
|                 this.openmct.menus.showMenu(event.x, event.y, menuItems); | ||||
|             } | ||||
|         }, | ||||
|         resetValues() { | ||||
|             this.value = '---'; | ||||
|   | ||||
| @@ -38,6 +38,7 @@ | ||||
|                 :domain-object="ladRow.domainObject" | ||||
|                 :path-to-table="objectPath" | ||||
|                 :has-units="hasUnits" | ||||
|                 @rowContextClick="updateViewContext" | ||||
|             /> | ||||
|         </tbody> | ||||
|     </table> | ||||
| @@ -51,7 +52,7 @@ export default { | ||||
|     components: { | ||||
|         LadRow | ||||
|     }, | ||||
|     inject: ['openmct'], | ||||
|     inject: ['openmct', 'currentView'], | ||||
|     props: { | ||||
|         domainObject: { | ||||
|             type: Object, | ||||
| @@ -64,7 +65,8 @@ export default { | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             items: [] | ||||
|             items: [], | ||||
|             viewContext: {} | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
| @@ -114,6 +116,12 @@ export default { | ||||
|             let metadataWithUnits = valueMetadatas.filter(metadatum => metadatum.unit); | ||||
|  | ||||
|             return metadataWithUnits.length > 0; | ||||
|         }, | ||||
|         updateViewContext(rowContext) { | ||||
|             this.viewContext.row = rowContext; | ||||
|         }, | ||||
|         getViewContext() { | ||||
|             return this.viewContext; | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|   | ||||
| @@ -48,6 +48,7 @@ | ||||
|                 :domain-object="ladRow.domainObject" | ||||
|                 :path-to-table="ladTable.objectPath" | ||||
|                 :has-units="hasUnits" | ||||
|                 @rowContextClick="updateViewContext" | ||||
|             /> | ||||
|         </template> | ||||
|     </tbody> | ||||
| @@ -61,7 +62,7 @@ export default { | ||||
|     components: { | ||||
|         LadRow | ||||
|     }, | ||||
|     inject: ['openmct', 'objectPath'], | ||||
|     inject: ['openmct', 'objectPath', 'currentView'], | ||||
|     props: { | ||||
|         domainObject: { | ||||
|             type: Object, | ||||
| @@ -72,7 +73,8 @@ export default { | ||||
|         return { | ||||
|             ladTableObjects: [], | ||||
|             ladTelemetryObjects: {}, | ||||
|             compositions: [] | ||||
|             compositions: [], | ||||
|             viewContext: {} | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
| @@ -166,6 +168,12 @@ export default { | ||||
|  | ||||
|                 this.$set(this.ladTelemetryObjects, ladTable.key, telemetryObjects); | ||||
|             }; | ||||
|         }, | ||||
|         updateViewContext(rowContext) { | ||||
|             this.viewContext.row = rowContext; | ||||
|         }, | ||||
|         getViewContext() { | ||||
|             return this.viewContext; | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|   | ||||
| @@ -45,6 +45,7 @@ export default class URLTimeSettingsSynchronizer { | ||||
|     } | ||||
|  | ||||
|     initialize() { | ||||
|         this.updateTimeSettings(); | ||||
|         this.openmct.router.on('change:params', this.updateTimeSettings); | ||||
|  | ||||
|         TIME_EVENTS.forEach(event => { | ||||
|   | ||||
| @@ -41,7 +41,7 @@ export default class ConditionManager extends EventEmitter { | ||||
|         this.subscriptions = {}; | ||||
|         this.telemetryObjects = {}; | ||||
|         this.testData = { | ||||
|             conditionTestData: [], | ||||
|             conditionTestInputs: this.conditionSetDomainObject.configuration.conditionTestData, | ||||
|             applied: false | ||||
|         }; | ||||
|         this.initialize(); | ||||
| @@ -154,8 +154,10 @@ export default class ConditionManager extends EventEmitter { | ||||
|  | ||||
|     updateConditionDescription(condition) { | ||||
|         const found = this.conditionSetDomainObject.configuration.conditionCollection.find(conditionConfiguration => (conditionConfiguration.id === condition.id)); | ||||
|         found.summary = condition.description; | ||||
|         this.persistConditions(); | ||||
|         if (found.summary !== condition.description) { | ||||
|             found.summary = condition.description; | ||||
|             this.persistConditions(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     initCondition(conditionConfiguration, index) { | ||||
| @@ -414,8 +416,10 @@ export default class ConditionManager extends EventEmitter { | ||||
|     } | ||||
|  | ||||
|     updateTestData(testData) { | ||||
|         this.testData = testData; | ||||
|         this.openmct.objects.mutate(this.conditionSetDomainObject, 'configuration.conditionTestData', this.testData.conditionTestInputs); | ||||
|         if (!_.isEqual(testData, this.testData)) { | ||||
|             this.testData = testData; | ||||
|             this.openmct.objects.mutate(this.conditionSetDomainObject, 'configuration.conditionTestData', this.testData.conditionTestInputs); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     persistConditions() { | ||||
|   | ||||
| @@ -215,7 +215,8 @@ export default { | ||||
|         }, | ||||
|         isEditing: { | ||||
|             type: Boolean, | ||||
|             required: true | ||||
|             required: true, | ||||
|             default: false | ||||
|         }, | ||||
|         telemetry: { | ||||
|             type: Array, | ||||
|   | ||||
| @@ -20,71 +20,78 @@ | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define([ | ||||
|     './components/AlphanumericFormatView.vue', | ||||
|     'vue' | ||||
| ], function (AlphanumericFormatView, Vue) { | ||||
| import AlphanumericFormat from './components/AlphanumericFormat.vue'; | ||||
|  | ||||
|     function AlphanumericFormatViewProvider(openmct, options) { | ||||
|         function isTelemetryObject(selectionPath) { | ||||
|             let selectedObject = selectionPath[0].context.item; | ||||
|             let parentObject = selectionPath[1].context.item; | ||||
|             let selectedLayoutItem = selectionPath[0].context.layoutItem; | ||||
| import Vue from 'vue'; | ||||
|  | ||||
|             return parentObject | ||||
|                 && parentObject.type === 'layout' | ||||
|                 && selectedObject | ||||
|                 && selectedLayoutItem | ||||
|                 && selectedLayoutItem.type === 'telemetry-view' | ||||
|                 && openmct.telemetry.isTelemetryObject(selectedObject) | ||||
|                 && !options.showAsView.includes(selectedObject.type); | ||||
|         } | ||||
|  | ||||
|         return { | ||||
|             key: 'alphanumeric-format', | ||||
|             name: 'Alphanumeric Format', | ||||
|             canView: function (selection) { | ||||
|                 if (selection.length === 0 || selection[0].length === 1) { | ||||
|                     return false; | ||||
|                 } | ||||
|  | ||||
|                 return selection.every(isTelemetryObject); | ||||
|             }, | ||||
|             view: function (domainObject, objectPath) { | ||||
|                 let component; | ||||
|  | ||||
|                 return { | ||||
|                     show: function (element) { | ||||
|                         component = new Vue({ | ||||
|                             el: element, | ||||
|                             components: { | ||||
|                                 AlphanumericFormatView: AlphanumericFormatView.default | ||||
|                             }, | ||||
|                             provide: { | ||||
|                                 openmct, | ||||
|                                 objectPath | ||||
|                             }, | ||||
|                             template: '<alphanumeric-format-view ref="alphanumericFormatView"></alphanumeric-format-view>' | ||||
|                         }); | ||||
|                     }, | ||||
|                     getViewContext() { | ||||
|                         if (component) { | ||||
|                             return component.$refs.alphanumericFormatView.getViewContext(); | ||||
|                         } else { | ||||
|                             return {}; | ||||
|                         } | ||||
|                     }, | ||||
|                     destroy: function () { | ||||
|                         component.$destroy(); | ||||
|                         component = undefined; | ||||
|                     } | ||||
|                 }; | ||||
|             }, | ||||
|             priority: function () { | ||||
|                 return 1; | ||||
|             } | ||||
|         }; | ||||
| class AlphanumericFormatView { | ||||
|     constructor(openmct, domainObject, objectPath) { | ||||
|         this.openmct = openmct; | ||||
|         this.domainObject = domainObject; | ||||
|         this.objectPath = objectPath; | ||||
|         this.component = undefined; | ||||
|     } | ||||
|  | ||||
|     return AlphanumericFormatViewProvider; | ||||
| }); | ||||
|     show(element) { | ||||
|         this.component = new Vue({ | ||||
|             el: element, | ||||
|             name: 'AlphanumericFormat', | ||||
|             components: { | ||||
|                 AlphanumericFormat | ||||
|             }, | ||||
|             provide: { | ||||
|                 openmct: this.openmct, | ||||
|                 objectPath: this.objectPath, | ||||
|                 currentView: this | ||||
|             }, | ||||
|             template: '<alphanumeric-format ref="alphanumericFormat"></alphanumeric-format>' | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     getViewContext() { | ||||
|         if (this.component) { | ||||
|             return {}; | ||||
|         } | ||||
|  | ||||
|         return this.component.$refs.alphanumericFormat.getViewContext(); | ||||
|     } | ||||
|  | ||||
|     destroy() { | ||||
|         this.component.$destroy(); | ||||
|         this.component = undefined; | ||||
|     } | ||||
| } | ||||
|  | ||||
| export default function AlphanumericFormatViewProvider(openmct, options) { | ||||
|     function isTelemetryObject(selectionPath) { | ||||
|         let selectedObject = selectionPath[0].context.item; | ||||
|         let parentObject = selectionPath[1].context.item; | ||||
|         let selectedLayoutItem = selectionPath[0].context.layoutItem; | ||||
|  | ||||
|         return parentObject | ||||
|             && parentObject.type === 'layout' | ||||
|             && selectedObject | ||||
|             && selectedLayoutItem | ||||
|             && selectedLayoutItem.type === 'telemetry-view' | ||||
|             && openmct.telemetry.isTelemetryObject(selectedObject) | ||||
|             && !options.showAsView.includes(selectedObject.type); | ||||
|     } | ||||
|  | ||||
|     return { | ||||
|         key: 'alphanumeric-format', | ||||
|         name: 'Alphanumeric Format', | ||||
|         canView: function (selection) { | ||||
|             if (selection.length === 0 || selection[0].length === 1) { | ||||
|                 return false; | ||||
|             } | ||||
|  | ||||
|             return selection.every(isTelemetryObject); | ||||
|         }, | ||||
|         view: function (domainObject, objectPath) { | ||||
|             return new AlphanumericFormatView(openmct, domainObject, objectPath); | ||||
|         }, | ||||
|         priority: function () { | ||||
|             return 1; | ||||
|         } | ||||
|     }; | ||||
| } | ||||
|   | ||||
| @@ -14,7 +14,7 @@ export default class CopyToClipboardAction { | ||||
|  | ||||
|     invoke(objectPath, view = {}) { | ||||
|         const viewContext = view.getViewContext && view.getViewContext(); | ||||
|         const formattedValue = viewContext.formattedValueForCopy(); | ||||
|         const formattedValue = viewContext.row.formattedValueForCopy(); | ||||
|  | ||||
|         clipboard.updateClipboard(formattedValue) | ||||
|             .then(() => { | ||||
| @@ -26,9 +26,13 @@ export default class CopyToClipboardAction { | ||||
|     } | ||||
|  | ||||
|     appliesTo(objectPath, view = {}) { | ||||
|         let viewContext = view.getViewContext && view.getViewContext(); | ||||
|         const viewContext = view.getViewContext && view.getViewContext(); | ||||
|         const row = viewContext && viewContext.row; | ||||
|         if (!row) { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         return viewContext && viewContext.formattedValueForCopy | ||||
|             && typeof viewContext.formattedValueForCopy === 'function'; | ||||
|         return row.formattedValueForCopy | ||||
|             && typeof row.formattedValueForCopy === 'function'; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -52,7 +52,8 @@ | ||||
| 
 | ||||
| <script> | ||||
| export default { | ||||
|     inject: ['openmct'], | ||||
|     name: 'AlphanumericFormat', | ||||
|     inject: ['openmct', 'objectPath'], | ||||
|     data() { | ||||
|         return { | ||||
|             isEditing: this.openmct.editor.isEditing(), | ||||
| @@ -56,6 +56,7 @@ | ||||
|         :index="index" | ||||
|         :multi-select="selectedLayoutItems.length > 1" | ||||
|         :is-editing="isEditing" | ||||
|         @contextClick="updateViewContext" | ||||
|         @move="move" | ||||
|         @endMove="endMove" | ||||
|         @endLineResize="endLineResize" | ||||
| @@ -140,7 +141,7 @@ function getItemDefinition(itemType, ...options) { | ||||
|  | ||||
| export default { | ||||
|     components: components, | ||||
|     inject: ['openmct', 'options', 'objectPath'], | ||||
|     inject: ['openmct', 'objectPath', 'options', 'objectUtils', 'currentView'], | ||||
|     props: { | ||||
|         domainObject: { | ||||
|             type: Object, | ||||
| @@ -155,7 +156,8 @@ export default { | ||||
|         return { | ||||
|             initSelectIndex: undefined, | ||||
|             selection: [], | ||||
|             showGrid: true | ||||
|             showGrid: true, | ||||
|             viewContext: {} | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
| @@ -819,6 +821,12 @@ export default { | ||||
|         }, | ||||
|         toggleGrid() { | ||||
|             this.showGrid = !this.showGrid; | ||||
|         }, | ||||
|         updateViewContext(viewContext) { | ||||
|             this.viewContext.row = viewContext; | ||||
|         }, | ||||
|         getViewContext() { | ||||
|             return this.viewContext; | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|   | ||||
| @@ -102,7 +102,7 @@ export default { | ||||
|         LayoutFrame | ||||
|     }, | ||||
|     mixins: [conditionalStylesMixin], | ||||
|     inject: ['openmct', 'objectPath'], | ||||
|     inject: ['openmct', 'objectPath', 'currentView'], | ||||
|     props: { | ||||
|         item: { | ||||
|             type: Object, | ||||
| @@ -294,16 +294,6 @@ export default { | ||||
|                 this.requestHistoricalData(this.domainObject); | ||||
|             } | ||||
|         }, | ||||
|         getView() { | ||||
|             return { | ||||
|                 getViewContext: () => { | ||||
|                     return { | ||||
|                         viewHistoricalData: true, | ||||
|                         formattedValueForCopy: this.formattedValueForCopy | ||||
|                     }; | ||||
|                 } | ||||
|             }; | ||||
|         }, | ||||
|         setObject(domainObject) { | ||||
|             this.domainObject = domainObject; | ||||
|             this.mutablePromise = undefined; | ||||
| @@ -338,30 +328,38 @@ export default { | ||||
|  | ||||
|             this.$emit('formatChanged', this.item, format); | ||||
|         }, | ||||
|         updateViewContext() { | ||||
|             this.$emit('contextClick', { | ||||
|                 viewHistoricalData: true, | ||||
|                 formattedValueForCopy: this.formattedValueForCopy | ||||
|             }); | ||||
|         }, | ||||
|         async getContextMenuActions() { | ||||
|             const defaultNotebook = getDefaultNotebook(); | ||||
|             const domainObject = defaultNotebook && await this.openmct.objects.get(defaultNotebook.notebookMeta.identifier); | ||||
|             const actionCollection = this.openmct.actions.get(this.currentObjectPath, this.getView()); | ||||
|             const actionsObject = actionCollection.getActionsObject(); | ||||
|  | ||||
|             let copyToNotebookAction = actionsObject.copyToNotebook; | ||||
|  | ||||
|             let defaultNotebookName; | ||||
|             if (defaultNotebook) { | ||||
|                 const defaultPath = domainObject && `${domainObject.name} - ${defaultNotebook.section.name} - ${defaultNotebook.page.name}`; | ||||
|                 copyToNotebookAction.name = `Copy to Notebook ${defaultPath}`; | ||||
|             } else { | ||||
|                 actionsObject.copyToNotebook = undefined; | ||||
|                 delete actionsObject.copyToNotebook; | ||||
|                 defaultNotebookName = `Copy to Notebook ${defaultPath}`; | ||||
|             } | ||||
|  | ||||
|             return CONTEXT_MENU_ACTIONS.map(actionKey => { | ||||
|                 return actionsObject[actionKey]; | ||||
|             }).filter(action => action !== undefined); | ||||
|             return CONTEXT_MENU_ACTIONS | ||||
|                 .map(actionKey => { | ||||
|                     const action = this.openmct.actions.getAction(actionKey); | ||||
|                     if (action.key === 'copyToNotebook') { | ||||
|                         action.name = defaultNotebookName; | ||||
|                     } | ||||
|  | ||||
|                     return action; | ||||
|                 }) | ||||
|                 .filter(action => action.name !== undefined); | ||||
|         }, | ||||
|         async showContextMenu(event) { | ||||
|             this.updateViewContext(); | ||||
|             const contextMenuActions = await this.getContextMenuActions(); | ||||
|  | ||||
|             this.openmct.menus.showMenu(event.x, event.y, contextMenuActions); | ||||
|             const menuItems = this.openmct.menus.actionsToMenuItems(contextMenuActions, this.currentObjectPath, this.currentView); | ||||
|             this.openmct.menus.showMenu(event.x, event.y, menuItems); | ||||
|         }, | ||||
|         setStatus(status) { | ||||
|             this.status = status; | ||||
|   | ||||
| @@ -20,13 +20,81 @@ | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| import Layout from './components/DisplayLayout.vue'; | ||||
| import Vue from 'vue'; | ||||
| import objectUtils from 'objectUtils'; | ||||
| import DisplayLayoutType from './DisplayLayoutType.js'; | ||||
| import DisplayLayoutToolbar from './DisplayLayoutToolbar.js'; | ||||
| import AlphaNumericFormatViewProvider from './AlphanumericFormatViewProvider.js'; | ||||
| import CopyToClipboardAction from './actions/CopyToClipboardAction'; | ||||
| import DisplayLayout from './components/DisplayLayout.vue'; | ||||
| import DisplayLayoutToolbar from './DisplayLayoutToolbar.js'; | ||||
| import DisplayLayoutType from './DisplayLayoutType.js'; | ||||
|  | ||||
| import objectUtils from 'objectUtils'; | ||||
|  | ||||
| import Vue from 'vue'; | ||||
|  | ||||
| class DisplayLayoutView { | ||||
|     constructor(openmct, domainObject, objectPath, options) { | ||||
|         this.openmct = openmct; | ||||
|         this.domainObject = domainObject; | ||||
|         this.objectPath = objectPath; | ||||
|         this.options = options; | ||||
|  | ||||
|         this.component = undefined; | ||||
|     } | ||||
|  | ||||
|     show(container, isEditing) { | ||||
|         this.component = new Vue({ | ||||
|             el: container, | ||||
|             components: { | ||||
|                 DisplayLayout | ||||
|             }, | ||||
|             provide: { | ||||
|                 openmct: this.openmct, | ||||
|                 objectPath: this.objectPath, | ||||
|                 options: this.options, | ||||
|                 objectUtils, | ||||
|                 currentView: this | ||||
|             }, | ||||
|             data: () => { | ||||
|                 return { | ||||
|                     domainObject: this.domainObject, | ||||
|                     isEditing | ||||
|                 }; | ||||
|             }, | ||||
|             template: '<display-layout ref="displayLayout" :domain-object="domainObject" :is-editing="isEditing"></display-layout>' | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     getViewContext() { | ||||
|         if (!this.component) { | ||||
|             return {}; | ||||
|         } | ||||
|  | ||||
|         return this.component.$refs.displayLayout.getViewContext(); | ||||
|     } | ||||
|  | ||||
|     getSelectionContext() { | ||||
|         return { | ||||
|             item: this.domainObject, | ||||
|             supportsMultiSelect: true, | ||||
|             addElement: this.component && this.component.$refs.displayLayout.addElement, | ||||
|             removeItem: this.component && this.component.$refs.displayLayout.removeItem, | ||||
|             orderItem: this.component && this.component.$refs.displayLayout.orderItem, | ||||
|             duplicateItem: this.component && this.component.$refs.displayLayout.duplicateItem, | ||||
|             switchViewType: this.component && this.component.$refs.displayLayout.switchViewType, | ||||
|             mergeMultipleTelemetryViews: this.component && this.component.$refs.displayLayout.mergeMultipleTelemetryViews, | ||||
|             mergeMultipleOverlayPlots: this.component && this.component.$refs.displayLayout.mergeMultipleOverlayPlots, | ||||
|             toggleGrid: this.component && this.component.$refs.displayLayout.toggleGrid | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     onEditModeChange(isEditing) { | ||||
|         this.component.isEditing = isEditing; | ||||
|     } | ||||
|  | ||||
|     destroy() { | ||||
|         this.component.$destroy(); | ||||
|         this.component = undefined; | ||||
|     } | ||||
| } | ||||
|  | ||||
| export default function DisplayLayoutPlugin(options) { | ||||
|     return function (openmct) { | ||||
| @@ -41,51 +109,7 @@ export default function DisplayLayoutPlugin(options) { | ||||
|                 return domainObject.type === 'layout'; | ||||
|             }, | ||||
|             view: function (domainObject, objectPath) { | ||||
|                 let component; | ||||
|  | ||||
|                 return { | ||||
|                     show(container) { | ||||
|                         component = new Vue({ | ||||
|                             el: container, | ||||
|                             components: { | ||||
|                                 Layout | ||||
|                             }, | ||||
|                             provide: { | ||||
|                                 openmct, | ||||
|                                 objectUtils, | ||||
|                                 options, | ||||
|                                 objectPath | ||||
|                             }, | ||||
|                             data() { | ||||
|                                 return { | ||||
|                                     domainObject: domainObject, | ||||
|                                     isEditing: openmct.editor.isEditing() | ||||
|                                 }; | ||||
|                             }, | ||||
|                             template: '<layout ref="displayLayout" :domain-object="domainObject" :is-editing="isEditing"></layout>' | ||||
|                         }); | ||||
|                     }, | ||||
|                     getSelectionContext() { | ||||
|                         return { | ||||
|                             item: domainObject, | ||||
|                             supportsMultiSelect: true, | ||||
|                             addElement: component && component.$refs.displayLayout.addElement, | ||||
|                             removeItem: component && component.$refs.displayLayout.removeItem, | ||||
|                             orderItem: component && component.$refs.displayLayout.orderItem, | ||||
|                             duplicateItem: component && component.$refs.displayLayout.duplicateItem, | ||||
|                             switchViewType: component && component.$refs.displayLayout.switchViewType, | ||||
|                             mergeMultipleTelemetryViews: component && component.$refs.displayLayout.mergeMultipleTelemetryViews, | ||||
|                             mergeMultipleOverlayPlots: component && component.$refs.displayLayout.mergeMultipleOverlayPlots, | ||||
|                             toggleGrid: component && component.$refs.displayLayout.toggleGrid | ||||
|                         }; | ||||
|                     }, | ||||
|                     onEditModeChange: function (isEditing) { | ||||
|                         component.isEditing = isEditing; | ||||
|                     }, | ||||
|                     destroy() { | ||||
|                         component.$destroy(); | ||||
|                     } | ||||
|                 }; | ||||
|                 return new DisplayLayoutView(openmct, domainObject, objectPath, options); | ||||
|             }, | ||||
|             priority() { | ||||
|                 return 100; | ||||
|   | ||||
| @@ -37,7 +37,15 @@ export default class DuplicateAction { | ||||
|         let duplicationTask = new DuplicateTask(this.openmct); | ||||
|         let originalObject = objectPath[0]; | ||||
|         let parent = objectPath[1]; | ||||
|         let userInput = await this.getUserInput(originalObject, parent); | ||||
|         let userInput; | ||||
|  | ||||
|         try { | ||||
|             userInput = await this.getUserInput(originalObject, parent); | ||||
|         } catch (error) { | ||||
|             // user most likely canceled | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         let newParent = userInput.location; | ||||
|         let inNavigationPath = this.inNavigationPath(originalObject); | ||||
|  | ||||
|   | ||||
| @@ -23,6 +23,11 @@ | ||||
|     body.mobile & { | ||||
|         flex: 1 0 auto; | ||||
|     } | ||||
|  | ||||
|     [class*='l-overlay'] & { | ||||
|         // When this view is in an overlay, prevent navigation | ||||
|         pointer-events: none; | ||||
|     } | ||||
| } | ||||
|  | ||||
| /******************************* GRID ITEMS */ | ||||
|   | ||||
| @@ -22,4 +22,9 @@ | ||||
|             @include isAlias(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     [class*='l-overlay'] & { | ||||
|         // When this view is in an overlay, prevent navigation | ||||
|         pointer-events: none; | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										37
									
								
								src/plugins/imagery/ImageryView.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								src/plugins/imagery/ImageryView.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | ||||
| import ImageryViewLayout from './components/ImageryViewLayout.vue'; | ||||
|  | ||||
| import Vue from 'vue'; | ||||
|  | ||||
| export default class ImageryView { | ||||
|     constructor(openmct, domainObject, objectPath) { | ||||
|         this.openmct = openmct; | ||||
|         this.domainObject = domainObject; | ||||
|         this.objectPath = objectPath; | ||||
|         this.component = undefined; | ||||
|     } | ||||
|  | ||||
|     show(element) { | ||||
|         this.component = new Vue({ | ||||
|             el: element, | ||||
|             components: { | ||||
|                 ImageryViewLayout | ||||
|             }, | ||||
|             provide: { | ||||
|                 openmct: this.openmct, | ||||
|                 domainObject: this.domainObject, | ||||
|                 objectPath: this.objectPath, | ||||
|                 currentView: this | ||||
|             }, | ||||
|             template: '<imagery-view-layout ref="ImageryLayout"></imagery-view-layout>' | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     destroy() { | ||||
|         this.component.$destroy(); | ||||
|         this.component = undefined; | ||||
|     } | ||||
|  | ||||
|     _getInstance() { | ||||
|         return this.component; | ||||
|     } | ||||
| } | ||||
| @@ -19,9 +19,7 @@ | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| import ImageryViewLayout from './components/ImageryViewLayout.vue'; | ||||
| import Vue from 'vue'; | ||||
| import ImageryView from './ImageryView'; | ||||
|  | ||||
| export default function ImageryViewProvider(openmct) { | ||||
|     const type = 'example.imagery'; | ||||
| @@ -42,28 +40,8 @@ export default function ImageryViewProvider(openmct) { | ||||
|         canView: function (domainObject) { | ||||
|             return hasImageTelemetry(domainObject); | ||||
|         }, | ||||
|         view: function (domainObject) { | ||||
|             let component; | ||||
|  | ||||
|             return { | ||||
|                 show: function (element) { | ||||
|                     component = new Vue({ | ||||
|                         el: element, | ||||
|                         components: { | ||||
|                             ImageryViewLayout | ||||
|                         }, | ||||
|                         provide: { | ||||
|                             openmct, | ||||
|                             domainObject | ||||
|                         }, | ||||
|                         template: '<imagery-view-layout ref="ImageryLayout"></imagery-view-layout>' | ||||
|                     }); | ||||
|                 }, | ||||
|                 destroy: function () { | ||||
|                     component.$destroy(); | ||||
|                     component = undefined; | ||||
|                 } | ||||
|             }; | ||||
|         view: function (domainObject, objectPath) { | ||||
|             return new ImageryView(openmct, domainObject, objectPath); | ||||
|         } | ||||
|     }; | ||||
| } | ||||
|   | ||||
| @@ -23,7 +23,7 @@ | ||||
| <template> | ||||
| <div | ||||
|     class="c-compass" | ||||
|     :style="`width: ${ sizedImageDimensions.width }px; height: ${ sizedImageDimensions.height }px`" | ||||
|     :style="`width: 100%; height: 100%`" | ||||
| > | ||||
|     <CompassHUD | ||||
|         v-if="hasCameraFieldOfView" | ||||
| @@ -33,13 +33,12 @@ | ||||
|     /> | ||||
|     <CompassRose | ||||
|         v-if="hasCameraFieldOfView" | ||||
|         :heading="heading" | ||||
|         :sized-image-width="sizedImageDimensions.width" | ||||
|         :sun-heading="sunHeading" | ||||
|         :camera-angle-of-view="cameraAngleOfView" | ||||
|         :camera-pan="cameraPan" | ||||
|         :lock-compass="lockCompass" | ||||
|         @toggle-lock-compass="toggleLockCompass" | ||||
|         :compass-rose-sizing-classes="compassRoseSizingClasses" | ||||
|         :heading="heading" | ||||
|         :sized-image-dimensions="sizedImageDimensions" | ||||
|         :sun-heading="sunHeading" | ||||
|     /> | ||||
| </div> | ||||
| </template> | ||||
| @@ -56,42 +55,20 @@ export default { | ||||
|         CompassRose | ||||
|     }, | ||||
|     props: { | ||||
|         containerWidth: { | ||||
|             type: Number, | ||||
|             required: true | ||||
|         }, | ||||
|         containerHeight: { | ||||
|             type: Number, | ||||
|             required: true | ||||
|         }, | ||||
|         naturalAspectRatio: { | ||||
|             type: Number, | ||||
|         compassRoseSizingClasses: { | ||||
|             type: String, | ||||
|             required: true | ||||
|         }, | ||||
|         image: { | ||||
|             type: Object, | ||||
|             required: true | ||||
|         }, | ||||
|         lockCompass: { | ||||
|             type: Boolean, | ||||
|         sizedImageDimensions: { | ||||
|             type: Object, | ||||
|             required: true | ||||
|         } | ||||
|     }, | ||||
|     computed: { | ||||
|         sizedImageDimensions() { | ||||
|             let sizedImageDimensions = {}; | ||||
|             if ((this.containerWidth / this.containerHeight) > this.naturalAspectRatio) { | ||||
|                 // container is wider than image | ||||
|                 sizedImageDimensions.width = this.containerHeight * this.naturalAspectRatio; | ||||
|                 sizedImageDimensions.height = this.containerHeight; | ||||
|             } else { | ||||
|                 // container is taller than image | ||||
|                 sizedImageDimensions.width = this.containerWidth; | ||||
|                 sizedImageDimensions.height = this.containerWidth * this.naturalAspectRatio; | ||||
|             } | ||||
|  | ||||
|             return sizedImageDimensions; | ||||
|         }, | ||||
|         hasCameraFieldOfView() { | ||||
|             return this.cameraPan !== undefined && this.cameraAngleOfView > 0; | ||||
|         }, | ||||
|   | ||||
| @@ -21,152 +21,203 @@ | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| <template> | ||||
| <div | ||||
|     class="w-direction-rose" | ||||
|     :class="compassRoseSizingClasses" | ||||
| <div ref="compassRoseWrapper" | ||||
|      class="w-direction-rose" | ||||
|      :class="compassRoseSizingClasses" | ||||
|      @click="toggleLockCompass" | ||||
| > | ||||
|     <div | ||||
|         class="c-direction-rose" | ||||
|         @click="toggleLockCompass" | ||||
|     <svg ref="compassRoseSvg" | ||||
|          class="c-compass-rose-svg" | ||||
|          viewBox="0 0 100 100" | ||||
|     > | ||||
|         <div | ||||
|             class="c-nsew" | ||||
|             :style="compassRoseStyle" | ||||
|         <mask id="mask0" | ||||
|               mask-type="alpha" | ||||
|               maskUnits="userSpaceOnUse" | ||||
|               x="0" | ||||
|               y="0" | ||||
|               width="100" | ||||
|               height="100" | ||||
|         > | ||||
|             <svg | ||||
|                 class="c-nsew__minor-ticks" | ||||
|                 viewBox="0 0 100 100" | ||||
|             <circle cx="50" | ||||
|                     cy="50" | ||||
|                     r="50" | ||||
|                     fill="black" | ||||
|             /> | ||||
|         </mask> | ||||
|         <g class="c-cr__compass-wrapper"> | ||||
|             <g class="c-cr__compass-main" | ||||
|                mask="url(#mask0)" | ||||
|             > | ||||
|                 <rect | ||||
|                     class="c-nsew__tick c-tick-ne" | ||||
|                     x="49" | ||||
|                     y="0" | ||||
|                     width="2" | ||||
|                     height="5" | ||||
|                 <!-- Background and clipped elements --> | ||||
|                 <rect class="c-cr__bg" | ||||
|                       width="100" | ||||
|                       height="100" | ||||
|                       fill="black" | ||||
|                 /> | ||||
|                 <rect | ||||
|                     class="c-nsew__tick c-tick-se" | ||||
|                     x="95" | ||||
|                     y="49" | ||||
|                     width="5" | ||||
|                     height="2" | ||||
|                 <rect class="c-cr__edge" | ||||
|                       width="100" | ||||
|                       height="100" | ||||
|                       fill="url(#paint0_radial)" | ||||
|                 /> | ||||
|                 <rect | ||||
|                     class="c-nsew__tick c-tick-sw" | ||||
|                     x="49" | ||||
|                     y="95" | ||||
|                     width="2" | ||||
|                     height="5" | ||||
|                 /> | ||||
|                 <rect | ||||
|                     class="c-nsew__tick c-tick-nw" | ||||
|                     x="0" | ||||
|                     y="49" | ||||
|                     width="5" | ||||
|                     height="2" | ||||
|                 <rect v-if="hasSunHeading" | ||||
|                       class="c-cr__sun" | ||||
|                       width="100" | ||||
|                       height="100" | ||||
|                       fill="url(#paint1_radial)" | ||||
|                       :style="sunHeadingStyle" | ||||
|                 /> | ||||
|  | ||||
|             </svg> | ||||
|                 <!-- Camera FOV --> | ||||
|                 <mask id="mask2" | ||||
|                       class="c-cr__cam-fov-l-mask" | ||||
|                       mask-type="alpha" | ||||
|                       maskUnits="userSpaceOnUse" | ||||
|                       x="0" | ||||
|                       y="0" | ||||
|                       width="50" | ||||
|                       height="100" | ||||
|                 > | ||||
|                     <rect width="51" | ||||
|                           height="100" | ||||
|                     /> | ||||
|                 </mask> | ||||
|                 <mask id="mask1" | ||||
|                       class="c-cr__cam-fov-r-mask" | ||||
|                       mask-type="alpha" | ||||
|                       maskUnits="userSpaceOnUse" | ||||
|                       x="50" | ||||
|                       y="0" | ||||
|                       width="50" | ||||
|                       height="100" | ||||
|                 > | ||||
|                     <rect x="49" | ||||
|                           width="51" | ||||
|                           height="100" | ||||
|                     /> | ||||
|                 </mask> | ||||
|                 <g class="c-cr__cam-fov" | ||||
|                    :style="cameraPanStyle" | ||||
|                 > | ||||
|                     <g mask="url(#mask2)"> | ||||
|                         <rect class="c-cr__cam-fov-r" | ||||
|                               x="49" | ||||
|                               width="51" | ||||
|                               height="100" | ||||
|                               :style="cameraFOVStyleRightHalf" | ||||
|                         /> | ||||
|                     </g> | ||||
|                     <g mask="url(#mask1)"> | ||||
|                         <rect class="c-cr__cam-fov-l" | ||||
|                               width="51" | ||||
|                               height="100" | ||||
|                               :style="cameraFOVStyleLeftHalf" | ||||
|                         /> | ||||
|                     </g> | ||||
|                 </g> | ||||
|             </g> | ||||
|  | ||||
|             <svg | ||||
|                 class="c-nsew__ticks" | ||||
|                 viewBox="0 0 100 100" | ||||
|             <!-- Spacecraft body --> | ||||
|             <path v-if="hasHeading" | ||||
|                   class="c-cr__spacecraft-body" | ||||
|                   fill-rule="evenodd" | ||||
|                   clip-rule="evenodd" | ||||
|                   d="M37 49C35.3431 49 34 50.3431 34 52V82C34 83.6569 35.3431 85 37 85H63C64.6569 85 66 83.6569 66 82V52C66 50.3431 64.6569 49 63 49H37ZM50 52L58 60H55V67H45V60H42L50 52Z" | ||||
|                   :style="headingStyle" | ||||
|             /> | ||||
|  | ||||
|             <!-- NSEW and ticks --> | ||||
|             <g class="c-cr__nsew" | ||||
|                :style="compassRoseStyle" | ||||
|             > | ||||
|                 <polygon | ||||
|                     class="c-nsew__tick c-tick-n" | ||||
|                     points="50,0 60,10 40,10" | ||||
|                 <g class="c-cr__ticks-major"> | ||||
|                     <path d="M50 3L43 10H57L50 3Z" /> | ||||
|                     <path d="M4 51V49H10V51H4Z" | ||||
|                           class="--hide-min" | ||||
|                     /> | ||||
|                     <path d="M49 96V90H51V96H49Z" | ||||
|                           class="--hide-min" | ||||
|                     /> | ||||
|                     <path d="M90 49V51H96V49H90Z" | ||||
|                           class="--hide-min" | ||||
|                     /> | ||||
|                 </g> | ||||
|                 <g class="c-cr__ticks-minor --hide-small"> | ||||
|                     <path d="M4 51V49H10V51H4Z" /> | ||||
|                     <path d="M90 51V49H96V51H90Z" /> | ||||
|                     <path d="M51 96H49V90H51V96Z" /> | ||||
|                     <path d="M51 10L49 10V4L51 4V10Z" /> | ||||
|                 </g> | ||||
|                 <g class="c-cr__nsew-text"> | ||||
|                     <path :style="cardinalTextRotateW" | ||||
|                           class="c-cr__nsew-w --hide-small" | ||||
|                           d="M56.7418 45.004H54.1378L52.7238 52.312H52.6958L51.2258 45.004H48.7758L47.3058 52.312H47.2778L45.8638 45.004H43.2598L45.9618 55H48.6078L49.9798 48.112H50.0078L51.3798 55H53.9838L56.7418 45.004Z" | ||||
|                     /> | ||||
|                     <path :style="cardinalTextRotateE" | ||||
|                           class="c-cr__nsew-e --hide-small" | ||||
|                           d="M46.104 55H54.21V52.76H48.708V50.856H53.608V48.84H48.708V47.09H54.07V45.004H46.104V55Z" | ||||
|                     /> | ||||
|                     <path :style="cardinalTextRotateS" | ||||
|                           class="c-cr__nsew-s --hide-small" | ||||
|                           d="M45.6531 51.64C45.6671 54.202 47.6971 55.21 49.9931 55.21C52.1911 55.21 54.3471 54.398 54.3471 51.864C54.3471 50.058 52.8911 49.386 51.4491 48.98C49.9931 48.574 48.5511 48.434 48.5511 47.664C48.5511 47.006 49.2511 46.81 49.8111 46.81C50.6091 46.81 51.4631 47.104 51.4211 48.014H54.0251C54.0111 45.76 52.0091 44.794 50.0211 44.794C48.1451 44.794 45.9471 45.648 45.9471 47.832C45.9471 49.666 47.4451 50.31 48.8731 50.716C50.3151 51.122 51.7431 51.29 51.7431 52.172C51.7431 52.914 50.9311 53.194 50.1471 53.194C49.0411 53.194 48.3131 52.816 48.2571 51.64H45.6531Z" | ||||
|                     /> | ||||
|                     <path :style="cardinalTextRotateN" | ||||
|                           class="c-cr__nsew-n" | ||||
|                           d="M42.5935 60H46.7935V49.32H46.8415L52.7935 60H57.3775V42.864H53.1775V53.424H53.1295L47.1775 42.864H42.5935V60Z" | ||||
|                     /> | ||||
|                 </g> | ||||
|             </g> | ||||
|         </g> | ||||
|         <defs> | ||||
|             <radialGradient id="paint0_radial" | ||||
|                             cx="0" | ||||
|                             cy="0" | ||||
|                             r="1" | ||||
|                             gradientUnits="userSpaceOnUse" | ||||
|                             gradientTransform="translate(50 50) rotate(90) scale(50)" | ||||
|             > | ||||
|                 <stop offset="0.751387" | ||||
|                       stop-opacity="0" | ||||
|                 /> | ||||
|                 <rect | ||||
|                     class="c-nsew__tick c-tick-e" | ||||
|                     x="95" | ||||
|                     y="49" | ||||
|                     width="5" | ||||
|                     height="2" | ||||
|                 <stop offset="1" | ||||
|                       stop-color="white" | ||||
|                 /> | ||||
|                 <rect | ||||
|                     class="c-nsew__tick c-tick-w" | ||||
|                     x="0" | ||||
|                     y="49" | ||||
|                     width="5" | ||||
|                     height="2" | ||||
|             </radialGradient> | ||||
|             <radialGradient id="paint1_radial" | ||||
|                             cx="0" | ||||
|                             cy="0" | ||||
|                             r="1" | ||||
|                             gradientUnits="userSpaceOnUse" | ||||
|                             gradientTransform="translate(50 -7) rotate(-90) scale(18.5)" | ||||
|             > | ||||
|                 <stop offset="0.716377" | ||||
|                       stop-color="#FFCC00" | ||||
|                 /> | ||||
|                 <rect | ||||
|                     class="c-nsew__tick c-tick-s" | ||||
|                     x="49" | ||||
|                     y="95" | ||||
|                     width="2" | ||||
|                     height="5" | ||||
|                 <stop offset="1" | ||||
|                       stop-color="#FF9900" | ||||
|                       stop-opacity="0" | ||||
|                 /> | ||||
|  | ||||
|                 <text | ||||
|                     class="c-nsew__label c-label-n" | ||||
|                     text-anchor="middle" | ||||
|                     :transform="northTextTransform" | ||||
|                 >N</text> | ||||
|                 <text | ||||
|                     class="c-nsew__label c-label-e" | ||||
|                     text-anchor="middle" | ||||
|                     :transform="eastTextTransform" | ||||
|                 >E</text> | ||||
|                 <text | ||||
|                     class="c-nsew__label c-label-w" | ||||
|                     text-anchor="middle" | ||||
|                     :transform="southTextTransform" | ||||
|                 >W</text> | ||||
|                 <text | ||||
|                     class="c-nsew__label c-label-s" | ||||
|                     text-anchor="middle" | ||||
|                     :transform="westTextTransform" | ||||
|                 >S</text> | ||||
|             </svg> | ||||
|         </div> | ||||
|  | ||||
|         <div | ||||
|             v-if="hasHeading" | ||||
|             class="c-spacecraft-body" | ||||
|             :style="headingStyle" | ||||
|         > | ||||
|         </div> | ||||
|  | ||||
|         <div | ||||
|             v-if="hasSunHeading" | ||||
|             class="c-sun" | ||||
|             :style="sunHeadingStyle" | ||||
|         ></div> | ||||
|  | ||||
|         <div | ||||
|             class="c-cam-field" | ||||
|             :style="cameraPanStyle" | ||||
|         > | ||||
|             <div class="cam-field-half cam-field-half-l"> | ||||
|                 <div | ||||
|                     class="cam-field-area" | ||||
|                     :style="cameraFOVStyleLeftHalf" | ||||
|                 ></div> | ||||
|             </div> | ||||
|             <div class="cam-field-half cam-field-half-r"> | ||||
|                 <div | ||||
|                     class="cam-field-area" | ||||
|                     :style="cameraFOVStyleRightHalf" | ||||
|                 ></div> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
|             </radialGradient> | ||||
|         </defs> | ||||
|     </svg> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import { rotate } from './utils'; | ||||
| import { throttle } from 'lodash'; | ||||
|  | ||||
| export default { | ||||
|     props: { | ||||
|         sizedImageWidth: { | ||||
|             type: Number, | ||||
|         compassRoseSizingClasses: { | ||||
|             type: String, | ||||
|             required: true | ||||
|         }, | ||||
|         heading: { | ||||
|             type: Number, | ||||
|             required: true | ||||
|             required: true, | ||||
|             default() { | ||||
|                 return 0; | ||||
|             } | ||||
|         }, | ||||
|         sunHeading: { | ||||
|             type: Number, | ||||
| @@ -178,58 +229,39 @@ export default { | ||||
|         }, | ||||
|         cameraPan: { | ||||
|             type: Number, | ||||
|             required: true | ||||
|             required: true, | ||||
|             default() { | ||||
|                 return 0; | ||||
|             } | ||||
|         }, | ||||
|         lockCompass: { | ||||
|             type: Boolean, | ||||
|         sizedImageDimensions: { | ||||
|             type: Object, | ||||
|             required: true | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             lockCompass: true | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
|         compassRoseSizingClasses() { | ||||
|             let compassRoseSizingClasses = ''; | ||||
|             if (this.sizedImageWidth < 300) { | ||||
|                 compassRoseSizingClasses = '--rose-small --rose-min'; | ||||
|             } else if (this.sizedImageWidth < 500) { | ||||
|                 compassRoseSizingClasses = '--rose-small'; | ||||
|             } else if (this.sizedImageWidth > 1000) { | ||||
|                 compassRoseSizingClasses = '--rose-max'; | ||||
|             } | ||||
|  | ||||
|             return compassRoseSizingClasses; | ||||
|         }, | ||||
|         compassRoseStyle() { | ||||
|             return { transform: `rotate(${ this.north }deg)` }; | ||||
|         }, | ||||
|         north() { | ||||
|             return this.lockCompass ? rotate(-this.cameraPan) : 0; | ||||
|         }, | ||||
|         northTextTransform() { | ||||
|             return this.cardinalPointsTextTransform.north; | ||||
|         cardinalTextRotateN() { | ||||
|             return { transform: `translateY(-27%) rotate(${ -this.north }deg)` }; | ||||
|         }, | ||||
|         eastTextTransform() { | ||||
|             return this.cardinalPointsTextTransform.east; | ||||
|         cardinalTextRotateS() { | ||||
|             return { transform: `translateY(30%) rotate(${ -this.north }deg)` }; | ||||
|         }, | ||||
|         southTextTransform() { | ||||
|             return this.cardinalPointsTextTransform.south; | ||||
|         cardinalTextRotateE() { | ||||
|             return { transform: `translateX(30%) rotate(${ -this.north }deg)` }; | ||||
|         }, | ||||
|         westTextTransform() { | ||||
|             return this.cardinalPointsTextTransform.west; | ||||
|         }, | ||||
|         cardinalPointsTextTransform() { | ||||
|             /** | ||||
|              * cardinal points text must be rotated | ||||
|              * in the opposite direction that north is rotated | ||||
|              * to keep text vertically oriented | ||||
|              */ | ||||
|             const rotation = `rotate(${ -this.north })`; | ||||
|  | ||||
|             return { | ||||
|                 north: `translate(50,23) ${ rotation }`, | ||||
|                 east: `translate(82,50) ${ rotation }`, | ||||
|                 south: `translate(18,50) ${ rotation }`, | ||||
|                 west: `translate(50,82) ${ rotation }` | ||||
|             }; | ||||
|         cardinalTextRotateW() { | ||||
|             return { transform: `translateX(-30%) rotate(${ -this.north }deg)` }; | ||||
|         }, | ||||
|         hasHeading() { | ||||
|             return this.heading !== undefined; | ||||
| @@ -238,7 +270,7 @@ export default { | ||||
|             const rotation = rotate(this.north, this.heading); | ||||
|  | ||||
|             return { | ||||
|                 transform: `translateX(-50%) rotate(${ rotation }deg)` | ||||
|                 transform: `rotate(${ rotation }deg)` | ||||
|             }; | ||||
|         }, | ||||
|         hasSunHeading() { | ||||
| @@ -262,20 +294,37 @@ export default { | ||||
|         // rotated counter-clockwise from camera pan angle | ||||
|         cameraFOVStyleLeftHalf() { | ||||
|             return { | ||||
|                 transform: `translateX(50%) rotate(${ -this.cameraAngleOfView / 2 }deg)` | ||||
|                 transform: `rotate(${ this.cameraAngleOfView / 2 }deg)` | ||||
|             }; | ||||
|         }, | ||||
|         // right half of camera field of view | ||||
|         // rotated clockwise from camera pan angle | ||||
|         cameraFOVStyleRightHalf() { | ||||
|             return { | ||||
|                 transform: `translateX(-50%) rotate(${ this.cameraAngleOfView / 2 }deg)` | ||||
|                 transform: `rotate(${ -this.cameraAngleOfView / 2 }deg)` | ||||
|             }; | ||||
|         } | ||||
|     }, | ||||
|     watch: { | ||||
|         sizedImageDimensions() { | ||||
|             this.debounceResizeSvg(); | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.debounceResizeSvg = throttle(this.resizeSvg, 100); | ||||
|  | ||||
|         this.$nextTick(() => { | ||||
|             this.debounceResizeSvg(); | ||||
|         }); | ||||
|     }, | ||||
|     methods: { | ||||
|         resizeSvg() { | ||||
|             const svg = this.$refs.compassRoseSvg; | ||||
|             svg.setAttribute('width', this.$refs.compassRoseWrapper.clientWidth); | ||||
|             svg.setAttribute('height', this.$refs.compassRoseWrapper.clientHeight); | ||||
|         }, | ||||
|         toggleLockCompass() { | ||||
|             this.$emit('toggle-lock-compass'); | ||||
|             this.lockCompass = !this.lockCompass; | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|   | ||||
| @@ -12,9 +12,8 @@ $elemBg: rgba(black, 0.7); | ||||
| .c-compass { | ||||
|     pointer-events: none; // This allows the image element to receive a browser-level context click | ||||
|     position: absolute; | ||||
|     left: 50%; | ||||
|     top: 50%; | ||||
|     transform: translate(-50%, -50%); | ||||
|     left: 0; | ||||
|     top: 0; | ||||
|     z-index: 1; | ||||
|     @include userSelectNone; | ||||
| } | ||||
| @@ -81,114 +80,55 @@ $elemBg: rgba(black, 0.7); | ||||
|         transform: translateX(-50%); | ||||
|         z-index: 1; | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| /***************************** COMPASS DIRECTIONS */ | ||||
| .c-nsew { | ||||
| /***************************** COMPASS SVG */ | ||||
| .c-compass-rose-svg { | ||||
|     $color: $interfaceKeyColor; | ||||
|     $inset: 5%; | ||||
|     $tickHeightPerc: 15%; | ||||
|     text-shadow: black 0 0 10px; | ||||
|     top: $inset; | ||||
|     right: $inset; | ||||
|     bottom: $inset; | ||||
|     left: $inset; | ||||
|     z-index: 3; | ||||
|     position: absolute; | ||||
|     top: 0; left: 0; | ||||
|  | ||||
|     &__tick, | ||||
|     &__label { | ||||
|         fill: $color; | ||||
|     } | ||||
|  | ||||
|     &__minor-ticks { | ||||
|         opacity: 0.5; | ||||
|     g, path, rect { | ||||
|         // In an SVG, rotation occurs about the center of the SVG, not the element | ||||
|         transform-origin: center; | ||||
|         transform: rotate(45deg); | ||||
|     } | ||||
|  | ||||
|     &__label { | ||||
|         dominant-baseline: central; | ||||
|         font-size: 1.25em; | ||||
|         font-weight: bold; | ||||
|     } | ||||
|  | ||||
|     .c-label-n { | ||||
|         font-size: 2em; | ||||
|     } | ||||
| } | ||||
|  | ||||
| /***************************** CAMERA FIELD ANGLE */ | ||||
| .c-cam-field { | ||||
|     $color: white; | ||||
|     opacity: 0.3; | ||||
|     top: 0; | ||||
|     right: 0; | ||||
|     bottom: 0; | ||||
|     left: 0; | ||||
|     z-index: 2; | ||||
|  | ||||
|     .cam-field-half { | ||||
|         top: 0; | ||||
|         right: 0; | ||||
|         bottom: 0; | ||||
|         left: 0; | ||||
|  | ||||
|         .cam-field-area { | ||||
|             background: $color; | ||||
|             top: -30%; | ||||
|             right: 0; | ||||
|             bottom: -30%; | ||||
|             left: 0; | ||||
|     .c-cr { | ||||
|         &__bg { | ||||
|             fill: #000; | ||||
|             opacity: 0.8; | ||||
|         } | ||||
|  | ||||
|         // clip-paths overlap a bit to avoid a gap between halves | ||||
|         &-l { | ||||
|             clip-path: polygon(0 0, 50.5% 0, 50.5% 100%, 0 100%); | ||||
|  | ||||
|             .cam-field-area { | ||||
|                 transform-origin: left center; | ||||
|             } | ||||
|         &__edge { | ||||
|             opacity: 0.1; | ||||
|         } | ||||
|  | ||||
|         &-r { | ||||
|             clip-path: polygon(49.5% 0, 100% 0, 100% 100%, 49.5% 100%); | ||||
|  | ||||
|             .cam-field-area { | ||||
|                 transform-origin: right center; | ||||
|             } | ||||
|         &__sun { | ||||
|             opacity: 0.7; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| /***************************** SPACECRAFT BODY */ | ||||
| .c-spacecraft-body { | ||||
|     $color: $interfaceKeyColor; | ||||
|     $s: 30%; | ||||
|     background: $color; | ||||
|     border-radius: 3px; | ||||
|     height: $s; | ||||
|     width: $s; | ||||
|     left: 50%; | ||||
|     top: 50%; | ||||
|     opacity: 0.4; | ||||
|     transform-origin: center top; | ||||
|     transform: translateX(-50%); // center by default, overridden by CompassRose.vue / headingStyle() | ||||
|         &__cam-fov-l, | ||||
|         &__cam-fov-r { | ||||
|             // Cam FOV indication | ||||
|             opacity: 0.2; | ||||
|             fill: #fff; | ||||
|         } | ||||
|  | ||||
|     &:before { | ||||
|         // Direction arrow | ||||
|         $color: rgba(black, 0.5); | ||||
|         $arwPointerY: 60%; | ||||
|         $arwBodyOffset: 25%; | ||||
|         background: $color; | ||||
|         content: ''; | ||||
|         display: block; | ||||
|         position: absolute; | ||||
|         top: 10%; | ||||
|         right: 20%; | ||||
|         bottom: 50%; | ||||
|         left: 20%; | ||||
|         clip-path: polygon(50% 0, 100% $arwPointerY, 100%-$arwBodyOffset $arwPointerY, 100%-$arwBodyOffset 100%, $arwBodyOffset 100%, $arwBodyOffset $arwPointerY, 0 $arwPointerY); | ||||
|         &__nsew-text, | ||||
|         &__spacecraft-body, | ||||
|         &__ticks-major, | ||||
|         &__ticks-minor { | ||||
|             fill: $color; | ||||
|         } | ||||
|  | ||||
|         &__ticks-minor { | ||||
|             opacity: 0.5; | ||||
|             transform: rotate(45deg); | ||||
|         } | ||||
|  | ||||
|         &__spacecraft-body { | ||||
|             opacity: 0.3; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @@ -196,32 +136,28 @@ $elemBg: rgba(black, 0.7); | ||||
| .w-direction-rose { | ||||
|     $s: 10%; | ||||
|     $m: 2%; | ||||
|     cursor: pointer; | ||||
|     pointer-events: all; | ||||
|     position: absolute; | ||||
|     bottom: $m; | ||||
|     left: $m; | ||||
|     width: $s; | ||||
|     padding-top: $s; | ||||
|     z-index: 2; | ||||
|  | ||||
|     &.--rose-min { | ||||
|         $s: 30px; | ||||
|         width: $s; | ||||
|         padding-top: $s; | ||||
|         .--hide-min { | ||||
|             display: none; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     &.--rose-small { | ||||
|         .c-nsew__minor-ticks, | ||||
|         .c-tick-w, | ||||
|         .c-tick-s, | ||||
|         .c-tick-e, | ||||
|         .c-label-w, | ||||
|         .c-label-s, | ||||
|         .c-label-e { | ||||
|         .--hide-small { | ||||
|             display: none; | ||||
|         } | ||||
|  | ||||
|         .c-label-n { | ||||
|             font-size: 2.5em; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     &.--rose-max { | ||||
| @@ -230,44 +166,3 @@ $elemBg: rgba(black, 0.7); | ||||
|         padding-top: $s; | ||||
|     } | ||||
| } | ||||
|  | ||||
| .c-direction-rose { | ||||
|     $c2: rgba(white, 0.1); | ||||
|     background: $elemBg; | ||||
|     background-image: radial-gradient(circle closest-side, transparent, transparent 80%, $c2); | ||||
|     transform-origin: 0 0; | ||||
|     position: absolute; | ||||
|     top: 0; | ||||
|     right: 0; | ||||
|     bottom: 0; | ||||
|     left: 0; | ||||
|     clip-path: circle(50% at 50% 50%); | ||||
|     border-radius: 100%; | ||||
|     pointer-events: all; | ||||
|  | ||||
|     svg, div { | ||||
|         position: absolute; | ||||
|     } | ||||
|  | ||||
|     // Sun | ||||
|     .c-sun { | ||||
|         top: 0; | ||||
|         right: 0; | ||||
|         bottom: 0; | ||||
|         left: 0; | ||||
|  | ||||
|         &:before { | ||||
|             $s: 35%; | ||||
|             @include sun(); | ||||
|             content: ''; | ||||
|             display: block; | ||||
|             position: absolute; | ||||
|             opacity: 0.7; | ||||
|             top: 0; | ||||
|             left: 50%; | ||||
|             height: $s; | ||||
|             width: $s; | ||||
|             transform: translate(-50%, -60%); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -55,28 +55,34 @@ | ||||
|                 ></a> | ||||
|             </span> | ||||
|         </div> | ||||
|         <div class="c-imagery__main-image__bg" | ||||
|         <div ref="imageBG" | ||||
|              class="c-imagery__main-image__bg" | ||||
|              :class="{'paused unnsynced': isPaused,'stale':false }" | ||||
|              @click="expand" | ||||
|         > | ||||
|             <img | ||||
|                 ref="focusedImage" | ||||
|                 class="c-imagery__main-image__image js-imageryView-image" | ||||
|                 :src="imageUrl" | ||||
|                 :style="{ | ||||
|                     'filter': `brightness(${filters.brightness}%) contrast(${filters.contrast}%)` | ||||
|                 }" | ||||
|                 :data-openmct-image-timestamp="time" | ||||
|                 :data-openmct-object-keystring="keyString" | ||||
|             <div class="image-wrapper" | ||||
|                  :style="{ | ||||
|                      'width': `${sizedImageDimensions.width}px`, | ||||
|                      'height': `${sizedImageDimensions.height}px` | ||||
|                  }" | ||||
|             > | ||||
|             <Compass | ||||
|                 v-if="shouldDisplayCompass" | ||||
|                 :container-width="imageContainerWidth" | ||||
|                 :container-height="imageContainerHeight" | ||||
|                 :natural-aspect-ratio="focusedImageNaturalAspectRatio" | ||||
|                 :image="focusedImage" | ||||
|                 :lock-compass="lockCompass" | ||||
|                 @toggle-lock-compass="toggleLockCompass" | ||||
|             /> | ||||
|                 <img ref="focusedImage" | ||||
|                      class="c-imagery__main-image__image js-imageryView-image" | ||||
|                      :src="imageUrl" | ||||
|                      :style="{ | ||||
|                          'filter': `brightness(${filters.brightness}%) contrast(${filters.contrast}%)` | ||||
|                      }" | ||||
|                      :data-openmct-image-timestamp="time" | ||||
|                      :data-openmct-object-keystring="keyString" | ||||
|                 > | ||||
|                 <Compass | ||||
|                     v-if="shouldDisplayCompass" | ||||
|                     :compass-rose-sizing-classes="compassRoseSizingClasses" | ||||
|                     :image="focusedImage" | ||||
|                     :natural-aspect-ratio="focusedImageNaturalAspectRatio" | ||||
|                     :sized-image-dimensions="sizedImageDimensions" | ||||
|                 /> | ||||
|             </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" | ||||
| @@ -124,27 +130,40 @@ | ||||
|         </div> | ||||
|     </div> | ||||
|     <div | ||||
|         ref="thumbsWrapper" | ||||
|         class="c-imagery__thumbs-wrapper" | ||||
|         :class="{'is-paused': isPaused}" | ||||
|         @scroll="handleScroll" | ||||
|         :class="[ | ||||
|             { 'is-paused': isPaused }, | ||||
|             { 'is-autoscroll-off': !resizingWindow && !autoScroll && !isPaused } | ||||
|         ]" | ||||
|     > | ||||
|         <div v-for="(image, index) in imageHistory" | ||||
|              :key="image.url + image.time" | ||||
|              class="c-imagery__thumb c-thumb" | ||||
|              :class="{ selected: focusedImageIndex === index && isPaused }" | ||||
|              @click="setFocusedImage(index, thumbnailClick)" | ||||
|         <div | ||||
|             ref="thumbsWrapper" | ||||
|             class="c-imagery__thumbs-scroll-area" | ||||
|             @scroll="handleScroll" | ||||
|         > | ||||
|             <a href="" | ||||
|                :download="image.imageDownloadName" | ||||
|                @click.prevent | ||||
|             <div v-for="(image, index) in imageHistory" | ||||
|                  :key="image.url + image.time" | ||||
|                  class="c-imagery__thumb c-thumb" | ||||
|                  :class="{ selected: focusedImageIndex === index && isPaused }" | ||||
|                  @click="setFocusedImage(index, thumbnailClick)" | ||||
|             > | ||||
|                 <img class="c-thumb__image" | ||||
|                      :src="image.url" | ||||
|                 <a href="" | ||||
|                    :download="image.imageDownloadName" | ||||
|                    @click.prevent | ||||
|                 > | ||||
|             </a> | ||||
|             <div class="c-thumb__timestamp">{{ image.formattedTime }}</div> | ||||
|                     <img class="c-thumb__image" | ||||
|                          :src="image.url" | ||||
|                     > | ||||
|                 </a> | ||||
|                 <div class="c-thumb__timestamp">{{ image.formattedTime }}</div> | ||||
|             </div> | ||||
|         </div> | ||||
|  | ||||
|         <button | ||||
|             class="c-imagery__auto-scroll-resume-button c-icon-button icon-play" | ||||
|             title="Resume automatic scrolling of image thumbnails" | ||||
|             @click="scrollToRight('reset')" | ||||
|         ></button> | ||||
|     </div> | ||||
| </div> | ||||
| </template> | ||||
| @@ -152,8 +171,9 @@ | ||||
| <script> | ||||
| import _ from 'lodash'; | ||||
| import moment from 'moment'; | ||||
| import Compass from './Compass/Compass.vue'; | ||||
|  | ||||
| import RelatedTelemetry from './RelatedTelemetry/RelatedTelemetry'; | ||||
| import Compass from './Compass/Compass.vue'; | ||||
|  | ||||
| const DEFAULT_DURATION_FORMATTER = 'duration'; | ||||
| const REFRESH_CSS_MS = 500; | ||||
| @@ -171,11 +191,13 @@ const TWENTYFOUR_HOURS = EIGHT_HOURS * 3; | ||||
| const ARROW_RIGHT = 39; | ||||
| const ARROW_LEFT = 37; | ||||
|  | ||||
| const SCROLL_LATENCY = 250; | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
|         Compass | ||||
|     }, | ||||
|     inject: ['openmct', 'domainObject'], | ||||
|     inject: ['openmct', 'domainObject', 'objectPath', 'currentView'], | ||||
|     data() { | ||||
|         let timeSystem = this.openmct.time.timeSystem(); | ||||
|  | ||||
| @@ -204,10 +226,23 @@ export default { | ||||
|             focusedImageNaturalAspectRatio: undefined, | ||||
|             imageContainerWidth: undefined, | ||||
|             imageContainerHeight: undefined, | ||||
|             lockCompass: true | ||||
|             lockCompass: true, | ||||
|             resizingWindow: false | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
|         compassRoseSizingClasses() { | ||||
|             let compassRoseSizingClasses = ''; | ||||
|             if (this.sizedImageDimensions.width < 300) { | ||||
|                 compassRoseSizingClasses = '--rose-small --rose-min'; | ||||
|             } else if (this.sizedImageDimensions.width < 500) { | ||||
|                 compassRoseSizingClasses = '--rose-small'; | ||||
|             } else if (this.sizedImageDimensions.width > 1000) { | ||||
|                 compassRoseSizingClasses = '--rose-max'; | ||||
|             } | ||||
|  | ||||
|             return compassRoseSizingClasses; | ||||
|         }, | ||||
|         time() { | ||||
|             return this.formatTime(this.focusedImage); | ||||
|         }, | ||||
| @@ -331,6 +366,20 @@ export default { | ||||
|             } | ||||
|  | ||||
|             return isFresh; | ||||
|         }, | ||||
|         sizedImageDimensions() { | ||||
|             let sizedImageDimensions = {}; | ||||
|             if ((this.imageContainerWidth / this.imageContainerHeight) > this.focusedImageNaturalAspectRatio) { | ||||
|                 // container is wider than image | ||||
|                 sizedImageDimensions.width = this.imageContainerHeight * this.focusedImageNaturalAspectRatio; | ||||
|                 sizedImageDimensions.height = this.imageContainerHeight; | ||||
|             } else { | ||||
|                 // container is taller than image | ||||
|                 sizedImageDimensions.width = this.imageContainerWidth; | ||||
|                 sizedImageDimensions.height = this.imageContainerWidth * this.focusedImageNaturalAspectRatio; | ||||
|             } | ||||
|  | ||||
|             return sizedImageDimensions; | ||||
|         } | ||||
|     }, | ||||
|     watch: { | ||||
| @@ -379,10 +428,14 @@ export default { | ||||
|         _.debounce(this.resizeImageContainer, 400); | ||||
|  | ||||
|         this.imageContainerResizeObserver = new ResizeObserver(this.resizeImageContainer); | ||||
|         this.imageContainerResizeObserver.observe(this.$refs.focusedImage); | ||||
|     }, | ||||
|     updated() { | ||||
|         this.scrollToRight(); | ||||
|         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.thumbWrapperResizeObserver = new ResizeObserver(this.handleThumbWindowResizeStart); | ||||
|         this.thumbWrapperResizeObserver.observe(this.$refs.thumbsWrapper); | ||||
|     }, | ||||
|     beforeDestroy() { | ||||
|         if (this.unsubscribe) { | ||||
| @@ -394,6 +447,10 @@ export default { | ||||
|             this.imageContainerResizeObserver.disconnect(); | ||||
|         } | ||||
|  | ||||
|         if (this.thumbWrapperResizeObserver) { | ||||
|             this.thumbWrapperResizeObserver.disconnect(); | ||||
|         } | ||||
|  | ||||
|         if (this.relatedTelemetry.hasRelatedTelemetry) { | ||||
|             this.relatedTelemetry.destroy(); | ||||
|         } | ||||
| @@ -413,6 +470,16 @@ export default { | ||||
|         } | ||||
|     }, | ||||
|     methods: { | ||||
|         expand() { | ||||
|             const actionCollection = this.openmct.actions.getActionsCollection(this.objectPath, this.currentView); | ||||
|             const visibleActions = actionCollection.getVisibleActions(); | ||||
|             const viewLargeAction = visibleActions | ||||
|                 && visibleActions.find(action => action.key === 'large.view'); | ||||
|  | ||||
|             if (viewLargeAction && viewLargeAction.appliesTo(this.objectPath, this.currentView)) { | ||||
|                 viewLargeAction.onItemClicked(); | ||||
|             } | ||||
|         }, | ||||
|         async initializeRelatedTelemetry() { | ||||
|             this.relatedTelemetry = new RelatedTelemetry( | ||||
|                 this.openmct, | ||||
| @@ -561,17 +628,15 @@ export default { | ||||
|         }, | ||||
|         handleScroll() { | ||||
|             const thumbsWrapper = this.$refs.thumbsWrapper; | ||||
|             if (!thumbsWrapper) { | ||||
|             if (!thumbsWrapper || this.resizingWindow) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             const { scrollLeft, scrollWidth, clientWidth, scrollTop, scrollHeight, clientHeight } = thumbsWrapper; | ||||
|             const disableScroll = (scrollWidth - scrollLeft) > 2 * clientWidth | ||||
|                     || (scrollHeight - scrollTop) > 2 * clientHeight; | ||||
|             const { scrollLeft, scrollWidth, clientWidth } = thumbsWrapper; | ||||
|             const disableScroll = scrollWidth > Math.ceil(scrollLeft + clientWidth); | ||||
|             this.autoScroll = !disableScroll; | ||||
|         }, | ||||
|         paused(state, type) { | ||||
|  | ||||
|             this.isPaused = state; | ||||
|  | ||||
|             if (type === 'button') { | ||||
| @@ -584,6 +649,7 @@ export default { | ||||
|             } | ||||
|  | ||||
|             this.autoScroll = true; | ||||
|             this.scrollToRight(); | ||||
|         }, | ||||
|         scrollToFocused() { | ||||
|             const thumbsWrapper = this.$refs.thumbsWrapper; | ||||
| @@ -600,8 +666,8 @@ export default { | ||||
|                 }); | ||||
|             } | ||||
|         }, | ||||
|         scrollToRight() { | ||||
|             if (this.isPaused || !this.$refs.thumbsWrapper || !this.autoScroll) { | ||||
|         scrollToRight(type) { | ||||
|             if (type !== 'reset' && (this.isPaused || !this.$refs.thumbsWrapper || !this.autoScroll)) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
| @@ -610,7 +676,9 @@ export default { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             setTimeout(() => this.$refs.thumbsWrapper.scrollLeft = scrollWidth, 0); | ||||
|             this.$nextTick(() => { | ||||
|                 this.$refs.thumbsWrapper.scrollLeft = scrollWidth; | ||||
|             }); | ||||
|         }, | ||||
|         setFocusedImage(index, thumbnailClick = false) { | ||||
|             if (this.isPaused && !thumbnailClick) { | ||||
| @@ -678,9 +746,9 @@ export default { | ||||
|             image.imageDownloadName = this.getImageDownloadName(datum); | ||||
|  | ||||
|             this.imageHistory.push(image); | ||||
|  | ||||
|             if (setFocused) { | ||||
|                 this.setFocusedImage(this.imageHistory.length - 1); | ||||
|                 this.scrollToRight(); | ||||
|             } | ||||
|         }, | ||||
|         getFormatter(key) { | ||||
| @@ -808,16 +876,31 @@ export default { | ||||
|             }, { once: true }); | ||||
|         }, | ||||
|         resizeImageContainer() { | ||||
|             if (this.$refs.focusedImage.clientWidth !== this.imageContainerWidth) { | ||||
|                 this.imageContainerWidth = this.$refs.focusedImage.clientWidth; | ||||
|             if (this.$refs.imageBG.clientWidth !== this.imageContainerWidth) { | ||||
|                 this.imageContainerWidth = this.$refs.imageBG.clientWidth; | ||||
|             } | ||||
|  | ||||
|             if (this.$refs.focusedImage.clientHeight !== this.imageContainerHeight) { | ||||
|                 this.imageContainerHeight = this.$refs.focusedImage.clientHeight; | ||||
|             if (this.$refs.imageBG.clientHeight !== this.imageContainerHeight) { | ||||
|                 this.imageContainerHeight = this.$refs.imageBG.clientHeight; | ||||
|             } | ||||
|         }, | ||||
|         toggleLockCompass() { | ||||
|             this.lockCompass = !this.lockCompass; | ||||
|         handleThumbWindowResizeStart() { | ||||
|             if (!this.autoScroll) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             // To hide resume button while scrolling | ||||
|             this.resizingWindow = true; | ||||
|             this.handleThumbWindowResizeEnded(); | ||||
|         }, | ||||
|         handleThumbWindowResizeEnded() { | ||||
|             if (!this.isPaused) { | ||||
|                 this.scrollToRight('reset'); | ||||
|             } | ||||
|  | ||||
|             this.$nextTick(() => { | ||||
|                 this.resizingWindow = false; | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|   | ||||
| @@ -22,6 +22,9 @@ | ||||
|         &__bg { | ||||
|             background-color: $colorPlotBg; | ||||
|             border: 1px solid transparent; | ||||
|             display: flex; | ||||
|             align-items: center; | ||||
|             justify-content: center; | ||||
|             flex: 1 1 auto; | ||||
|             height: 0; | ||||
|  | ||||
| @@ -33,7 +36,6 @@ | ||||
|         &__image { | ||||
|             height: 100%; | ||||
|             width: 100%; | ||||
|             object-fit: contain; | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -93,24 +95,43 @@ | ||||
|     } | ||||
|  | ||||
|     &__thumbs-wrapper { | ||||
|         flex: 0 0 auto; | ||||
|         display: flex; // Uses row layout | ||||
|  | ||||
|         &.is-autoscroll-off { | ||||
|             background: $colorInteriorBorder; | ||||
|             [class*='__auto-scroll-resume-button'] { | ||||
|                 display: block; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         &.is-paused { | ||||
|             background: rgba($colorPausedBg, 0.4); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     &__thumbs-scroll-area { | ||||
|         flex: 0 1 auto; | ||||
|         display: flex; | ||||
|         flex-direction: row; | ||||
|         height: 135px; | ||||
|         overflow-x: auto; | ||||
|         overflow-y: hidden; | ||||
|         margin-bottom: 1px; | ||||
|         padding-bottom: $interiorMarginSm; | ||||
|  | ||||
|         &.is-paused { | ||||
|             background: rgba($colorPausedBg, 0.4); | ||||
|         } | ||||
|  | ||||
|         .c-thumb:last-child { | ||||
|             // Hilite the lastest thumb | ||||
|             background: $colorBodyFg; | ||||
|             color: $colorBodyBg; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     &__auto-scroll-resume-button { | ||||
|         display: none; // Set to block when __thumbs-wrapper has .is-autoscroll-off | ||||
|         flex: 0 0 auto; | ||||
|         font-size: 0.8em; | ||||
|         margin: $interiorMarginSm; | ||||
|     } | ||||
| } | ||||
|  | ||||
| /*************************************** THUMBS */ | ||||
| @@ -142,7 +163,7 @@ | ||||
|  | ||||
| .l-layout, | ||||
| .c-fl { | ||||
|     .c-imagery__thumbs-wrapper { | ||||
|     .c-imagery__thumbs-scroll-area { | ||||
|         //  When Imagery is in a layout, hide the thumbs area | ||||
|         display: none; | ||||
|     } | ||||
| @@ -173,6 +194,10 @@ | ||||
|                 margin-right: $interiorMarginSm; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         .s-status-taking-snapshot & { | ||||
|             display: none; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     &__lc { | ||||
| @@ -254,6 +279,10 @@ | ||||
|             content: $glyph-icon-play; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     .s-status-taking-snapshot & { | ||||
|         display: none; | ||||
|     } | ||||
| } | ||||
|  | ||||
| .c-imagery__prev-next-buttons { | ||||
| @@ -268,6 +297,10 @@ | ||||
|     .c-nav { | ||||
|         pointer-events: all; | ||||
|     } | ||||
|  | ||||
|     .s-status-taking-snapshot & { | ||||
|         display: none; | ||||
|     } | ||||
| } | ||||
|  | ||||
| .c-nav { | ||||
|   | ||||
| @@ -92,6 +92,7 @@ describe("The Imagery View Layout", () => { | ||||
|     let resolveFunction; | ||||
|  | ||||
|     let openmct; | ||||
|     let appHolder; | ||||
|     let parent; | ||||
|     let child; | ||||
|     let imageTelemetry = generateTelemetry(START - TEN_MINUTES, COUNT); | ||||
| @@ -195,7 +196,7 @@ describe("The Imagery View Layout", () => { | ||||
|  | ||||
|     // this setups up the app | ||||
|     beforeEach((done) => { | ||||
|         const appHolder = document.createElement('div'); | ||||
|         appHolder = document.createElement('div'); | ||||
|         appHolder.style.width = '640px'; | ||||
|         appHolder.style.height = '480px'; | ||||
|  | ||||
| @@ -209,6 +210,8 @@ describe("The Imagery View Layout", () => { | ||||
|         child = document.createElement('div'); | ||||
|         parent.appendChild(child); | ||||
|  | ||||
|         // document.querySelector('body').append(parent); | ||||
|  | ||||
|         spyOn(window, 'ResizeObserver').and.returnValue({ | ||||
|             observe() {}, | ||||
|             disconnect() {} | ||||
| @@ -277,7 +280,7 @@ describe("The Imagery View Layout", () => { | ||||
|             expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 1].timeId)).not.toEqual(-1); | ||||
|         }); | ||||
|  | ||||
|         it("should show the clicked thumbnail as the main image", (done) => { | ||||
|         xit("should show the clicked thumbnail as the main image", (done) => { | ||||
|             const target = imageTelemetry[5].url; | ||||
|             parent.querySelectorAll(`img[src='${target}']`)[0].click(); | ||||
|             Vue.nextTick(() => { | ||||
| @@ -314,7 +317,7 @@ describe("The Imagery View Layout", () => { | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         it("should navigate via arrow keys", (done) => { | ||||
|         xit("should navigate via arrow keys", (done) => { | ||||
|             let keyOpts = { | ||||
|                 element: parent.querySelector('.c-imagery'), | ||||
|                 key: 'ArrowLeft', | ||||
| @@ -362,5 +365,21 @@ describe("The Imagery View Layout", () => { | ||||
|                 done(); | ||||
|             }); | ||||
|         }); | ||||
|         it ('shows an auto scroll button when scroll to left', async () => { | ||||
|             // to mock what a scroll would do | ||||
|             imageryView._getInstance().$refs.ImageryLayout.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; | ||||
|             await Vue.nextTick(); | ||||
|             parent.querySelector('.c-imagery__auto-scroll-resume-button').click(); | ||||
|             expect(imageryView._getInstance().$refs.ImageryLayout.scrollToRight).toHaveBeenCalledWith('reset'); | ||||
|  | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
|   | ||||
| @@ -25,16 +25,20 @@ export default class CopyToNotebookAction { | ||||
|             }); | ||||
|     } | ||||
|  | ||||
|     invoke(objectPath, view = {}) { | ||||
|         let viewContext = view.getViewContext && view.getViewContext(); | ||||
|     invoke(objectPath, view) { | ||||
|         const formattedValueForCopy = view.getViewContext().row.formattedValueForCopy; | ||||
|  | ||||
|         this.copyToNotebook(viewContext.formattedValueForCopy()); | ||||
|         this.copyToNotebook(formattedValueForCopy()); | ||||
|     } | ||||
|  | ||||
|     appliesTo(objectPath, view = {}) { | ||||
|         let viewContext = view.getViewContext && view.getViewContext(); | ||||
|         const viewContext = view.getViewContext && view.getViewContext(); | ||||
|         const row = viewContext && viewContext.row; | ||||
|         if (!row) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         return viewContext && viewContext.formattedValueForCopy | ||||
|             && typeof viewContext.formattedValueForCopy === 'function'; | ||||
|         return row.formattedValueForCopy | ||||
|             && typeof row.formattedValueForCopy === 'function'; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -31,6 +31,7 @@ import PainterroInstance from '../utils/painterroInstance'; | ||||
| import SnapshotTemplate from './snapshot-template.html'; | ||||
|  | ||||
| import { updateNotebookImageDomainObject } from '../utils/notebook-image'; | ||||
| import ImageExporter from '../../../exporters/ImageExporter'; | ||||
|  | ||||
| import PopupMenu from './PopupMenu.vue'; | ||||
| import Vue from 'vue'; | ||||
| @@ -71,7 +72,7 @@ export default { | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.addPopupMenuItems(); | ||||
|         this.exportImageService = this.openmct.$injector.get('exportImageService'); | ||||
|         this.imageExporter = new ImageExporter(this.openmct); | ||||
|     }, | ||||
|     methods: { | ||||
|         addPopupMenuItems() { | ||||
| @@ -101,7 +102,6 @@ export default { | ||||
|                 buttons: [ | ||||
|                     { | ||||
|                         label: 'Cancel', | ||||
|                         emphasis: true, | ||||
|                         callback: () => { | ||||
|                             painterroInstance.dismiss(); | ||||
|                             annotateOverlay.dismiss(); | ||||
| @@ -109,6 +109,7 @@ export default { | ||||
|                     }, | ||||
|                     { | ||||
|                         label: 'Save', | ||||
|                         emphasis: true, | ||||
|                         callback: () => { | ||||
|                             painterroInstance.save((snapshotObject) => { | ||||
|                                 annotateOverlay.dismiss(); | ||||
| @@ -234,9 +235,9 @@ export default { | ||||
|             let element = this.snapshot.$refs['snapshot-image']; | ||||
|  | ||||
|             if (type === 'png') { | ||||
|                 this.exportImageService.exportPNG(element, this.embed.name); | ||||
|                 this.imageExporter.exportPNG(element, this.embed.name); | ||||
|             } else { | ||||
|                 this.exportImageService.exportJPG(element, this.embed.name); | ||||
|                 this.imageExporter.exportJPG(element, this.embed.name); | ||||
|             } | ||||
|         }, | ||||
|         previewEmbed() { | ||||
|   | ||||
| @@ -80,7 +80,7 @@ export default { | ||||
|                 notebookTypes.push({ | ||||
|                     cssClass: 'icon-notebook', | ||||
|                     name: `Save to Notebook ${defaultPath}`, | ||||
|                     callBack: () => { | ||||
|                     onItemClicked: () => { | ||||
|                         return this.snapshot(NOTEBOOK_DEFAULT); | ||||
|                     } | ||||
|                 }); | ||||
| @@ -89,7 +89,7 @@ export default { | ||||
|             notebookTypes.push({ | ||||
|                 cssClass: 'icon-camera', | ||||
|                 name: 'Save to Notebook Snapshots', | ||||
|                 callBack: () => { | ||||
|                 onItemClicked: () => { | ||||
|                     return this.snapshot(NOTEBOOK_SNAPSHOT); | ||||
|                 } | ||||
|             }); | ||||
|   | ||||
| @@ -7,10 +7,10 @@ | ||||
|                     <div class="c-object-label__type-icon icon-camera"></div> | ||||
|                     <div class="c-object-label__name"> | ||||
|                         Notebook Snapshots | ||||
|                         <span v-if="snapshots.length" | ||||
|                               class="l-browse-bar__object-details" | ||||
|                         > {{ snapshots.length }} of {{ getNotebookSnapshotMaxCount() }} | ||||
|                         </span> | ||||
|                     </div> | ||||
|                     <div v-if="snapshots.length" | ||||
|                          class="l-browse-bar__object-details" | ||||
|                     >{{ snapshots.length }} of {{ getNotebookSnapshotMaxCount() }} | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <PopupMenu v-if="snapshots.length > 0" | ||||
|   | ||||
| @@ -4,8 +4,10 @@ | ||||
|         <div class="l-browse-bar__start"> | ||||
|             <div class="l-browse-bar__object-name--w"> | ||||
|                 <span class="c-object-label l-browse-bar__object-name" | ||||
|                     v-bind:class="cssClass" | ||||
|                 > | ||||
|                     <span class="c-object-label__type-icon" | ||||
|                           v-bind:class="cssClass" | ||||
|                     ></span> | ||||
|                     <span class="c-object-label__name">{{ name }}</span> | ||||
|                 </span> | ||||
|             </div> | ||||
|   | ||||
| @@ -117,6 +117,10 @@ export default function NotebookPlugin() { | ||||
|             key: 'notebook-snapshot-indicator' | ||||
|         }; | ||||
|  | ||||
|         openmct.once('destroy', () => { | ||||
|             snapshotContainer.destroy(); | ||||
|         }); | ||||
|  | ||||
|         openmct.indicators.add(indicator); | ||||
|  | ||||
|         openmct.objectViews.addProvider({ | ||||
|   | ||||
| @@ -80,4 +80,8 @@ export default class SnapshotContainer extends EventEmitter { | ||||
|  | ||||
|         return this.saveSnapshots(updatedSnapshots); | ||||
|     } | ||||
|  | ||||
|     destroy() { | ||||
|         delete SnapshotContainer.instance; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -4,24 +4,24 @@ import { NOTEBOOK_DEFAULT } from '@/plugins/notebook/notebook-constants'; | ||||
| import { createNotebookImageDomainObject, DEFAULT_SIZE } from './utils/notebook-image'; | ||||
|  | ||||
| import SnapshotContainer from './snapshot-container'; | ||||
| import ImageExporter from '../../exporters/ImageExporter'; | ||||
|  | ||||
| export default class Snapshot { | ||||
|     constructor(openmct) { | ||||
|         this.openmct = openmct; | ||||
|         this.snapshotContainer = new SnapshotContainer(openmct); | ||||
|         this.imageExporter = new ImageExporter(openmct); | ||||
|  | ||||
|         this.capture = this.capture.bind(this); | ||||
|         this._saveSnapShot = this._saveSnapShot.bind(this); | ||||
|     } | ||||
|  | ||||
|     capture(snapshotMeta, notebookType, domElement) { | ||||
|         const exportImageService = this.openmct.$injector.get('exportImageService'); | ||||
|  | ||||
|         const options = { | ||||
|             className: 's-status-taking-snapshot', | ||||
|             thumbnailSize: DEFAULT_SIZE | ||||
|         }; | ||||
|         exportImageService.exportPNGtoSRC(domElement, options) | ||||
|         this.imageExporter.exportPNGtoSRC(domElement, options) | ||||
|             .then(function ({blob, thumbnail}) { | ||||
|                 const reader = new window.FileReader(); | ||||
|                 reader.readAsDataURL(blob); | ||||
|   | ||||
| @@ -1,3 +1,25 @@ | ||||
| <!-- | ||||
|  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 ref="plan" | ||||
|      class="c-plan c-timeline-holder" | ||||
| @@ -28,7 +50,6 @@ import SwimLane from "@/ui/components/swim-lane/SwimLane.vue"; | ||||
| import { getValidatedPlan } from "./util"; | ||||
| import Vue from "vue"; | ||||
|  | ||||
| //TODO: UI direction needed for the following property values | ||||
| const PADDING = 1; | ||||
| const OUTER_TEXT_PADDING = 12; | ||||
| const INNER_TEXT_PADDING = 17; | ||||
| @@ -281,7 +302,9 @@ export default { | ||||
|                                 exceeds: { | ||||
|                                     start: this.xScale(this.viewBounds.start) > this.xScale(activity.start), | ||||
|                                     end: this.xScale(this.viewBounds.end) < this.xScale(activity.end) | ||||
|                                 } | ||||
|                                 }, | ||||
|                                 start: activity.start, | ||||
|                                 end: activity.end | ||||
|                             }, | ||||
|                             textLines: textLines, | ||||
|                             textStart: textStart, | ||||
| @@ -339,6 +362,9 @@ export default { | ||||
|                 components: { | ||||
|                     SwimLane | ||||
|                 }, | ||||
|                 provide: { | ||||
|                     openmct: this.openmct | ||||
|                 }, | ||||
|                 data() { | ||||
|                     return { | ||||
|                         heading, | ||||
| @@ -376,7 +402,6 @@ export default { | ||||
|                 activityRows.forEach((row) => { | ||||
|                     const items = activitiesByRow[row]; | ||||
|                     items.forEach(item => { | ||||
|                     //TODO: Don't draw the left-border of the rectangle if the activity started before viewBounds.start | ||||
|                         this.plotActivity(item, parseInt(row, 10), groupSVG); | ||||
|                     }); | ||||
|                 }); | ||||
| @@ -399,6 +424,9 @@ export default { | ||||
|                 element.setAttributeNS(null, key, attributes[key]); | ||||
|             }); | ||||
|         }, | ||||
|         getNSAttributesForElement(element, attribute) { | ||||
|             return element.getAttributeNS(null, attribute); | ||||
|         }, | ||||
|         // Experimental for now - unused | ||||
|         addForeignElement(svgElement, label, x, y) { | ||||
|             let foreign = document.createElementNS('http://www.w3.org/2000/svg', "foreignObject"); | ||||
| @@ -443,6 +471,10 @@ export default { | ||||
|                 fill: activity.color | ||||
|             }); | ||||
|  | ||||
|             rectElement.addEventListener('click', (event) => { | ||||
|                 this.setSelectionForActivity(event.currentTarget, activity, event.metaKey); | ||||
|             }); | ||||
|  | ||||
|             svgElement.appendChild(rectElement); | ||||
|  | ||||
|             item.textLines.forEach((line, index) => { | ||||
| @@ -456,6 +488,9 @@ export default { | ||||
|  | ||||
|                 const textNode = document.createTextNode(line); | ||||
|                 textElement.appendChild(textNode); | ||||
|                 textElement.addEventListener('click', (event) => { | ||||
|                     this.setSelectionForActivity(event.currentTarget, activity, event.metaKey); | ||||
|                 }); | ||||
|                 svgElement.appendChild(textElement); | ||||
|             }); | ||||
|             // this.addForeignElement(svgElement, activity.name, item.textStart, item.textY - LINE_HEIGHT); | ||||
| @@ -482,6 +517,22 @@ export default { | ||||
|             const cBrightness = ((hR * 299) + (hG * 587) + (hB * 114)) / 1000; | ||||
|  | ||||
|             return cBrightness > cThreshold ? "#000000" : "#ffffff"; | ||||
|         }, | ||||
|         setSelectionForActivity(element, activity, multiSelect) { | ||||
|             this.openmct.selection.select([{ | ||||
|                 element: element, | ||||
|                 context: { | ||||
|                     type: 'activity', | ||||
|                     activity: activity | ||||
|                 } | ||||
|             }, { | ||||
|                 element: this.openmct.layout.$refs.browseObject.$el, | ||||
|                 context: { | ||||
|                     item: this.domainObject, | ||||
|                     supportsMultiSelect: true | ||||
|                 } | ||||
|             }], multiSelect); | ||||
|             event.stopPropagation(); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|   | ||||
							
								
								
									
										52
									
								
								src/plugins/plan/inspector/ActivityProperty.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								src/plugins/plan/inspector/ActivityProperty.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | ||||
| <!-- | ||||
|  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> | ||||
| <li class="c-inspect-properties__row"> | ||||
|     <div class="c-inspect-properties__label"> | ||||
|         {{ label }} | ||||
|     </div> | ||||
|     <div class="c-inspect-properties__value"> | ||||
|         {{ value }} | ||||
|     </div> | ||||
| </li> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
|  | ||||
| export default { | ||||
|     props: { | ||||
|         label: { | ||||
|             type: String, | ||||
|             default() { | ||||
|                 return ''; | ||||
|             } | ||||
|         }, | ||||
|         value: { | ||||
|             type: String, | ||||
|             default() { | ||||
|                 return ''; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| </script> | ||||
							
								
								
									
										206
									
								
								src/plugins/plan/inspector/PlanActivitiesView.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										206
									
								
								src/plugins/plan/inspector/PlanActivitiesView.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,206 @@ | ||||
| <!-- | ||||
|  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-inspector__properties c-inspect-properties"> | ||||
|     <plan-activity-view v-for="activity in activities" | ||||
|                         :key="activity.id" | ||||
|                         :activity="activity" | ||||
|                         :heading="heading" | ||||
|     /> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import PlanActivityView from "./PlanActivityView.vue"; | ||||
| import { getPreciseDuration } from "utils/duration"; | ||||
| import uuid from 'uuid'; | ||||
|  | ||||
| const propertyLabels = { | ||||
|     'start': 'Start DateTime', | ||||
|     'end': 'End DateTime', | ||||
|     'duration': 'Duration', | ||||
|     'earliestStart': 'Earliest Start', | ||||
|     'latestEnd': 'Latest End', | ||||
|     'gap': 'Gap', | ||||
|     'overlap': 'Overlap', | ||||
|     'totalTime': 'Total Time' | ||||
| }; | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
|         PlanActivityView | ||||
|     }, | ||||
|     inject: ['openmct', 'selection'], | ||||
|     data() { | ||||
|         return { | ||||
|             name: '', | ||||
|             activities: [], | ||||
|             heading: '' | ||||
|         }; | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.setFormatters(); | ||||
|         this.getPlanData(this.selection); | ||||
|         this.getActivities(); | ||||
|         this.openmct.selection.on('change', this.updateSelection); | ||||
|         this.openmct.time.on('timeSystem', this.setFormatters); | ||||
|     }, | ||||
|     beforeDestroy() { | ||||
|         this.openmct.selection.off('change', this.updateSelection); | ||||
|         this.openmct.time.off('timeSystem', this.setFormatters); | ||||
|     }, | ||||
|     methods: { | ||||
|         setFormatters() { | ||||
|             let timeSystem = this.openmct.time.timeSystem(); | ||||
|             this.timeFormatter = this.openmct.telemetry.getValueFormatter({ | ||||
|                 format: timeSystem.timeFormat | ||||
|             }).formatter; | ||||
|         }, | ||||
|         updateSelection(newSelection) { | ||||
|             this.getPlanData(newSelection); | ||||
|             this.getActivities(); | ||||
|         }, | ||||
|         getPlanData(selection) { | ||||
|             this.selectedActivities = []; | ||||
|             selection.forEach((selectionItem) => { | ||||
|                 if (selectionItem[0].context.type === 'activity') { | ||||
|                     const activity = selectionItem[0].context.activity; | ||||
|                     if (activity) { | ||||
|                         this.selectedActivities.push(activity); | ||||
|                     } | ||||
|                 } | ||||
|             }); | ||||
|         }, | ||||
|         getActivities() { | ||||
|             if (this.selectedActivities.length <= 1) { | ||||
|                 this.heading = 'Time'; | ||||
|                 this.setSingleActivityProperties(); | ||||
|             } else { | ||||
|                 this.heading = 'Convex Hull'; | ||||
|                 this.setMultipleActivityProperties(); | ||||
|             } | ||||
|         }, | ||||
|         setSingleActivityProperties() { | ||||
|             this.activities.splice(0); | ||||
|             this.selectedActivities.forEach((selectedActivity, index) => { | ||||
|                 const activity = { | ||||
|                     id: uuid(), | ||||
|                     start: { | ||||
|                         label: propertyLabels.start, | ||||
|                         value: this.formatTime(selectedActivity.start) | ||||
|                     }, | ||||
|                     end: { | ||||
|                         label: propertyLabels.end, | ||||
|                         value: this.formatTime(selectedActivity.end) | ||||
|                     }, | ||||
|                     duration: { | ||||
|                         label: propertyLabels.duration, | ||||
|                         value: this.formatDuration(selectedActivity.end - selectedActivity.start) | ||||
|                     } | ||||
|                 }; | ||||
|                 this.$set(this.activities, index, activity); | ||||
|             }); | ||||
|         }, | ||||
|         sortFn(a, b) { | ||||
|             const numA = parseInt(a.start, 10); | ||||
|             const numB = parseInt(b.start, 10); | ||||
|             if (numA > numB) { | ||||
|                 return 1; | ||||
|             } | ||||
|  | ||||
|             if (numA < numB) { | ||||
|                 return -1; | ||||
|             } | ||||
|  | ||||
|             return 0; | ||||
|         }, | ||||
|         setMultipleActivityProperties() { | ||||
|             this.activities.splice(0); | ||||
|  | ||||
|             let earliestStart; | ||||
|             let latestEnd; | ||||
|             let gap; | ||||
|             let overlap; | ||||
|  | ||||
|             //Sort by start time | ||||
|             let selectedActivities = this.selectedActivities.sort(this.sortFn); | ||||
|             selectedActivities.forEach((selectedActivity, index) => { | ||||
|                 if (selectedActivities.length === 2 && index > 0) { | ||||
|                     const previous = selectedActivities[index - 1]; | ||||
|                     //they're on different rows so there must be overlap | ||||
|                     if (previous.end > selectedActivity.start) { | ||||
|                         overlap = previous.end - selectedActivity.start; | ||||
|                     } else if (previous.end < selectedActivity.start) { | ||||
|                         gap = selectedActivity.start - previous.end; | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 if (index > 0) { | ||||
|                     earliestStart = Math.min(earliestStart, selectedActivity.start); | ||||
|                     latestEnd = Math.max(latestEnd, selectedActivity.end); | ||||
|                 } else { | ||||
|                     earliestStart = selectedActivity.start; | ||||
|                     latestEnd = selectedActivity.end; | ||||
|                 } | ||||
|             }); | ||||
|             let totalTime = latestEnd - earliestStart; | ||||
|  | ||||
|             const activity = { | ||||
|                 id: uuid(), | ||||
|                 'earliestStart': { | ||||
|                     label: propertyLabels.earliestStart, | ||||
|                     value: this.formatTime(earliestStart) | ||||
|                 }, | ||||
|                 'latestEnd': { | ||||
|                     label: propertyLabels.latestEnd, | ||||
|                     value: this.formatTime(latestEnd) | ||||
|                 } | ||||
|             }; | ||||
|  | ||||
|             if (gap) { | ||||
|                 activity.gap = { | ||||
|                     label: propertyLabels.gap, | ||||
|                     value: this.formatDuration(gap) | ||||
|                 }; | ||||
|             } else if (overlap) { | ||||
|                 activity.overlap = { | ||||
|                     label: propertyLabels.overlap, | ||||
|                     value: this.formatDuration(overlap) | ||||
|                 }; | ||||
|             } | ||||
|  | ||||
|             activity.totalTime = { | ||||
|                 label: propertyLabels.totalTime, | ||||
|                 value: this.formatDuration(totalTime) | ||||
|             }; | ||||
|  | ||||
|             this.$set(this.activities, 0, activity); | ||||
|         }, | ||||
|         formatDuration(duration) { | ||||
|             return getPreciseDuration(duration); | ||||
|         }, | ||||
|         formatTime(time) { | ||||
|             return this.timeFormatter.format(time); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| </script> | ||||
							
								
								
									
										84
									
								
								src/plugins/plan/inspector/PlanActivityView.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								src/plugins/plan/inspector/PlanActivityView.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,84 @@ | ||||
| <!-- | ||||
|  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 v-if="timeProperties.length" | ||||
|      class="u-contents" | ||||
| > | ||||
|     <div class="c-inspect-properties__header"> | ||||
|         {{ heading }} | ||||
|     </div> | ||||
|     <ul v-for="timeProperty in timeProperties" | ||||
|         :key="timeProperty.id" | ||||
|         class="c-inspect-properties__section" | ||||
|     > | ||||
|         <activity-property :label="timeProperty.label" | ||||
|                            :value="timeProperty.value" | ||||
|         /> | ||||
|     </ul> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import ActivityProperty from './ActivityProperty.vue'; | ||||
| import uuid from 'uuid'; | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
|         ActivityProperty | ||||
|     }, | ||||
|     props: { | ||||
|         activity: { | ||||
|             type: Object, | ||||
|             required: true | ||||
|         }, | ||||
|         heading: { | ||||
|             type: String, | ||||
|             required: true | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             timeProperties: [] | ||||
|         }; | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.setProperties(); | ||||
|     }, | ||||
|     methods: { | ||||
|         setProperties() { | ||||
|             Object.keys(this.activity).forEach((key) => { | ||||
|                 if (this.activity[key].label) { | ||||
|                     const label = this.activity[key].label; | ||||
|                     const value = String(this.activity[key].value); | ||||
|  | ||||
|                     this.$set(this.timeProperties, this.timeProperties.length, { | ||||
|                         id: uuid(), | ||||
|                         label, | ||||
|                         value | ||||
|                     }); | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| </script> | ||||
							
								
								
									
										69
									
								
								src/plugins/plan/inspector/PlanInspectorViewProvider.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								src/plugins/plan/inspector/PlanInspectorViewProvider.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,69 @@ | ||||
| /***************************************************************************** | ||||
|  * 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 PlanActivitiesView from "./PlanActivitiesView.vue"; | ||||
| import Vue from 'vue'; | ||||
|  | ||||
| export default function PlanInspectorViewProvider(openmct) { | ||||
|     return { | ||||
|         key: 'plan-inspector', | ||||
|         name: 'Plan Inspector View', | ||||
|         canView: function (selection) { | ||||
|             if (selection.length === 0 || selection[0].length === 0) { | ||||
|                 return false; | ||||
|             } | ||||
|  | ||||
|             let context = selection[0][0].context; | ||||
|  | ||||
|             return context | ||||
|                 && context.type === 'activity'; | ||||
|         }, | ||||
|         view: function (selection) { | ||||
|             let component; | ||||
|  | ||||
|             return { | ||||
|                 show: function (element) { | ||||
|                     component = new Vue({ | ||||
|                         el: element, | ||||
|                         components: { | ||||
|                             PlanActivitiesView: PlanActivitiesView | ||||
|                         }, | ||||
|                         provide: { | ||||
|                             openmct, | ||||
|                             selection: selection | ||||
|                         }, | ||||
|                         template: '<plan-activities-view></plan-activities-view>' | ||||
|                     }); | ||||
|                 }, | ||||
|                 destroy: function () { | ||||
|                     if (component) { | ||||
|                         component.$destroy(); | ||||
|                         component = undefined; | ||||
|                     } | ||||
|                 } | ||||
|             }; | ||||
|         }, | ||||
|         priority: function () { | ||||
|             return 1; | ||||
|         } | ||||
|     }; | ||||
| } | ||||
| @@ -1,3 +1,25 @@ | ||||
| /***************************************************************************** | ||||
|  * 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. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| .c-plan { | ||||
|   svg { | ||||
|     text-rendering: geometricPrecision; | ||||
|   | ||||
| @@ -21,6 +21,7 @@ | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| import PlanViewProvider from './PlanViewProvider'; | ||||
| import PlanInspectorViewProvider from "./inspector/PlanInspectorViewProvider"; | ||||
|  | ||||
| export default function () { | ||||
|     return function install(openmct) { | ||||
| @@ -44,6 +45,7 @@ export default function () { | ||||
|             } | ||||
|         }); | ||||
|         openmct.objectViews.addProvider(new PlanViewProvider(openmct)); | ||||
|         openmct.inspectorViews.addProvider(new PlanInspectorViewProvider(openmct)); | ||||
|     }; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -49,10 +49,6 @@ describe('the plugin', function () { | ||||
|         child.style.height = '480px'; | ||||
|         element.appendChild(child); | ||||
|  | ||||
|         openmct.time.timeSystem('utc', { | ||||
|             start: 1597160002854, | ||||
|             end: 1597181232854 | ||||
|         }); | ||||
|         openmct.on('start', done); | ||||
|         openmct.start(appHolder); | ||||
|     }); | ||||
| @@ -105,6 +101,11 @@ describe('the plugin', function () { | ||||
|         let planView; | ||||
|  | ||||
|         beforeEach(() => { | ||||
|             openmct.time.timeSystem('utc', { | ||||
|                 start: 1597160002854, | ||||
|                 end: 1597181232854 | ||||
|             }); | ||||
|  | ||||
|             planDomainObject = { | ||||
|                 identifier: { | ||||
|                     key: 'test-object', | ||||
|   | ||||
| @@ -1,3 +1,25 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2021, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| export function getValidatedPlan(domainObject) { | ||||
|     let body = domainObject.selectFile.body; | ||||
|     let json = {}; | ||||
|   | ||||
| @@ -74,7 +74,9 @@ | ||||
|                 </div> | ||||
|  | ||||
|                 <div class="gl-plot__local-controls h-local-controls h-local-controls--overlay-content c-local-controls--show-on-hover"> | ||||
|                     <div class="c-button-set c-button-set--strip-h"> | ||||
|                     <div v-if="!options.compact" | ||||
|                          class="c-button-set c-button-set--strip-h js-zoom" | ||||
|                     > | ||||
|                         <button class="c-button icon-minus" | ||||
|                                 title="Zoom out" | ||||
|                                 @click="zoom('out', 0.2)" | ||||
| @@ -86,8 +88,8 @@ | ||||
|                         > | ||||
|                         </button> | ||||
|                     </div> | ||||
|                     <div v-if="plotHistory.length" | ||||
|                          class="c-button-set c-button-set--strip-h" | ||||
|                     <div v-if="plotHistory.length && !options.compact" | ||||
|                          class="c-button-set c-button-set--strip-h js-pan" | ||||
|                     > | ||||
|                         <button class="c-button icon-arrow-left" | ||||
|                                 title="Restore previous pan/zoom" | ||||
| @@ -100,8 +102,8 @@ | ||||
|                         > | ||||
|                         </button> | ||||
|                     </div> | ||||
|                     <div v-if="isRealTime" | ||||
|                          class="c-button-set c-button-set--strip-h" | ||||
|                     <div v-if="isRealTime && !options.compact" | ||||
|                          class="c-button-set c-button-set--strip-h js-pause" | ||||
|                     > | ||||
|                         <button v-if="!isFrozen" | ||||
|                                 class="c-button icon-pause" | ||||
| @@ -425,9 +427,12 @@ export default { | ||||
|                 this.skipReloadOnInteraction = false; | ||||
|                 this.loadMoreData(newRange, true); | ||||
|             } else { | ||||
|                 // If we're not panning or zooming (time conductor and plot x-axis times are not out of sync) | ||||
|                 // Drop any data that is more than 1x (max-min) before min. | ||||
|                 // Limit these purges to once a second. | ||||
|                 if (!this.nextPurge || this.nextPurge < Date.now()) { | ||||
|                 const isPanningOrZooming = this.isTimeOutOfSync; | ||||
|                 const purgeRecords = !isPanningOrZooming && (!this.nextPurge || (this.nextPurge < Date.now())); | ||||
|                 if (purgeRecords) { | ||||
|                     const keepRange = { | ||||
|                         min: newRange.min - (newRange.max - newRange.min), | ||||
|                         max: newRange.max | ||||
| @@ -493,10 +498,12 @@ export default { | ||||
|  | ||||
|             this.canvas = this.$refs.chartContainer.querySelectorAll('canvas')[1]; | ||||
|  | ||||
|             this.listenTo(this.canvas, 'mousemove', this.trackMousePosition, this); | ||||
|             this.listenTo(this.canvas, 'mouseleave', this.untrackMousePosition, this); | ||||
|             this.listenTo(this.canvas, 'mousedown', this.onMouseDown, this); | ||||
|             this.listenTo(this.canvas, 'wheel', this.wheelZoom, this); | ||||
|             if (!this.options.compact) { | ||||
|                 this.listenTo(this.canvas, 'mousemove', this.trackMousePosition, this); | ||||
|                 this.listenTo(this.canvas, 'mouseleave', this.untrackMousePosition, this); | ||||
|                 this.listenTo(this.canvas, 'mousedown', this.onMouseDown, this); | ||||
|                 this.listenTo(this.canvas, 'wheel', this.wheelZoom, this); | ||||
|             } | ||||
|         }, | ||||
|  | ||||
|         initialize() { | ||||
| @@ -514,12 +521,7 @@ export default { | ||||
|             this.chartElementBounds = undefined; | ||||
|             this.tickUpdate = false; | ||||
|  | ||||
|             this.canvas = this.$refs.chartContainer.querySelectorAll('canvas')[1]; | ||||
|  | ||||
|             this.listenTo(this.canvas, 'mousemove', this.trackMousePosition, this); | ||||
|             this.listenTo(this.canvas, 'mouseleave', this.untrackMousePosition, this); | ||||
|             this.listenTo(this.canvas, 'mousedown', this.onMouseDown, this); | ||||
|             this.listenTo(this.canvas, 'wheel', this.wheelZoom, this); | ||||
|             this.initCanvas(); | ||||
|  | ||||
|             this.config.yAxisLabel = this.config.yAxis.get('label'); | ||||
|  | ||||
|   | ||||
| @@ -72,7 +72,8 @@ | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import eventHelpers from "./lib/eventHelpers"; | ||||
| import eventHelpers from './lib/eventHelpers'; | ||||
| import ImageExporter from '../../exporters/ImageExporter'; | ||||
| import MctPlot from './MctPlot.vue'; | ||||
|  | ||||
| export default { | ||||
| @@ -102,8 +103,7 @@ export default { | ||||
|     }, | ||||
|     mounted() { | ||||
|         eventHelpers.extend(this); | ||||
|  | ||||
|         this.exportImageService = this.openmct.$injector.get('exportImageService'); | ||||
|         this.imageExporter = new ImageExporter(this.openmct); | ||||
|     }, | ||||
|     beforeDestroy() { | ||||
|         this.destroy(); | ||||
| @@ -118,14 +118,12 @@ export default { | ||||
|  | ||||
|         exportJPG() { | ||||
|             const plotElement = this.$refs.plotContainer; | ||||
|  | ||||
|             this.exportImageService.exportJPG(plotElement, 'plot.jpg', 'export-plot'); | ||||
|             this.imageExporter.exportJPG(plotElement, 'plot.jpg', 'export-plot'); | ||||
|         }, | ||||
|  | ||||
|         exportPNG() { | ||||
|             const plotElement = this.$refs.plotContainer; | ||||
|  | ||||
|             this.exportImageService.exportPNG(plotElement, 'plot.png', 'export-plot'); | ||||
|             this.imageExporter.exportPNG(plotElement, 'plot.png', 'export-plot'); | ||||
|         }, | ||||
|  | ||||
|         toggleCursorGuide() { | ||||
|   | ||||
| @@ -24,19 +24,23 @@ import Plot from './Plot.vue'; | ||||
| import Vue from 'vue'; | ||||
|  | ||||
| export default function PlotViewProvider(openmct) { | ||||
|     function hasTelemetry(domainObject) { | ||||
|     function hasNumericTelemetry(domainObject) { | ||||
|         if (!Object.prototype.hasOwnProperty.call(domainObject, 'telemetry')) { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         let metadata = openmct.telemetry.getMetadata(domainObject); | ||||
|  | ||||
|         return metadata.values().length > 0 && hasDomainAndRange(metadata); | ||||
|         return metadata.values().length > 0 && hasDomainAndNumericRange(metadata); | ||||
|     } | ||||
|  | ||||
|     function hasDomainAndRange(metadata) { | ||||
|         return (metadata.valuesForHints(['range']).length > 0 | ||||
|             && metadata.valuesForHints(['domain']).length > 0); | ||||
|     function hasDomainAndNumericRange(metadata) { | ||||
|         const rangeValues = metadata.valuesForHints(['range']); | ||||
|         const domains = metadata.valuesForHints(['domain']); | ||||
|  | ||||
|         return domains.length > 0 | ||||
|             && rangeValues.length > 0 | ||||
|             && !rangeValues.every(value => value.format === 'string'); | ||||
|     } | ||||
|  | ||||
|     function isCompactView(objectPath) { | ||||
| @@ -44,11 +48,11 @@ export default function PlotViewProvider(openmct) { | ||||
|     } | ||||
|  | ||||
|     return { | ||||
|         key: 'plot-simple', | ||||
|         key: 'plot-single', | ||||
|         name: 'Plot', | ||||
|         cssClass: 'icon-telemetry', | ||||
|         canView(domainObject, objectPath) { | ||||
|             return hasTelemetry(domainObject, openmct); | ||||
|             return hasNumericTelemetry(domainObject); | ||||
|         }, | ||||
|  | ||||
|         view: function (domainObject, objectPath) { | ||||
|   | ||||
| @@ -201,15 +201,57 @@ describe("the plugin", function () { | ||||
|                         hints: { | ||||
|                             range: 1 | ||||
|                         } | ||||
|                     }, | ||||
|                     { | ||||
|                         key: "yet-another-key", | ||||
|                         format: "string", | ||||
|                         hints: { | ||||
|                             range: 2 | ||||
|                         } | ||||
|                     }] | ||||
|                 } | ||||
|             }; | ||||
|  | ||||
|             const applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath); | ||||
|             let plotView = applicableViews.find((viewProvider) => viewProvider.key === "plot-simple"); | ||||
|             const plotView = applicableViews.find((viewProvider) => viewProvider.key === "plot-single"); | ||||
|  | ||||
|             expect(plotView).toBeDefined(); | ||||
|         }); | ||||
|  | ||||
|         it("does not provide a plot view if the telemetry is entirely non numeric", () => { | ||||
|             const testTelemetryObject = { | ||||
|                 id: "test-object", | ||||
|                 type: "test-object", | ||||
|                 telemetry: { | ||||
|                     values: [{ | ||||
|                         key: "some-key", | ||||
|                         hints: { | ||||
|                             domain: 1 | ||||
|                         } | ||||
|                     }, | ||||
|                     { | ||||
|                         key: "other-key", | ||||
|                         format: "string", | ||||
|                         hints: { | ||||
|                             range: 1 | ||||
|                         } | ||||
|                     }, | ||||
|                     { | ||||
|                         key: "yet-another-key", | ||||
|                         format: "string", | ||||
|                         hints: { | ||||
|                             range: 1 | ||||
|                         } | ||||
|                     }] | ||||
|                 } | ||||
|             }; | ||||
|  | ||||
|             const applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath); | ||||
|             const plotView = applicableViews.find((viewProvider) => viewProvider.key === "plot-single"); | ||||
|  | ||||
|             expect(plotView).toBeUndefined(); | ||||
|         }); | ||||
|  | ||||
|         it("provides an overlay plot view for objects with telemetry", () => { | ||||
|             const testTelemetryObject = { | ||||
|                 id: "test-object", | ||||
| @@ -279,14 +321,10 @@ describe("the plugin", function () { | ||||
|         let plotView; | ||||
|  | ||||
|         beforeEach(() => { | ||||
|             const getFunc = openmct.$injector.get; | ||||
|             spyOn(openmct.$injector, "get") | ||||
|                 .withArgs("exportImageService").and.returnValue({ | ||||
|                     exportPNG: () => {}, | ||||
|                     exportJPG: () => {} | ||||
|                 }) | ||||
|                 .and.callFake(getFunc); | ||||
|  | ||||
|             openmct.time.timeSystem("utc", { | ||||
|                 start: 0, | ||||
|                 end: 4 | ||||
|             }); | ||||
|             testTelemetryObject = { | ||||
|                 identifier: { | ||||
|                     namespace: "", | ||||
| @@ -319,7 +357,7 @@ describe("the plugin", function () { | ||||
|             }; | ||||
|  | ||||
|             applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath); | ||||
|             plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === "plot-simple"); | ||||
|             plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === "plot-single"); | ||||
|             plotView = plotViewProvider.view(testTelemetryObject, [testTelemetryObject]); | ||||
|             plotView.show(child, true); | ||||
|  | ||||
| @@ -403,6 +441,25 @@ describe("the plugin", function () { | ||||
|  | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         describe('controls in time strip view', () => { | ||||
|  | ||||
|             it('zoom controls are hidden', () => { | ||||
|                 let pauseEl = element.querySelectorAll(".c-button-set .js-zoom"); | ||||
|                 expect(pauseEl.length).toBe(0); | ||||
|             }); | ||||
|  | ||||
|             it('pan controls are hidden', () => { | ||||
|                 let pauseEl = element.querySelectorAll(".c-button-set .js-pan"); | ||||
|                 expect(pauseEl.length).toBe(0); | ||||
|             }); | ||||
|  | ||||
|             it('pause/play controls are hidden', () => { | ||||
|                 let pauseEl = element.querySelectorAll(".c-button-set .js-pause"); | ||||
|                 expect(pauseEl.length).toBe(0); | ||||
|             }); | ||||
|  | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe("The stacked plot view", () => { | ||||
| @@ -548,7 +605,7 @@ describe("the plugin", function () { | ||||
|             expect(legend.length).toBe(6); | ||||
|         }); | ||||
|  | ||||
|         it("Renders X-axis ticks for the telemetry object", () => { | ||||
|         xit("Renders X-axis ticks for the telemetry object", () => { | ||||
|             let xAxisElement = element.querySelectorAll(".gl-plot-axis-area.gl-plot-x .gl-plot-tick-wrapper"); | ||||
|             expect(xAxisElement.length).toBe(1); | ||||
|  | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user