Compare commits
	
		
			28 Commits
		
	
	
		
			plots-insp
			...
			issue#3926
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 2f865e952d | ||
|   | a40867d544 | ||
|   | dbed9262c0 | ||
|   | 43ac66233e | ||
|   | 04e85c176a | ||
|   | 8274c23129 | ||
|   | 5fafde5f23 | ||
|   | 80a6e7f719 | ||
|   | 2c13aeecce | ||
|   | ac015c3e45 | ||
|   | ae1a4bcc6a | ||
|   | e1e0eeac56 | ||
|   | c90dfb2a1f | ||
|   | 1dfa5e5b8c | ||
|   | 99896b72ea | ||
|   | 979ba77c8e | ||
|   | aebb5df611 | ||
|   | 605eeff9d7 | ||
|   | a83ee1f90f | ||
|   | fe899cbcc8 | ||
|   | 633bac2ed5 | ||
|   | dacec48aec | ||
|   | 3ca133c782 | ||
|   | 12416b8079 | ||
|   | 9920e67c83 | ||
|   | 0e80a5b8a0 | ||
|   | 05f9202fe4 | ||
|   | 0da35a44b0 | 
							
								
								
									
										43
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | ||||
| <!--- This is for filing bugs. If you have a general question, please --> | ||||
| <!--- visit https://github.com/nasa/openmct/discussions --> | ||||
|  | ||||
| --- | ||||
| name: Bug Report | ||||
| about: File a Bug ! | ||||
|  | ||||
| --- | ||||
| <!--- Focus on user impact in the title. Use the Summary Field to --> | ||||
| <!--- describe the problem technically. --> | ||||
|  | ||||
| #### Summary | ||||
| <!--- A description of the issue encountered. When possible, a description --> | ||||
| <!--- of the impact of the issue. What use case does it impede?--> | ||||
|  | ||||
| #### Expected vs Current Behavior | ||||
| <!--- Tell us what should have happened --> | ||||
|  | ||||
| #### Impact Check List | ||||
| <!--- Please select from the following options --> | ||||
|  | ||||
| - [ ] Data loss or misrepresented data? | ||||
| - [ ] Regression? Did this used to work or has it always been broken? | ||||
| - [ ] Is there a workaround available? | ||||
| - [ ] Does this impact a critical component? | ||||
| - [ ] Is this just a visual bug? | ||||
|  | ||||
| #### Steps to Reproduce | ||||
| <!--- Provide a link to a live example, or an unambiguous set of steps to --> | ||||
| <!--- reproduce this bug. Include code to reproduce, if relevant --> | ||||
| 1. | ||||
| 2. | ||||
| 3. | ||||
| 4. | ||||
|  | ||||
| #### Environment | ||||
| * Open MCT Version: <!--- date of build, version, or SHA --> | ||||
| * Deployment Type: <!--- npm dev? VIPER Dev? openmct-yams? --> | ||||
| * OS: | ||||
| * Browser: | ||||
|  | ||||
| #### Additional Information | ||||
| <!--- Include any screenshots, gifs, or logs which will expedite triage --> | ||||
							
								
								
									
										1
									
								
								.github/ISSUE_TEMPLATE/config.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								.github/ISSUE_TEMPLATE/config.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| blank_issues_enabled: false | ||||
							
								
								
									
										23
									
								
								.github/ISSUE_TEMPLATE/feature_request.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								.github/ISSUE_TEMPLATE/feature_request.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| <!--- This is for filing enhancements or features. If you have a general --> | ||||
| <!--- question, please visit https://github.com/nasa/openmct/discussions --> | ||||
|  | ||||
| --- | ||||
| name: Feature Request | ||||
| about: Suggest an idea for this project | ||||
|  | ||||
| --- | ||||
|  | ||||
| <!-- | ||||
| Thank you for suggesting an idea to make Open MCT better. | ||||
|  | ||||
| Please fill in as much of the template below as you're able. | ||||
| --> | ||||
|  | ||||
| **Is your feature request related to a problem? Please describe.** | ||||
| <!-- Please describe the problem you are trying to solve. --> | ||||
|  | ||||
| **Describe the solution you'd like** | ||||
| <!--- Please describe the desired behavior. --> | ||||
|  | ||||
| **Describe alternatives you've considered** | ||||
| <!--- Please describe alternative solutions or features you have considered. --> | ||||
							
								
								
									
										12
									
								
								.github/PULL_REQUEST_TEMPLATE.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								.github/PULL_REQUEST_TEMPLATE.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| ### All Submissions: | ||||
|  | ||||
| * [ ] Have you followed the guidelines in our [Contributing document](https://github.com/nasa/openmct/blob/master/CONTRIBUTING.md)? | ||||
| * [ ] Have you checked to ensure there aren't other open [Pull Requests](https://github.com/nasa/openmct/pulls) for the same update/change? | ||||
|  | ||||
| ### Author Checklist | ||||
|  | ||||
| * [ ] Changes address original issue? | ||||
| * [ ] Unit tests included and/or updated with changes? | ||||
| * [ ] Command line build passes? | ||||
| * [ ] Has this been smoke tested? | ||||
| * [ ] Testing instructions included in associated issue? | ||||
							
								
								
									
										3
									
								
								API.md
									
									
									
									
									
								
							
							
						
						
									
										3
									
								
								API.md
									
									
									
									
									
								
							| @@ -423,13 +423,14 @@ attribute      | type   | flags    | notes | ||||
|  | ||||
| ###### Value Hints | ||||
|  | ||||
| Each telemetry value description has an object defining hints.  Keys in this this object represent the hint itself, and the value represents the weight of that hint.  A lower weight means the hint has a higher priority.  For example, multiple values could be hinted for use as the y-axis of a plot (raw, engineering), but the highest priority would be the default choice.  Likewise, a table will use hints to determine the default order of columns. | ||||
| Each telemetry value description has an object defining hints.  Keys in this object represent the hint itself, and the value represents the weight of that hint.  A lower weight means the hint has a higher priority.  For example, multiple values could be hinted for use as the y-axis of a plot (raw, engineering), but the highest priority would be the default choice.  Likewise, a table will use hints to determine the default order of columns. | ||||
|  | ||||
| Known hints: | ||||
|  | ||||
| * `domain`: Values with a `domain` hint will be used for the x-axis of a plot, and tables will render columns for these values first. | ||||
| * `range`: Values with a `range` hint will be used as the y-axis on a plot, and tables will render columns for these values after the `domain` values. | ||||
| * `image`: Indicates that the value may be interpreted as the URL to an image file, in which case appropriate views will be made available. | ||||
| * `imageDownloadName`: Indicates that the value may be interpreted as the name of the image file. | ||||
|  | ||||
| ##### The Time Conductor and Telemetry  | ||||
|  | ||||
|   | ||||
| @@ -10,7 +10,7 @@ accept changes from external contributors. | ||||
|  | ||||
| The short version: | ||||
|  | ||||
| 1. Write your contribution. | ||||
| 1. Write your contribution or describe your idea in the form of an [GitHub issue](https://github.com/nasa/openmct/issues/new/choose) or [Starting a GitHub Discussion](https://github.com/nasa/openmct/discussions) | ||||
| 2. Make sure your contribution meets code, test, and commit message | ||||
|    standards as described below. | ||||
| 3. Submit a pull request from a topic branch back to `master`. Include a check | ||||
| @@ -18,6 +18,7 @@ The short version: | ||||
|    for review.) | ||||
| 4. Respond to any discussion. When the reviewer decides it's ready, they | ||||
|    will merge back `master` and fill out their own check list. | ||||
| 5. If you are a first-time contributor, please see [this discussion](https://github.com/nasa/openmct/discussions/3821) for further information.    | ||||
|  | ||||
| ## Contribution Process | ||||
|  | ||||
| @@ -115,7 +116,7 @@ the pull request containing the reviewer checklist (from below) and complete | ||||
| the merge back to the master branch. | ||||
|  | ||||
| Additionally: | ||||
| * Every pull request must link to the issue that it addresses. Eg. “Addresses #1234” or “Closes #1234”. This is the responsibility of the pull request’s __author__. If no issue exists, create one. | ||||
| * Every pull request must link to the issue that it addresses. Eg. “Addresses #1234” or “Closes #1234”. This is the responsibility of the pull request’s __author__. If no issue exists, [create one](https://github.com/nasa/openmct/issues/new/choose). | ||||
| * Every __author__ must include testing instructions. These instructions should identify the areas of code affected, and some minimal test steps. If addressing a bug, reproduction steps should be included, if they were not included in the original issue. If reproduction steps were included on the original issue, and are sufficient, refer to them. | ||||
| * A pull request that closes an issue should say so in the description. Including the text “Closes #1234” will cause the linked issue to be automatically closed when the pull request is merged. This is the responsibility of the pull request’s __author__. | ||||
| * When a pull request is merged, and the corresponding issue closed, the __reviewer__ must add the tag “unverified” to the original issue. This will indicate that although the issue is closed, it has not been tested yet. | ||||
| @@ -296,23 +297,12 @@ these standards. | ||||
|  | ||||
| Issues are tracked at https://github.com/nasa/openmct/issues. | ||||
|  | ||||
| Issues should include: | ||||
|  | ||||
| * A short description of the issue encountered. | ||||
| * A longer-form description of the issue encountered. When possible, steps to | ||||
|   reproduce the issue. | ||||
| * When possible, a description of the impact of the issue. What use case does | ||||
|   it impede? | ||||
| * An assessment of the severity of the issue. | ||||
|  | ||||
| Issue severity is categorized as follows (in ascending order): | ||||
|  | ||||
| * _Trivial_: Minimal impact on the usefulness and functionality of the | ||||
|   software; a "nice-to-have." | ||||
| * _(Unspecified)_: Major loss of functionality or impairment of use. | ||||
| * _Critical_: Large-scale loss of functionality or impairment of use, | ||||
|   such that remaining utility becomes marginal. | ||||
| * _Blocker_: Harmful or otherwise unacceptable behavior. Must fix. | ||||
| * _Trivial_: Minimal impact on the usefulness and functionality of the software; a "nice-to-have." Visual impact without functional impact, | ||||
| * _Medium_: Some impairment of use, but simple workarounds exist | ||||
| * _Critical_: Significant loss of functionality or impairment of use. Display of telemetry data is not affected though. | ||||
| * _Blocker_: Major functionality is impaired or lost, threatening mission success. Display of telemetry data is impaired or blocked by the bug, which could lead to loss of situational awareness. | ||||
|  | ||||
| ## Check Lists | ||||
|  | ||||
| @@ -322,16 +312,19 @@ checklist). | ||||
|  | ||||
| ### Author Checklist | ||||
|  | ||||
| 1. Changes address original issue? | ||||
| 2. Unit tests included and/or updated with changes? | ||||
| 3. Command line build passes? | ||||
| 4. Changes have been smoke-tested? | ||||
| 5. Testing instructions included? | ||||
| [Within PR Template](.github/PULL_REQUEST_TEMPLATE.md) | ||||
|  | ||||
| ### Reviewer Checklist | ||||
|  | ||||
| 1. Changes appear to address issue? | ||||
| 2. Appropriate unit tests included? | ||||
| 3. Code style and in-line documentation are appropriate? | ||||
| 4. Commit messages meet standards? | ||||
| 5. Has associated issue been labelled `unverified`? (only applicable if this PR closes the issue) | ||||
| * [ ] Changes appear to address issue? | ||||
| * [ ] Appropriate unit tests included? | ||||
| * [ ] Code style and in-line documentation are appropriate? | ||||
| * [ ] Commit messages meet standards? | ||||
| * [ ] Has associated issue been labelled `unverified`? (only applicable if this PR closes the issue) | ||||
| * [ ] Has associated issue been labelled `bug`? (only applicable if this PR is for a bug fix) | ||||
| * [ ] List of Acceptance Tests Performed. | ||||
|  | ||||
| Write out a small list of tests performed with just enough detail for another developer on the team  | ||||
| to execute.  | ||||
|  | ||||
| i.e. ```When Clicking on Add button, new `object` appears in dropdown.``` | ||||
| @@ -44,7 +44,7 @@ The clearest examples for developing Open MCT plugins are in the | ||||
| our documentation. | ||||
|  | ||||
| We want Open MCT to be as easy to use, install, run, and develop for as | ||||
| possible, and your feedback will help us get there! Feedback can be provided via [GitHub issues](https://github.com/nasa/openmct/issues), or by emailing us at [arc-dl-openmct@mail.nasa.gov](mailto:arc-dl-openmct@mail.nasa.gov). | ||||
| possible, and your feedback will help us get there! Feedback can be provided via [GitHub issues](https://github.com/nasa/openmct/issues/new/choose), [Starting a GitHub Discussion](https://github.com/nasa/openmct/discussions), or by emailing us at [arc-dl-openmct@mail.nasa.gov](mailto:arc-dl-openmct@mail.nasa.gov). | ||||
|  | ||||
| ## Building Applications With Open MCT | ||||
|  | ||||
|   | ||||
| @@ -73,11 +73,11 @@ acceptance testing (e.g. by resolving any blockers found); any | ||||
| resources not needed for this effort should be used to begin work | ||||
| for the subsequent sprint. | ||||
|  | ||||
| | Week  | Mon                       | Tue    | Wed | Thu                          | Fri         | | ||||
| |:-----:|:-------------------------:|:------:|:---:|:----------------------------:|:-----------:| | ||||
| | __1__ | Sprint plan               | Tag-up |     |                              |             | | ||||
| | __2__ |                           | Tag-up |     |                              | Code freeze | | ||||
| | __3__ | Per-sprint testing        | Triage |     | _Per-sprint testing*_        | Ship        | | ||||
| | Week  | Mon                       | Tue    | Wed | Thu                          | Fri                                   | | ||||
| |:-----:|:-------------------------:|:------:|:---:|:----------------------------:|:-------------------------------------:| | ||||
| | __1__ | Sprint plan               | Tag-up |     |                              |                                       | | ||||
| | __2__ |                           | Tag-up |     |                              | Code freeze  and sprint branch        | | ||||
| | __3__ | Per-sprint testing        | Triage |     | _Per-sprint testing*_        | Ship and merge sprint branch to master| | ||||
|  | ||||
| * If necessary. | ||||
|  | ||||
| @@ -105,14 +105,20 @@ emphasis on testing. | ||||
|     that team may begin work for that sprint during the | ||||
|     third week, since testing and blocker resolution is unlikely | ||||
|     to require all available resources. | ||||
|   * Testing success criteria identified per issue (where necessary). This could be in the form of acceptance tests on the issue or detailing performance tests, for example. | ||||
| * __Tag-up.__ Check in and status update among development team. | ||||
|   May amend plan for sprint as-needed. | ||||
| * __Code freeze.__ Any new work from this sprint | ||||
|   (features, bug fixes, enhancements) must be integrated by the | ||||
|   end of the second week of the sprint. After code freeze | ||||
|   (and until the end of the sprint) the only changes that should be | ||||
|   merged into the master branch should directly address issues | ||||
|   needed to pass acceptance testing. | ||||
|   end of the second week of the sprint. After code freeze, a sprint | ||||
|   branch will be created (and until the end of the sprint) the only  | ||||
|   changes that should be merged into the sprint branch should  | ||||
|   directly address issues needed to pass acceptance testing. | ||||
|   During this time, any other feature development will continue to | ||||
|   be merged into the master branch for the next sprint. | ||||
| * __Sprint branch merge to master.__ After acceptance testing, the sprint branch | ||||
|   will be merged back to the master branch. Any code conflicts that  | ||||
|   arise will be resolved by the team. | ||||
| * [__Per-release Testing.__](testing/plan.md#per-release-testing) | ||||
|   Structured testing with predefined | ||||
|   success criteria. No release should ship without passing | ||||
| @@ -126,8 +132,8 @@ emphasis on testing. | ||||
|   * [__Testathon.__](testing/plan.md#user-testing) | ||||
|     Multi-user testing, involving as many users as | ||||
|     is feasible, plus development team. Open-ended; should verify | ||||
|     completed work from this sprint, test exploratorily for | ||||
|     regressions, et cetera. | ||||
|     completed work from this sprint using the sprint branch, test  | ||||
|     exploratorily for regressions, et cetera. | ||||
|   * [__Long-Duration Test.__](testing/plan.md#long-duration-testing) A | ||||
|     test to verify that the software remains | ||||
|     stable after running for longer durations. May include some | ||||
| @@ -143,7 +149,7 @@ emphasis on testing. | ||||
|   Subset of Pre-release Testing | ||||
|   which should be performed before shipping at the end of any | ||||
|   sprint. Time is allocated for a second round of | ||||
|   Pre-release Testing if the first round is not passed. | ||||
|   Pre-release Testing if the first round is not passed. Smoke tests collected from issues/PRs | ||||
| * __Triage.__ Team reviews issues from acceptance testing and uses | ||||
|   success criteria to determine whether or not they should block | ||||
|   release, then formulates a plan to address these issues before | ||||
|   | ||||
| @@ -19,7 +19,7 @@ Testing for Open MCT includes: | ||||
|  | ||||
| Manual, non-rigorous testing of the software and/or specific features | ||||
| of interest. Verifies that the software runs and that basic functionality | ||||
| is present. | ||||
| is present. The outcome of Smoke Testing should be a simplified list of Acceptance Tests which could be executed by another team member with sufficient context. | ||||
|  | ||||
| ### Unit Testing | ||||
|  | ||||
| @@ -49,7 +49,7 @@ User testing will focus on the following activities: | ||||
| * General "trying to break things." | ||||
|  | ||||
| During user testing, users will | ||||
| [report issues](https://github.com/nasa/openmctweb/blob/master/CONTRIBUTING.md#issue-reporting) | ||||
| [report issues](https://github.com/nasa/openmct/issues/new/choose) | ||||
| as they are encountered. | ||||
|  | ||||
| Desired outcomes of user testing are: | ||||
| @@ -71,7 +71,7 @@ usage. After twenty-four hours, the software is evaluated for: | ||||
|   at the start of the test? Is it as responsive? | ||||
|  | ||||
| Any defects or unexpected behavior identified during testing should be | ||||
| [reported as issues](https://github.com/nasa/openmctweb/blob/master/CONTRIBUTING.md#issue-reporting) | ||||
| [reported as issues](https://github.com/nasa/openmct/issues/new/choose) | ||||
| and reviewed for severity. | ||||
|  | ||||
| ## Test Performance | ||||
|   | ||||
| @@ -92,8 +92,8 @@ should update (or delegate the task of updating) Open MCT version | ||||
| numbers by the following process: | ||||
|  | ||||
| 1. Update version number in `package.json` | ||||
|   1. Create a new branch off the `master` branch. | ||||
|   2. Remove `-SNAPSHOT` suffix from the version in `package.json`. | ||||
|   1. Checkout branch created for the last sprint that has been successfully tested. | ||||
|   2. Remove a `-SNAPSHOT` suffix from the version in `package.json`. | ||||
|   3. Verify that resulting version number meets semantic versioning | ||||
|      requirements relative to previous stable version. Increment the  | ||||
|      version number if necessary. | ||||
|   | ||||
| @@ -93,5 +93,36 @@ define([ | ||||
|         }; | ||||
|     }; | ||||
|  | ||||
|     SinewaveLimitProvider.prototype.getLimits = function (domainObject) { | ||||
|         return { | ||||
|             limits: function () { | ||||
|                 return { | ||||
|                     WARNING: { | ||||
|                         low: { | ||||
|                             cssClass: "is-limit--lwr is-limit--yellow", | ||||
|                             sin: -YELLOW.sin, | ||||
|                             cos: -YELLOW.cos | ||||
|                         }, | ||||
|                         high: { | ||||
|                             cssClass: "is-limit--upr is-limit--yellow", | ||||
|                             ...YELLOW | ||||
|                         } | ||||
|                     }, | ||||
|                     DISTRESS: { | ||||
|                         low: { | ||||
|                             cssClass: "is-limit--lwr is-limit--red", | ||||
|                             sin: -RED.sin, | ||||
|                             cos: -RED.cos | ||||
|                         }, | ||||
|                         high: { | ||||
|                             cssClass: "is-limit--upr is-limit--red", | ||||
|                             ...RED | ||||
|                         } | ||||
|                     } | ||||
|                 }; | ||||
|             } | ||||
|         }; | ||||
|     }; | ||||
|  | ||||
|     return SinewaveLimitProvider; | ||||
| }); | ||||
|   | ||||
| @@ -50,11 +50,16 @@ define([ | ||||
|         const IMAGE_DELAY = 20000; | ||||
|  | ||||
|         function pointForTimestamp(timestamp, name) { | ||||
|             const url = IMAGE_SAMPLES[Math.floor(timestamp / IMAGE_DELAY) % IMAGE_SAMPLES.length]; | ||||
|             const urlItems = url.split('/'); | ||||
|             const imageDownloadName = `example.imagery.${urlItems[urlItems.length - 1]}`; | ||||
|  | ||||
|             return { | ||||
|                 name: name, | ||||
|                 name, | ||||
|                 utc: Math.floor(timestamp / IMAGE_DELAY) * IMAGE_DELAY, | ||||
|                 local: Math.floor(timestamp / IMAGE_DELAY) * IMAGE_DELAY, | ||||
|                 url: IMAGE_SAMPLES[Math.floor(timestamp / IMAGE_DELAY) % IMAGE_SAMPLES.length] | ||||
|                 url, | ||||
|                 imageDownloadName | ||||
|             }; | ||||
|         } | ||||
|  | ||||
| @@ -139,6 +144,14 @@ define([ | ||||
|                                 hints: { | ||||
|                                     image: 1 | ||||
|                                 } | ||||
|                             }, | ||||
|                             { | ||||
|                                 name: 'Image Download Name', | ||||
|                                 key: 'imageDownloadName', | ||||
|                                 format: 'imageDownloadName', | ||||
|                                 hints: { | ||||
|                                     imageDownloadName: 1 | ||||
|                                 } | ||||
|                             } | ||||
|                         ] | ||||
|                     }; | ||||
|   | ||||
| @@ -88,7 +88,6 @@ | ||||
|         openmct.install(openmct.plugins.ExampleImagery()); | ||||
|         openmct.install(openmct.plugins.PlanLayout()); | ||||
|         openmct.install(openmct.plugins.Timeline()); | ||||
|         openmct.install(openmct.plugins.PlotVue()); | ||||
|         openmct.install(openmct.plugins.UTCTimeSystem()); | ||||
|         openmct.install(openmct.plugins.AutoflowView({ | ||||
|             type: "telemetry.panel" | ||||
|   | ||||
| @@ -78,6 +78,7 @@ module.exports = (config) => { | ||||
|             preserveDescribeNesting: true, | ||||
|             foldAll: false | ||||
|         }, | ||||
|         browserConsoleLogOptions: { level: "error",  format: "%b %T: %m",  terminal: true }, | ||||
|         coverageIstanbulReporter: { | ||||
|             fixWebpackSourcePaths: true, | ||||
|             dir: process.env.CIRCLE_ARTIFACTS ? | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "openmct", | ||||
|   "version": "1.7.1-SNAPSHOT", | ||||
|   "version": "1.7.3-SNAPSHOT", | ||||
|   "description": "The Open MCT core platform", | ||||
|   "dependencies": {}, | ||||
|   "devDependencies": { | ||||
| @@ -78,7 +78,8 @@ | ||||
|     "zepto": "^1.2.0" | ||||
|   }, | ||||
|   "scripts": { | ||||
|     "clean": "rm -rf ./dist", | ||||
|     "clean": "rm -rf ./dist /node_modules; rm package-lock.json", | ||||
|     "clean-test-lint": "npm run clean; npm install ; npm run test; npm run lint", | ||||
|     "start": "node app.js", | ||||
|     "lint": "eslint platform example src --ext .js,.vue openmct.js", | ||||
|     "lint:fix": "eslint platform example src --ext .js,.vue openmct.js --fix", | ||||
|   | ||||
| @@ -86,7 +86,7 @@ define( | ||||
|                         }) | ||||
|                         .join('/'); | ||||
|  | ||||
|                 window.location.href = url; | ||||
|                 openmct.router.navigate(url); | ||||
|  | ||||
|                 if (isFirstViewEditable(object.useCapability('adapter'), objectPath)) { | ||||
|                     openmct.editor.edit(); | ||||
|   | ||||
| @@ -141,11 +141,17 @@ define( | ||||
|                 if (mutationResult !== false) { | ||||
|                     // Copy values if result was a different object | ||||
|                     // (either our clone or some other new thing) | ||||
|                     if (model !== result) { | ||||
|                     let modelHasChanged = _.isEqual(model, result) === false; | ||||
|                     if (modelHasChanged) { | ||||
|                         copyValues(model, result); | ||||
|                     } | ||||
|  | ||||
|                     model.modified = useTimestamp ? timestamp : now(); | ||||
|                     if (modelHasChanged | ||||
|                         || (useTimestamp !== undefined) | ||||
|                         || (model.modified === undefined)) { | ||||
|                         model.modified = useTimestamp ? timestamp : now(); | ||||
|                     } | ||||
|  | ||||
|                     notifyListeners(model); | ||||
|                 } | ||||
|  | ||||
|   | ||||
| @@ -23,13 +23,11 @@ | ||||
| define([ | ||||
|     "moment-timezone", | ||||
|     "./src/indicators/ClockIndicator", | ||||
|     "./src/indicators/FollowIndicator", | ||||
|     "./src/services/TickerService", | ||||
|     "./src/services/TimerService", | ||||
|     "./src/controllers/ClockController", | ||||
|     "./src/controllers/TimerController", | ||||
|     "./src/controllers/RefreshingController", | ||||
|     "./src/actions/FollowTimerAction", | ||||
|     "./src/actions/StartTimerAction", | ||||
|     "./src/actions/RestartTimerAction", | ||||
|     "./src/actions/StopTimerAction", | ||||
| @@ -39,13 +37,11 @@ define([ | ||||
| ], function ( | ||||
|     MomentTimezone, | ||||
|     ClockIndicator, | ||||
|     FollowIndicator, | ||||
|     TickerService, | ||||
|     TimerService, | ||||
|     ClockController, | ||||
|     TimerController, | ||||
|     RefreshingController, | ||||
|     FollowTimerAction, | ||||
|     StartTimerAction, | ||||
|     RestartTimerAction, | ||||
|     StopTimerAction, | ||||
| @@ -144,15 +140,6 @@ define([ | ||||
|                     } | ||||
|                 ], | ||||
|                 "actions": [ | ||||
|                     { | ||||
|                         "key": "timer.follow", | ||||
|                         "implementation": FollowTimerAction, | ||||
|                         "depends": ["timerService"], | ||||
|                         "category": "contextual", | ||||
|                         "name": "Follow Timer", | ||||
|                         "cssClass": "icon-clock", | ||||
|                         "priority": "optional" | ||||
|                     }, | ||||
|                     { | ||||
|                         "key": "timer.start", | ||||
|                         "implementation": StartTimerAction, | ||||
| @@ -299,10 +286,7 @@ define([ | ||||
|                         } | ||||
|                     } | ||||
|                 ], | ||||
|                 "runs": [{ | ||||
|                     "implementation": FollowIndicator, | ||||
|                     "depends": ["openmct", "timerService"] | ||||
|                 }], | ||||
|                 "runs": [], | ||||
|                 "licenses": [ | ||||
|                     { | ||||
|                         "name": "moment-duration-format", | ||||
|   | ||||
| @@ -1,56 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2009-2016, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define( | ||||
|     [], | ||||
|     function () { | ||||
|  | ||||
|         /** | ||||
|          * Designates a specific timer for following. Timelines, for example, | ||||
|          * use the actively followed timer to display a time-of-interest line | ||||
|          * and interpret time conductor bounds in the Timeline's relative | ||||
|          * time frame. | ||||
|          * | ||||
|          * @implements {Action} | ||||
|          * @memberof platform/features/clock | ||||
|          * @constructor | ||||
|          * @param {ActionContext} context the context for this action | ||||
|          */ | ||||
|         function FollowTimerAction(timerService, context) { | ||||
|             var domainObject = | ||||
|                 context.domainObject | ||||
|                 && context.domainObject.useCapability('adapter'); | ||||
|             this.perform = | ||||
|                 timerService.setTimer.bind(timerService, domainObject); | ||||
|         } | ||||
|  | ||||
|         FollowTimerAction.appliesTo = function (context) { | ||||
|             var model = | ||||
|                 (context.domainObject && context.domainObject.getModel()) | ||||
|                 || {}; | ||||
|  | ||||
|             return model.type === 'timer'; | ||||
|         }; | ||||
|  | ||||
|         return FollowTimerAction; | ||||
|     } | ||||
| ); | ||||
| @@ -1,51 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2009-2018, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define([], function () { | ||||
|  | ||||
|     /** | ||||
|      * Indicator that displays the active timer, as well as its | ||||
|      * current state. | ||||
|      * @memberof platform/features/clock | ||||
|      */ | ||||
|     return function installFollowIndicator(openmct, timerService) { | ||||
|         var indicator = openmct.indicators.simpleIndicator(); | ||||
|         var timer = timerService.getTimer(); | ||||
|         setIndicatorStatus(timer); | ||||
|  | ||||
|         function setIndicatorStatus(newTimer) { | ||||
|             if (newTimer !== undefined) { | ||||
|                 indicator.iconClass('icon-timer'); | ||||
|                 indicator.statusClass('s-status-on'); | ||||
|                 indicator.text('Following timer ' + newTimer.name); | ||||
|             } else { | ||||
|                 indicator.iconClass('icon-timer'); | ||||
|                 indicator.statusClass('s-status-disabled'); | ||||
|                 indicator.text('No timer being followed'); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         timerService.on('change', setIndicatorStatus); | ||||
|  | ||||
|         openmct.indicators.add(indicator); | ||||
|     }; | ||||
| }); | ||||
| @@ -1,89 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2009-2016, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define([ | ||||
|     "../../src/actions/FollowTimerAction" | ||||
| ], function (FollowTimerAction) { | ||||
|     var TIMER_SERVICE_METHODS = | ||||
|         ['setTimer', 'getTimer', 'clearTimer', 'on', 'off']; | ||||
|  | ||||
|     describe("The Follow Timer action", function () { | ||||
|         var testContext; | ||||
|         var testModel; | ||||
|         var testAdaptedObject; | ||||
|  | ||||
|         beforeEach(function () { | ||||
|             testModel = {}; | ||||
|             testContext = { | ||||
|                 domainObject: jasmine.createSpyObj('domainObject', [ | ||||
|                     'getModel', | ||||
|                     'useCapability' | ||||
|                 ]) | ||||
|             }; | ||||
|             testAdaptedObject = { foo: 'bar' }; | ||||
|             testContext.domainObject.getModel.and.returnValue(testModel); | ||||
|             testContext.domainObject.useCapability.and.callFake(function (c) { | ||||
|                 return c === 'adapter' && testAdaptedObject; | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         it("is applicable to timers", function () { | ||||
|             testModel.type = "timer"; | ||||
|             expect(FollowTimerAction.appliesTo(testContext)).toBe(true); | ||||
|         }); | ||||
|  | ||||
|         it("is inapplicable to non-timers", function () { | ||||
|             testModel.type = "folder"; | ||||
|             expect(FollowTimerAction.appliesTo(testContext)).toBe(false); | ||||
|         }); | ||||
|  | ||||
|         describe("when instantiated", function () { | ||||
|             var mockTimerService; | ||||
|             var action; | ||||
|  | ||||
|             beforeEach(function () { | ||||
|                 mockTimerService = jasmine.createSpyObj( | ||||
|                     'timerService', | ||||
|                     TIMER_SERVICE_METHODS | ||||
|                 ); | ||||
|                 action = new FollowTimerAction(mockTimerService, testContext); | ||||
|             }); | ||||
|  | ||||
|             it("does not interact with the timer service", function () { | ||||
|                 TIMER_SERVICE_METHODS.forEach(function (method) { | ||||
|                     expect(mockTimerService[method]).not.toHaveBeenCalled(); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|             describe("and performed", function () { | ||||
|                 beforeEach(function () { | ||||
|                     action.perform(); | ||||
|                 }); | ||||
|  | ||||
|                 it("sets the active timer", function () { | ||||
|                     expect(mockTimerService.setTimer) | ||||
|                         .toHaveBeenCalledWith(testAdaptedObject); | ||||
|                 }); | ||||
|             }); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
| @@ -1,96 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2009-2018, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define([ | ||||
|     "../../src/indicators/FollowIndicator", | ||||
|     "../../src/services/TimerService", | ||||
|     "../../../../../src/MCT", | ||||
|     'zepto' | ||||
| ], function ( | ||||
|     FollowIndicator, | ||||
|     TimerService, | ||||
|     MCT, | ||||
|     $ | ||||
| ) { | ||||
|     xdescribe("The timer-following indicator", function () { | ||||
|         var timerService; | ||||
|         var openmct; | ||||
|  | ||||
|         beforeEach(function () { | ||||
|             openmct = new MCT(); | ||||
|             timerService = new TimerService(openmct); | ||||
|             spyOn(openmct.indicators, "add"); | ||||
|         }); | ||||
|  | ||||
|         it("adds an indicator when installed", function () { | ||||
|             FollowIndicator(openmct, timerService); | ||||
|             expect(openmct.indicators.add).toHaveBeenCalled(); | ||||
|         }); | ||||
|  | ||||
|         it("indicates that no timer is being followed", function () { | ||||
|             FollowIndicator(openmct, timerService); | ||||
|             var simpleIndicator = openmct.indicators.add.calls.mostRecent().args[0]; | ||||
|             var element = simpleIndicator.element; | ||||
|             var text = $('.indicator-text', element).text().trim(); | ||||
|             expect(text).toEqual('No timer being followed'); | ||||
|         }); | ||||
|  | ||||
|         describe("when a timer is set", function () { | ||||
|             var testObject; | ||||
|             var simpleIndicator; | ||||
|  | ||||
|             beforeEach(function () { | ||||
|                 testObject = { | ||||
|                     identifier: { | ||||
|                         namespace: 'namespace', | ||||
|                         key: 'key' | ||||
|                     }, | ||||
|                     name: "some timer!" | ||||
|                 }; | ||||
|                 timerService.setTimer(testObject); | ||||
|                 FollowIndicator(openmct, timerService); | ||||
|                 simpleIndicator = openmct.indicators.add.calls.mostRecent().args[0]; | ||||
|             }); | ||||
|  | ||||
|             it("displays the timer's name", function () { | ||||
|                 var element = simpleIndicator.element; | ||||
|                 var text = $('.indicator-text', element).text().trim(); | ||||
|                 expect(text).toEqual('Following timer ' + testObject.name); | ||||
|             }); | ||||
|  | ||||
|             it("displays the timer's name when it changes", function () { | ||||
|                 var secondTimer = { | ||||
|                     identifier: { | ||||
|                         namespace: 'namespace', | ||||
|                         key: 'key2' | ||||
|                     }, | ||||
|                     name: "Some other timer" | ||||
|                 }; | ||||
|                 var element = simpleIndicator.element; | ||||
|                 timerService.setTimer(secondTimer); | ||||
|                 var text = $('.indicator-text', element).text().trim(); | ||||
|                 expect(text).toEqual('Following timer ' + secondTimer.name); | ||||
|             }); | ||||
|  | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
| @@ -252,7 +252,7 @@ define([ | ||||
|  | ||||
|         this.status = new api.StatusAPI(this); | ||||
|  | ||||
|         this.router = new ApplicationRouter(); | ||||
|         this.router = new ApplicationRouter(this); | ||||
|  | ||||
|         this.branding = BrandingAPI.default; | ||||
|  | ||||
|   | ||||
| @@ -36,7 +36,8 @@ define([ | ||||
|     './views/installLegacyViews', | ||||
|     './policies/LegacyCompositionPolicyAdapter', | ||||
|     './actions/LegacyActionAdapter', | ||||
|     './services/LegacyPersistenceAdapter' | ||||
|     './services/LegacyPersistenceAdapter', | ||||
|     './services/ExportImageService' | ||||
| ], function ( | ||||
|     ActionDialogDecorator, | ||||
|     AdapterCapability, | ||||
| @@ -53,7 +54,8 @@ define([ | ||||
|     installLegacyViews, | ||||
|     legacyCompositionPolicyAdapter, | ||||
|     LegacyActionAdapter, | ||||
|     LegacyPersistenceAdapter | ||||
|     LegacyPersistenceAdapter, | ||||
|     ExportImageService | ||||
| ) { | ||||
|     return { | ||||
|         name: 'src/adapter', | ||||
| @@ -82,6 +84,13 @@ define([ | ||||
|                             "identifierService", | ||||
|                             "cacheService" | ||||
|                         ] | ||||
|                     }, | ||||
|                     { | ||||
|                         "key": "exportImageService", | ||||
|                         "implementation": ExportImageService, | ||||
|                         "depends": [ | ||||
|                             "dialogService" | ||||
|                         ] | ||||
|                     } | ||||
|                 ], | ||||
|                 components: [ | ||||
|   | ||||
| @@ -161,6 +161,22 @@ define([ | ||||
|             evaluate: function (datum, property) { | ||||
|                 return limitEvaluator.evaluate(datum, property && property.key); | ||||
|             } | ||||
|  | ||||
|         }; | ||||
|     }; | ||||
|  | ||||
|     LegacyTelemetryProvider.prototype.getLimits = function (domainObject) { | ||||
|         const oldObject = this.instantiate( | ||||
|             utils.toOldFormat(domainObject), | ||||
|             utils.makeKeyString(domainObject.identifier) | ||||
|         ); | ||||
|         const limitEvaluator = oldObject.getCapability("limit"); | ||||
|  | ||||
|         return { | ||||
|             limits: function () { | ||||
|                 return limitEvaluator.limits(); | ||||
|             } | ||||
|  | ||||
|         }; | ||||
|     }; | ||||
|  | ||||
|   | ||||
| @@ -24,13 +24,6 @@ define([ | ||||
|                     return false; | ||||
|                 } | ||||
|  | ||||
|                 //TODO: Remove this when plots Angular implementation is deprecated | ||||
|                 let parent = selection[0].length > 1 && selection[0][1].context.item; | ||||
|                 if (parent && parent.type === 'time-strip') { | ||||
|                     return (selectionContext.item.type === typeDefinition.key) | ||||
|                             && (typeDefinition.key !== 'telemetry.plot.overlay'); | ||||
|                 } | ||||
|  | ||||
|                 return selectionContext.item.type === typeDefinition.key; | ||||
|             }, | ||||
|             view: function (selection) { | ||||
|   | ||||
| @@ -119,7 +119,8 @@ describe('The ActionCollection', () => { | ||||
|  | ||||
|     afterEach(() => { | ||||
|         actionCollection.destroy(); | ||||
|         resetApplicationState(openmct); | ||||
|  | ||||
|         return resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|     describe("disable method invoked with action keys", () => { | ||||
|   | ||||
| @@ -99,7 +99,7 @@ describe('The Actions API', () => { | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         resetApplicationState(openmct); | ||||
|         return resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|     describe("register method", () => { | ||||
|   | ||||
| @@ -215,12 +215,12 @@ define([ | ||||
|      * @memberof {module:openmct.CompositionCollection#} | ||||
|      * @name load | ||||
|      */ | ||||
|     CompositionCollection.prototype.load = function () { | ||||
|     CompositionCollection.prototype.load = function (abortSignal) { | ||||
|         this.cleanUpMutables(); | ||||
|  | ||||
|         return this.provider.load(this.domainObject) | ||||
|             .then(function (children) { | ||||
|                 return Promise.all(children.map((c) => this.publicAPI.objects.get(c))); | ||||
|                 return Promise.all(children.map((c) => this.publicAPI.objects.get(c, abortSignal))); | ||||
|             }.bind(this)) | ||||
|             .then(function (childObjects) { | ||||
|                 childObjects.forEach(c => this.add(c, true)); | ||||
|   | ||||
| @@ -76,7 +76,7 @@ describe ('The Menu API', () => { | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         resetApplicationState(openmct); | ||||
|         return resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|     describe("showMenu method", () => { | ||||
|   | ||||
| @@ -161,6 +161,7 @@ ObjectAPI.prototype.addProvider = function (namespace, provider) { | ||||
|  | ||||
| ObjectAPI.prototype.get = function (identifier, abortSignal) { | ||||
|     let keystring = this.makeKeyString(identifier); | ||||
|  | ||||
|     if (this.cache[keystring] !== undefined) { | ||||
|         return this.cache[keystring]; | ||||
|     } | ||||
| @@ -176,15 +177,16 @@ ObjectAPI.prototype.get = function (identifier, abortSignal) { | ||||
|         throw new Error('Provider does not support get!'); | ||||
|     } | ||||
|  | ||||
|     let objectPromise = provider.get(identifier, abortSignal); | ||||
|     this.cache[keystring] = objectPromise; | ||||
|  | ||||
|     return objectPromise.then(result => { | ||||
|     let objectPromise = provider.get(identifier, abortSignal).then(result => { | ||||
|         delete this.cache[keystring]; | ||||
|         result = this.applyGetInterceptors(identifier, result); | ||||
|  | ||||
|         return result; | ||||
|     }); | ||||
|  | ||||
|     this.cache[keystring] = objectPromise; | ||||
|  | ||||
|     return objectPromise; | ||||
| }; | ||||
|  | ||||
| /** | ||||
| @@ -484,6 +486,12 @@ ObjectAPI.prototype.getOriginalPath = function (identifier, path = []) { | ||||
|     }); | ||||
| }; | ||||
|  | ||||
| ObjectAPI.prototype.isObjectPathToALink = function (domainObject, objectPath) { | ||||
|     return objectPath !== undefined | ||||
|         && objectPath.length > 1 | ||||
|         && domainObject.location !== this.makeKeyString(objectPath[1].identifier); | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Uniquely identifies a domain object. | ||||
|  * | ||||
|   | ||||
| @@ -22,7 +22,7 @@ describe("The Status API", () => { | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         resetApplicationState(openmct); | ||||
|         return resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|     describe("set function", () => { | ||||
|   | ||||
| @@ -504,6 +504,26 @@ define([ | ||||
|         return this.getLimitEvaluator(domainObject); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Get a limits for this domain object. | ||||
|      * Limits help you display limits and alarms of | ||||
|      * telemetry for display purposes without having to interact directly | ||||
|      * with the Limit API. | ||||
|      * | ||||
|      * This method is optional. | ||||
|      * If a provider does not implement this method, it is presumed | ||||
|      * that no limits are defined for this domain object's telemetry. | ||||
|      * | ||||
|      * @param {module:openmct.DomainObject} domainObject the domain | ||||
|      *        object for which to get limits | ||||
|      * @returns {module:openmct.TelemetryAPI~LimitEvaluator} | ||||
|      * @method limits | ||||
|      * @memberof module:openmct.TelemetryAPI~TelemetryProvider# | ||||
|      */ | ||||
|     TelemetryAPI.prototype.limitDefinition = function (domainObject) { | ||||
|         return this.getLimits(domainObject); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Get a limit evaluator for this domain object. | ||||
|      * Limit Evaluators help you evaluate limit and alarm status of individual | ||||
| @@ -531,5 +551,42 @@ define([ | ||||
|         return provider.getLimitEvaluator(domainObject); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Get a limit definitions for this domain object. | ||||
|      * Limit Definitions help you indicate limits and alarms of | ||||
|      * telemetry for display purposes without having to interact directly | ||||
|      * with the Limit API. | ||||
|      * | ||||
|      * This method is optional. | ||||
|      * If a provider does not implement this method, it is presumed | ||||
|      * that no limits are defined for this domain object's telemetry. | ||||
|      * | ||||
|      * @param {module:openmct.DomainObject} domainObject the domain | ||||
|      *        object for which to display limits | ||||
|      * @returns {module:openmct.TelemetryAPI~LimitEvaluator} | ||||
|      * @method limits returns a limits object of | ||||
|      * type { | ||||
|      *          level1: { | ||||
|      *              low: { key1: value1, key2: value2 }, | ||||
|      *              high: { key1: value1, key2: value2 } | ||||
|      *          }, | ||||
|      *          level2: { | ||||
|      *              low: { key1: value1, key2: value2 }, | ||||
|      *              high: { key1: value1, key2: value2 } | ||||
|      *          } | ||||
|      *       } | ||||
|      * @memberof module:openmct.TelemetryAPI~TelemetryProvider# | ||||
|      */ | ||||
|     TelemetryAPI.prototype.getLimits = function (domainObject) { | ||||
|         const provider = this.findLimitEvaluator(domainObject); | ||||
|         if (!provider) { | ||||
|             return { | ||||
|                 limits: function () {} | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         return provider.getLimits(domainObject); | ||||
|     }; | ||||
|  | ||||
|     return TelemetryAPI; | ||||
| }); | ||||
|   | ||||
| @@ -45,10 +45,14 @@ export default function LADTableSetViewProvider(openmct) { | ||||
|                         }, | ||||
|                         provide: { | ||||
|                             openmct, | ||||
|                             domainObject, | ||||
|                             objectPath | ||||
|                         }, | ||||
|                         template: '<lad-table-set></lad-table-set>' | ||||
|                         data() { | ||||
|                             return { | ||||
|                                 domainObject | ||||
|                             }; | ||||
|                         }, | ||||
|                         template: '<lad-table-set :domain-object="domainObject"></lad-table-set>' | ||||
|                     }); | ||||
|                 }, | ||||
|                 destroy: function (element) { | ||||
|   | ||||
| @@ -56,7 +56,7 @@ export default { | ||||
|             type: Object, | ||||
|             required: true | ||||
|         }, | ||||
|         objectPath: { | ||||
|         pathToTable: { | ||||
|             type: Array, | ||||
|             required: true | ||||
|         }, | ||||
| @@ -66,20 +66,19 @@ export default { | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
|         let currentObjectPath = this.objectPath.slice(); | ||||
|         currentObjectPath.unshift(this.domainObject); | ||||
|  | ||||
|         return { | ||||
|             timestamp: undefined, | ||||
|             value: '---', | ||||
|             valueClass: '', | ||||
|             currentObjectPath, | ||||
|             unit: '' | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
|         formattedTimestamp() { | ||||
|             return this.timestamp !== undefined ? this.getFormattedTimestamp(this.timestamp) : '---'; | ||||
|         }, | ||||
|         objectPath() { | ||||
|             return [this.domainObject, ...this.pathToTable]; | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
| @@ -182,7 +181,7 @@ export default { | ||||
|             }; | ||||
|         }, | ||||
|         showContextMenu(event) { | ||||
|             let actionCollection = this.openmct.actions.get(this.currentObjectPath, this.getView()); | ||||
|             let actionCollection = this.openmct.actions.get(this.objectPath, this.getView()); | ||||
|             let allActions = actionCollection.getActionsObject(); | ||||
|             let applicableActions = CONTEXT_MENU_ACTIONS.map(key => allActions[key]); | ||||
|  | ||||
|   | ||||
| @@ -33,10 +33,10 @@ | ||||
|         </thead> | ||||
|         <tbody> | ||||
|             <lad-row | ||||
|                 v-for="item in items" | ||||
|                 :key="item.key" | ||||
|                 :domain-object="item.domainObject" | ||||
|                 :object-path="objectPath" | ||||
|                 v-for="ladRow in items" | ||||
|                 :key="ladRow.key" | ||||
|                 :domain-object="ladRow.domainObject" | ||||
|                 :path-to-table="objectPath" | ||||
|                 :has-units="hasUnits" | ||||
|             /> | ||||
|         </tbody> | ||||
|   | ||||
| @@ -43,9 +43,10 @@ | ||||
|                 </td> | ||||
|             </tr> | ||||
|             <lad-row | ||||
|                 v-for="telemetryObject in ladTelemetryObjects[ladTable.key]" | ||||
|                 :key="telemetryObject.key" | ||||
|                 :domain-object="telemetryObject.domainObject" | ||||
|                 v-for="ladRow in ladTelemetryObjects[ladTable.key]" | ||||
|                 :key="ladRow.key" | ||||
|                 :domain-object="ladRow.domainObject" | ||||
|                 :path-to-table="ladTable.objectPath" | ||||
|                 :has-units="hasUnits" | ||||
|             /> | ||||
|         </template> | ||||
| @@ -60,7 +61,13 @@ export default { | ||||
|     components: { | ||||
|         LadRow | ||||
|     }, | ||||
|     inject: ['openmct', 'domainObject'], | ||||
|     inject: ['openmct', 'objectPath'], | ||||
|     props: { | ||||
|         domainObject: { | ||||
|             type: Object, | ||||
|             required: true | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             ladTableObjects: [], | ||||
| @@ -106,6 +113,7 @@ export default { | ||||
|             let ladTable = {}; | ||||
|             ladTable.domainObject = domainObject; | ||||
|             ladTable.key = this.openmct.objects.makeKeyString(domainObject.identifier); | ||||
|             ladTable.objectPath = [domainObject, ...this.objectPath]; | ||||
|  | ||||
|             this.$set(this.ladTelemetryObjects, ladTable.key, []); | ||||
|             this.ladTableObjects.push(ladTable); | ||||
|   | ||||
| @@ -292,6 +292,11 @@ describe("The LAD Table Set", () => { | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         openmct.time.timeSystem('utc', { | ||||
|             start: 0, | ||||
|             end: 1 | ||||
|         }); | ||||
|  | ||||
|         return resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|   | ||||
| @@ -19,10 +19,6 @@ | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
| import { | ||||
|     getAllSearchParams, | ||||
|     setAllSearchParams | ||||
| } from 'utils/openmctLocation'; | ||||
|  | ||||
| const TIME_EVENTS = ['timeSystem', 'clock', 'clockOffsets']; | ||||
| const SEARCH_MODE = 'tc.mode'; | ||||
| @@ -49,9 +45,8 @@ export default class URLTimeSettingsSynchronizer { | ||||
|     } | ||||
|  | ||||
|     initialize() { | ||||
|         this.updateTimeSettings(); | ||||
|         this.openmct.router.on('change:params', this.updateTimeSettings); | ||||
|  | ||||
|         window.addEventListener('hashchange', this.updateTimeSettings); | ||||
|         TIME_EVENTS.forEach(event => { | ||||
|             this.openmct.time.on(event, this.setUrlFromTimeApi); | ||||
|         }); | ||||
| @@ -59,7 +54,8 @@ export default class URLTimeSettingsSynchronizer { | ||||
|     } | ||||
|  | ||||
|     destroy() { | ||||
|         window.removeEventListener('hashchange', this.updateTimeSettings); | ||||
|         this.openmct.router.off('change:params', this.updateTimeSettings); | ||||
|  | ||||
|         this.openmct.off('start', this.initialize); | ||||
|         this.openmct.off('destroy', this.destroy); | ||||
|  | ||||
| @@ -70,22 +66,18 @@ export default class URLTimeSettingsSynchronizer { | ||||
|     } | ||||
|  | ||||
|     updateTimeSettings() { | ||||
|         // Prevent from triggering self | ||||
|         if (!this.isUrlUpdateInProgress) { | ||||
|             let timeParameters = this.parseParametersFromUrl(); | ||||
|         let timeParameters = this.parseParametersFromUrl(); | ||||
|  | ||||
|             if (this.areTimeParametersValid(timeParameters)) { | ||||
|                 this.setTimeApiFromUrl(timeParameters); | ||||
|             } else { | ||||
|                 this.setUrlFromTimeApi(); | ||||
|             } | ||||
|         if (this.areTimeParametersValid(timeParameters)) { | ||||
|             this.setTimeApiFromUrl(timeParameters); | ||||
|             this.openmct.router.setLocationFromUrl(); | ||||
|         } else { | ||||
|             this.isUrlUpdateInProgress = false; | ||||
|             this.setUrlFromTimeApi(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     parseParametersFromUrl() { | ||||
|         let searchParams = getAllSearchParams(); | ||||
|         let searchParams = this.openmct.router.getAllSearchParams(); | ||||
|  | ||||
|         let mode = searchParams.get(SEARCH_MODE); | ||||
|         let timeSystem = searchParams.get(SEARCH_TIME_SYSTEM); | ||||
| @@ -148,7 +140,7 @@ export default class URLTimeSettingsSynchronizer { | ||||
|     } | ||||
|  | ||||
|     setUrlFromTimeApi() { | ||||
|         let searchParams = getAllSearchParams(); | ||||
|         let searchParams = this.openmct.router.getAllSearchParams(); | ||||
|         let clock = this.openmct.time.clock(); | ||||
|         let bounds = this.openmct.time.bounds(); | ||||
|         let clockOffsets = this.openmct.time.clockOffsets(); | ||||
| @@ -176,8 +168,7 @@ export default class URLTimeSettingsSynchronizer { | ||||
|         } | ||||
|  | ||||
|         searchParams.set(SEARCH_TIME_SYSTEM, this.openmct.time.timeSystem().key); | ||||
|         this.isUrlUpdateInProgress = true; | ||||
|         setAllSearchParams(searchParams); | ||||
|         this.openmct.router.setAllSearchParams(searchParams); | ||||
|     } | ||||
|  | ||||
|     areTimeParametersValid(timeParameters) { | ||||
|   | ||||
| @@ -25,306 +25,118 @@ import { | ||||
| } from 'utils/testing'; | ||||
|  | ||||
| describe("The URLTimeSettingsSynchronizer", () => { | ||||
|     let appHolder; | ||||
|     let openmct; | ||||
|     let testClock; | ||||
|     let resolveFunction; | ||||
|     let oldHash; | ||||
|  | ||||
|     beforeEach((done) => { | ||||
|         openmct = createOpenMct(); | ||||
|         openmct.install(openmct.plugins.MyItems()); | ||||
|         openmct.install(openmct.plugins.LocalTimeSystem()); | ||||
|         testClock = jasmine.createSpyObj("testClock", ["start", "stop", "tick", "currentValue", "on", "off"]); | ||||
|         testClock.key = "test-clock"; | ||||
|         testClock.currentValue.and.returnValue(0); | ||||
|  | ||||
|         openmct.time.addClock(testClock); | ||||
|         openmct.install(openmct.plugins.UTCTimeSystem()); | ||||
|  | ||||
|         openmct.on('start', done); | ||||
|         openmct.startHeadless(); | ||||
|  | ||||
|         appHolder = document.createElement("div"); | ||||
|         openmct.start(appHolder); | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => resetApplicationState(openmct)); | ||||
|     afterEach(() => { | ||||
|         openmct.time.stopClock(); | ||||
|         openmct.router.removeListener('change:hash', resolveFunction); | ||||
|  | ||||
|     describe("realtime mode", () => { | ||||
|         it("when the clock is set via the time API, it is immediately reflected in the URL", () => { | ||||
|             //Test expected initial conditions | ||||
|         appHolder = undefined; | ||||
|         openmct = undefined; | ||||
|         resolveFunction = undefined; | ||||
|  | ||||
|         return resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|     it("initial clock is set to fixed is reflected in URL", (done) => { | ||||
|         resolveFunction = () => { | ||||
|             oldHash = window.location.hash; | ||||
|             expect(window.location.hash.includes('tc.mode=fixed')).toBe(true); | ||||
|  | ||||
|             openmct.router.removeListener('change:hash', resolveFunction); | ||||
|             done(); | ||||
|         }; | ||||
|  | ||||
|         openmct.router.on('change:hash', resolveFunction); | ||||
|     }); | ||||
|  | ||||
|     it("when the clock is set via the time API, it is reflected in the URL", (done) => { | ||||
|         let success; | ||||
|  | ||||
|         resolveFunction = () => { | ||||
|             openmct.time.clock('local', { | ||||
|                 start: -1000, | ||||
|                 end: 100 | ||||
|             }); | ||||
|  | ||||
|             expect(window.location.hash.includes('tc.mode=local')).toBe(true); | ||||
|  | ||||
|             //Test that expected initial conditions are no longer true | ||||
|             expect(window.location.hash.includes('tc.mode=fixed')).toBe(false); | ||||
|         }); | ||||
|         it("when offsets are set via the time API, they are immediately reflected in the URL", () => { | ||||
|             //Test expected initial conditions | ||||
|             expect(window.location.hash.includes('tc.startDelta')).toBe(false); | ||||
|             expect(window.location.hash.includes('tc.endDelta')).toBe(false); | ||||
|  | ||||
|             openmct.time.clock('local', { | ||||
|                 start: -1000, | ||||
|                 end: 100 | ||||
|             }); | ||||
|             expect(window.location.hash.includes('tc.startDelta=1000')).toBe(true); | ||||
|             expect(window.location.hash.includes('tc.endDelta=100')).toBe(true); | ||||
|  | ||||
|             openmct.time.clockOffsets({ | ||||
|                 start: -2000, | ||||
|                 end: 200 | ||||
|             }); | ||||
|             expect(window.location.hash.includes('tc.startDelta=2000')).toBe(true); | ||||
|             expect(window.location.hash.includes('tc.endDelta=200')).toBe(true); | ||||
|  | ||||
|             //Test that expected initial conditions are no longer true | ||||
|             expect(window.location.hash.includes('tc.mode=fixed')).toBe(false); | ||||
|         }); | ||||
|         describe("when set in the url", () => { | ||||
|             it("will change from fixed to realtime mode when the mode changes", () => { | ||||
|                 expectLocationToBeInFixedMode(); | ||||
|             const hasStartDelta = window.location.hash.includes('tc.startDelta=2000'); | ||||
|             const hasEndDelta = window.location.hash.includes('tc.endDelta=200'); | ||||
|             const hasLocalClock = window.location.hash.includes('tc.mode=local'); | ||||
|             success = hasStartDelta && hasEndDelta && hasLocalClock; | ||||
|             if (success) { | ||||
|                 expect(success).toBe(true); | ||||
|  | ||||
|                 return switchToRealtimeMode().then(() => { | ||||
|                     let clock = openmct.time.clock(); | ||||
|                 openmct.router.removeListener('change:hash', resolveFunction); | ||||
|                 done(); | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|                     expect(clock).toBeDefined(); | ||||
|                     expect(clock.key).toBe('local'); | ||||
|                 }); | ||||
|             }); | ||||
|             it("the clock is correctly set in the API from the URL parameters", () => { | ||||
|                 return switchToRealtimeMode().then(() => { | ||||
|                     let resolveFunction; | ||||
|  | ||||
|                     return new Promise((resolve) => { | ||||
|                         resolveFunction = resolve; | ||||
|  | ||||
|                         //The 'hashchange' event appears to be asynchronous, so we need to wait until a clock change has been | ||||
|                         //detected in the API. | ||||
|                         openmct.time.on('clock', resolveFunction); | ||||
|                         let hash = window.location.hash; | ||||
|                         hash = hash.replace('tc.mode=local', 'tc.mode=test-clock'); | ||||
|                         window.location.hash = hash; | ||||
|                     }).then(() => { | ||||
|                         let clock = openmct.time.clock(); | ||||
|                         expect(clock).toBeDefined(); | ||||
|                         expect(clock.key).toBe('test-clock'); | ||||
|                         openmct.time.off('clock', resolveFunction); | ||||
|                     }); | ||||
|                 }); | ||||
|             }); | ||||
|             it("the clock offsets are correctly set in the API from the URL parameters", () => { | ||||
|                 return switchToRealtimeMode().then(() => { | ||||
|                     let resolveFunction; | ||||
|  | ||||
|                     return new Promise((resolve) => { | ||||
|                         resolveFunction = resolve; | ||||
|                         //The 'hashchange' event appears to be asynchronous, so we need to wait until a clock change has been | ||||
|                         //detected in the API. | ||||
|                         openmct.time.on('clockOffsets', resolveFunction); | ||||
|                         let hash = window.location.hash; | ||||
|                         hash = hash.replace('tc.startDelta=1000', 'tc.startDelta=2000'); | ||||
|                         hash = hash.replace('tc.endDelta=100', 'tc.endDelta=200'); | ||||
|                         window.location.hash = hash; | ||||
|                     }).then(() => { | ||||
|                         let clockOffsets = openmct.time.clockOffsets(); | ||||
|                         expect(clockOffsets).toBeDefined(); | ||||
|                         expect(clockOffsets.start).toBe(-2000); | ||||
|                         expect(clockOffsets.end).toBe(200); | ||||
|                         openmct.time.off('clockOffsets', resolveFunction); | ||||
|                     }); | ||||
|                 }); | ||||
|             }); | ||||
|             it("the time system is correctly set in the API from the URL parameters", () => { | ||||
|                 return switchToRealtimeMode().then(() => { | ||||
|                     let resolveFunction; | ||||
|  | ||||
|                     return new Promise((resolve) => { | ||||
|                         resolveFunction = resolve; | ||||
|  | ||||
|                         //The 'hashchange' event appears to be asynchronous, so we need to wait until a clock change has been | ||||
|                         //detected in the API. | ||||
|                         openmct.time.on('timeSystem', resolveFunction); | ||||
|                         let hash = window.location.hash; | ||||
|                         hash = hash.replace('tc.timeSystem=utc', 'tc.timeSystem=local'); | ||||
|                         window.location.hash = hash; | ||||
|                     }).then(() => { | ||||
|                         let timeSystem = openmct.time.timeSystem(); | ||||
|                         expect(timeSystem).toBeDefined(); | ||||
|                         expect(timeSystem.key).toBe('local'); | ||||
|                         openmct.time.off('timeSystem', resolveFunction); | ||||
|                     }); | ||||
|                 }); | ||||
|             }); | ||||
|         }); | ||||
|     }); | ||||
|     describe("fixed timespan mode", () => { | ||||
|         beforeEach(() => { | ||||
|             openmct.time.stopClock(); | ||||
|             openmct.time.timeSystem('utc', { | ||||
|                 start: 0, | ||||
|                 end: 1 | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         it("when bounds are set via the time API, they are immediately reflected in the URL", () => { | ||||
|             //Test expected initial conditions | ||||
|             expect(window.location.hash.includes('tc.startBound=0')).toBe(true); | ||||
|             expect(window.location.hash.includes('tc.endBound=1')).toBe(true); | ||||
|  | ||||
|             openmct.time.bounds({ | ||||
|                 start: 10, | ||||
|                 end: 20 | ||||
|             }); | ||||
|  | ||||
|             expect(window.location.hash.includes('tc.startBound=10')).toBe(true); | ||||
|             expect(window.location.hash.includes('tc.endBound=20')).toBe(true); | ||||
|  | ||||
|             //Test that expected initial conditions are no longer true | ||||
|             expect(window.location.hash.includes('tc.startBound=0')).toBe(false); | ||||
|             expect(window.location.hash.includes('tc.endBound=1')).toBe(false); | ||||
|         }); | ||||
|  | ||||
|         it("when time system is set via the time API, it is immediately reflected in the URL", () => { | ||||
|             //Test expected initial conditions | ||||
|             expect(window.location.hash.includes('tc.timeSystem=utc')).toBe(true); | ||||
|  | ||||
|             openmct.time.timeSystem('local', { | ||||
|                 start: 20, | ||||
|                 end: 30 | ||||
|             }); | ||||
|  | ||||
|             expect(window.location.hash.includes('tc.timeSystem=local')).toBe(true); | ||||
|  | ||||
|             //Test that expected initial conditions are no longer true | ||||
|             expect(window.location.hash.includes('tc.timeSystem=utc')).toBe(false); | ||||
|         }); | ||||
|         describe("when set in the url", () => { | ||||
|             it("time system changes are reflected in the API", () => { | ||||
|                 let resolveFunction; | ||||
|  | ||||
|                 return new Promise((resolve) => { | ||||
|                     let timeSystem = openmct.time.timeSystem(); | ||||
|                     resolveFunction = resolve; | ||||
|  | ||||
|                     expect(timeSystem.key).toBe('utc'); | ||||
|                     window.location.hash = window.location.hash.replace('tc.timeSystem=utc', 'tc.timeSystem=local'); | ||||
|  | ||||
|                     openmct.time.on('timeSystem', resolveFunction); | ||||
|                 }).then(() => { | ||||
|                     let timeSystem = openmct.time.timeSystem(); | ||||
|                     expect(timeSystem.key).toBe('local'); | ||||
|  | ||||
|                     openmct.time.off('timeSystem', resolveFunction); | ||||
|                 }); | ||||
|             }); | ||||
|             it("mode can be changed from realtime to fixed", () => { | ||||
|                 return switchToRealtimeMode().then(() => { | ||||
|                     expectLocationToBeInRealtimeMode(); | ||||
|  | ||||
|                     expect(openmct.time.clock()).toBeDefined(); | ||||
|                 }).then(switchToFixedMode).then(() => { | ||||
|                     let clock = openmct.time.clock(); | ||||
|                     expect(clock).not.toBeDefined(); | ||||
|                 }); | ||||
|             }); | ||||
|             it("bounds are correctly set in the API from the URL parameters", () => { | ||||
|                 let resolveFunction; | ||||
|  | ||||
|                 expectLocationToBeInFixedMode(); | ||||
|  | ||||
|                 return new Promise((resolve) => { | ||||
|                     resolveFunction = resolve; | ||||
|                     openmct.time.on('bounds', resolveFunction); | ||||
|                     let hash = window.location.hash; | ||||
|                     hash = hash.replace('tc.startBound=0', 'tc.startBound=222') | ||||
|                         .replace('tc.endBound=1', 'tc.endBound=333'); | ||||
|                     window.location.hash = hash; | ||||
|                 }).then(() => { | ||||
|                     let bounds = openmct.time.bounds(); | ||||
|  | ||||
|                     expect(bounds).toBeDefined(); | ||||
|                     expect(bounds.start).toBe(222); | ||||
|                     expect(bounds.end).toBe(333); | ||||
|                 }); | ||||
|             }); | ||||
|             it("bounds are correctly set in the API from the URL parameters where only the end bound changes", () => { | ||||
|                 let resolveFunction; | ||||
|  | ||||
|                 expectLocationToBeInFixedMode(); | ||||
|  | ||||
|                 return new Promise((resolve) => { | ||||
|                     resolveFunction = resolve; | ||||
|                     openmct.time.on('bounds', resolveFunction); | ||||
|                     let hash = window.location.hash; | ||||
|                     hash = hash.replace('tc.endBound=1', 'tc.endBound=333'); | ||||
|                     window.location.hash = hash; | ||||
|                 }).then(() => { | ||||
|                     let bounds = openmct.time.bounds(); | ||||
|  | ||||
|                     expect(bounds).toBeDefined(); | ||||
|                     expect(bounds.start).toBe(0); | ||||
|                     expect(bounds.end).toBe(333); | ||||
|                 }); | ||||
|             }); | ||||
|         }); | ||||
|         openmct.router.on('change:hash', resolveFunction); | ||||
|     }); | ||||
|  | ||||
|     function setRealtimeLocationParameters() { | ||||
|         let hash = window.location.hash.toString() | ||||
|             .replace('tc.mode=fixed', 'tc.mode=local') | ||||
|             .replace('tc.startBound=0', 'tc.startDelta=1000') | ||||
|             .replace('tc.endBound=1', 'tc.endDelta=100'); | ||||
|     it("when the clock mode is set to local, it is reflected in the URL", (done) => { | ||||
|         let success; | ||||
|  | ||||
|         window.location.hash = hash; | ||||
|     } | ||||
|         resolveFunction = () => { | ||||
|             let hash = window.location.hash; | ||||
|             hash = hash.replace('tc.mode=fixed', 'tc.mode=local'); | ||||
|             window.location.hash = hash; | ||||
|  | ||||
|     function setFixedLocationParameters() { | ||||
|         let hash = window.location.hash.toString() | ||||
|             .replace('tc.mode=local', 'tc.mode=fixed') | ||||
|             .replace('tc.timeSystem=utc', 'tc.timeSystem=local') | ||||
|             .replace('tc.startDelta=1000', 'tc.startBound=50') | ||||
|             .replace('tc.endDelta=100', 'tc.endBound=60'); | ||||
|             success = window.location.hash.includes('tc.mode=local'); | ||||
|             if (success) { | ||||
|                 expect(success).toBe(true); | ||||
|                 done(); | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         window.location.hash = hash; | ||||
|     } | ||||
|         openmct.router.on('change:hash', resolveFunction); | ||||
|     }); | ||||
|  | ||||
|     function switchToRealtimeMode() { | ||||
|         let resolveFunction; | ||||
|     it("when the clock mode is set to local, it is reflected in the URL", (done) => { | ||||
|         let success; | ||||
|  | ||||
|         return new Promise((resolve) => { | ||||
|             resolveFunction = resolve; | ||||
|             openmct.time.on('clock', resolveFunction); | ||||
|             setRealtimeLocationParameters(); | ||||
|         }).then(() => { | ||||
|             openmct.time.off('clock', resolveFunction); | ||||
|         }); | ||||
|     } | ||||
|         resolveFunction = () => { | ||||
|             let hash = window.location.hash; | ||||
|  | ||||
|     function switchToFixedMode() { | ||||
|         let resolveFunction; | ||||
|             hash = hash.replace('tc.mode=fixed', 'tc.mode=local'); | ||||
|             window.location.hash = hash; | ||||
|             success = window.location.hash.includes('tc.mode=local'); | ||||
|             if (success) { | ||||
|                 expect(success).toBe(true); | ||||
|                 done(); | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         return new Promise((resolve) => { | ||||
|             resolveFunction = resolve; | ||||
|             //The 'hashchange' event appears to be asynchronous, so we need to wait until a clock change has been | ||||
|             //detected in the API. | ||||
|             openmct.time.on('clock', resolveFunction); | ||||
|             setFixedLocationParameters(); | ||||
|         }).then(() => { | ||||
|             openmct.time.off('clock', resolveFunction); | ||||
|         }); | ||||
|     } | ||||
|         openmct.router.on('change:hash', resolveFunction); | ||||
|     }); | ||||
|  | ||||
|     function expectLocationToBeInRealtimeMode() { | ||||
|         expect(window.location.hash.includes('tc.mode=local')).toBe(true); | ||||
|         expect(window.location.hash.includes('tc.startDelta=1000')).toBe(true); | ||||
|         expect(window.location.hash.includes('tc.endDelta=100')).toBe(true); | ||||
|         expect(window.location.hash.includes('tc.mode=fixed')).toBe(false); | ||||
|     } | ||||
|     it("reset hash", (done) => { | ||||
|         let success; | ||||
|  | ||||
|     function expectLocationToBeInFixedMode() { | ||||
|         expect(window.location.hash.includes('tc.mode=fixed')).toBe(true); | ||||
|         expect(window.location.hash.includes('tc.startBound=0')).toBe(true); | ||||
|         expect(window.location.hash.includes('tc.endBound=1')).toBe(true); | ||||
|         expect(window.location.hash.includes('tc.mode=local')).toBe(false); | ||||
|     } | ||||
|         window.location.hash = oldHash; | ||||
|         resolveFunction = () => { | ||||
|             success = window.location.hash === oldHash; | ||||
|             if (success) { | ||||
|                 expect(success).toBe(true); | ||||
|                 done(); | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         openmct.router.on('change:hash', resolveFunction); | ||||
|     }); | ||||
| }); | ||||
|   | ||||
| @@ -272,11 +272,11 @@ export default class Condition extends EventEmitter { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     requestLADConditionResult() { | ||||
|     requestLADConditionResult(options) { | ||||
|         let latestTimestamp; | ||||
|         let criteriaResults = {}; | ||||
|         const criteriaRequests = this.criteria | ||||
|             .map(criterion => criterion.requestLAD(this.conditionManager.telemetryObjects)); | ||||
|             .map(criterion => criterion.requestLAD(this.conditionManager.telemetryObjects, options)); | ||||
|  | ||||
|         return Promise.all(criteriaRequests) | ||||
|             .then(results => { | ||||
|   | ||||
| @@ -282,7 +282,7 @@ export default class ConditionManager extends EventEmitter { | ||||
|         return currentCondition; | ||||
|     } | ||||
|  | ||||
|     requestLADConditionSetOutput() { | ||||
|     requestLADConditionSetOutput(options) { | ||||
|         if (!this.conditions.length) { | ||||
|             return Promise.resolve([]); | ||||
|         } | ||||
| @@ -291,7 +291,7 @@ export default class ConditionManager extends EventEmitter { | ||||
|             let latestTimestamp; | ||||
|             let conditionResults = {}; | ||||
|             const conditionRequests = this.conditions | ||||
|                 .map(condition => condition.requestLADConditionResult()); | ||||
|                 .map(condition => condition.requestLADConditionResult(options)); | ||||
|  | ||||
|             return Promise.all(conditionRequests) | ||||
|                 .then((results) => { | ||||
|   | ||||
| @@ -40,10 +40,10 @@ export default class ConditionSetTelemetryProvider { | ||||
|         return domainObject.type === 'conditionSet'; | ||||
|     } | ||||
|  | ||||
|     request(domainObject) { | ||||
|     request(domainObject, options) { | ||||
|         let conditionManager = this.getConditionManager(domainObject); | ||||
|  | ||||
|         return conditionManager.requestLADConditionSetOutput() | ||||
|         return conditionManager.requestLADConditionSetOutput(options) | ||||
|             .then(latestOutput => { | ||||
|                 return latestOutput; | ||||
|             }); | ||||
| @@ -52,7 +52,9 @@ export default class ConditionSetTelemetryProvider { | ||||
|     subscribe(domainObject, callback) { | ||||
|         let conditionManager = this.getConditionManager(domainObject); | ||||
|  | ||||
|         conditionManager.on('conditionSetResultUpdated', callback); | ||||
|         conditionManager.on('conditionSetResultUpdated', (data) => { | ||||
|             callback(data); | ||||
|         }); | ||||
|  | ||||
|         return this.destroyConditionManager.bind(this, this.openmct.objects.makeKeyString(domainObject.identifier)); | ||||
|     } | ||||
|   | ||||
| @@ -35,6 +35,7 @@ export default class StyleRuleManager extends EventEmitter { | ||||
|         if (styleConfiguration) { | ||||
|             this.initialize(styleConfiguration); | ||||
|             if (styleConfiguration.conditionSetIdentifier) { | ||||
|                 this.openmct.time.on("bounds", this.refreshData.bind(this)); | ||||
|                 this.subscribeToConditionSet(); | ||||
|             } else { | ||||
|                 this.applyStaticStyle(); | ||||
| @@ -83,6 +84,25 @@ export default class StyleRuleManager extends EventEmitter { | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     refreshData(bounds, isTick) { | ||||
|         if (!isTick) { | ||||
|             let options = { | ||||
|                 start: bounds.start, | ||||
|                 end: bounds.end, | ||||
|                 size: 1, | ||||
|                 strategy: 'latest' | ||||
|             }; | ||||
|             this.openmct.objects.get(this.conditionSetIdentifier).then((conditionSetDomainObject) => { | ||||
|                 this.openmct.telemetry.request(conditionSetDomainObject, options) | ||||
|                     .then(output => { | ||||
|                         if (output && output.length) { | ||||
|                             this.handleConditionSetResultUpdated(output[0]); | ||||
|                         } | ||||
|                     }); | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     updateObjectStyleConfig(styleConfiguration) { | ||||
|         if (!styleConfiguration || !styleConfiguration.conditionSetIdentifier) { | ||||
|             this.initialize(styleConfiguration || {}); | ||||
| @@ -160,10 +180,14 @@ export default class StyleRuleManager extends EventEmitter { | ||||
|  | ||||
|     destroy() { | ||||
|         if (this.stopProvidingTelemetry) { | ||||
|  | ||||
|             this.stopProvidingTelemetry(); | ||||
|             delete this.stopProvidingTelemetry; | ||||
|         } | ||||
|  | ||||
|         this.openmct.time.off("bounds", this.refreshData); | ||||
|         this.openmct.editor.off('isEditing', this.toggleSubscription); | ||||
|  | ||||
|         this.conditionSetIdentifier = undefined; | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -52,7 +52,6 @@ | ||||
|         <div class="c-inspect-styles__content c-inspect-styles__condition-set"> | ||||
|             <a v-if="conditionSetDomainObject" | ||||
|                class="c-object-label icon-conditional" | ||||
|                :href="navigateToPath" | ||||
|                @click="navigateOrPreview" | ||||
|             > | ||||
|                 <span class="c-object-label__name">{{ conditionSetDomainObject.name }}</span> | ||||
| @@ -286,6 +285,8 @@ export default { | ||||
|             if (this.openmct.editor.isEditing()) { | ||||
|                 event.preventDefault(); | ||||
|                 this.previewAction.invoke(this.objectPath); | ||||
|             } else { | ||||
|                 this.openmct.router.navigate(this.navigateToPath); | ||||
|             } | ||||
|         }, | ||||
|         removeConditionSet() { | ||||
|   | ||||
| @@ -66,7 +66,6 @@ | ||||
|         <div class="c-inspect-styles__content c-inspect-styles__condition-set"> | ||||
|             <a v-if="conditionSetDomainObject" | ||||
|                class="c-object-label" | ||||
|                :href="navigateToPath" | ||||
|                @click="navigateOrPreview" | ||||
|             > | ||||
|                 <span class="c-object-label__type-icon icon-conditional"></span> | ||||
| @@ -309,6 +308,8 @@ export default { | ||||
|             if (this.openmct.editor.isEditing()) { | ||||
|                 event.preventDefault(); | ||||
|                 this.previewAction.invoke(this.objectPath); | ||||
|             } else { | ||||
|                 this.openmct.router.navigate(this.navigateToPath); | ||||
|             } | ||||
|         }, | ||||
|         isItemType(type, item) { | ||||
| @@ -344,6 +345,11 @@ export default { | ||||
|                 const layoutItem = selectionItem[0].context.layoutItem; | ||||
|                 const isChildItem = selectionItem.length > 1; | ||||
|  | ||||
|                 if (!item && !layoutItem) { | ||||
|                     // cases where selection is used for table cells | ||||
|                     return; | ||||
|                 } | ||||
|  | ||||
|                 if (!isChildItem) { | ||||
|                     domainObject = item; | ||||
|                     itemStyle = getApplicableStylesForItem(item); | ||||
|   | ||||
| @@ -147,12 +147,16 @@ export default class AllTelemetryCriterion extends TelemetryCriterion { | ||||
|         this.result = evaluateResults(Object.values(this.telemetryDataCache), this.telemetry); | ||||
|     } | ||||
|  | ||||
|     requestLAD(telemetryObjects) { | ||||
|         const options = { | ||||
|     requestLAD(telemetryObjects, requestOptions) { | ||||
|         let options = { | ||||
|             strategy: 'latest', | ||||
|             size: 1 | ||||
|         }; | ||||
|  | ||||
|         if (requestOptions !== undefined) { | ||||
|             options = Object.assign(options, requestOptions); | ||||
|         } | ||||
|  | ||||
|         if (!this.isValid()) { | ||||
|             return this.formatData({}, telemetryObjects); | ||||
|         } | ||||
|   | ||||
| @@ -137,12 +137,16 @@ export default class TelemetryCriterion extends EventEmitter { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     requestLAD() { | ||||
|         const options = { | ||||
|     requestLAD(telemetryObjects, requestOptions) { | ||||
|         let options = { | ||||
|             strategy: 'latest', | ||||
|             size: 1 | ||||
|         }; | ||||
|  | ||||
|         if (requestOptions !== undefined) { | ||||
|             options = Object.assign(options, requestOptions); | ||||
|         } | ||||
|  | ||||
|         if (!this.isValid()) { | ||||
|             return { | ||||
|                 id: this.id, | ||||
|   | ||||
| @@ -104,7 +104,7 @@ export function getConsolidatedStyleValues(multipleItemStyles) { | ||||
|     const properties = Object.keys(styleProps); | ||||
|     properties.forEach((property) => { | ||||
|         const values = aggregatedStyleValues[property]; | ||||
|         if (values.length) { | ||||
|         if (values && values.length) { | ||||
|             if (values.every(value => value === values[0])) { | ||||
|                 styleValues[property] = values[0]; | ||||
|             } else { | ||||
|   | ||||
| @@ -46,7 +46,7 @@ xdescribe("the plugin", () => { | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         resetApplicationState(openmct); | ||||
|         return resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|     it('installs the new folder action', () => { | ||||
|   | ||||
| @@ -235,7 +235,7 @@ define(['lodash'], function (_) { | ||||
|                                 message: `Warning! This action will remove this item from the Display Layout. Do you want to continue?`, | ||||
|                                 buttons: [ | ||||
|                                     { | ||||
|                                         label: 'Ok', | ||||
|                                         label: 'OK', | ||||
|                                         emphasis: 'true', | ||||
|                                         callback: function () { | ||||
|                                             removeItem(getAllTypes(selection)); | ||||
|   | ||||
| @@ -269,7 +269,12 @@ export default { | ||||
|         }, | ||||
|         subscribeToObject() { | ||||
|             this.subscription = this.openmct.telemetry.subscribe(this.domainObject, function (datum) { | ||||
|                 if (this.openmct.time.clock() !== undefined) { | ||||
|                 const key = this.openmct.time.timeSystem().key; | ||||
|                 const datumTimeStamp = datum[key]; | ||||
|                 if (this.openmct.time.clock() !== undefined | ||||
|                     || (datumTimeStamp | ||||
|                         && (this.openmct.time.bounds().end >= datumTimeStamp)) | ||||
|                 ) { | ||||
|                     this.updateView(datum); | ||||
|                 } | ||||
|             }.bind(this)); | ||||
|   | ||||
| @@ -112,7 +112,7 @@ describe("The Duplicate Action plugin", () => { | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         resetApplicationState(openmct); | ||||
|         return resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|     it("should be defined", () => { | ||||
|   | ||||
| @@ -97,7 +97,7 @@ function ToolbarProvider(openmct) { | ||||
|                             message: `This action will remove this frame from this Flexible Layout. Do you want to continue?`, | ||||
|                             buttons: [ | ||||
|                                 { | ||||
|                                     label: 'Ok', | ||||
|                                     label: 'OK', | ||||
|                                     emphasis: 'true', | ||||
|                                     callback: function () { | ||||
|                                         deleteFrameAction(primary.context.frameId); | ||||
| @@ -162,7 +162,7 @@ function ToolbarProvider(openmct) { | ||||
|                             message: 'This action will permanently delete this container from this Flexible Layout', | ||||
|                             buttons: [ | ||||
|                                 { | ||||
|                                     label: 'Ok', | ||||
|                                     label: 'OK', | ||||
|                                     emphasis: 'true', | ||||
|                                     callback: function () { | ||||
|                                         removeContainer(containerId); | ||||
|   | ||||
| @@ -5,7 +5,7 @@ | ||||
|         'is-alias': item.isAlias === true, | ||||
|         'c-grid-item--unknown': item.type.cssClass === undefined || item.type.cssClass.indexOf('unknown') !== -1 | ||||
|     }, statusClass]" | ||||
|     :href="objectLink" | ||||
|     @click="navigate" | ||||
| > | ||||
|     <div | ||||
|         class="c-grid-item__type-icon" | ||||
| @@ -49,11 +49,17 @@ import statusListener from './status-listener'; | ||||
|  | ||||
| export default { | ||||
|     mixins: [contextMenuGesture, objectLink, statusListener], | ||||
|     inject: ['openmct'], | ||||
|     props: { | ||||
|         item: { | ||||
|             type: Object, | ||||
|             required: true | ||||
|         } | ||||
|     }, | ||||
|     methods: { | ||||
|         navigate() { | ||||
|             this.openmct.router.navigate(this.objectLink); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| </script> | ||||
|   | ||||
| @@ -11,7 +11,7 @@ | ||||
|             ref="objectLink" | ||||
|             class="c-object-label" | ||||
|             :class="[statusClass]" | ||||
|             :href="objectLink" | ||||
|             @click="navigate" | ||||
|         > | ||||
|             <div | ||||
|                 class="c-object-label__type-icon c-list-item__name__type-icon" | ||||
| @@ -45,6 +45,7 @@ import statusListener from './status-listener'; | ||||
|  | ||||
| export default { | ||||
|     mixins: [contextMenuGesture, objectLink, statusListener], | ||||
|     inject: ['openmct'], | ||||
|     props: { | ||||
|         item: { | ||||
|             type: Object, | ||||
| @@ -56,7 +57,7 @@ export default { | ||||
|             return moment(timestamp).format(format); | ||||
|         }, | ||||
|         navigate() { | ||||
|             this.$refs.objectLink.click(); | ||||
|             this.openmct.router.navigate(this.objectLink); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|   | ||||
| @@ -19,6 +19,10 @@ | ||||
|             margin: 0 $interiorMargin $interiorMargin 0; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     body.mobile & { | ||||
|         flex: 1 0 auto; | ||||
|     } | ||||
| } | ||||
|  | ||||
| /******************************* GRID ITEMS */ | ||||
|   | ||||
| @@ -41,7 +41,7 @@ export default class GoToOriginalAction { | ||||
|                     .slice(1) | ||||
|                     .join('/'); | ||||
|  | ||||
|                 window.location.href = url; | ||||
|                 this._openmct.router.navigate(url); | ||||
|             }); | ||||
|     } | ||||
|     appliesTo(objectPath) { | ||||
|   | ||||
| @@ -47,7 +47,6 @@ describe("the plugin", () => { | ||||
|     }); | ||||
|  | ||||
|     describe('when invoked', () => { | ||||
|  | ||||
|         beforeEach(() => { | ||||
|             mockObjectPath = [{ | ||||
|                 name: 'mock folder', | ||||
| @@ -63,11 +62,15 @@ describe("the plugin", () => { | ||||
|                     key: 'test' | ||||
|                 } | ||||
|             })); | ||||
|  | ||||
|             goToFolderAction.invoke(mockObjectPath); | ||||
|         }); | ||||
|  | ||||
|         it('goes to the original location', () => { | ||||
|             expect(window.location.href).toContain('context.html#/browse/?tc.mode=fixed&tc.startBound=0&tc.endBound=1&tc.timeSystem=utc'); | ||||
|         it('goes to the original location', (done) => { | ||||
|             setTimeout(() => { | ||||
|                 expect(window.location.href).toContain('context.html#/browse/?tc.mode=fixed&tc.startBound=0&tc.endBound=1&tc.timeSystem=utc'); | ||||
|                 done(); | ||||
|             }, 1500); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
|   | ||||
| @@ -23,7 +23,7 @@ | ||||
| <template> | ||||
| <div | ||||
|     class="c-compass" | ||||
|     :style="compassDimensionsStyle" | ||||
|     :style="`width: ${ sizedImageDimensions.width }px; height: ${ sizedImageDimensions.height }px`" | ||||
| > | ||||
|     <CompassHUD | ||||
|         v-if="hasCameraFieldOfView" | ||||
| @@ -34,6 +34,7 @@ | ||||
|     <CompassRose | ||||
|         v-if="hasCameraFieldOfView" | ||||
|         :heading="heading" | ||||
|         :sized-image-width="sizedImageDimensions.width" | ||||
|         :sun-heading="sunHeading" | ||||
|         :camera-angle-of-view="cameraAngleOfView" | ||||
|         :camera-pan="cameraPan" | ||||
| @@ -77,6 +78,20 @@ export default { | ||||
|         } | ||||
|     }, | ||||
|     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; | ||||
|         }, | ||||
| @@ -94,25 +109,6 @@ export default { | ||||
|         }, | ||||
|         cameraAngleOfView() { | ||||
|             return CAMERA_ANGLE_OF_VIEW; | ||||
|         }, | ||||
|         compassDimensionsStyle() { | ||||
|             const containerAspectRatio = this.containerWidth / this.containerHeight; | ||||
|  | ||||
|             let width; | ||||
|             let height; | ||||
|  | ||||
|             if (containerAspectRatio < this.naturalAspectRatio) { | ||||
|                 width = '100%'; | ||||
|                 height = `${ this.containerWidth / this.naturalAspectRatio }px`; | ||||
|             } else { | ||||
|                 width = `${ this.containerHeight * this.naturalAspectRatio }px`; | ||||
|                 height = '100%'; | ||||
|             } | ||||
|  | ||||
|             return { | ||||
|                 width: width, | ||||
|                 height: height | ||||
|             }; | ||||
|         } | ||||
|     }, | ||||
|     methods: { | ||||
|   | ||||
| @@ -22,129 +22,134 @@ | ||||
|  | ||||
| <template> | ||||
| <div | ||||
|     class="c-direction-rose" | ||||
|     @click="toggleLockCompass" | ||||
|     class="w-direction-rose" | ||||
|     :class="compassRoseSizingClasses" | ||||
| > | ||||
|     <div | ||||
|         class="c-nsew" | ||||
|         :style="compassRoseStyle" | ||||
|         class="c-direction-rose" | ||||
|         @click="toggleLockCompass" | ||||
|     > | ||||
|         <svg | ||||
|             class="c-nsew__minor-ticks" | ||||
|             viewBox="0 0 100 100" | ||||
|         <div | ||||
|             class="c-nsew" | ||||
|             :style="compassRoseStyle" | ||||
|         > | ||||
|             <rect | ||||
|                 class="c-nsew__tick c-tick-ne" | ||||
|                 x="49" | ||||
|                 y="0" | ||||
|                 width="2" | ||||
|                 height="5" | ||||
|             /> | ||||
|             <rect | ||||
|                 class="c-nsew__tick c-tick-se" | ||||
|                 x="95" | ||||
|                 y="49" | ||||
|                 width="5" | ||||
|                 height="2" | ||||
|             /> | ||||
|             <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" | ||||
|             /> | ||||
|             <svg | ||||
|                 class="c-nsew__minor-ticks" | ||||
|                 viewBox="0 0 100 100" | ||||
|             > | ||||
|                 <rect | ||||
|                     class="c-nsew__tick c-tick-ne" | ||||
|                     x="49" | ||||
|                     y="0" | ||||
|                     width="2" | ||||
|                     height="5" | ||||
|                 /> | ||||
|                 <rect | ||||
|                     class="c-nsew__tick c-tick-se" | ||||
|                     x="95" | ||||
|                     y="49" | ||||
|                     width="5" | ||||
|                     height="2" | ||||
|                 /> | ||||
|                 <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" | ||||
|                 /> | ||||
|  | ||||
|         </svg> | ||||
|             </svg> | ||||
|  | ||||
|         <svg | ||||
|             class="c-nsew__ticks" | ||||
|             viewBox="0 0 100 100" | ||||
|         > | ||||
|             <polygon | ||||
|                 class="c-nsew__tick c-tick-n" | ||||
|                 points="50,0 57,5 43,5" | ||||
|             /> | ||||
|             <rect | ||||
|                 class="c-nsew__tick c-tick-e" | ||||
|                 x="95" | ||||
|                 y="49" | ||||
|                 width="5" | ||||
|                 height="2" | ||||
|             /> | ||||
|             <rect | ||||
|                 class="c-nsew__tick c-tick-w" | ||||
|                 x="0" | ||||
|                 y="49" | ||||
|                 width="5" | ||||
|                 height="2" | ||||
|             /> | ||||
|             <rect | ||||
|                 class="c-nsew__tick c-tick-s" | ||||
|                 x="49" | ||||
|                 y="95" | ||||
|                 width="2" | ||||
|                 height="5" | ||||
|             /> | ||||
|             <svg | ||||
|                 class="c-nsew__ticks" | ||||
|                 viewBox="0 0 100 100" | ||||
|             > | ||||
|                 <polygon | ||||
|                     class="c-nsew__tick c-tick-n" | ||||
|                     points="50,0 60,10 40,10" | ||||
|                 /> | ||||
|                 <rect | ||||
|                     class="c-nsew__tick c-tick-e" | ||||
|                     x="95" | ||||
|                     y="49" | ||||
|                     width="5" | ||||
|                     height="2" | ||||
|                 /> | ||||
|                 <rect | ||||
|                     class="c-nsew__tick c-tick-w" | ||||
|                     x="0" | ||||
|                     y="49" | ||||
|                     width="5" | ||||
|                     height="2" | ||||
|                 /> | ||||
|                 <rect | ||||
|                     class="c-nsew__tick c-tick-s" | ||||
|                     x="49" | ||||
|                     y="95" | ||||
|                     width="2" | ||||
|                     height="5" | ||||
|                 /> | ||||
|  | ||||
|             <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> | ||||
|                 <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 class="cam-field-half cam-field-half-r"> | ||||
|             <div | ||||
|                 class="cam-field-area" | ||||
|                 :style="cameraFOVStyleRightHalf" | ||||
|             ></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> | ||||
| </div> | ||||
| @@ -155,6 +160,10 @@ import { rotate } from './utils'; | ||||
|  | ||||
| export default { | ||||
|     props: { | ||||
|         sizedImageWidth: { | ||||
|             type: Number, | ||||
|             required: true | ||||
|         }, | ||||
|         heading: { | ||||
|             type: Number, | ||||
|             required: true | ||||
| @@ -177,12 +186,24 @@ export default { | ||||
|         } | ||||
|     }, | ||||
|     computed: { | ||||
|         north() { | ||||
|             return this.lockCompass ? rotate(-this.cameraPan) : 0; | ||||
|         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; | ||||
|         }, | ||||
| @@ -204,10 +225,10 @@ export default { | ||||
|             const rotation = `rotate(${ -this.north })`; | ||||
|  | ||||
|             return { | ||||
|                 north: `translate(50,15) ${ rotation }`, | ||||
|                 east: `translate(87,50) ${ rotation }`, | ||||
|                 south: `translate(13,50) ${ rotation }`, | ||||
|                 west: `translate(50,87) ${ rotation }` | ||||
|                 north: `translate(50,23) ${ rotation }`, | ||||
|                 east: `translate(82,50) ${ rotation }`, | ||||
|                 south: `translate(18,50) ${ rotation }`, | ||||
|                 west: `translate(50,82) ${ rotation }` | ||||
|             }; | ||||
|         }, | ||||
|         hasHeading() { | ||||
|   | ||||
| @@ -10,6 +10,7 @@ $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%; | ||||
| @@ -20,195 +21,253 @@ $elemBg: rgba(black, 0.7); | ||||
|  | ||||
| /***************************** COMPASS HUD */ | ||||
| .c-hud { | ||||
|   // To be placed within a imagery view, in the bounding box of the image | ||||
|   $m: 1px; | ||||
|   $padTB: 2px; | ||||
|   $padLR: $padTB; | ||||
|   color: $interfaceKeyColor; | ||||
|   font-size: 0.8em; | ||||
|   position: absolute; | ||||
|   top: $m; right: $m; left: $m; | ||||
|   height: 18px; | ||||
|  | ||||
|   svg, div { | ||||
|     // To be placed within a imagery view, in the bounding box of the image | ||||
|     $m: 1px; | ||||
|     $padTB: 2px; | ||||
|     $padLR: $padTB; | ||||
|     color: $interfaceKeyColor; | ||||
|     font-size: 0.8em; | ||||
|     position: absolute; | ||||
|   } | ||||
|     top: $m; | ||||
|     right: $m; | ||||
|     left: $m; | ||||
|     height: 18px; | ||||
|  | ||||
|   &__display { | ||||
|       height: 30px; | ||||
|       pointer-events: all; | ||||
|       position: absolute; | ||||
|       top: 0; | ||||
|       right: 0; | ||||
|       left: 0; | ||||
|   } | ||||
|     svg, div { | ||||
|         position: absolute; | ||||
|     } | ||||
|  | ||||
|   &__range { | ||||
|     border: 1px solid $interfaceKeyColor; | ||||
|     border-top-color: transparent; | ||||
|     position: absolute; | ||||
|     top: 50%; right: $padLR; bottom: $padTB; left: $padLR; | ||||
|   } | ||||
|     &__display { | ||||
|         height: 30px; | ||||
|         pointer-events: all; | ||||
|         position: absolute; | ||||
|         top: 0; | ||||
|         right: 0; | ||||
|         left: 0; | ||||
|     } | ||||
|  | ||||
|   [class*="__dir"] { | ||||
|     // NSEW | ||||
|     display: inline-block; | ||||
|     font-weight: bold; | ||||
|     text-shadow: 0 1px 2px black; | ||||
|     top: 50%; | ||||
|     transform: translate(-50%,-50%); | ||||
|     z-index: 2; | ||||
|   } | ||||
|     &__range { | ||||
|         border: 1px solid $interfaceKeyColor; | ||||
|         border-top-color: transparent; | ||||
|         position: absolute; | ||||
|         top: 50%; | ||||
|         right: $padLR; | ||||
|         bottom: $padTB; | ||||
|         left: $padLR; | ||||
|     } | ||||
|  | ||||
|   [class*="__dir--sub"] { | ||||
|     font-weight: normal; | ||||
|     opacity: 0.5; | ||||
|   } | ||||
|     [class*="__dir"] { | ||||
|         // NSEW | ||||
|         display: inline-block; | ||||
|         font-weight: bold; | ||||
|         text-shadow: 0 1px 2px black; | ||||
|         top: 50%; | ||||
|         transform: translate(-50%, -50%); | ||||
|         z-index: 2; | ||||
|     } | ||||
|  | ||||
|   &__sun { | ||||
|     $s: 10px; | ||||
|     @include sun('circle farthest-side at bottom'); | ||||
|     bottom: $padTB + 2px; | ||||
|     height: $s; width: $s*2; | ||||
|     opacity: 0.8; | ||||
|     transform: translateX(-50%); | ||||
|     z-index: 1; | ||||
|   } | ||||
|     [class*="__dir--sub"] { | ||||
|         font-weight: normal; | ||||
|         opacity: 0.5; | ||||
|     } | ||||
|  | ||||
|     &__sun { | ||||
|         $s: 10px; | ||||
|         @include sun('circle farthest-side at bottom'); | ||||
|         bottom: $padTB + 2px; | ||||
|         height: $s; | ||||
|         width: $s*2; | ||||
|         opacity: 0.8; | ||||
|         transform: translateX(-50%); | ||||
|         z-index: 1; | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| /***************************** COMPASS DIRECTIONS */ | ||||
| .c-nsew { | ||||
|   $color: $interfaceKeyColor; | ||||
|   $inset: 7%; | ||||
|   $tickHeightPerc: 15%; | ||||
|   text-shadow: black 0 0 10px; | ||||
|   top: $inset; right: $inset; bottom: $inset; left: $inset; | ||||
|   z-index: 3; | ||||
|     $color: $interfaceKeyColor; | ||||
|     $inset: 5%; | ||||
|     $tickHeightPerc: 15%; | ||||
|     text-shadow: black 0 0 10px; | ||||
|     top: $inset; | ||||
|     right: $inset; | ||||
|     bottom: $inset; | ||||
|     left: $inset; | ||||
|     z-index: 3; | ||||
|  | ||||
|   &__tick, | ||||
|   &__label { | ||||
|     fill: $color; | ||||
|   } | ||||
|     &__tick, | ||||
|     &__label { | ||||
|         fill: $color; | ||||
|     } | ||||
|  | ||||
|   &__minor-ticks { | ||||
|     opacity: 0.5; | ||||
|     transform-origin: center; | ||||
|     transform: rotate(45deg); | ||||
|   } | ||||
|     &__minor-ticks { | ||||
|         opacity: 0.5; | ||||
|         transform-origin: center; | ||||
|         transform: rotate(45deg); | ||||
|     } | ||||
|  | ||||
|   &__label { | ||||
|     dominant-baseline: central; | ||||
|     font-size: 0.8em; | ||||
|     font-weight: bold; | ||||
|   } | ||||
|     &__label { | ||||
|         dominant-baseline: central; | ||||
|         font-size: 1.25em; | ||||
|         font-weight: bold; | ||||
|     } | ||||
|  | ||||
|   .c-label-n { | ||||
|     font-size: 1.1em; | ||||
|   } | ||||
|     .c-label-n { | ||||
|         font-size: 2em; | ||||
|     } | ||||
| } | ||||
|  | ||||
| /***************************** CAMERA FIELD ANGLE */ | ||||
| .c-cam-field { | ||||
|   $color: white; | ||||
|   opacity: 0.2; | ||||
|   top: 0; | ||||
|   right: 0; | ||||
|   bottom: 0; | ||||
|   left: 0; | ||||
|   z-index: 2; | ||||
|  | ||||
|   .cam-field-half { | ||||
|     $color: white; | ||||
|     opacity: 0.3; | ||||
|     top: 0; | ||||
|     right: 0; | ||||
|     bottom: 0; | ||||
|     left: 0; | ||||
|     z-index: 2; | ||||
|  | ||||
|     .cam-field-area { | ||||
|       background: $color; | ||||
|       top: -30%; | ||||
|       right: 0; | ||||
|       bottom: -30%; | ||||
|       left: 0; | ||||
|     } | ||||
|     .cam-field-half { | ||||
|         top: 0; | ||||
|         right: 0; | ||||
|         bottom: 0; | ||||
|         left: 0; | ||||
|  | ||||
|     // 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; | ||||
|       } | ||||
|     } | ||||
|         .cam-field-area { | ||||
|             background: $color; | ||||
|             top: -30%; | ||||
|             right: 0; | ||||
|             bottom: -30%; | ||||
|             left: 0; | ||||
|         } | ||||
|  | ||||
|     &-r { | ||||
|       clip-path: polygon(49.5% 0, 100% 0, 100% 100%, 49.5% 100%); | ||||
|       .cam-field-area { | ||||
|         transform-origin: right center; | ||||
|       } | ||||
|         // 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; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         &-r { | ||||
|             clip-path: polygon(49.5% 0, 100% 0, 100% 100%, 49.5% 100%); | ||||
|  | ||||
|             .cam-field-area { | ||||
|                 transform-origin: right center; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| /***************************** 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; | ||||
|  | ||||
|   &:before { | ||||
|     // Direction arrow | ||||
|     $color: rgba(black, 0.5); | ||||
|     $arwPointerY: 60%; | ||||
|     $arwBodyOffset: 25%; | ||||
|     $color: $interfaceKeyColor; | ||||
|     $s: 30%; | ||||
|     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); | ||||
|   } | ||||
|     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() | ||||
|  | ||||
|     &: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); | ||||
|     } | ||||
| } | ||||
|  | ||||
| /***************************** DIRECTION ROSE */ | ||||
| .c-direction-rose { | ||||
|   $d: 100px; | ||||
|   $c2: rgba(white, 0.1); | ||||
|   background: $elemBg; | ||||
|   background-image: radial-gradient(circle closest-side, transparent, transparent 80%, $c2); | ||||
|   width: $d; | ||||
|   height: $d; | ||||
|   transform-origin: 0 0; | ||||
|   position: absolute; | ||||
|   bottom: 10px; left: 10px; | ||||
|   clip-path: circle(50% at 50% 50%); | ||||
|   border-radius: 100%; | ||||
|  | ||||
|   svg, div { | ||||
| .w-direction-rose { | ||||
|     $s: 10%; | ||||
|     $m: 2%; | ||||
|     position: absolute; | ||||
|   } | ||||
|     bottom: $m; | ||||
|     left: $m; | ||||
|     width: $s; | ||||
|     padding-top: $s; | ||||
|  | ||||
|   // Sun | ||||
|   .c-sun { | ||||
|     &.--rose-min { | ||||
|         $s: 30px; | ||||
|         width: $s; | ||||
|         padding-top: $s; | ||||
|     } | ||||
|  | ||||
|     &.--rose-small { | ||||
|         .c-nsew__minor-ticks, | ||||
|         .c-tick-w, | ||||
|         .c-tick-s, | ||||
|         .c-tick-e, | ||||
|         .c-label-w, | ||||
|         .c-label-s, | ||||
|         .c-label-e { | ||||
|             display: none; | ||||
|         } | ||||
|  | ||||
|         .c-label-n { | ||||
|             font-size: 2.5em; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     &.--rose-max { | ||||
|         $s: 100px; | ||||
|         width: $s; | ||||
|         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; | ||||
|  | ||||
|     &: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%); | ||||
|     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%); | ||||
|         } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -135,9 +135,14 @@ | ||||
|              :class="{ selected: focusedImageIndex === index && isPaused }" | ||||
|              @click="setFocusedImage(index, thumbnailClick)" | ||||
|         > | ||||
|             <img class="c-thumb__image" | ||||
|                  :src="image.url" | ||||
|             <a href="" | ||||
|                :download="image.imageDownloadName" | ||||
|                @click.prevent | ||||
|             > | ||||
|                 <img class="c-thumb__image" | ||||
|                      :src="image.url" | ||||
|                 > | ||||
|             </a> | ||||
|             <div class="c-thumb__timestamp">{{ image.formattedTime }}</div> | ||||
|         </div> | ||||
|     </div> | ||||
| @@ -218,6 +223,9 @@ export default { | ||||
|         canTrackDuration() { | ||||
|             return this.openmct.time.clock() && this.timeSystem.isUTCBased; | ||||
|         }, | ||||
|         focusedImageDownloadName() { | ||||
|             return this.getImageDownloadName(this.focusedImage); | ||||
|         }, | ||||
|         isNextDisabled() { | ||||
|             let disabled = false; | ||||
|  | ||||
| @@ -345,6 +353,7 @@ export default { | ||||
|         this.imageHints = { ...this.metadata.valuesForHints(['image'])[0] }; | ||||
|         this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER); | ||||
|         this.imageFormatter = this.openmct.telemetry.getValueFormatter(this.imageHints); | ||||
|         this.imageDownloadNameHints = { ...this.metadata.valuesForHints(['imageDownloadName'])[0]}; | ||||
|  | ||||
|         // related telemetry keys | ||||
|         this.spacecraftPositionKeys = ['positionX', 'positionY', 'positionZ']; | ||||
| @@ -381,7 +390,9 @@ export default { | ||||
|             delete this.unsubscribe; | ||||
|         } | ||||
|  | ||||
|         this.imageContainerResizeObserver.disconnect(); | ||||
|         if (this.imageContainerResizeObserver) { | ||||
|             this.imageContainerResizeObserver.disconnect(); | ||||
|         } | ||||
|  | ||||
|         if (this.relatedTelemetry.hasRelatedTelemetry) { | ||||
|             this.relatedTelemetry.destroy(); | ||||
| @@ -532,6 +543,15 @@ export default { | ||||
|             // Replace ISO "T" with a space to allow wrapping | ||||
|             return dateTimeStr.replace("T", " "); | ||||
|         }, | ||||
|         getImageDownloadName(datum) { | ||||
|             let imageDownloadName = ''; | ||||
|             if (datum) { | ||||
|                 const key = this.imageDownloadNameHints.key; | ||||
|                 imageDownloadName = datum[key]; | ||||
|             } | ||||
|  | ||||
|             return imageDownloadName; | ||||
|         }, | ||||
|         parseTime(datum) { | ||||
|             if (!datum) { | ||||
|                 return; | ||||
| @@ -655,6 +675,7 @@ export default { | ||||
|             image.formattedTime = this.formatTime(datum); | ||||
|             image.url = this.formatImageUrl(datum); | ||||
|             image.time = datum[this.timeKey]; | ||||
|             image.imageDownloadName = this.getImageDownloadName(datum); | ||||
|  | ||||
|             this.imageHistory.push(image); | ||||
|  | ||||
| @@ -683,7 +704,7 @@ export default { | ||||
|             window.clearInterval(this.durationTracker); | ||||
|         }, | ||||
|         updateDuration() { | ||||
|             let currentTime = this.openmct.time.clock().currentValue(); | ||||
|             let currentTime = this.openmct.time.clock() && this.openmct.time.clock().currentValue(); | ||||
|             this.numericDuration = currentTime - this.parsedSelectedTime; | ||||
|         }, | ||||
|         resetAgeCSS() { | ||||
| @@ -777,6 +798,9 @@ export default { | ||||
|             this.focusedImageNaturalAspectRatio = undefined; | ||||
|  | ||||
|             const img = this.$refs.focusedImage; | ||||
|             if (!img) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             // TODO - should probably cache this | ||||
|             img.addEventListener('load', () => { | ||||
|   | ||||
| @@ -19,7 +19,7 @@ | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
| import ImageryPlugin from './plugin.js'; | ||||
|  | ||||
| import Vue from 'vue'; | ||||
| import { | ||||
|     createOpenMct, | ||||
| @@ -89,15 +89,11 @@ describe("The Imagery View Layout", () => { | ||||
|     const START = Date.now(); | ||||
|     const COUNT = 10; | ||||
|  | ||||
|     let resolveFunction; | ||||
|  | ||||
|     let openmct; | ||||
|     let imageryPlugin; | ||||
|     let parent; | ||||
|     let child; | ||||
|     let timeFormat = 'utc'; | ||||
|     let bounds = { | ||||
|         start: START - TEN_MINUTES, | ||||
|         end: START | ||||
|     }; | ||||
|     let imageTelemetry = generateTelemetry(START - TEN_MINUTES, COUNT); | ||||
|     let imageryObject = { | ||||
|         identifier: { | ||||
| @@ -205,6 +201,10 @@ describe("The Imagery View Layout", () => { | ||||
|  | ||||
|         openmct = createOpenMct(); | ||||
|  | ||||
|         openmct.install(openmct.plugins.MyItems()); | ||||
|         openmct.install(openmct.plugins.LocalTimeSystem()); | ||||
|         openmct.install(openmct.plugins.UTCTimeSystem()); | ||||
|  | ||||
|         parent = document.createElement('div'); | ||||
|         child = document.createElement('div'); | ||||
|         parent.appendChild(child); | ||||
| @@ -215,22 +215,18 @@ describe("The Imagery View Layout", () => { | ||||
|         }); | ||||
|  | ||||
|         spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([])); | ||||
|  | ||||
|         imageryPlugin = new ImageryPlugin(); | ||||
|         openmct.install(imageryPlugin); | ||||
|  | ||||
|         spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({})); | ||||
|  | ||||
|         openmct.time.timeSystem(timeFormat, { | ||||
|             start: 0, | ||||
|             end: 4 | ||||
|         }); | ||||
|  | ||||
|         openmct.on('start', done); | ||||
|         openmct.startHeadless(appHolder); | ||||
|         openmct.start(appHolder); | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         openmct.time.timeSystem('utc', { | ||||
|             start: 0, | ||||
|             end: 1 | ||||
|         }); | ||||
|  | ||||
|         return resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
| @@ -248,7 +244,7 @@ describe("The Imagery View Layout", () => { | ||||
|         let imageryViewProvider; | ||||
|         let imageryView; | ||||
|  | ||||
|         beforeEach(async (done) => { | ||||
|         beforeEach(async () => { | ||||
|             let telemetryRequestResolve; | ||||
|             let telemetryRequestPromise = new Promise((resolve) => { | ||||
|                 telemetryRequestResolve = resolve; | ||||
| @@ -260,23 +256,18 @@ describe("The Imagery View Layout", () => { | ||||
|                 return telemetryRequestPromise; | ||||
|             }); | ||||
|  | ||||
|             openmct.time.clock('local', { | ||||
|                 start: bounds.start, | ||||
|                 end: bounds.end + 100 | ||||
|             }); | ||||
|  | ||||
|             applicableViews = openmct.objectViews.get(imageryObject, []); | ||||
|             imageryViewProvider = applicableViews.find(viewProvider => viewProvider.key === imageryKey); | ||||
|             imageryView = imageryViewProvider.view(imageryObject); | ||||
|             imageryView.show(child); | ||||
|  | ||||
|             await telemetryRequestPromise; | ||||
|             await Vue.nextTick(); | ||||
|  | ||||
|             return done(); | ||||
|         }); | ||||
|  | ||||
|         afterEach(() => { | ||||
|             openmct.time.stopClock(); | ||||
|             openmct.router.removeListener('change:hash', resolveFunction); | ||||
|  | ||||
|             imageryView.destroy(); | ||||
|         }); | ||||
|  | ||||
| @@ -286,43 +277,44 @@ 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", async () => { | ||||
|         it("should show the clicked thumbnail as the main image", (done) => { | ||||
|             const target = imageTelemetry[5].url; | ||||
|             parent.querySelectorAll(`img[src='${target}']`)[0].click(); | ||||
|             await Vue.nextTick(); | ||||
|             const imageInfo = getImageInfo(parent); | ||||
|             Vue.nextTick(() => { | ||||
|                 const imageInfo = getImageInfo(parent); | ||||
|  | ||||
|             expect(imageInfo.url.indexOf(imageTelemetry[5].timeId)).not.toEqual(-1); | ||||
|         }); | ||||
|  | ||||
|         it("should show that an image is new", async (done) => { | ||||
|             await Vue.nextTick(); | ||||
|  | ||||
|             // used in code, need to wait to the 500ms here too | ||||
|             setTimeout(() => { | ||||
|                 const imageIsNew = isNew(parent); | ||||
|  | ||||
|                 expect(imageIsNew).toBeTrue(); | ||||
|                 expect(imageInfo.url.indexOf(imageTelemetry[5].timeId)).not.toEqual(-1); | ||||
|                 done(); | ||||
|             }, REFRESH_CSS_MS); | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         it("should show that an image is not new", async (done) => { | ||||
|         xit("should show that an image is new", (done) => { | ||||
|             Vue.nextTick(() => { | ||||
|                 // used in code, need to wait to the 500ms here too | ||||
|                 setTimeout(() => { | ||||
|                     const imageIsNew = isNew(parent); | ||||
|                     expect(imageIsNew).toBeTrue(); | ||||
|                     done(); | ||||
|                 }, REFRESH_CSS_MS); | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         xit("should show that an image is not new", (done) => { | ||||
|             const target = imageTelemetry[2].url; | ||||
|             parent.querySelectorAll(`img[src='${target}']`)[0].click(); | ||||
|  | ||||
|             await Vue.nextTick(); | ||||
|             Vue.nextTick(() => { | ||||
|                 // used in code, need to wait to the 500ms here too | ||||
|                 setTimeout(() => { | ||||
|                     const imageIsNew = isNew(parent); | ||||
|  | ||||
|             // used in code, need to wait to the 500ms here too | ||||
|             setTimeout(() => { | ||||
|                 const imageIsNew = isNew(parent); | ||||
|  | ||||
|                 expect(imageIsNew).toBeFalse(); | ||||
|                 done(); | ||||
|             }, REFRESH_CSS_MS); | ||||
|                     expect(imageIsNew).toBeFalse(); | ||||
|                     done(); | ||||
|                 }, REFRESH_CSS_MS); | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         it("should navigate via arrow keys", async () => { | ||||
|         it("should navigate via arrow keys", (done) => { | ||||
|             let keyOpts = { | ||||
|                 element: parent.querySelector('.c-imagery'), | ||||
|                 key: 'ArrowLeft', | ||||
| @@ -332,14 +324,15 @@ describe("The Imagery View Layout", () => { | ||||
|  | ||||
|             simulateKeyEvent(keyOpts); | ||||
|  | ||||
|             await Vue.nextTick(); | ||||
|             Vue.nextTick(() => { | ||||
|                 const imageInfo = getImageInfo(parent); | ||||
|  | ||||
|             const imageInfo = getImageInfo(parent); | ||||
|  | ||||
|             expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 2].timeId)).not.toEqual(-1); | ||||
|                 expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 2].timeId)).not.toEqual(-1); | ||||
|                 done(); | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         it("should navigate via numerous arrow keys", async () => { | ||||
|         it("should navigate via numerous arrow keys", (done) => { | ||||
|             let element = parent.querySelector('.c-imagery'); | ||||
|             let type = 'keyup'; | ||||
|             let leftKeyOpts = { | ||||
| @@ -362,12 +355,12 @@ describe("The Imagery View Layout", () => { | ||||
|             // right once | ||||
|             simulateKeyEvent(rightKeyOpts); | ||||
|  | ||||
|             await Vue.nextTick(); | ||||
|             Vue.nextTick(() => { | ||||
|                 const imageInfo = getImageInfo(parent); | ||||
|  | ||||
|             const imageInfo = getImageInfo(parent); | ||||
|  | ||||
|             expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 3].timeId)).not.toEqual(-1); | ||||
|                 expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 3].timeId)).not.toEqual(-1); | ||||
|                 done(); | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|     }); | ||||
| }); | ||||
|   | ||||
| @@ -55,7 +55,7 @@ describe("The local time", () => { | ||||
|         beforeEach(() => { | ||||
|             localTimeSystem = openmct.time.timeSystem(LOCAL_SYSTEM_KEY, { | ||||
|                 start: 0, | ||||
|                 end: 4 | ||||
|                 end: 1 | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|   | ||||
| @@ -81,7 +81,7 @@ describe("The Move Action plugin", () => { | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         resetApplicationState(openmct); | ||||
|         return resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|     it("should be defined", () => { | ||||
|   | ||||
| @@ -135,6 +135,7 @@ import SearchResults from './SearchResults.vue'; | ||||
| import Sidebar from './Sidebar.vue'; | ||||
| import { clearDefaultNotebook, getDefaultNotebook, setDefaultNotebook, setDefaultNotebookSection, setDefaultNotebookPage } from '../utils/notebook-storage'; | ||||
| import { addNotebookEntry, createNewEmbed, getEntryPosById, getNotebookEntries, mutateObject } from '../utils/notebook-entries'; | ||||
| import { NOTEBOOK_VIEW_TYPE } from '../notebook-constants'; | ||||
| import objectUtils from 'objectUtils'; | ||||
|  | ||||
| import { debounce } from 'lodash'; | ||||
| @@ -189,14 +190,14 @@ export default { | ||||
|         selectedPage() { | ||||
|             const pages = this.getPages(); | ||||
|             if (!pages) { | ||||
|                 return null; | ||||
|                 return {}; | ||||
|             } | ||||
|  | ||||
|             return pages.find(page => page.isSelected); | ||||
|         }, | ||||
|         selectedSection() { | ||||
|             if (!this.sections.length) { | ||||
|                 return null; | ||||
|                 return {}; | ||||
|             } | ||||
|  | ||||
|             return this.sections.find(section => section.isSelected); | ||||
| @@ -216,6 +217,7 @@ export default { | ||||
|  | ||||
|         window.addEventListener('orientationchange', this.formatSidebar); | ||||
|         window.addEventListener("hashchange", this.navigateToSectionPage, false); | ||||
|         this.openmct.router.on('change:params', this.changeSectionPage); | ||||
|  | ||||
|         this.navigateToSectionPage(); | ||||
|     }, | ||||
| @@ -226,6 +228,7 @@ export default { | ||||
|  | ||||
|         window.removeEventListener('orientationchange', this.formatSidebar); | ||||
|         window.removeEventListener("hashchange", this.navigateToSectionPage); | ||||
|         this.openmct.router.off('change:params', this.changeSectionPage); | ||||
|     }, | ||||
|     updated: function () { | ||||
|         this.$nextTick(() => { | ||||
| @@ -233,6 +236,28 @@ export default { | ||||
|         }); | ||||
|     }, | ||||
|     methods: { | ||||
|         changeSectionPage(newParams, oldParams, changedParams) { | ||||
|             if (newParams.view !== NOTEBOOK_VIEW_TYPE) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             let pageId = newParams.pageId; | ||||
|             let sectionId = newParams.sectionId; | ||||
|  | ||||
|             if (!pageId && !sectionId) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             this.sections.forEach(section => { | ||||
|                 section.isSelected = Boolean(section.id === sectionId); | ||||
|  | ||||
|                 if (section.isSelected) { | ||||
|                     section.pages.forEach(page => { | ||||
|                         page.isSelected = Boolean(page.id === pageId); | ||||
|                     }); | ||||
|                 } | ||||
|             }); | ||||
|         }, | ||||
|         changeSelectedSection({ sectionId, pageId }) { | ||||
|             const sections = this.sections.map(s => { | ||||
|                 s.isSelected = false; | ||||
| @@ -518,9 +543,11 @@ export default { | ||||
|             return this.sections.find(section => section.isSelected); | ||||
|         }, | ||||
|         navigateToSectionPage() { | ||||
|             const { pageId, sectionId } = this.openmct.router.getParams(); | ||||
|             let { pageId, sectionId } = this.openmct.router.getParams(); | ||||
|  | ||||
|             if (!pageId || !sectionId) { | ||||
|                 return; | ||||
|                 sectionId = this.selectedSection.id; | ||||
|                 pageId = this.selectedPage.id; | ||||
|             } | ||||
|  | ||||
|             const sections = this.sections.map(s => { | ||||
|   | ||||
| @@ -145,7 +145,7 @@ export default { | ||||
|  | ||||
|             const relativeHash = hash.slice(hash.indexOf('#')); | ||||
|             const url = new URL(relativeHash, `${location.protocol}//${location.host}${location.pathname}`); | ||||
|             window.location.hash = url.hash; | ||||
|             this.openmct.router.navigate(url.hash); | ||||
|         }, | ||||
|         formatTime(unixTime, timeFormat) { | ||||
|             return Moment.utc(unixTime).format(timeFormat); | ||||
|   | ||||
| @@ -111,10 +111,6 @@ export default { | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
|         pages() { | ||||
|             const selectedSection = this.sections.find(section => section.isSelected); | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| export const EVENT_SNAPSHOTS_UPDATED = 'SNAPSHOTS_UPDATED'; | ||||
| export const NOTEBOOK_DEFAULT = 'DEFAULT'; | ||||
| export const NOTEBOOK_SNAPSHOT = 'SNAPSHOT'; | ||||
| export const NOTEBOOK_VIEW_TYPE = 'notebook-vue'; | ||||
|   | ||||
| @@ -65,7 +65,8 @@ describe("Notebook plugin:", () => { | ||||
|  | ||||
|     afterAll(() => { | ||||
|         appHolder.remove(); | ||||
|         resetApplicationState(openmct); | ||||
|  | ||||
|         return resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|     it("has type as Notebook", () => { | ||||
|   | ||||
| @@ -140,7 +140,8 @@ describe('Notebook Entries:', () => { | ||||
|  | ||||
|     afterEach(() => { | ||||
|         notebookDomainObject.configuration.entries[selectedSection.id][selectedPage.id] = []; | ||||
|         resetApplicationState(openmct); | ||||
|  | ||||
|         return resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|     it('getNotebookEntries has no entries', () => { | ||||
|   | ||||
| @@ -83,7 +83,7 @@ describe('Notebook Storage:', () => { | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         resetApplicationState(openmct); | ||||
|         return resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|     it('has empty local Storage', () => { | ||||
|   | ||||
| @@ -91,7 +91,7 @@ export default class CouchObjectProvider { | ||||
|      * persist any queued objects | ||||
|      * @private | ||||
|      */ | ||||
|     checkResponse(response, intermediateResponse) { | ||||
|     checkResponse(response, intermediateResponse, key) { | ||||
|         let requestSuccess = false; | ||||
|         const id = response ? response.id : undefined; | ||||
|         let rev; | ||||
| @@ -113,6 +113,8 @@ export default class CouchObjectProvider { | ||||
|             if (this.objectQueue[id].hasNext()) { | ||||
|                 this.updateQueued(id); | ||||
|             } | ||||
|         } else { | ||||
|             this.objectQueue[key].pending = false; | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -132,8 +134,7 @@ export default class CouchObjectProvider { | ||||
|             } | ||||
|  | ||||
|             //Sometimes CouchDB returns the old rev which fetching the object if there is a document update in progress | ||||
|             //Only update the rev if it's the first time we're getting the object from CouchDB. Subsequent revs should only be updated by updates. | ||||
|             if (!this.objectQueue[key].pending && !this.objectQueue[key].rev) { | ||||
|             if (!this.objectQueue[key].pending) { | ||||
|                 this.objectQueue[key].updateRevision(response[REV]); | ||||
|             } | ||||
|  | ||||
| @@ -458,7 +459,7 @@ export default class CouchObjectProvider { | ||||
|         const queued = this.objectQueue[key].dequeue(); | ||||
|         let document = new CouchDocument(key, queued.model); | ||||
|         this.request(key, "PUT", document).then((response) => { | ||||
|             this.checkResponse(response, queued.intermediateResponse); | ||||
|             this.checkResponse(response, queued.intermediateResponse, key); | ||||
|         }); | ||||
|  | ||||
|         return intermediateResponse.promise; | ||||
| @@ -473,7 +474,7 @@ export default class CouchObjectProvider { | ||||
|             const queued = this.objectQueue[key].dequeue(); | ||||
|             let document = new CouchDocument(key, queued.model, this.objectQueue[key].rev); | ||||
|             this.request(key, "PUT", document).then((response) => { | ||||
|                 this.checkResponse(response, queued.intermediateResponse); | ||||
|                 this.checkResponse(response, queued.intermediateResponse, key); | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -117,7 +117,7 @@ describe('the plugin', () => { | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         it('updates an object', () => { | ||||
|         it('updates an object', (done) => { | ||||
|             return openmct.objects.save(mockDomainObject).then((result) => { | ||||
|                 expect(result).toBeTrue(); | ||||
|                 expect(provider.create).toHaveBeenCalled(); | ||||
| @@ -128,6 +128,7 @@ describe('the plugin', () => { | ||||
|                 return openmct.objects.save(mockDomainObject).then((updatedResult) => { | ||||
|                     expect(updatedResult).toBeTrue(); | ||||
|                     expect(provider.update).toHaveBeenCalled(); | ||||
|                     done(); | ||||
|                 }); | ||||
|             }); | ||||
|         }); | ||||
|   | ||||
| @@ -28,6 +28,7 @@ | ||||
|                  :series="config.series.models" | ||||
|                  :highlights="highlights" | ||||
|                  :legend="config.legend" | ||||
|                  @legendHoverChanged="legendHoverChanged" | ||||
|     /> | ||||
|     <div class="plot-wrapper-axis-and-display-area flex-elem grows"> | ||||
|         <y-axis v-if="config.series.models.length > 0" | ||||
| @@ -65,9 +66,9 @@ | ||||
|                 <div ref="chartContainer" | ||||
|                      class="gl-plot-chart-wrapper" | ||||
|                 > | ||||
|                     <mct-chart :series-config="config" | ||||
|                                :rectangles="rectangles" | ||||
|                     <mct-chart :rectangles="rectangles" | ||||
|                                :highlights="highlights" | ||||
|                                :show-limit-line-labels="showLimitLineLabels" | ||||
|                                @plotReinitializeCanvas="initCanvas" | ||||
|                     /> | ||||
|                 </div> | ||||
| @@ -85,8 +86,8 @@ | ||||
|                         > | ||||
|                         </button> | ||||
|                     </div> | ||||
|                     <div class="c-button-set c-button-set--strip-h" | ||||
|                          :disabled="!plotHistory.length" | ||||
|                     <div v-if="plotHistory.length" | ||||
|                          class="c-button-set c-button-set--strip-h" | ||||
|                     > | ||||
|                         <button class="c-button icon-arrow-left" | ||||
|                                 title="Restore previous pan/zoom" | ||||
| @@ -99,6 +100,31 @@ | ||||
|                         > | ||||
|                         </button> | ||||
|                     </div> | ||||
|                     <div v-if="isRealTime" | ||||
|                          class="c-button-set c-button-set--strip-h" | ||||
|                     > | ||||
|                         <button v-if="!isFrozen" | ||||
|                                 class="c-button icon-pause" | ||||
|                                 title="Pause incoming real-time data" | ||||
|                                 @click="pause()" | ||||
|                         > | ||||
|                         </button> | ||||
|                         <button v-if="isFrozen" | ||||
|                                 class="c-button icon-arrow-right pause-play is-paused" | ||||
|                                 title="Resume displaying real-time data" | ||||
|                                 @click="play()" | ||||
|                         > | ||||
|                         </button> | ||||
|                     </div> | ||||
|                     <div v-if="isTimeOutOfSync || isFrozen" | ||||
|                          class="c-button-set c-button-set--strip-h" | ||||
|                     > | ||||
|                         <button class="c-button icon-clock" | ||||
|                                 title="Synchronize Time Conductor" | ||||
|                                 @click="showSynchronizeDialog()" | ||||
|                         > | ||||
|                         </button> | ||||
|                     </div> | ||||
|                 </div> | ||||
| 
 | ||||
|                 <!--Cursor guides--> | ||||
| @@ -135,6 +161,7 @@ import MctTicks from "./MctTicks.vue"; | ||||
| import MctChart from "./chart/MctChart.vue"; | ||||
| import XAxis from "./axis/XAxis.vue"; | ||||
| import YAxis from "./axis/YAxis.vue"; | ||||
| import _ from "lodash"; | ||||
| 
 | ||||
| export default { | ||||
|     components: { | ||||
| @@ -186,10 +213,16 @@ export default { | ||||
|             xKeyOptions: [], | ||||
|             config: {}, | ||||
|             pending: 0, | ||||
|             loaded: false | ||||
|             isRealTime: this.openmct.time.clock() !== undefined, | ||||
|             loaded: false, | ||||
|             isTimeOutOfSync: false, | ||||
|             showLimitLineLabels: undefined | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
|         isFrozen() { | ||||
|             return this.config.xAxis.get('frozen') === true && this.config.yAxis.get('frozen') === true; | ||||
|         }, | ||||
|         plotLegendPositionClass() { | ||||
|             return `plot-legend-${this.config.legend.get('position')}`; | ||||
|         }, | ||||
| @@ -227,6 +260,7 @@ export default { | ||||
|             'configuration.filters', | ||||
|             this.updateFiltersAndResubscribe | ||||
|         ); | ||||
|         this.removeStatusListener = this.openmct.status.observe(this.domainObject.identifier, this.updateStatus); | ||||
| 
 | ||||
|         this.openmct.objectViews.on('clearData', this.clearData); | ||||
|         this.followTimeConductor(); | ||||
| @@ -243,6 +277,7 @@ export default { | ||||
|     }, | ||||
|     methods: { | ||||
|         followTimeConductor() { | ||||
|             this.openmct.time.on('clock', this.updateRealTime); | ||||
|             this.openmct.time.on('bounds', this.updateDisplayBounds); | ||||
|             this.synchronized(true); | ||||
|         }, | ||||
| @@ -371,6 +406,9 @@ export default { | ||||
|             const displayRange = series.getDisplayRange(xKey); | ||||
|             this.config.xAxis.set('range', displayRange); | ||||
|         }, | ||||
|         updateRealTime(clock) { | ||||
|             this.isRealTime = clock !== undefined; | ||||
|         }, | ||||
| 
 | ||||
|         /** | ||||
|        * Track latest display bounds.  Forces update when not receiving ticks. | ||||
| @@ -424,19 +462,30 @@ export default { | ||||
|        * displays can update accordingly. | ||||
|        */ | ||||
|         synchronized(value) { | ||||
|             const isLocalClock = this.openmct.time.clock(); | ||||
| 
 | ||||
|             if (typeof value !== 'undefined') { | ||||
|                 this._synchronized = value; | ||||
|                 const isUnsynced = !value && this.openmct.time.clock(); | ||||
|                 const domainObject = this.openmct.legacyObject(this.domainObject); | ||||
|                 if (domainObject.getCapability('status')) { | ||||
|                     domainObject.getCapability('status') | ||||
|                         .set('timeconductor-unsynced', isUnsynced); | ||||
|                 } | ||||
|                 this.isTimeOutOfSync = value !== true; | ||||
| 
 | ||||
|                 const isUnsynced = isLocalClock && !value; | ||||
|                 this.setStatus(isUnsynced); | ||||
|             } | ||||
| 
 | ||||
|             return this._synchronized; | ||||
|         }, | ||||
| 
 | ||||
|         setStatus(isNotInSync) { | ||||
|             const outOfSync = isNotInSync === true | ||||
|                 || this.isTimeOutOfSync === true | ||||
|                 || this.isFrozen === true; | ||||
|             if (outOfSync === true) { | ||||
|                 this.openmct.status.set(this.domainObject.identifier, 'timeconductor-unsynced'); | ||||
|             } else { | ||||
|                 this.openmct.status.set(this.domainObject.identifier, ''); | ||||
|             } | ||||
|         }, | ||||
| 
 | ||||
|         initCanvas() { | ||||
|             if (this.canvas) { | ||||
|                 this.stopListening(this.canvas); | ||||
| @@ -451,6 +500,10 @@ export default { | ||||
|         }, | ||||
| 
 | ||||
|         initialize() { | ||||
|             _.debounce(this.handleWindowResize, 400); | ||||
|             this.plotContainerResizeObserver = new ResizeObserver(this.handleWindowResize); | ||||
|             this.plotContainerResizeObserver.observe(this.$parent.$refs.plotWrapper); | ||||
| 
 | ||||
|             // Setup canvas etc. | ||||
|             this.xScale = new LinearScale(this.config.xAxis.get('displayRange')); | ||||
|             this.yScale = new LinearScale(this.config.yAxis.get('displayRange')); | ||||
| @@ -729,7 +782,8 @@ export default { | ||||
|             const ZOOM_AMT = 0.1; | ||||
|             event.preventDefault(); | ||||
| 
 | ||||
|             if (!this.positionOverPlot) { | ||||
|             if (event.wheelDelta === undefined | ||||
|                 || !this.positionOverPlot) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
| @@ -847,11 +901,13 @@ export default { | ||||
|         freeze() { | ||||
|             this.config.yAxis.set('frozen', true); | ||||
|             this.config.xAxis.set('frozen', true); | ||||
|             this.setStatus(); | ||||
|         }, | ||||
| 
 | ||||
|         clear() { | ||||
|             this.config.yAxis.set('frozen', false); | ||||
|             this.config.xAxis.set('frozen', false); | ||||
|             this.setStatus(); | ||||
|             this.plotHistory = []; | ||||
|             this.userViewportChangeEnd(); | ||||
|         }, | ||||
| @@ -881,6 +937,59 @@ export default { | ||||
|             this.config.series.models[0].emit('change:yKey', yKey); | ||||
|         }, | ||||
| 
 | ||||
|         pause() { | ||||
|             this.freeze(); | ||||
|         }, | ||||
| 
 | ||||
|         play() { | ||||
|             this.clear(); | ||||
|         }, | ||||
| 
 | ||||
|         showSynchronizeDialog() { | ||||
|             const isLocalClock = this.openmct.time.clock(); | ||||
|             if (isLocalClock !== undefined) { | ||||
|                 const message = ` | ||||
|                 This action will change the Time Conductor to Fixed Timespan mode with this plot view's current time bounds. | ||||
|                 Do you want to continue? | ||||
|             `; | ||||
| 
 | ||||
|                 let dialog = this.openmct.overlays.dialog({ | ||||
|                     title: 'Synchronize Time Conductor', | ||||
|                     iconClass: 'alert', | ||||
|                     size: 'fit', | ||||
|                     message: message, | ||||
|                     buttons: [ | ||||
|                         { | ||||
|                             label: 'OK', | ||||
|                             callback: () => { | ||||
|                                 dialog.dismiss(); | ||||
|                                 this.synchronizeTimeConductor(); | ||||
|                             } | ||||
|                         }, | ||||
|                         { | ||||
|                             label: 'Cancel', | ||||
|                             callback: () => { | ||||
|                                 dialog.dismiss(); | ||||
|                             } | ||||
|                         } | ||||
|                     ] | ||||
|                 }); | ||||
|             } else { | ||||
|                 this.openmct.notifications.alert('Time conductor bounds have changed.'); | ||||
|                 this.synchronizeTimeConductor(); | ||||
|             } | ||||
|         }, | ||||
| 
 | ||||
|         synchronizeTimeConductor() { | ||||
|             this.openmct.time.stopClock(); | ||||
|             const range = this.config.xAxis.get('displayRange'); | ||||
|             this.openmct.time.bounds({ | ||||
|                 start: range.min, | ||||
|                 end: range.max | ||||
|             }); | ||||
|             this.isTimeOutOfSync = false; | ||||
|         }, | ||||
| 
 | ||||
|         destroy() { | ||||
|             configStore.deleteStore(this.config.id); | ||||
| 
 | ||||
| @@ -894,8 +1003,27 @@ export default { | ||||
|                 this.filterObserver(); | ||||
|             } | ||||
| 
 | ||||
|             if (this.removeStatusListener) { | ||||
|                 this.removeStatusListener(); | ||||
|             } | ||||
| 
 | ||||
|             this.plotContainerResizeObserver.disconnect(); | ||||
| 
 | ||||
|             this.openmct.time.off('clock', this.updateRealTime); | ||||
|             this.openmct.time.off('bounds', this.updateDisplayBounds); | ||||
|             this.openmct.objectViews.off('clearData', this.clearData); | ||||
|         }, | ||||
|         updateStatus(status) { | ||||
|             this.$emit('statusUpdated', status); | ||||
|         }, | ||||
|         handleWindowResize() { | ||||
|             if (this.offsetWidth !== this.$parent.$refs.plotWrapper.offsetWidth) { | ||||
|                 this.offsetWidth = this.$parent.$refs.plotWrapper.offsetWidth; | ||||
|                 this.config.series.models.forEach(this.loadSeriesData, this); | ||||
|             } | ||||
|         }, | ||||
|         legendHoverChanged(data) { | ||||
|             this.showLimitLineLabels = data; | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| @@ -228,15 +228,16 @@ export default { | ||||
| 
 | ||||
|         doTickUpdate() { | ||||
|             if (this.shouldCheckWidth) { | ||||
|                 const tickElements = this.$refs.tickContainer.querySelectorAll('.gl-plot-tick > span'); | ||||
|                 const tickElements = this.$refs.tickContainer && this.$refs.tickContainer.querySelectorAll('.gl-plot-tick > span'); | ||||
|                 if (tickElements) { | ||||
|                     const tickWidth = Number([].reduce.call(tickElements, function (memo, first) { | ||||
|                         return Math.max(memo, first.offsetWidth); | ||||
|                     }, 0)); | ||||
| 
 | ||||
|                 const tickWidth = Number([].reduce.call(tickElements, function (memo, first) { | ||||
|                     return Math.max(memo, first.offsetWidth); | ||||
|                 }, 0)); | ||||
| 
 | ||||
|                 this.tickWidth = tickWidth; | ||||
|                 this.$emit('plotTickWidth', tickWidth); | ||||
|                 this.shouldCheckWidth = false; | ||||
|                     this.tickWidth = tickWidth; | ||||
|                     this.$emit('plotTickWidth', tickWidth); | ||||
|                     this.shouldCheckWidth = false; | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             this.tickUpdate = false; | ||||
| @@ -56,6 +56,7 @@ | ||||
| 
 | ||||
|     <div ref="plotContainer" | ||||
|          class="l-view-section u-style-receiver js-style-receiver" | ||||
|          :class="{'s-status-timeconductor-unsynced': status && status === 'timeconductor-unsynced'}" | ||||
|     > | ||||
|         <div v-show="!!loading" | ||||
|              class="c-loading--overlay loading" | ||||
| @@ -64,6 +65,7 @@ | ||||
|                   :cursor-guide="cursorGuide" | ||||
|                   :options="options" | ||||
|                   @loadingUpdated="loadingUpdated" | ||||
|                   @statusUpdated="setStatus" | ||||
|         /> | ||||
|     </div> | ||||
| </div> | ||||
| @@ -94,7 +96,8 @@ export default { | ||||
|             // hideExportButtons: false | ||||
|             cursorGuide: false, | ||||
|             gridLines: !this.options.compact, | ||||
|             loading: false | ||||
|             loading: false, | ||||
|             status: '' | ||||
|         }; | ||||
|     }, | ||||
|     mounted() { | ||||
| @@ -131,6 +134,9 @@ export default { | ||||
| 
 | ||||
|         toggleGridLines() { | ||||
|             this.gridLines = !this.gridLines; | ||||
|         }, | ||||
|         setStatus(status) { | ||||
|             this.status = status; | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| @@ -48,7 +48,7 @@ export default function PlotViewProvider(openmct) { | ||||
|         name: 'Plot', | ||||
|         cssClass: 'icon-telemetry', | ||||
|         canView(domainObject, objectPath) { | ||||
|             return isCompactView(objectPath) && hasTelemetry(domainObject, openmct); | ||||
|             return hasTelemetry(domainObject, openmct); | ||||
|         }, | ||||
| 
 | ||||
|         view: function (domainObject, objectPath) { | ||||
							
								
								
									
										46
									
								
								src/plugins/plot/chart/LimitLabel.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/plugins/plot/chart/LimitLabel.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | ||||
| <template> | ||||
| <div class="plot-series-limit-label" | ||||
|      :style="styleObj" | ||||
|      :class="limit.cssClass" | ||||
| > | ||||
|     <span class="plot-series-limit-value">{{ limit.value }}</span> | ||||
|     <span class="plot-series-color-swatch" | ||||
|           :style="{ 'background-color': limit.color }" | ||||
|     ></span> | ||||
|     <span class="plot-series-name">{{ limit.name }}</span> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| export default { | ||||
|     props: { | ||||
|         limit: { | ||||
|             type: Object, | ||||
|             required: true, | ||||
|             default() { | ||||
|                 return {}; | ||||
|             } | ||||
|         }, | ||||
|         point: { | ||||
|             type: Object, | ||||
|             required: true, | ||||
|             default() { | ||||
|                 return {}; | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     computed: { | ||||
|         styleObj() { | ||||
|             const top = `${this.point.top - 10}px`; | ||||
|             const left = `${this.point.left + 5}px`; | ||||
|  | ||||
|             return { | ||||
|                 'position': 'absolute', | ||||
|                 'top': top, | ||||
|                 'left': left, | ||||
|                 'color': '#fff' | ||||
|             }; | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| </script> | ||||
							
								
								
									
										44
									
								
								src/plugins/plot/chart/LimitLine.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								src/plugins/plot/chart/LimitLine.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | ||||
| <template> | ||||
| <hr :style="styleObj" | ||||
|     :class="cssWithoutUprLwr" | ||||
| > | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| export default { | ||||
|     props: { | ||||
|         point: { | ||||
|             type: Object, | ||||
|             required: true, | ||||
|             default() { | ||||
|                 return {}; | ||||
|             } | ||||
|         }, | ||||
|         cssClass: { | ||||
|             type: String, | ||||
|             default() { | ||||
|                 return ''; | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     computed: { | ||||
|         styleObj() { | ||||
|             const top = `${this.point.top}px`; | ||||
|             const left = `${this.point.left}px`; | ||||
|  | ||||
|             return { | ||||
|                 'position': 'absolute', | ||||
|                 'width': '100%', | ||||
|                 'top': top, | ||||
|                 'left': left | ||||
|             }; | ||||
|         }, | ||||
|         cssWithoutUprLwr() { | ||||
|             let cssClass = this.cssClass.replace(/is-limit--upr/gi, 'is-limit--line'); | ||||
|             cssClass = cssClass.replace(/is-limit--lwr/gi, 'is-limit--line'); | ||||
|  | ||||
|             return cssClass; | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| </script> | ||||
							
								
								
									
										105
									
								
								src/plugins/plot/chart/MCTChartAlarmLineSet.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								src/plugins/plot/chart/MCTChartAlarmLineSet.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,105 @@ | ||||
| /***************************************************************************** | ||||
|  * 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 eventHelpers from '../lib/eventHelpers'; | ||||
|  | ||||
| export default class MCTChartAlarmLineSet { | ||||
|     constructor(series, chart, offset, bounds) { | ||||
|         this.series = series; | ||||
|         this.chart = chart; | ||||
|         this.offset = offset; | ||||
|         this.bounds = bounds; | ||||
|         this.limits = []; | ||||
|  | ||||
|         eventHelpers.extend(this); | ||||
|         this.listenTo(series, 'limitBounds', this.updateBounds, this); | ||||
|         this.listenTo(series, 'change:xKey', this.getLimitPoints, this); | ||||
|  | ||||
|         if (series.limits) { | ||||
|             this.getLimitPoints(series); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     updateBounds(bounds) { | ||||
|         this.bounds = bounds; | ||||
|         this.getLimitPoints(this.series); | ||||
|     } | ||||
|  | ||||
|     color() { | ||||
|         return this.series.get('color'); | ||||
|     } | ||||
|  | ||||
|     name() { | ||||
|         return this.series.get('name'); | ||||
|     } | ||||
|  | ||||
|     makePoint(point, series) { | ||||
|         if (!this.offset.xVal) { | ||||
|             this.chart.setOffset(point, undefined, series); | ||||
|         } | ||||
|  | ||||
|         return { | ||||
|             x: this.offset.xVal(point, series), | ||||
|             y: this.offset.yVal(point, series) | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     getLimitPoints(series) { | ||||
|         this.limits = []; | ||||
|         let xKey = series.get('xKey'); | ||||
|         Object.keys(series.limits).forEach((key) => { | ||||
|             const limitForLevel = series.limits[key]; | ||||
|             if (limitForLevel.high) { | ||||
|                 const point = this.makePoint(Object.assign({ [xKey]: this.bounds.start }, limitForLevel.high), series); | ||||
|                 this.limits.push({ | ||||
|                     seriesKey: series.keyString, | ||||
|                     value: series.getYVal(limitForLevel.high), | ||||
|                     color: this.color().asHexString(), | ||||
|                     name: this.name(), | ||||
|                     point, | ||||
|                     cssClass: limitForLevel.high.cssClass | ||||
|                 }); | ||||
|             } | ||||
|  | ||||
|             if (limitForLevel.low) { | ||||
|                 const point = this.makePoint(Object.assign({ [xKey]: this.bounds.start }, limitForLevel.low), series); | ||||
|                 this.limits.push({ | ||||
|                     seriesKey: series.keyString, | ||||
|                     value: series.getYVal(limitForLevel.low), | ||||
|                     color: this.color().asHexString(), | ||||
|                     name: this.name(), | ||||
|                     point, | ||||
|                     cssClass: limitForLevel.low.cssClass | ||||
|                 }); | ||||
|             } | ||||
|         }, this); | ||||
|     } | ||||
|  | ||||
|     reset() { | ||||
|         this.limits = []; | ||||
|     } | ||||
|  | ||||
|     destroy() { | ||||
|         this.stopListening(); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -23,6 +23,9 @@ | ||||
| <div class="gl-plot-chart-area"> | ||||
|     <span v-html="canvasTemplate"></span> | ||||
|     <span v-html="canvasTemplate"></span> | ||||
|     <div ref="limitArea" | ||||
|          class="js-limit-area" | ||||
|     ></div> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| @@ -34,8 +37,12 @@ import MCTChartLineLinear from './MCTChartLineLinear'; | ||||
| import MCTChartLineStepAfter from './MCTChartLineStepAfter'; | ||||
| import MCTChartPointSet from './MCTChartPointSet'; | ||||
| import MCTChartAlarmPointSet from './MCTChartAlarmPointSet'; | ||||
| import MCTChartAlarmLineSet from "./MCTChartAlarmLineSet"; | ||||
| import configStore from "../configuration/configStore"; | ||||
| import PlotConfigurationModel from "../configuration/PlotConfigurationModel"; | ||||
| import LimitLine from "./LimitLine.vue"; | ||||
| import LimitLabel from "./LimitLabel.vue"; | ||||
| import Vue from 'vue'; | ||||
| 
 | ||||
| const MARKER_SIZE = 6.0; | ||||
| const HIGHLIGHT_SIZE = MARKER_SIZE * 2.0; | ||||
| @@ -54,6 +61,12 @@ export default { | ||||
|             default() { | ||||
|                 return []; | ||||
|             } | ||||
|         }, | ||||
|         showLimitLineLabels: { | ||||
|             type: Object, | ||||
|             default() { | ||||
|                 return {}; | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
| @@ -67,18 +80,22 @@ export default { | ||||
|         }, | ||||
|         rectangles() { | ||||
|             this.scheduleDraw(); | ||||
|         }, | ||||
|         showLimitLineLabels() { | ||||
|             this.drawLimitLines(); | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|         eventHelpers.extend(this); | ||||
| 
 | ||||
|         this.config = this.getConfig(); | ||||
|         this.isDestroyed = false; | ||||
|         this.lines = []; | ||||
|         this.limitLines = []; | ||||
|         this.pointSets = []; | ||||
|         this.alarmSets = []; | ||||
|         this.offset = {}; | ||||
|         this.seriesElements = new WeakMap(); | ||||
|         this.seriesLimits = new WeakMap(); | ||||
| 
 | ||||
|         let canvasEls = this.$parent.$refs.chartContainer.querySelectorAll("canvas"); | ||||
|         const mainCanvas = canvasEls[1]; | ||||
| @@ -90,8 +107,8 @@ export default { | ||||
|         this.listenTo(this.config.series, 'add', this.onSeriesAdd, this); | ||||
|         this.listenTo(this.config.series, 'remove', this.onSeriesRemove, this); | ||||
|         this.listenTo(this.config.yAxis, 'change:key', this.clearOffset, this); | ||||
|         this.listenTo(this.config.yAxis, 'change', this.scheduleDraw); | ||||
|         this.listenTo(this.config.xAxis, 'change', this.scheduleDraw); | ||||
|         this.listenTo(this.config.yAxis, 'change', this.updateLimitsAndDraw); | ||||
|         this.listenTo(this.config.xAxis, 'change', this.updateLimitsAndDraw); | ||||
|         this.config.series.forEach(this.onSeriesAdd, this); | ||||
|     }, | ||||
|     beforeDestroy() { | ||||
| @@ -116,15 +133,18 @@ export default { | ||||
|             this.changeInterpolate(mode, o, series); | ||||
|             this.changeMarkers(mode, o, series); | ||||
|             this.changeAlarmMarkers(mode, o, series); | ||||
|             this.changeLimitLines(mode, o, series); | ||||
|         }, | ||||
|         onSeriesAdd(series) { | ||||
|             this.listenTo(series, 'change:xKey', this.reDraw, this); | ||||
|             this.listenTo(series, 'change:interpolate', this.changeInterpolate, this); | ||||
|             this.listenTo(series, 'change:markers', this.changeMarkers, this); | ||||
|             this.listenTo(series, 'change:alarmMarkers', this.changeAlarmMarkers, this); | ||||
|             this.listenTo(series, 'change:limitLines', this.changeLimitLines, this); | ||||
|             this.listenTo(series, 'change', this.scheduleDraw); | ||||
|             this.listenTo(series, 'add', this.scheduleDraw); | ||||
|             this.makeChartElement(series); | ||||
|             this.makeLimitLines(series); | ||||
|         }, | ||||
|         changeInterpolate(mode, o, series) { | ||||
|             if (mode === o) { | ||||
| @@ -178,6 +198,14 @@ export default { | ||||
|                 this.pointSets.push(pointSet); | ||||
|             } | ||||
|         }, | ||||
|         changeLimitLines(mode, o, series) { | ||||
|             if (mode === o) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             this.makeLimitLines(series); | ||||
|             this.updateLimitsAndDraw(); | ||||
|         }, | ||||
|         onSeriesRemove(series) { | ||||
|             this.stopListening(series); | ||||
|             this.removeChartElement(series); | ||||
| @@ -187,6 +215,7 @@ export default { | ||||
|             this.isDestroyed = true; | ||||
|             this.stopListening(); | ||||
|             this.lines.forEach(line => line.destroy()); | ||||
|             this.limitLines.forEach(line => line.destroy()); | ||||
|             DrawLoader.releaseDrawAPI(this.drawAPI); | ||||
|         }, | ||||
|         clearOffset() { | ||||
| @@ -199,6 +228,9 @@ export default { | ||||
|             this.lines.forEach(function (line) { | ||||
|                 line.reset(); | ||||
|             }); | ||||
|             this.limitLines.forEach(function (line) { | ||||
|                 line.reset(); | ||||
|             }); | ||||
|             this.pointSets.forEach(function (pointSet) { | ||||
|                 pointSet.reset(); | ||||
|             }); | ||||
| @@ -269,6 +301,8 @@ export default { | ||||
|             } | ||||
| 
 | ||||
|             this.seriesElements.delete(series); | ||||
| 
 | ||||
|             this.clearLimitLines(series); | ||||
|         }, | ||||
|         lineForSeries(series) { | ||||
|             if (series.get('interpolate') === 'linear') { | ||||
| @@ -287,6 +321,14 @@ export default { | ||||
|                 ); | ||||
|             } | ||||
|         }, | ||||
|         limitLineForSeries(series) { | ||||
|             return new MCTChartAlarmLineSet( | ||||
|                 series, | ||||
|                 this, | ||||
|                 this.offset, | ||||
|                 this.openmct.time.bounds() | ||||
|             ); | ||||
|         }, | ||||
|         pointSetForSeries(series) { | ||||
|             if (series.get('markers')) { | ||||
|                 return new MCTChartPointSet( | ||||
| @@ -308,7 +350,8 @@ export default { | ||||
|         makeChartElement(series) { | ||||
|             const elements = { | ||||
|                 lines: [], | ||||
|                 pointSets: [] | ||||
|                 pointSets: [], | ||||
|                 limitLines: [] | ||||
|             }; | ||||
| 
 | ||||
|             const line = this.lineForSeries(series); | ||||
| @@ -330,6 +373,37 @@ export default { | ||||
| 
 | ||||
|             this.seriesElements.set(series, elements); | ||||
|         }, | ||||
|         makeLimitLines(series) { | ||||
|             this.clearLimitLines(series); | ||||
| 
 | ||||
|             if (!series.get('limitLines')) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             const limitElements = { | ||||
|                 limitLines: [] | ||||
|             }; | ||||
| 
 | ||||
|             const limitLine = this.limitLineForSeries(series); | ||||
|             if (limitLine) { | ||||
|                 limitElements.limitLines.push(limitLine); | ||||
|                 this.limitLines.push(limitLine); | ||||
|             } | ||||
| 
 | ||||
|             this.seriesLimits.set(series, limitElements); | ||||
|         }, | ||||
|         clearLimitLines(series) { | ||||
|             const seriesLimits = this.seriesLimits.get(series); | ||||
| 
 | ||||
|             if (seriesLimits) { | ||||
|                 seriesLimits.limitLines.forEach(function (line) { | ||||
|                     this.limitLines.splice(this.limitLines.indexOf(line), 1); | ||||
|                     line.destroy(); | ||||
|                 }, this); | ||||
| 
 | ||||
|                 this.seriesLimits.delete(series); | ||||
|             } | ||||
|         }, | ||||
|         canDraw() { | ||||
|             if (!this.offset.x || !this.offset.y) { | ||||
|                 return false; | ||||
| @@ -337,6 +411,10 @@ export default { | ||||
| 
 | ||||
|             return true; | ||||
|         }, | ||||
|         updateLimitsAndDraw() { | ||||
|             this.drawLimitLines(); | ||||
|             this.scheduleDraw(); | ||||
|         }, | ||||
|         scheduleDraw() { | ||||
|             if (!this.drawScheduled) { | ||||
|                 requestAnimationFrame(this.draw); | ||||
| @@ -385,6 +463,68 @@ export default { | ||||
|             this.pointSets.forEach(this.drawPoints, this); | ||||
|             this.alarmSets.forEach(this.drawAlarmPoints, this); | ||||
|         }, | ||||
|         drawLimitLines() { | ||||
|             if (this.canDraw()) { | ||||
|                 this.updateViewport(); | ||||
| 
 | ||||
|                 if (!this.drawAPI.origin) { | ||||
|                     return; | ||||
|                 } | ||||
| 
 | ||||
|                 Array.from(this.$refs.limitArea.children).forEach((el) => el.remove()); | ||||
| 
 | ||||
|                 this.limitLines.forEach((limitLine) => { | ||||
|                     let limitContainerEl = this.$refs.limitArea; | ||||
|                     limitLine.limits.forEach((limit) => { | ||||
|                         const showLabels = this.showLabels(limit.seriesKey); | ||||
|                         if (showLabels) { | ||||
|                             let limitLabelEl = this.getLimitLabel(limit); | ||||
|                             limitContainerEl.appendChild(limitLabelEl); | ||||
|                         } | ||||
| 
 | ||||
|                         let limitEl = this.getLimitElement(limit); | ||||
|                         limitContainerEl.appendChild(limitEl); | ||||
| 
 | ||||
|                     }, this); | ||||
|                 }); | ||||
|             } | ||||
|         }, | ||||
|         showLabels(seriesKey) { | ||||
|             return this.showLimitLineLabels.seriesKey | ||||
|                     && (this.showLimitLineLabels.seriesKey === seriesKey); | ||||
|         }, | ||||
|         getLimitElement(limit) { | ||||
|             let point = { | ||||
|                 left: 0, | ||||
|                 top: this.drawAPI.y(limit.point.y) | ||||
|             }; | ||||
|             let LimitLineClass = Vue.extend(LimitLine); | ||||
|             const component = new LimitLineClass({ | ||||
|                 propsData: { | ||||
|                     point, | ||||
|                     cssClass: limit.cssClass | ||||
|                 } | ||||
|             }); | ||||
|             component.$mount(); | ||||
| 
 | ||||
|             return component.$el; | ||||
|         }, | ||||
|         getLimitLabel(limit) { | ||||
|             let point = { | ||||
|                 left: 0, | ||||
|                 top: this.drawAPI.y(limit.point.y) | ||||
|             }; | ||||
|             let LimitLabelClass = Vue.extend(LimitLabel); | ||||
|             const component = new LimitLabelClass({ | ||||
|                 propsData: { | ||||
|                     limit, | ||||
|                     point | ||||
|                 } | ||||
|             }); | ||||
|             component.$mount(); | ||||
| 
 | ||||
|             return component.$el; | ||||
|         }, | ||||
|         drawAlarmPoints(alarmSet) { | ||||
|             this.drawAPI.drawLimitPoints( | ||||
|                 alarmSet.points, | ||||
| @@ -401,11 +541,12 @@ export default { | ||||
|                 chartElement.series.get('markerShape') | ||||
|             ); | ||||
|         }, | ||||
|         drawLine(chartElement) { | ||||
|         drawLine(chartElement, disconnected) { | ||||
|             this.drawAPI.drawLine( | ||||
|                 chartElement.getBuffer(), | ||||
|                 chartElement.color().asRGBAArray(), | ||||
|                 chartElement.count | ||||
|                 chartElement.count, | ||||
|                 disconnected | ||||
|             ); | ||||
|         }, | ||||
|         drawHighlights() { | ||||
| @@ -97,7 +97,8 @@ export default class PlotSeries extends Model { | ||||
|             markers: true, | ||||
|             markerShape: 'point', | ||||
|             markerSize: 2.0, | ||||
|             alarmMarkers: true | ||||
|             alarmMarkers: true, | ||||
|             limitLines: false | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
| @@ -115,9 +116,16 @@ export default class PlotSeries extends Model { | ||||
|         this.domainObject = options.domainObject; | ||||
|         this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier); | ||||
|         this.limitEvaluator = this.openmct.telemetry.limitEvaluator(options.domainObject); | ||||
|         this.limitDefinition = this.openmct.telemetry.limitDefinition(options.domainObject); | ||||
|         this.limits = this.limitDefinition.limits(); | ||||
|         this.openmct.time.on('bounds', this.updateLimits); | ||||
|         this.on('destroy', this.onDestroy, this); | ||||
|     } | ||||
| 
 | ||||
|     updateLimits(bounds) { | ||||
|         this.emit('limitBounds', bounds); | ||||
|     } | ||||
| 
 | ||||
|     locateOldObject(oldStyleParent) { | ||||
|         return oldStyleParent.useCapability('composition') | ||||
|             .then(function (children) { | ||||
| @@ -166,7 +166,6 @@ export default class YAxisModel extends Model { | ||||
|          * Update yAxis format, values, and label from known series. | ||||
|          */ | ||||
|     updateFromSeries(series) { | ||||
|         this.unset('displayRange'); | ||||
|         const plotModel = this.plot.get('domainObject'); | ||||
|         const label = _.get(plotModel, 'configuration.yAxis.label'); | ||||
|         const sampleSeries = series.first(); | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user