Compare commits

...

38 Commits

Author SHA1 Message Date
Andrew Henry
7ac37a481c Revert Layout.vue 2021-09-03 16:57:50 -07:00
Andrew Henry
d6474045c3 Cleaned up messages 2021-09-03 16:45:40 -07:00
Andrew Henry
23c10d40a8 Cleaned up MCT.js 2021-09-03 16:39:01 -07:00
Andrew Henry
cc37a5f899 No memory leaks when run from browser 2021-09-02 16:17:33 -07:00
Andrew Henry
177dbec0c5 So close. The final references appear to be from native code 2021-08-05 16:32:40 -07:00
Andrew Henry
38952b8eb3 Improvements to memory leak reporter 2021-07-30 11:50:20 -07:00
Andrew Henry
f19c5699a6 Merge branch 'master' into memory-leak-detector 2021-07-30 09:42:33 -07:00
Scott Bell
5c15e53abb Mct4039 (#4057)
Re-implements ImageExportService as ES6 class instead of Angular managed service.

Co-authored-by: John Hill <jchill2.spam@gmail.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2021-07-29 22:05:18 -07:00
John Hill
f58b3881f2 Not everything fits into the two types of issue (#4064)
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2021-07-29 17:17:51 -07:00
Nikhil
071a13b219 [Imagery] Click on image to get a large view (#3770)
* [Imagery] Click on image to get a large view #3582
* Created new viewLargeAction.
* Changes in view registry to add parent element property inside view object.
* Separate class for views and added missing changes for LadTableSet.
* Renamed callBack to onItemClicked.

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2021-07-29 09:19:07 -07:00
Jamie V
ca66898e51 [Plugin] Remote Clock based off Telemetry (#3998)
* Remote-clock plugin
* Added a default clock class that can be extended by other classes
* Updated local clock to use the parent default clock class
* added a period check to make sure its been a certain amount of time before emitting
Co-authored-by: John Hill <jchill2.spam@gmail.com>
2021-07-28 16:46:08 -07:00
John Hill
94c7b2343a Add Enhancement request template (#4063)
* Add Enhancement request template

* Link to github discussions

* Update config.yml
2021-07-27 15:02:30 -07:00
Khalid Adil
c397c336ab [Telemetry Tables] Observe for changes in table configuration (#4053) 2021-07-27 14:08:44 -07:00
John Hill
eea23f2caf Fix typo in template (#4059) 2021-07-27 11:05:02 -07:00
John Hill
6665641c02 Update issue templates (#4058)
Add a bug report directly to the project
2021-07-27 10:38:24 -07:00
Andrew Henry
cc017e879c Prototype memory leak detector 2021-07-27 10:14:09 -07:00
Andrew Henry
93172eb180 Fixed memory leak in import-export plugin 2021-07-27 10:13:43 -07:00
Shefali Joshi
c3ebf52dd2 Prepare for sprint 1.7.6 (#4052) 2021-07-26 14:04:26 -07:00
Henry Hsu
f8f2e7da9b Remove deprecated timeline bundle (#4048) 2021-07-23 13:51:32 -07:00
David Tsay
240f58b2d0 [Telemetry API] wrap limits function return in promise if needed (#4044) 2021-07-20 07:25:43 -07:00
Henry Hsu
7d3baee7b5 URL Params to hide tree and inspector (#3951)
* Add checks and hide panes accordingly, toggle hide params when toggling panes, add params on change event
* add tests

Co-authored-by: Henry Hsu <henry.hsu@nasa.gov>
Co-authored-by: John Hill <jchill2.spam@gmail.com>
2021-07-19 10:01:05 -07:00
Khalid Adil
1f5cb7ca42 Prevent default on click events for conductor delta buttons (#4022)
* Prevent default on click events for conductor delta buttons
2021-07-15 14:46:53 -07:00
David Tsay
4a7ebe326c Plot view policy fix (#3995)
* deny plot view for non-numeric telemetry

* revert plot type for backwards compatibility
2021-07-14 18:12:26 -07:00
Khalid Adil
10da314a4a Add resize event and disable pointer events on iframes on trigger of resize (#4016) 2021-07-14 17:32:45 -07:00
John Hill
b3ceccd7fb temporarily skip test on chrome (#4021) 2021-07-14 15:58:44 -07:00
John Hill
1bde4c9a0c Update all unit test packages, set node engine rules, new circleci workflow, pin to stable (#3957)
* Update all unit test packages, set node engine rules, new circleci workflow

Co-authored-by: unlikelyzero <jchill2@gmail.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2021-07-14 09:26:38 -07:00
Shefali Joshi
4b85360446 1.7.4 stable master (#4015) 2021-07-13 13:59:37 -07:00
Charles Hacskaylo
41b860a547 Fixes for Imagery Snapshotting #3963 (#4001)
* refactored compass structure and code.
* resize image wrapper
* center image properly
* Refactor imagery compass rose as SVG
* Suppress prev/next image arrows from Snapshot

Co-authored-by: Nikhil Mandlik <nikhil.k.mandlik@nasa.gov>
2021-07-09 18:03:11 -07:00
Nikhil
254b3db966 added mock values to compass for example imagery. (#3999) 2021-07-09 15:13:54 -07:00
Shefali Joshi
cbb3f32d1e 1.7.4 into master (#3985)
catching any errors from a user canceling from dialog (#3968)
Fix navigation errors (#3970)
Only add listeners to observables on creation
Do not double destroy mutable objects
Disallow pause and play in time strip view for plots. (#3972)
Update version

Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2021-06-29 09:40:30 -07:00
Henry Hsu
e3bf72e77f New time conductor popups should be closed by hitting ESC key (#3978) 2021-06-29 09:03:58 -07:00
Charles Hacskaylo
0b63b782cf Image thumbs autoscroll resumption functionality (#3892)
* add logs for testing

* check autoScroll value

* Add auto scroll button at thumbnails

* add auto scroll button functionality

* turn on auto scroll whenever scroll bar is not at the right end

* check if scroll right when the page load

* update scroll to right condition

* remove resetScroll method, refactor scrollToRight and some cleaning

* check initial status

* remove logs

* Styling for imagery thumbs 'autoscroll resume' button

- Refined look and feel of the approach;
- CSS classes now assigned at the wrapper level;
- Markup changed to allow wrapping of scroll area and button;
- TODO: prevent a drag resize of the main view area from forcing
autoscroll to pause;

* Add tests for auto scroll

* add resize observer for thumb wrapper

* reset scroll bar position when window is resized

* Revert "upgrade to webpack5 (#3871)" (#3907) (#3908)

This reverts commit e1e0eeac56.

Co-authored-by: Nikhil <nikhil.k.mandlik@nasa.gov>

* Check for getlimits api (#3910)

* add debounce and resizing observer to thumbwrapper

* handling resizing window and auto scroll

* 1.clean up comments and logs 2.make variable names more descriptive 3.add scroll to right for play button

* fix eslint formate issue

* update class name in test

* fix eslint format error

* remove a couple variables that were created but not used.

Co-authored-by: Henry Hsu <henry.hsu@nasa.gov>
Co-authored-by: Henry Hsu <hhsu0219@gmail.com>
Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
Co-authored-by: Nikhil <nikhil.k.mandlik@nasa.gov>
Co-authored-by: Jamie Vigliotta <jamie.j.vigliotta@nasa.gov>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2021-06-29 07:52:27 -07:00
Charles Hacskaylo
da39fd0c70 Overlay fixes and improvements (#3901)
* Overlay-related fixes
- Prevent navigation when a folder's grid or list view is displayed in
an overlay;

* Overlay-related fixes
- Get rid of theme-based special overlay coloring that was making the
overlay hard to use;
- Add margin to `l-overlay-large` to make it clearer that user is in an
overlay when interacting with that view;
- Refinements to colors and layout in About screens;
2021-06-28 14:35:35 -07:00
Charles Hacskaylo
96dd581a67 Timestrip plan Inspection (#3863)
* Stub in static HTML for Timestrip Activity Inspection
- Added static markup with placeholder values and display logic;
- Refined approach for Links;
* Refactor duration formatting
* Display activity name when it's available
* Don't use indices for keys
* Don't show properties with no labels
2021-06-28 10:04:54 -07:00
Jamie V
2a1e322230 adding link to discussions in readme to encourage users to showcase their use of Open MCT (#3933)
Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
2021-06-22 09:34:36 -07:00
Shefali Joshi
300b98bd54 Disallow pan, zoom and pause/play controls in time strip view (#3936)
* Disallow pan and zoom when in time strip view

* Disable plot controls in time strip view
2021-06-22 09:25:12 -07:00
Andrew Henry
c946609d13 Added LGTM code quality badge (#3960)
We score an A, we should flaunt it! (We should also aim for A+).
2021-06-22 09:10:08 -07:00
David Tsay
7ca559fbe4 [Styles] add unit tests (#3557)
* unit tests for inspector styles feature
* add mock capability for local storage

Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2021-06-22 06:50:49 -07:00
163 changed files with 4112 additions and 2092 deletions

View File

@@ -1,36 +1,69 @@
version: 2
jobs:
build:
version: 2.1
executors:
linux:
docker:
- image: circleci/node:13-browsers
environment:
CHROME_BIN: "/usr/bin/google-chrome"
steps:
- checkout
- run:
name: Update npm
command: 'sudo npm install -g npm@latest'
- restore_cache:
key: dependency-cache-{{ checksum "package.json" }}
- run:
name: Installing dependencies (npm install)
command: npm install
- save_cache:
key: dependency-cache-{{ checksum "package.json" }}
paths:
- node_modules
- run:
name: npm run test:coverage
command: npm run test:coverage
- run:
name: npm run lint
command: npm run lint
- store_artifacts:
path: dist
prefix: dist
workflows:
version: 2
- image: cimg/base:stable
orbs:
node: circleci/node@4.5.1
browser-tools: circleci/browser-tools@1.1.3
jobs:
test:
parameters:
node-version:
type: string
browser:
type: string
always-pass:
type: boolean
executor: linux
steps:
- checkout
- restore_cache:
key: deps-{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }}
- node/install:
node-version: << parameters.node-version >>
- node/install-packages:
override-ci-command: npm install
- when: # Just to save time until caching saves the browser bin
condition:
equal: [ "FirefoxESR", <<parameters.browser>> ]
steps:
- browser-tools/install-firefox:
version: "78.11.0esr" #https://archive.mozilla.org/pub/firefox/releases/
- when: # Just to save time until caching saves the browser bin
condition:
equal: [ "ChromeHeadless", <<parameters.browser>> ]
steps:
- browser-tools/install-chrome:
replace-existing: false
- save_cache:
key: deps-{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }}
paths:
- ~/.npm
- ~/.cache
- node_modules
- run: npm run test:coverage -- --browsers=<<parameters.browser>> || <<parameters.always-pass>>
- store_test_results:
path: dist/reports/tests/
- store_artifacts:
path: dist/reports/
workflows:
matrix-tests:
jobs:
- build
- test:
name: node10-chrome
node-version: lts/dubnium
browser: ChromeHeadless
always-pass: false
- test:
name: node12-firefoxESR
node-version: lts/erbium
browser: FirefoxESR
always-pass: true
- test:
name: node14-chrome
node-version: lts/fermium
browser: ChromeHeadless
always-pass: true

View File

@@ -1,11 +1,12 @@
<!--- This is for filing bugs. If you have a general question, please -->
<!--- visit https://github.com/nasa/openmct/discussions -->
---
name: Bug Report
name: Bug report
about: File a Bug !
title: ''
labels: type:bug
assignees: ''
---
<!--- Focus on user impact in the title. Use the Summary Field to -->
<!--- describe the problem technically. -->
@@ -35,7 +36,7 @@ about: File a Bug !
#### Environment
* Open MCT Version: <!--- date of build, version, or SHA -->
* Deployment Type: <!--- npm dev? VIPER Dev? openmct-yams? -->
* Deployment Type: <!--- npm dev? VIPER Dev? openmct-yamcs? -->
* OS:
* Browser:

View File

@@ -1 +1,5 @@
blank_issues_enabled: false
blank_issues_enabled: true
contact_links:
- name: Discussions
url: https://github.com/nasa/openmct/discussions
about: Got a question?

View File

@@ -0,0 +1,20 @@
---
name: Enhancement request
about: Suggest an enhancement or new improvement for this project
title: ''
labels: type:enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -13,6 +13,8 @@ jobs:
- uses: actions/checkout@v2
with:
ref: ${{ github.event.inputs.version }}
- uses: actions/setup-node@v1
- uses: actions/setup-node@v2
with:
node-version: '14'
- run: npm install && npm install -g @lhci/cli #Don't want to include this in our deps
- run: lhci autorun

View File

@@ -1,9 +1,11 @@
# Open MCT [![license](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](http://www.apache.org/licenses/LICENSE-2.0)
# Open MCT [![license](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](http://www.apache.org/licenses/LICENSE-2.0) [![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/nasa/openmct.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/nasa/openmct/context:javascript)
Open MCT (Open Mission Control Technologies) is a next-generation mission control framework for visualization of data on desktop and mobile devices. It is developed at NASA's Ames Research Center, and is being used by NASA for data analysis of spacecraft missions, as well as planning and operation of experimental rover systems. As a generalizable and open source framework, Open MCT could be used as the basis for building applications for planning, operation, and analysis of any systems producing telemetry data.
Please visit our [Official Site](https://nasa.github.io/openmct/) and [Getting Started Guide](https://nasa.github.io/openmct/getting-started/)
Once you've created something amazing with Open MCT, showcase your work in our GitHub Discussions [Show and Tell](https://github.com/nasa/openmct/discussions/categories/show-and-tell) section. We love seeing unique and wonderful implementations of Open MCT!
## See Open MCT in Action
Try Open MCT now with our [live demo](https://openmct-demo.herokuapp.com/).

10
app.js
View File

@@ -42,11 +42,11 @@ app.use('/proxyUrl', function proxyRequest(req, res, next) {
const webpack = require('webpack');
const webpackConfig = require('./webpack.config.js');
webpackConfig.plugins.push(new webpack.HotModuleReplacementPlugin());
webpackConfig.plugins.push(function() { this.plugin('watch-run', function(watching, callback) { console.log('Begin compile at ' + new Date()); callback(); }) });
//webpackConfig.plugins.push(new webpack.HotModuleReplacementPlugin());
//webpackConfig.plugins.push(function() { this.plugin('watch-run', function(watching, callback) { console.log('Begin compile at ' + new Date()); callback(); }) });
webpackConfig.entry.openmct = [
'webpack-hot-middleware/client?reload=true',
// 'webpack-hot-middleware/client?reload=true',
webpackConfig.entry.openmct
];
@@ -60,12 +60,12 @@ app.use(require('webpack-dev-middleware')(
}
));
app.use(require('webpack-hot-middleware')(
/*app.use(require('webpack-hot-middleware')(
compiler,
{
}
));
));*/
// Expose index.html for development users.
app.get('/', function (req, res) {

View File

@@ -41,11 +41,6 @@ define([
"$scope"
]
}
],
"routes": [
{
"templateUrl": "templates/exampleForm.html"
}
]
}
}

View File

@@ -49,6 +49,10 @@ define([
];
const IMAGE_DELAY = 20000;
function getCompassValues(min, max) {
return min + Math.random() * (max - min);
}
function pointForTimestamp(timestamp, name) {
const url = IMAGE_SAMPLES[Math.floor(timestamp / IMAGE_DELAY) % IMAGE_SAMPLES.length];
const urlItems = url.split('/');
@@ -59,6 +63,9 @@ define([
utc: Math.floor(timestamp / IMAGE_DELAY) * IMAGE_DELAY,
local: Math.floor(timestamp / IMAGE_DELAY) * IMAGE_DELAY,
url,
sunOrientation: getCompassValues(0, 360),
cameraPan: getCompassValues(0, 360),
heading: getCompassValues(0, 360),
imageDownloadName
};
}

View File

@@ -1,4 +1,4 @@
import Vue from 'Vue';
import Vue from 'vue';
import HelloWorld from './HelloWorld.vue';
function SimpleVuePlugin() {

View File

@@ -66,134 +66,143 @@
<body>
</body>
<script>
const THIRTY_SECONDS = 30 * 1000;
const ONE_MINUTE = THIRTY_SECONDS * 2;
const FIVE_MINUTES = ONE_MINUTE * 5;
const FIFTEEN_MINUTES = FIVE_MINUTES * 3;
const THIRTY_MINUTES = FIFTEEN_MINUTES * 2;
const ONE_HOUR = THIRTY_MINUTES * 2;
const TWO_HOURS = ONE_HOUR * 2;
const ONE_DAY = ONE_HOUR * 24;
(function() {
const THIRTY_SECONDS = 30 * 1000;
const ONE_MINUTE = THIRTY_SECONDS * 2;
const FIVE_MINUTES = ONE_MINUTE * 5;
const FIFTEEN_MINUTES = FIVE_MINUTES * 3;
const THIRTY_MINUTES = FIFTEEN_MINUTES * 2;
const ONE_HOUR = THIRTY_MINUTES * 2;
const TWO_HOURS = ONE_HOUR * 2;
const ONE_DAY = ONE_HOUR * 24;
[
'example/eventGenerator'
].forEach(
openmct.legacyRegistry.enable.bind(openmct.legacyRegistry)
);
let app = new openmct();
openmct.install(openmct.plugins.LocalStorage());
openmct.install(openmct.plugins.Espresso());
openmct.install(openmct.plugins.MyItems());
openmct.install(openmct.plugins.Generator());
openmct.install(openmct.plugins.ExampleImagery());
openmct.install(openmct.plugins.PlanLayout());
openmct.install(openmct.plugins.Timeline());
openmct.install(openmct.plugins.UTCTimeSystem());
openmct.install(openmct.plugins.AutoflowView({
type: "telemetry.panel"
}));
openmct.install(openmct.plugins.DisplayLayout({
showAsView: ['summary-widget', 'example.imagery']
}));
openmct.install(openmct.plugins.Conductor({
menuOptions: [
{
name: "Fixed",
timeSystem: 'utc',
bounds: {
start: Date.now() - THIRTY_MINUTES,
end: Date.now()
[
'example/eventGenerator'
].forEach(
app.legacyRegistry.enable.bind(app.legacyRegistry)
);
app.install(app.plugins.LocalStorage());
app.install(app.plugins.Espresso());
app.install(app.plugins.MyItems());
//app.install(app.plugins.Generator());
app.install(app.plugins.ExampleImagery());
app.install(app.plugins.PlanLayout());
app.install(app.plugins.Timeline());
//app.install(app.plugins.Hyperlink());
app.install(app.plugins.UTCTimeSystem());
app.install(app.plugins.AutoflowView({
type: "telemetry.panel"
}));
app.install(app.plugins.DisplayLayout({
showAsView: ['summary-widget', 'example.imagery']
}));
app.install(app.plugins.Conductor({
menuOptions: [
{
name: "Fixed",
timeSystem: 'utc',
bounds: {
start: Date.now() - THIRTY_MINUTES,
end: Date.now()
},
// commonly used bounds can be stored in history
// bounds (start and end) can accept either a milliseconds number
// or a callback function returning a milliseconds number
// a function is useful for invoking Date.now() at exact moment of preset selection
presets: [
{
label: 'Last Day',
bounds: {
start: () => Date.now() - ONE_DAY,
end: () => Date.now()
}
},
{
label: 'Last 2 hours',
bounds: {
start: () => Date.now() - TWO_HOURS,
end: () => Date.now()
}
},
{
label: 'Last hour',
bounds: {
start: () => Date.now() - ONE_HOUR,
end: () => Date.now()
}
}
],
// maximum recent bounds to retain in conductor history
records: 10
// maximum duration between start and end bounds
// for utc-based time systems this is in milliseconds
// limit: ONE_DAY
},
// commonly used bounds can be stored in history
// bounds (start and end) can accept either a milliseconds number
// or a callback function returning a milliseconds number
// a function is useful for invoking Date.now() at exact moment of preset selection
presets: [
{
label: 'Last Day',
bounds: {
start: () => Date.now() - ONE_DAY,
end: () => Date.now()
}
{
name: "Realtime",
timeSystem: 'utc',
clock: 'local',
clockOffsets: {
start: - THIRTY_MINUTES,
end: THIRTY_SECONDS
},
{
label: 'Last 2 hours',
bounds: {
start: () => Date.now() - TWO_HOURS,
end: () => Date.now()
presets: [
{
label: '1 Hour',
bounds: {
start: - ONE_HOUR,
end: THIRTY_SECONDS
}
},
{
label: '30 Minutes',
bounds: {
start: - THIRTY_MINUTES,
end: THIRTY_SECONDS
}
},
{
label: '15 Minutes',
bounds: {
start: - FIFTEEN_MINUTES,
end: THIRTY_SECONDS
}
},
{
label: '5 Minutes',
bounds: {
start: - FIVE_MINUTES,
end: THIRTY_SECONDS
}
},
{
label: '1 Minute',
bounds: {
start: - ONE_MINUTE,
end: THIRTY_SECONDS
}
}
},
{
label: 'Last hour',
bounds: {
start: () => Date.now() - ONE_HOUR,
end: () => Date.now()
}
}
],
// maximum recent bounds to retain in conductor history
records: 10
// maximum duration between start and end bounds
// for utc-based time systems this is in milliseconds
// limit: ONE_DAY
},
{
name: "Realtime",
timeSystem: 'utc',
clock: 'local',
clockOffsets: {
start: - THIRTY_MINUTES,
end: THIRTY_SECONDS
},
presets: [
{
label: '1 Hour',
bounds: {
start: - ONE_HOUR,
end: THIRTY_SECONDS
}
},
{
label: '30 Minutes',
bounds: {
start: - THIRTY_MINUTES,
end: THIRTY_SECONDS
}
},
{
label: '15 Minutes',
bounds: {
start: - FIFTEEN_MINUTES,
end: THIRTY_SECONDS
}
},
{
label: '5 Minutes',
bounds: {
start: - FIVE_MINUTES,
end: THIRTY_SECONDS
}
},
{
label: '1 Minute',
bounds: {
start: - ONE_MINUTE,
end: THIRTY_SECONDS
}
}
]
}
]
}));
openmct.install(openmct.plugins.SummaryWidget());
openmct.install(openmct.plugins.Notebook());
openmct.install(openmct.plugins.LADTable());
openmct.install(openmct.plugins.Filters(['table', 'telemetry.plot.overlay']));
openmct.install(openmct.plugins.ObjectMigration());
openmct.install(openmct.plugins.ClearData(
['table', 'telemetry.plot.overlay', 'telemetry.plot.stacked'],
{indicator: true}
));
openmct.start();
]
}
]
}));
app.install(app.plugins.SummaryWidget());
app.install(app.plugins.Notebook());
app.install(app.plugins.LADTable());
app.install(app.plugins.Filters(['table', 'telemetry.plot.overlay']));
app.install(app.plugins.ObjectMigration());
app.install(app.plugins.ClearData(
['table', 'telemetry.plot.overlay', 'telemetry.plot.stacked'],
{indicator: true}
));
app.start();
setTimeout(() => {
app.destroy();
}, 5000)
})();
</script>
</html>

View File

@@ -1,3 +1,7 @@
const testsContext = require.context('.', true, /\/(src|platform)\/.*Spec.js$/);
import MemoryLeaksReporter from "./src/utils/testing/MemoryLeaksReporter";
jasmine.getEnv().addReporter(new MemoryLeaksReporter());
const testsContext = require.context('.', true, /\/(src|platform)\/.*Spec.js$/);
testsContext.keys().forEach(testsContext);
console.error("Done running tests?");

View File

@@ -23,9 +23,9 @@
/*global module,process*/
const devMode = process.env.NODE_ENV !== 'production';
const browsers = [process.env.NODE_ENV === 'debug' ? 'ChromeDebugging' : 'FirefoxHeadless'];
const browsers = [process.env.NODE_ENV === 'debug' ? 'ChromeDebugging' : 'ChromeMemory'];
const coverageEnabled = process.env.COVERAGE === 'true';
const reporters = ['progress', 'html'];
const reporters = ['progress', 'html', 'junit'];
if (coverageEnabled) {
reporters.push('coverage-istanbul');
@@ -59,15 +59,24 @@ module.exports = (config) => {
browsers: browsers,
client: {
jasmine: {
random: false
random: false,
timeoutInterval: 30000
}
},
customLaunchers: {
ChromeDebugging: {
ChromeMemory: {
base: 'ChromeHeadless',
flags: ['--enable-precise-memory-info', '--js-flags="--expose-gc --nocrankshaft --noopt"']
}
/* ChromeDebugging: {
base: 'Chrome',
flags: ['--remote-debugging-port=9222'],
debug: true
}
},
FirefoxESR: {
base: 'FirefoxHeadless',
name: 'FirefoxESR'
}*/
},
colors: true,
logLevel: config.LOG_INFO,
@@ -78,12 +87,21 @@ module.exports = (config) => {
preserveDescribeNesting: true,
foldAll: false
},
browserConsoleLogOptions: { level: "error", format: "%b %T: %m", terminal: true },
junitReporter: {
outputDir: "dist/reports/tests",
outputFile: "test-results.xml",
useBrowserName: false
},
browserConsoleLogOptions: {
level: "error",
format: "%b %T: %m",
terminal: true
},
coverageIstanbulReporter: {
fixWebpackSourcePaths: true,
dir: process.env.CIRCLE_ARTIFACTS ?
process.env.CIRCLE_ARTIFACTS + '/coverage' :
"dist/reports/coverage",
dir: process.env.CIRCLE_ARTIFACTS
? process.env.CIRCLE_ARTIFACTS + '/coverage'
: "dist/reports/coverage",
reports: ['html', 'lcovonly', 'text-summary'],
thresholds: {
global: {

View File

@@ -32,6 +32,6 @@ if (document.currentScript) {
const MCT = require('./src/MCT');
const openmct = new MCT();
//const openmct = new MCT();
module.exports = openmct;
module.exports = MCT;

View File

@@ -1,9 +1,12 @@
{
"name": "openmct",
"version": "1.7.4-SNAPSHOT",
"version": "1.7.6-SNAPSHOT",
"description": "The Open MCT core platform",
"dependencies": {},
"dependencies": {
"http-server": "^0.12.3"
},
"devDependencies": {
"@webpack-cli/serve": "^1.5.1",
"angular": ">=1.8.0",
"angular-route": "1.4.14",
"babel-eslint": "10.0.3",
@@ -34,20 +37,21 @@
"git-rev-sync": "^1.4.0",
"glob": ">= 3.0.0",
"html-loader": "^0.5.5",
"html2canvas": "^1.0.0-alpha.12",
"html2canvas": "^1.0.0-rc.7",
"imports-loader": "^0.8.0",
"istanbul-instrumenter-loader": "^3.0.1",
"jasmine-core": "^3.1.0",
"jasmine-core": "^3.7.1",
"jsdoc": "^3.3.2",
"karma": "5.1.1",
"karma": "6.3.4",
"karma-chrome-launcher": "3.1.0",
"karma-cli": "2.0.0",
"karma-coverage": "2.0.3",
"karma-coverage-istanbul-reporter": "3.0.3",
"karma-firefox-launcher": "1.3.0",
"karma-firefox-launcher": "2.1.0",
"karma-html-reporter": "0.2.7",
"karma-jasmine": "3.3.1",
"karma-sourcemap-loader": "0.3.7",
"karma-jasmine": "4.0.1",
"karma-junit-reporter": "2.0.1",
"karma-sourcemap-loader": "0.3.8",
"karma-webpack": "4.0.2",
"location-bar": "^3.0.1",
"lodash": "^4.17.12",
@@ -74,6 +78,7 @@
"webpack": "^4.16.2",
"webpack-cli": "^3.1.0",
"webpack-dev-middleware": "^3.1.3",
"webpack-dev-server": "^3.11.2",
"webpack-hot-middleware": "^2.22.3",
"zepto": "^1.2.0"
},
@@ -89,6 +94,7 @@
"test": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run",
"test:debug": "cross-env NODE_ENV=debug karma start --no-single-run",
"test:coverage": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" COVERAGE=true karma start --single-run",
"test:coverage:firefox": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run --browsers=FirefoxHeadless",
"test:watch": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --no-single-run",
"verify": "concurrently 'npm:test' 'npm:lint'",
"jsdoc": "jsdoc -c jsdoc.json -R API.md -r -d dist/docs/api",
@@ -100,6 +106,9 @@
"type": "git",
"url": "https://github.com/nasa/openmct.git"
},
"engines": {
"node": ">=10.10.2 <16.0.0"
},
"author": "",
"license": "Apache-2.0",
"private": true

View File

@@ -164,16 +164,6 @@ define([
"license": "license-apache",
"link": "http://logging.apache.org/log4net/license.html"
}
],
"routes": [
{
"when": "/licenses",
"template": licensesTemplate
},
{
"when": "/licenses-md",
"template": licensesExportMdTemplate
}
]
}
}

View File

@@ -50,8 +50,6 @@ define([
name: "platform/commonUI/browse",
definition: {
"extensions": {
"routes": [
],
"constants": [
{
"key": "DEFAULT_PATH",

View File

@@ -40,8 +40,8 @@ define(
this.checks = [];
this.$window = $window;
this.oldUnload = $window.onbeforeunload;
$window.onbeforeunload = this.onBeforeUnload.bind(this);
/* this.oldUnload = $window.onbeforeunload;
$window.onbeforeunload = this.onBeforeUnload.bind(this);*/
}
/**

View File

@@ -89,7 +89,8 @@ define([
"implementation": TickerService,
"depends": [
"$timeout",
"now"
"now",
"$rootScope"
]
},
{
@@ -181,7 +182,7 @@ define([
],
"category": "contextual",
"name": "Stop",
"cssClass": "icon-box",
"cssClass": "icon-box-round-corners",
"priority": "preferred"
}
],

View File

@@ -32,8 +32,13 @@ define(
* @param $timeout Angular's $timeout
* @param {Function} now function to provide the current time in ms
*/
function TickerService($timeout, now) {
function TickerService($timeout, now, $rootScope) {
var self = this;
var timeoutId;
$rootScope.$on('$destroy', function () {
$timeout.cancel(timeoutId);
});
function tick() {
var timestamp = now(),
@@ -48,7 +53,7 @@ define(
}
// Try to update at exactly the next second
$timeout(tick, 1000 - millis, true);
timeoutId = $timeout(tick, 1000 - millis, true);
}
tick();

View File

@@ -101,7 +101,7 @@ define(
name: "Pause"
});
mockStop.getMetadata.and.returnValue({
cssClass: "icon-box",
cssClass: "icon-box-round-corners",
name: "Stop"
});
mockScope.domainObject = mockDomainObject;

View File

@@ -1,70 +0,0 @@
This bundle provides the Timeline domain object type, as well
as other associated domain object types and relevant views.
# Implementation notes
## Model Properties
The properties below record properties relevant to using and
understanding timelines based on their JSON representation.
Additional common properties, such as `modified`
or `persisted` timestamps, may also be present.
### Timeline Model
A timeline's model looks like:
```
{
"type": "timeline",
"start": {
"timestamp": <number> (milliseconds since epoch),
"epoch": <string> (currently, always "SET")
},
"capacity": <number> (optional; battery capacity in watt-hours)
"composition": <string[]> (array of identifiers for contained objects)
}
```
The identifiers in a timeline's `composition` field should refer to
other Timeline objects, or to Activity objects.
### Activity Model
An activity's model looks like:
```
{
"type": "activity",
"start": {
"timestamp": <number> (milliseconds since epoch),
"epoch": <string> (currently, always "SET")
},
"duration": {
"timestamp": <number> (duration of this activity, in milliseconds)
"epoch": "SET" (this is ignored)
},
"relationships": {
"modes": <string[]> (array of applicable Activity Mode ids)
},
"link": <string> (optional; URL linking to associated external resource)
"composition": <string[]> (array of identifiers for contained objects)
}
```
The identifiers in a timeline's `composition` field should only refer to
other Activity objects.
### Activity Mode Model
An activity mode's model looks like:
```
{
"type": "mode",
"resources": {
"comms": <number> (communications utilization, in Kbps)
"power": <number> (power utilization, in watts)
}
}
```

View File

@@ -1,10 +0,0 @@
<div>
Timeline, Activity and Activity Mode objects have been deprecated and will no longer be supported.
</div>
<div>
Please open an issue in the
<a href="https://github.com/nasa/openmct/issues" target="_blank">
Open MCT Issue tracker
</a>
if you have any questions about the timeline plugin.
</div>

View File

@@ -58,7 +58,7 @@ define([
) {
var $http = this.$http,
$log = this.$log,
app = angular.module(Constants.MODULE_NAME, ["ngRoute"]),
app = angular.module(Constants.MODULE_NAME, []),
loader = new BundleLoader($http, $log, openmct.legacyRegistry),
resolver = new BundleResolver(
new ExtensionResolver(

View File

@@ -28,8 +28,7 @@
define(
[
'./FrameworkLayer',
'angular',
'angular-route'
'angular'
],
function (
FrameworkLayer,

View File

@@ -57,7 +57,6 @@ define(
$log = this.$log;
return new Promise(function (resolve, reject) {
$log.info("Bootstrapping application " + (app || {}).name);
angular.element(document).ready(function () {
angular.bootstrap(document, [app.name], { strictDi: true });
resolve(angular);

View File

@@ -138,34 +138,6 @@ define(
}
}
// Custom registration function for extensions of category "route"
function registerRoute(extension) {
var app = this.app,
$log = this.$log,
route = Object.create(extension);
// Adjust path for bundle
if (route.templateUrl) {
route.templateUrl = [
route.bundle.path,
route.bundle.resources,
route.templateUrl
].join(Constants.SEPARATOR);
}
// Log the registration
$log.info("Registering route: " + (route.key || route.when));
// Register the route with Angular
app.config(['$routeProvider', function ($routeProvider) {
if (route.when) {
$routeProvider.when(route.when, route);
} else {
$routeProvider.otherwise(route);
}
}]);
}
// Handle service compositing
function registerComponents(components) {
var app = this.app,
@@ -194,13 +166,6 @@ define(
CustomRegistrars.prototype.constants =
mapUpon(registerConstant);
/**
* Register Angular routes.
* @param {Array} extensions the resolved extensions
*/
CustomRegistrars.prototype.routes =
mapUpon(registerRoute);
/**
* Register Angular directives.
* @param {Array} extensions the resolved extensions

View File

@@ -57,7 +57,6 @@ define(
expect(customRegistrars.directives).toBeTruthy();
expect(customRegistrars.controllers).toBeTruthy();
expect(customRegistrars.services).toBeTruthy();
expect(customRegistrars.routes).toBeTruthy();
expect(customRegistrars.constants).toBeTruthy();
expect(customRegistrars.runs).toBeTruthy();
});
@@ -139,47 +138,6 @@ define(
expect(mockLog.warn.calls.count()).toEqual(0);
});
it("allows routes to be registered", function () {
var mockRouteProvider = jasmine.createSpyObj(
"$routeProvider",
["when", "otherwise"]
),
bundle = {
path: "test/bundle",
resources: "res"
},
routes = [
{
when: "foo",
templateUrl: "templates/test.html",
bundle: bundle
},
{
templateUrl: "templates/default.html",
bundle: bundle
}
];
customRegistrars.routes(routes);
// Give it the route provider based on its config call
mockApp.config.calls.all().forEach(function (call) {
// Invoke the provided callback
call.args[0][1](mockRouteProvider);
});
// The "when" clause should have been mapped to the when method...
expect(mockRouteProvider.when).toHaveBeenCalled();
expect(mockRouteProvider.when.calls.mostRecent().args[0]).toEqual("foo");
expect(mockRouteProvider.when.calls.mostRecent().args[1].templateUrl)
.toEqual("test/bundle/res/templates/test.html");
// ...while the other should have been treated as a default route
expect(mockRouteProvider.otherwise).toHaveBeenCalled();
expect(mockRouteProvider.otherwise.calls.mostRecent().args[0].templateUrl)
.toEqual("test/bundle/res/templates/default.html");
});
it("accepts components for service compositing", function () {
// Most relevant code will be exercised in service compositor spec
expect(customRegistrars.components).toBeTruthy();

View File

@@ -30,8 +30,8 @@ define([
return function ImportExportPlugin() {
return function (openmct) {
ExportAsJSONAction.appliesTo = function (context) {
return openmct.$injector.get('policyService')
ExportAsJSONAction.prototype.appliesTo = function (context) {
return this.openmct.$injector.get('policyService')
.allow("creation", context.domainObject.getCapability("type")
);
};

View File

@@ -110,8 +110,15 @@ define([
worker = workerService.run('bareBonesSearchWorker');
}
worker.addEventListener('message', function (messageEvent) {
function handleWorkerMessage(messageEvent) {
provider.onWorkerMessage(messageEvent);
}
worker.addEventListener('message', handleWorkerMessage);
this.openmct.once('destroy', () => {
worker.removeEventListener('message', handleWorkerMessage);
worker.terminate();
});
return worker;

View File

@@ -31,7 +31,6 @@ define([
'objectUtils',
'./plugins/plugins',
'./adapter/indicators/legacy-indicators-plugin',
'./plugins/buildInfo/plugin',
'./ui/registries/ViewRegistry',
'./plugins/imagery/plugin',
'./ui/registries/InspectorViewRegistry',
@@ -40,6 +39,7 @@ define([
'./ui/router/Browse',
'../platform/framework/src/Main',
'./ui/layout/Layout.vue',
'./ui/inspector/styles/StylesManager',
'../platform/core/src/objects/DomainObjectImpl',
'../platform/core/src/capabilities/ContextualDomainObject',
'./ui/preview/plugin',
@@ -60,7 +60,6 @@ define([
objectUtils,
plugins,
LegacyIndicatorsPlugin,
buildInfoPlugin,
ViewRegistry,
ImageryPlugin,
InspectorViewRegistry,
@@ -69,6 +68,7 @@ define([
Browse,
Main,
Layout,
stylesManager,
DomainObjectImpl,
ContextualDomainObject,
PreviewPlugin,
@@ -262,7 +262,7 @@ define([
// Plugins that are installed by default
this.install(this.plugins.Plot());
this.install(this.plugins.TelemetryTable());
this.install(this.plugins.TelemetryTable.default());
this.install(PreviewPlugin.default());
this.install(LegacyIndicatorsPlugin());
this.install(LicensesPlugin.default());
@@ -283,6 +283,7 @@ define([
this.install(this.plugins.NotificationIndicator());
this.install(this.plugins.NewFolderAction());
this.install(this.plugins.ViewDatumAction());
this.install(this.plugins.ViewLargeAction());
this.install(this.plugins.ObjectInterceptors());
this.install(this.plugins.NonEditableFolder());
}
@@ -373,6 +374,7 @@ define([
* MCT; if undefined, MCT will be run in the body of the document
*/
MCT.prototype.start = function (domElement = document.body, isHeadlessMode = false) {
if (this.types.get('layout') === undefined) {
this.install(this.plugins.DisplayLayout({
showAsView: ['summary-widget']
@@ -431,6 +433,9 @@ define([
domElement.appendChild(appLayout.$mount().$el);
this.layout = appLayout.$refs.layout;
this.once('destroy', () => {
this.layout.$destroy();
});
Browse(this);
}
@@ -458,7 +463,31 @@ define([
MCT.prototype.destroy = function () {
this.emit('destroy');
this.router.destroy();
this.removeAllListeners();
if (this.$injector) {
this.$injector.get('$rootScope').$destroy();
this.$injector = null;
}
if (this.$angular) {
this.$angular.element(this.element).off().removeData();
this.$angular.element(this.element).empty();
this.$angular = null;
}
this.overlays.destroy();
if (this.element) {
this.element.remove();
}
stylesManager.default.removeAllListeners();
window.angular = null;
window.openmct = null;
Object.keys(require.cache).forEach(key => delete require.cache[key]);
};
MCT.prototype.plugins = plugins;

View File

@@ -26,80 +26,93 @@ define([
'utils/testing'
], function (plugins, legacyRegistry, testUtils) {
describe("MCT", function () {
let openmct;
let mockPlugin;
let mockPlugin2;
let mockListener;
let oldBundles;
beforeEach(function () {
mockPlugin = jasmine.createSpy('plugin');
mockPlugin2 = jasmine.createSpy('plugin2');
mockListener = jasmine.createSpy('listener');
oldBundles = legacyRegistry.list();
this.mockPlugin = jasmine.createSpy('plugin');
this.mockPlugin2 = jasmine.createSpy('plugin2');
this.mockListener = jasmine.createSpy('listener');
this.oldBundles = legacyRegistry.list();
openmct = testUtils.createOpenMct();
this.openmct = testUtils.createOpenMct();
openmct.install(mockPlugin);
openmct.install(mockPlugin2);
openmct.on('start', mockListener);
this.openmct.install(this.mockPlugin);
this.openmct.install(this.mockPlugin2);
this.openmct.once('start', this.mockListener);
});
// Clean up the dirty singleton.
afterEach(function () {
legacyRegistry.list().forEach(function (bundle) {
if (oldBundles.indexOf(bundle) === -1) {
if (this.oldBundles.indexOf(bundle) === -1) {
legacyRegistry.delete(bundle);
}
});
return testUtils.resetApplicationState(openmct);
return testUtils.resetApplicationState(this.openmct)
.then(() => {
this.openmct = null;
this.mockPlugin = null;
this.mockPlugin2 = null;
this.mockListener = null;
this.oldBundles = null;
console.error('Done clearing test variables');
});
});
it("exposes plugins", function () {
expect(openmct.plugins).toEqual(plugins);
expect(this.openmct.plugins).toEqual(plugins);
});
it("does not issue a start event before started", function () {
expect(mockListener).not.toHaveBeenCalled();
expect(this.mockListener).not.toHaveBeenCalled();
});
describe("start", function () {
let appHolder;
beforeEach(function (done) {
appHolder = document.createElement("div");
openmct.on('start', done);
openmct.start(appHolder);
beforeEach(function () {
this.appHolder = document.createElement("div");
const startPromise = new Promise(resolve => {
this.openmct.once('start', resolve);
});
this.openmct.startHeadless();
return startPromise;
});
afterEach(() => {
//this.appHolder.remove();
this.appHolder = null;
});
it("calls plugins for configuration", function () {
expect(mockPlugin).toHaveBeenCalledWith(openmct);
expect(mockPlugin2).toHaveBeenCalledWith(openmct);
expect(this.mockPlugin).toHaveBeenCalledWith(this.openmct);
expect(this.mockPlugin2).toHaveBeenCalledWith(this.openmct);
});
it("emits a start event", function () {
expect(mockListener).toHaveBeenCalled();
expect(this.mockListener).toHaveBeenCalled();
});
it("Renders the application into the provided container element", function () {
let openMctShellElements = appHolder.querySelectorAll('div.l-shell');
let openMctShellElements = this.appHolder.querySelectorAll('div.l-shell');
expect(openMctShellElements.length).toBe(1);
});
});
describe("startHeadless", function () {
beforeEach(function (done) {
openmct.on('start', done);
openmct.startHeadless();
console.error('HERE');
this.openmct.on('start', done);
this.openmct.startHeadless();
});
it("calls plugins for configuration", function () {
expect(mockPlugin).toHaveBeenCalledWith(openmct);
expect(mockPlugin2).toHaveBeenCalledWith(openmct);
expect(this.mockPlugin).toHaveBeenCalledWith(this.openmct);
expect(this.mockPlugin2).toHaveBeenCalledWith(this.openmct);
});
it("emits a start event", function () {
expect(mockListener).toHaveBeenCalled();
expect(this.mockListener).toHaveBeenCalled();
});
it("Does not render Open MCT", function () {
@@ -112,19 +125,19 @@ define([
let testAssetPath;
beforeEach(function () {
openmct.legacyExtension = jasmine.createSpy('legacyExtension');
this.openmct.legacyExtension = jasmine.createSpy('legacyExtension');
});
it("configures the path for assets", function () {
testAssetPath = "some/path/";
openmct.setAssetPath(testAssetPath);
expect(openmct.getAssetPath()).toBe(testAssetPath);
this.openmct.setAssetPath(testAssetPath);
expect(this.openmct.getAssetPath()).toBe(testAssetPath);
});
it("adds a trailing /", function () {
testAssetPath = "some/path";
openmct.setAssetPath(testAssetPath);
expect(openmct.getAssetPath()).toBe(testAssetPath + "/");
this.openmct.setAssetPath(testAssetPath);
expect(this.openmct.getAssetPath()).toBe(testAssetPath + "/");
});
});
});

View File

@@ -36,8 +36,7 @@ define([
'./views/installLegacyViews',
'./policies/LegacyCompositionPolicyAdapter',
'./actions/LegacyActionAdapter',
'./services/LegacyPersistenceAdapter',
'./services/ExportImageService'
'./services/LegacyPersistenceAdapter'
], function (
ActionDialogDecorator,
AdapterCapability,
@@ -54,8 +53,7 @@ define([
installLegacyViews,
legacyCompositionPolicyAdapter,
LegacyActionAdapter,
LegacyPersistenceAdapter,
ExportImageService
LegacyPersistenceAdapter
) {
return {
name: 'src/adapter',
@@ -84,13 +82,6 @@ define([
"identifierService",
"cacheService"
]
},
{
"key": "exportImageService",
"implementation": ExportImageService,
"depends": [
"dialogService"
]
}
],
components: [

View File

@@ -32,6 +32,10 @@ define([
// cannot be injected.
function AlternateCompositionInitializer(openmct) {
AlternateCompositionCapability.appliesTo = function (model, id) {
openmct.once('destroy', () => {
delete AlternateCompositionCapability.appliesTo;
});
model = objectUtils.toNewFormat(model, id || '');
return Boolean(openmct.composition.get(model));

View File

@@ -173,10 +173,11 @@ define([
const limitEvaluator = oldObject.getCapability("limit");
return {
limits: function () {
return limitEvaluator.limits();
limits: () => {
return limitEvaluator.limits.then !== undefined
? limitEvaluator.limits()
: Promise.resolve(limitEvaluator.limits());
}
};
};

View File

@@ -1,218 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/**
* Module defining ExportImageService. Created by hudsonfoo on 09/02/16
*/
define(
[
"html2canvas",
"saveAs"
],
function (
html2canvas,
{ saveAs }
) {
/**
* The export image service will export any HTML node to
* JPG, or PNG.
* @param {object} dialogService
* @constructor
*/
function ExportImageService(dialogService) {
this.dialogService = dialogService;
this.exportCount = 0;
}
/**
* Converts an HTML element into a PNG or JPG Blob.
* @private
* @param {node} element that will be converted to an image
* @param {object} options Image options.
* @returns {promise}
*/
ExportImageService.prototype.renderElement = function (element, {imageType, className, thumbnailSize}) {
const self = this;
const dialogService = this.dialogService;
const dialog = dialogService.showBlockingMessage({
title: "Capturing...",
hint: "Capturing an image",
unknownProgress: true,
severity: "info",
delay: true
});
let mimeType = "image/png";
if (imageType === "jpg") {
mimeType = "image/jpeg";
}
let exportId = undefined;
let oldId = undefined;
if (className) {
exportId = 'export-element-' + this.exportCount;
this.exportCount++;
oldId = element.id;
element.id = exportId;
}
return html2canvas(element, {
onclone: function (document) {
if (className) {
const clonedElement = document.getElementById(exportId);
clonedElement.classList.add(className);
}
element.id = oldId;
},
removeContainer: true // Set to false to debug what html2canvas renders
}).then(function (canvas) {
dialog.dismiss();
return new Promise(function (resolve, reject) {
if (thumbnailSize) {
const thumbnail = self.getThumbnail(canvas, mimeType, thumbnailSize);
return canvas.toBlob(blob => resolve({
blob,
thumbnail
}), mimeType);
}
return canvas.toBlob(blob => resolve({ blob }), mimeType);
});
}, function (error) {
console.log('error capturing image', error);
dialog.dismiss();
const errorDialog = dialogService.showBlockingMessage({
title: "Error capturing image",
severity: "error",
hint: "Image was not captured successfully!",
options: [{
label: "OK",
callback: function () {
errorDialog.dismiss();
}
}]
});
});
};
ExportImageService.prototype.getThumbnail = function (canvas, mimeType, size) {
const thumbnailCanvas = document.createElement('canvas');
thumbnailCanvas.setAttribute('width', size.width);
thumbnailCanvas.setAttribute('height', size.height);
const ctx = thumbnailCanvas.getContext('2d');
ctx.globalCompositeOperation = "copy";
ctx.drawImage(canvas, 0, 0, canvas.width, canvas.height, 0, 0, size.width, size.height);
return thumbnailCanvas.toDataURL(mimeType);
};
/**
* Takes a screenshot of a DOM node and exports to JPG.
* @param {node} element to be exported
* @param {string} filename the exported image
* @param {string} className to be added to element before capturing (optional)
* @returns {promise}
*/
ExportImageService.prototype.exportJPG = function (element, filename, className) {
const processedFilename = replaceDotsWithUnderscores(filename);
return this.renderElement(element, {
imageType: 'jpg',
className
})
.then(function (img) {
saveAs(img.blob, processedFilename);
});
};
/**
* Takes a screenshot of a DOM node and exports to PNG.
* @param {node} element to be exported
* @param {string} filename the exported image
* @param {string} className to be added to element before capturing (optional)
* @returns {promise}
*/
ExportImageService.prototype.exportPNG = function (element, filename, className) {
const processedFilename = replaceDotsWithUnderscores(filename);
return this.renderElement(element, {
imageType: 'png',
className
})
.then(function (img) {
saveAs(img.blob, processedFilename);
});
};
/**
* Takes a screenshot of a DOM node in PNG format.
* @param {node} element to be exported
* @param {string} filename the exported image
* @returns {promise}
*/
ExportImageService.prototype.exportPNGtoSRC = function (element, options) {
return this.renderElement(element, {
imageType: 'png',
...options
});
};
function replaceDotsWithUnderscores(filename) {
const regex = /\./gi;
return filename.replace(regex, '_');
}
/**
* canvas.toBlob() not supported in IE < 10, Opera, and Safari. This polyfill
* implements the method in browsers that would not otherwise support it.
* https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob
*/
function polyfillToBlob() {
if (!HTMLCanvasElement.prototype.toBlob) {
Object.defineProperty(HTMLCanvasElement.prototype, "toBlob", {
value: function (callback, mimeType, quality) {
const binStr = atob(this.toDataURL(mimeType, quality).split(',')[1]);
const len = binStr.length;
const arr = new Uint8Array(len);
for (let i = 0; i < len; i++) {
arr[i] = binStr.charCodeAt(i);
}
callback(new Blob([arr], {type: mimeType || "image/png"}));
}
});
}
}
polyfillToBlob();
return ExportImageService;
}
);

View File

@@ -28,6 +28,10 @@ export default class Editor extends EventEmitter {
super();
this.editing = false;
this.openmct = openmct;
openmct.once('destroy', () => {
this.removeAllListeners();
});
}
/**

View File

@@ -46,8 +46,6 @@ class ActionCollection extends EventEmitter {
this._observeObjectPath();
this.openmct.editor.on('isEditing', this._updateActions);
}
this._initializeActions();
}
disable(actionKeys) {
@@ -156,19 +154,10 @@ class ActionCollection extends EventEmitter {
});
}
_initializeActions() {
Object.keys(this.applicableActions).forEach(key => {
this.applicableActions[key].callBack = () => {
return this.applicableActions[key].invoke(this.objectPath, this.view);
};
});
}
_updateActions() {
let newApplicableActions = this.openmct.actions._applicableActions(this.objectPath, this.view);
this.applicableActions = this._mergeOldAndNewActions(this.applicableActions, newApplicableActions);
this._initializeActions();
this._update();
}

View File

@@ -34,7 +34,7 @@ class ActionsAPI extends EventEmitter {
this._groupOrder = ['windowing', 'undefined', 'view', 'action', 'json'];
this.register = this.register.bind(this);
this.get = this.get.bind(this);
this.getActionsCollection = this.getActionsCollection.bind(this);
this._applicableActions = this._applicableActions.bind(this);
this._updateCachedActionCollections = this._updateCachedActionCollections.bind(this);
}
@@ -43,12 +43,14 @@ class ActionsAPI extends EventEmitter {
this._allActions[actionDefinition.key] = actionDefinition;
}
get(objectPath, view) {
if (view) {
getAction(key) {
return this._allActions[key];
}
getActionsCollection(objectPath, view) {
if (view) {
return this._getCachedActionCollection(objectPath, view) || this._newActionCollection(objectPath, view, true);
} else {
return this._newActionCollection(objectPath, view, true);
}
}
@@ -57,15 +59,6 @@ class ActionsAPI extends EventEmitter {
this._groupOrder = groupArray;
}
_get(objectPath, view) {
let actionCollection = this._newActionCollection(objectPath, view);
this._actionCollections.set(view, actionCollection);
actionCollection.on('destroy', this._updateCachedActionCollections);
return actionCollection;
}
_getCachedActionCollection(objectPath, view) {
let cachedActionCollection = this._actionCollections.get(view);
@@ -75,7 +68,17 @@ class ActionsAPI extends EventEmitter {
_newActionCollection(objectPath, view, skipEnvironmentObservers) {
let applicableActions = this._applicableActions(objectPath, view);
return new ActionCollection(applicableActions, objectPath, view, this._openmct, skipEnvironmentObservers);
const actionCollection = new ActionCollection(applicableActions, objectPath, view, this._openmct, skipEnvironmentObservers);
if (view) {
this._cacheActionCollection(view, actionCollection);
}
return actionCollection;
}
_cacheActionCollection(view, actionCollection) {
this._actionCollections.set(view, actionCollection);
actionCollection.on('destroy', this._updateCachedActionCollections);
}
_updateCachedActionCollections(key) {

View File

@@ -106,7 +106,7 @@ describe('The Actions API', () => {
it("adds action to ActionsAPI", () => {
actionsAPI.register(mockAction);
let actionCollection = actionsAPI.get(mockObjectPath, mockViewContext1);
let actionCollection = actionsAPI.getActionsCollection(mockObjectPath, mockViewContext1);
let action = actionCollection.getActionsObject()[mockAction.key];
expect(action.key).toEqual(mockAction.key);
@@ -121,21 +121,21 @@ describe('The Actions API', () => {
});
it("returns an ActionCollection when invoked with an objectPath only", () => {
let actionCollection = actionsAPI.get(mockObjectPath);
let actionCollection = actionsAPI.getActionsCollection(mockObjectPath);
let instanceOfActionCollection = actionCollection instanceof ActionCollection;
expect(instanceOfActionCollection).toBeTrue();
});
it("returns an ActionCollection when invoked with an objectPath and view", () => {
let actionCollection = actionsAPI.get(mockObjectPath, mockViewContext1);
let actionCollection = actionsAPI.getActionsCollection(mockObjectPath, mockViewContext1);
let instanceOfActionCollection = actionCollection instanceof ActionCollection;
expect(instanceOfActionCollection).toBeTrue();
});
it("returns relevant actions when invoked with objectPath only", () => {
let actionCollection = actionsAPI.get(mockObjectPath);
let actionCollection = actionsAPI.getActionsCollection(mockObjectPath);
let action = actionCollection.getActionsObject()[mockObjectPathAction.key];
expect(action.key).toEqual(mockObjectPathAction.key);
@@ -143,7 +143,7 @@ describe('The Actions API', () => {
});
it("returns relevant actions when invoked with objectPath and view", () => {
let actionCollection = actionsAPI.get(mockObjectPath, mockViewContext1);
let actionCollection = actionsAPI.getActionsCollection(mockObjectPath, mockViewContext1);
let action = actionCollection.getActionsObject()[mockAction.key];
expect(action.key).toEqual(mockAction.key);

View File

@@ -37,7 +37,7 @@ import Menu, { MENU_PLACEMENT } from './menu.js';
* @property {Boolean} isDisabled adds disable class if true
* @property {String} name Menu item text
* @property {String} description Menu item description
* @property {Function} callBack callback function: invoked when item is clicked
* @property {Function} onItemClicked callback function: invoked when item is clicked
*/
/**
@@ -66,12 +66,27 @@ class MenuAPI {
* @param {Array.<Action>|Array.<Array.<Action>>} actions collection of actions{@link Action} or collection of groups of actions {@link Action}
* @param {MenuOptions} [menuOptions] [Optional] The {@link MenuOptions} options for Menu
*/
showMenu(x, y, actions, menuOptions) {
this._createMenuComponent(x, y, actions, menuOptions);
showMenu(x, y, items, menuOptions) {
this._createMenuComponent(x, y, items, menuOptions);
this.menuComponent.showMenu();
}
actionsToMenuItems(actions, objectPath, view) {
return actions.map(action => {
const isActionGroup = Array.isArray(action);
if (isActionGroup) {
action = this.actionsToMenuItems(action, objectPath, view);
} else {
action.onItemClicked = () => {
action.invoke(objectPath, view);
};
}
return action;
});
}
/**
* Show popup menu with description of item on hover
* @param {number} x x-coordinates for popup

View File

@@ -57,7 +57,7 @@ describe ('The Menu API', () => {
name: 'Test Action 1',
cssClass: 'icon-clock',
description: 'This is a test action',
callBack: () => {
onItemClicked: () => {
result = 'Test Action 1 Invoked';
}
},
@@ -66,7 +66,7 @@ describe ('The Menu API', () => {
name: 'Test Action 2',
cssClass: 'icon-clock',
description: 'This is a test action',
callBack: () => {
onItemClicked: () => {
result = 'Test Action 2 Invoked';
}
}

View File

@@ -11,7 +11,7 @@
:key="action.name"
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
:title="action.description"
@click="action.callBack"
@click="action.onItemClicked"
>
{{ action.name }}
</li>
@@ -36,7 +36,7 @@
:key="action.name"
:class="action.cssClass"
:title="action.description"
@click="action.callBack"
@click="action.onItemClicked"
>
{{ action.name }}
</li>

View File

@@ -13,7 +13,7 @@
:key="action.name"
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
:title="action.description"
@click="action.callBack"
@click="action.onItemClicked"
@mouseover="toggleItemDescription(action)"
@mouseleave="toggleItemDescription()"
>
@@ -42,7 +42,7 @@
:key="action.name"
:class="action.cssClass"
:title="action.description"
@click="action.callBack"
@click="action.onItemClicked"
@mouseover="toggleItemDescription(action)"
@mouseleave="toggleItemDescription()"
>

View File

@@ -71,12 +71,12 @@ class Menu extends EventEmitter {
showMenu() {
this.component = new Vue({
provide: {
options: this.options
},
components: {
MenuComponent
},
provide: {
options: this.options
},
template: '<menu-component />'
});
@@ -85,12 +85,12 @@ class Menu extends EventEmitter {
showSuperMenu() {
this.component = new Vue({
provide: {
options: this.options
},
components: {
SuperMenuComponent
},
provide: {
options: this.options
},
template: '<super-menu-component />'
});

View File

@@ -399,25 +399,25 @@ ObjectAPI.prototype._toMutable = function (object) {
mutableObject = object;
} else {
mutableObject = MutableDomainObject.createMutable(object, this.eventEmitter);
}
// Check if provider supports realtime updates
let identifier = utils.parseKeyString(mutableObject.identifier);
let provider = this.getProvider(identifier);
// Check if provider supports realtime updates
let identifier = utils.parseKeyString(mutableObject.identifier);
let provider = this.getProvider(identifier);
if (provider !== undefined
&& provider.observe !== undefined
&& this.SYNCHRONIZED_OBJECT_TYPES.includes(object.type)) {
let unobserve = provider.observe(identifier, (updatedModel) => {
if (updatedModel.persisted > mutableObject.modified) {
//Don't replace with a stale model. This can happen on slow connections when multiple mutations happen
//in rapid succession and intermediate persistence states are returned by the observe function.
mutableObject.$refresh(updatedModel);
}
});
mutableObject.$on('$_destroy', () => {
unobserve();
});
if (provider !== undefined
&& provider.observe !== undefined
&& this.SYNCHRONIZED_OBJECT_TYPES.includes(object.type)) {
let unobserve = provider.observe(identifier, (updatedModel) => {
if (updatedModel.persisted > mutableObject.modified) {
//Don't replace with a stale model. This can happen on slow connections when multiple mutations happen
//in rapid succession and intermediate persistence states are returned by the observe function.
mutableObject.$refresh(updatedModel);
}
});
mutableObject.$on('$_destroy', () => {
unobserve();
});
}
}
return mutableObject;

View File

@@ -17,11 +17,7 @@ class OverlayAPI {
this.dismissLastOverlay = this.dismissLastOverlay.bind(this);
document.addEventListener('keyup', (event) => {
if (event.key === 'Escape') {
this.dismissLastOverlay();
}
});
document.addEventListener('keyup', this.dismissLastOverlay);
}
@@ -49,10 +45,12 @@ class OverlayAPI {
/**
* private
*/
dismissLastOverlay() {
let lastOverlay = this.activeOverlays[this.activeOverlays.length - 1];
if (lastOverlay && lastOverlay.dismissable) {
lastOverlay.dismiss();
dismissLastOverlay(event) {
if (event.key === 'Escape') {
let lastOverlay = this.activeOverlays[this.activeOverlays.length - 1];
if (lastOverlay && lastOverlay.dismissable) {
lastOverlay.dismiss();
}
}
}
@@ -129,6 +127,10 @@ class OverlayAPI {
return progressDialog;
}
destroy() {
document.removeEventListener('keyup', this.dismissLastOverlay);
}
}
export default OverlayAPI;

View File

@@ -21,8 +21,7 @@
&__outer {
@include abs();
background: $overlayColorBg;
color: $overlayColorFg;
background: $colorBodyBg;
display: flex;
flex-direction: column;
padding: $overlayInnerMargin;
@@ -30,7 +29,6 @@
&__close-button {
$p: $interiorMargin + 2px;
color: $overlayColorFg;
font-size: 1.5em;
position: absolute;
top: $p; right: $p;
@@ -82,11 +80,6 @@
}
}
.c-button,
.c-click-icon {
filter: $overlayBrightnessAdjust;
}
.c-object-label__name {
filter: $objectLabelNameFilter;
}
@@ -103,6 +96,7 @@ body.desktop {
}
// Overlay types, styling for desktop. Appended to .l-overlay-wrapper element.
.l-overlay-large,
.l-overlay-small,
.l-overlay-fit {
.c-overlay__outer {
@@ -124,12 +118,8 @@ body.desktop {
$tbPad: floor($pad * 0.8);
$lrPad: $pad;
.c-overlay {
&__blocker {
display: none;
}
&__outer {
@include overlaySizing($overlayOuterMarginFullscreen);
@include overlaySizing($overlayOuterMarginLarge);
padding: $tbPad $lrPad;
}

View File

@@ -32,6 +32,10 @@ export default class StatusAPI extends EventEmitter {
this.get = this.get.bind(this);
this.set = this.set.bind(this);
this.observe = this.observe.bind(this);
openmct.once('destroy', () => {
this.removeAllListeners();
});
}
get(identifier) {

View File

@@ -0,0 +1,185 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/**
* Class defining an image exporter for JPG/PNG output.
* Originally created by hudsonfoo on 09/02/16
*/
function replaceDotsWithUnderscores(filename) {
const regex = /\./gi;
return filename.replace(regex, '_');
}
import {saveAs} from 'file-saver/FileSaver';
import html2canvas from 'html2canvas';
import uuid from 'uuid';
class ImageExporter {
constructor(openmct) {
this.openmct = openmct;
}
/**
* Converts an HTML element into a PNG or JPG Blob.
* @private
* @param {node} element that will be converted to an image
* @param {object} options Image options.
* @returns {promise}
*/
renderElement(element, { imageType, className, thumbnailSize }) {
const self = this;
const overlays = this.openmct.overlays;
const dialog = overlays.dialog({
iconClass: 'info',
message: 'Caputuring an image',
buttons: [
{
label: 'Cancel',
emphasis: true,
callback: function () {
dialog.dismiss();
}
}
]
});
let mimeType = 'image/png';
if (imageType === 'jpg') {
mimeType = 'image/jpeg';
}
let exportId = undefined;
let oldId = undefined;
if (className) {
const newUUID = uuid();
exportId = `$export-element-${newUUID}`;
oldId = element.id;
element.id = exportId;
}
return html2canvas(element, {
onclone: function (document) {
if (className) {
const clonedElement = document.getElementById(exportId);
clonedElement.classList.add(className);
}
element.id = oldId;
},
removeContainer: true // Set to false to debug what html2canvas renders
}).then(function (canvas) {
dialog.dismiss();
return new Promise(function (resolve, reject) {
if (thumbnailSize) {
const thumbnail = self.getThumbnail(canvas, mimeType, thumbnailSize);
return canvas.toBlob(blob => resolve({
blob,
thumbnail
}), mimeType);
}
return canvas.toBlob(blob => resolve({ blob }), mimeType);
});
}, function (error) {
console.log('error capturing image', error);
dialog.dismiss();
const errorDialog = overlays.dialog({
iconClass: 'error',
message: 'Image was not captured successfully!',
buttons: [
{
label: "OK",
emphasis: true,
callback: function () {
errorDialog.dismiss();
}
}
]
});
});
}
getThumbnail(canvas, mimeType, size) {
const thumbnailCanvas = document.createElement('canvas');
thumbnailCanvas.setAttribute('width', size.width);
thumbnailCanvas.setAttribute('height', size.height);
const ctx = thumbnailCanvas.getContext('2d');
ctx.globalCompositeOperation = "copy";
ctx.drawImage(canvas, 0, 0, canvas.width, canvas.height, 0, 0, size.width, size.height);
return thumbnailCanvas.toDataURL(mimeType);
}
/**
* Takes a screenshot of a DOM node and exports to JPG.
* @param {node} element to be exported
* @param {string} filename the exported image
* @param {string} className to be added to element before capturing (optional)
* @returns {promise}
*/
async exportJPG(element, filename, className) {
const processedFilename = replaceDotsWithUnderscores(filename);
const img = await this.renderElement(element, {
imageType: 'jpg',
className
});
saveAs(img.blob, processedFilename);
}
/**
* Takes a screenshot of a DOM node and exports to PNG.
* @param {node} element to be exported
* @param {string} filename the exported image
* @param {string} className to be added to element before capturing (optional)
* @returns {promise}
*/
async exportPNG(element, filename, className) {
const processedFilename = replaceDotsWithUnderscores(filename);
const img = await this.renderElement(element, {
imageType: 'png',
className
});
saveAs(img.blob, processedFilename);
}
/**
* Takes a screenshot of a DOM node in PNG format.
* @param {node} element to be exported
* @param {string} filename the exported image
* @returns {promise}
*/
exportPNGtoSRC(element, options) {
return this.renderElement(element, {
imageType: 'png',
...options
});
}
}
export default ImageExporter;

View File

@@ -0,0 +1,58 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import ImageExporter from './ImageExporter';
import { createOpenMct, resetApplicationState } from '../utils/testing';
describe('The Image Exporter', () => {
let openmct;
let imageExporter;
beforeEach(() => {
openmct = createOpenMct();
});
afterEach(() => {
return resetApplicationState(openmct);
});
describe("basic instatation", () => {
it("can be instatiated", () => {
imageExporter = new ImageExporter(openmct);
expect(imageExporter).not.toEqual(null);
});
it("can render an element to a blob", async () => {
const mockHeadElement = document.createElement("h1");
const mockTextNode = document.createTextNode('foo bar');
mockHeadElement.appendChild(mockTextNode);
document.body.appendChild(mockHeadElement);
imageExporter = new ImageExporter(openmct);
const returnedBlob = await imageExporter.renderElement(document.body, {
imageType: 'png'
});
expect(returnedBlob).not.toEqual(null);
expect(returnedBlob.blob).not.toEqual(null);
expect(returnedBlob.blob).toBeInstanceOf(Blob);
});
});
});

View File

@@ -39,11 +39,10 @@ const DEFAULTS = [
'platform/telemetry',
'platform/features/clock',
'platform/features/hyperlink',
'platform/features/timeline',
'platform/forms',
'platform/identity',
'platform/persistence/aggregator',
'platform/persistence/queue',
//'platform/persistence/queue',
'platform/policy',
'platform/entanglement',
'platform/search',
@@ -84,7 +83,6 @@ define([
'../platform/features/my-items/bundle',
'../platform/features/hyperlink/bundle',
'../platform/features/static-markup/bundle',
'../platform/features/timeline/bundle',
'../platform/forms/bundle',
'../platform/framework/bundle',
'../platform/framework/src/load/Bundle',
@@ -92,7 +90,7 @@ define([
'../platform/persistence/aggregator/bundle',
'../platform/persistence/elastic/bundle',
'../platform/persistence/local/bundle',
'../platform/persistence/queue/bundle',
//'../platform/persistence/queue/bundle',
'../platform/policy/bundle',
'../platform/representation/bundle',
'../platform/search/bundle',

View File

@@ -19,8 +19,8 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import LadTableSet from './components/LadTableSet.vue';
import Vue from 'vue';
import LadTableSetView from './LadTableSetView';
export default function LADTableSetViewProvider(openmct) {
return {
@@ -34,32 +34,7 @@ export default function LADTableSetViewProvider(openmct) {
return domainObject.type === 'LadTableSet';
},
view: function (domainObject, objectPath) {
let component;
return {
show: function (element) {
component = new Vue({
el: element,
components: {
LadTableSet: LadTableSet
},
provide: {
openmct,
objectPath
},
data() {
return {
domainObject
};
},
template: '<lad-table-set :domain-object="domainObject"></lad-table-set>'
});
},
destroy: function (element) {
component.$destroy();
component = undefined;
}
};
return new LadTableSetView(openmct, domainObject, objectPath);
},
priority: function () {
return 1;

View File

@@ -0,0 +1,45 @@
import LadTable from './components/LADTable.vue';
import Vue from 'vue';
export default class LADTableView {
constructor(openmct, domainObject, objectPath) {
this.openmct = openmct;
this.domainObject = domainObject;
this.objectPath = objectPath;
this.component = undefined;
}
show(element) {
this.component = new Vue({
el: element,
components: {
LadTable
},
provide: {
openmct: this.openmct,
currentView: this
},
data: () => {
return {
domainObject: this.domainObject,
objectPath: this.objectPath
};
},
template: '<lad-table ref="ladTable" :domain-object="domainObject" :object-path="objectPath"></lad-table>'
});
}
getViewContext() {
if (!this.component) {
return {};
}
return this.component.$refs.ladTable.getViewContext();
}
destroy(element) {
this.component.$destroy();
this.component = undefined;
}
}

View File

@@ -19,50 +19,30 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import LadTable from './components/LADTable.vue';
import Vue from 'vue';
export default function LADTableViewProvider(openmct) {
return {
key: 'LadTable',
name: 'LAD Table',
cssClass: 'icon-tabular-lad',
canView: function (domainObject) {
return domainObject.type === 'LadTable';
},
canEdit: function (domainObject) {
return domainObject.type === 'LadTable';
},
view: function (domainObject, objectPath) {
let component;
import LADTableView from './LADTableView';
return {
show: function (element) {
component = new Vue({
el: element,
components: {
LadTableComponent: LadTable
},
provide: {
openmct
},
data: () => {
return {
domainObject,
objectPath
};
},
template: '<lad-table-component :domain-object="domainObject" :object-path="objectPath"></lad-table-component>'
});
},
destroy: function (element) {
component.$destroy();
component = undefined;
}
};
},
priority: function () {
return 1;
}
};
export default class LADTableViewProvider {
constructor(openmct) {
this.openmct = openmct;
this.name = 'LAD Table';
this.key = 'LadTable';
this.cssClass = 'icon-tabular-lad';
}
canView(domainObject) {
return domainObject.type === 'LadTable';
}
canEdit(domainObject) {
return domainObject.type === 'LadTable';
}
view(domainObject, objectPath) {
return new LADTableView(this.openmct, domainObject, objectPath);
}
priority(domainObject) {
return 1;
}
}

View File

@@ -0,0 +1,45 @@
import LadTableSet from './components/LadTableSet.vue';
import Vue from 'vue';
export default class LadTableSetView {
constructor(openmct, domainObject, objectPath) {
this.openmct = openmct;
this.domainObject = domainObject;
this.objectPath = objectPath;
this.component = undefined;
}
show(element) {
this.component = new Vue({
el: element,
components: {
LadTableSet
},
provide: {
openmct: this.openmct,
objectPath: this.objectPath,
currentView: this
},
data: () => {
return {
domainObject: this.domainObject
};
},
template: '<lad-table-set ref="ladTableSet" :domain-object="domainObject"></lad-table-set>'
});
}
getViewContext() {
if (!this.component) {
return {};
}
return this.component.$refs.ladTableSet.getViewContext();
}
destroy(element) {
this.component.$destroy();
this.component = undefined;
}
}

View File

@@ -50,7 +50,7 @@ const CONTEXT_MENU_ACTIONS = [
];
export default {
inject: ['openmct'],
inject: ['openmct', 'currentView'],
props: {
domainObject: {
type: Object,
@@ -167,25 +167,23 @@ export default {
this.resetValues();
this.timestampKey = timeSystem.key;
},
getView() {
return {
getViewContext: () => {
return {
viewHistoricalData: true,
viewDatumAction: true,
getDatum: () => {
return this.datum;
}
};
updateViewContext() {
this.$emit('rowContextClick', {
viewHistoricalData: true,
viewDatumAction: true,
getDatum: () => {
return this.datum;
}
};
});
},
showContextMenu(event) {
let actionCollection = this.openmct.actions.get(this.objectPath, this.getView());
let allActions = actionCollection.getActionsObject();
let applicableActions = CONTEXT_MENU_ACTIONS.map(key => allActions[key]);
this.updateViewContext();
this.openmct.menus.showMenu(event.x, event.y, applicableActions);
const actions = CONTEXT_MENU_ACTIONS.map(key => this.openmct.actions.getAction(key));
const menuItems = this.openmct.menus.actionsToMenuItems(actions, this.objectPath, this.currentView);
if (menuItems.length) {
this.openmct.menus.showMenu(event.x, event.y, menuItems);
}
},
resetValues() {
this.value = '---';

View File

@@ -38,6 +38,7 @@
:domain-object="ladRow.domainObject"
:path-to-table="objectPath"
:has-units="hasUnits"
@rowContextClick="updateViewContext"
/>
</tbody>
</table>
@@ -51,7 +52,7 @@ export default {
components: {
LadRow
},
inject: ['openmct'],
inject: ['openmct', 'currentView'],
props: {
domainObject: {
type: Object,
@@ -64,7 +65,8 @@ export default {
},
data() {
return {
items: []
items: [],
viewContext: {}
};
},
computed: {
@@ -114,6 +116,12 @@ export default {
let metadataWithUnits = valueMetadatas.filter(metadatum => metadatum.unit);
return metadataWithUnits.length > 0;
},
updateViewContext(rowContext) {
this.viewContext.row = rowContext;
},
getViewContext() {
return this.viewContext;
}
}
};

View File

@@ -48,6 +48,7 @@
:domain-object="ladRow.domainObject"
:path-to-table="ladTable.objectPath"
:has-units="hasUnits"
@rowContextClick="updateViewContext"
/>
</template>
</tbody>
@@ -61,7 +62,7 @@ export default {
components: {
LadRow
},
inject: ['openmct', 'objectPath'],
inject: ['openmct', 'objectPath', 'currentView'],
props: {
domainObject: {
type: Object,
@@ -72,7 +73,8 @@ export default {
return {
ladTableObjects: [],
ladTelemetryObjects: {},
compositions: []
compositions: [],
viewContext: {}
};
},
computed: {
@@ -166,6 +168,12 @@ export default {
this.$set(this.ladTelemetryObjects, ladTable.key, telemetryObjects);
};
},
updateViewContext(rowContext) {
this.viewContext.row = rowContext;
},
getViewContext() {
return this.viewContext;
}
}
};

View File

@@ -45,6 +45,7 @@ export default class URLTimeSettingsSynchronizer {
}
initialize() {
this.updateTimeSettings();
this.openmct.router.on('change:params', this.updateTimeSettings);
TIME_EVENTS.forEach(event => {

View File

@@ -41,7 +41,7 @@ export default class ConditionManager extends EventEmitter {
this.subscriptions = {};
this.telemetryObjects = {};
this.testData = {
conditionTestData: [],
conditionTestInputs: this.conditionSetDomainObject.configuration.conditionTestData,
applied: false
};
this.initialize();
@@ -154,8 +154,10 @@ export default class ConditionManager extends EventEmitter {
updateConditionDescription(condition) {
const found = this.conditionSetDomainObject.configuration.conditionCollection.find(conditionConfiguration => (conditionConfiguration.id === condition.id));
found.summary = condition.description;
this.persistConditions();
if (found.summary !== condition.description) {
found.summary = condition.description;
this.persistConditions();
}
}
initCondition(conditionConfiguration, index) {
@@ -414,8 +416,10 @@ export default class ConditionManager extends EventEmitter {
}
updateTestData(testData) {
this.testData = testData;
this.openmct.objects.mutate(this.conditionSetDomainObject, 'configuration.conditionTestData', this.testData.conditionTestInputs);
if (!_.isEqual(testData, this.testData)) {
this.testData = testData;
this.openmct.objects.mutate(this.conditionSetDomainObject, 'configuration.conditionTestData', this.testData.conditionTestInputs);
}
}
persistConditions() {

View File

@@ -215,7 +215,8 @@ export default {
},
isEditing: {
type: Boolean,
required: true
required: true,
default: false
},
telemetry: {
type: Array,

View File

@@ -20,71 +20,78 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
'./components/AlphanumericFormatView.vue',
'vue'
], function (AlphanumericFormatView, Vue) {
import AlphanumericFormat from './components/AlphanumericFormat.vue';
function AlphanumericFormatViewProvider(openmct, options) {
function isTelemetryObject(selectionPath) {
let selectedObject = selectionPath[0].context.item;
let parentObject = selectionPath[1].context.item;
let selectedLayoutItem = selectionPath[0].context.layoutItem;
import Vue from 'vue';
return parentObject
&& parentObject.type === 'layout'
&& selectedObject
&& selectedLayoutItem
&& selectedLayoutItem.type === 'telemetry-view'
&& openmct.telemetry.isTelemetryObject(selectedObject)
&& !options.showAsView.includes(selectedObject.type);
}
return {
key: 'alphanumeric-format',
name: 'Alphanumeric Format',
canView: function (selection) {
if (selection.length === 0 || selection[0].length === 1) {
return false;
}
return selection.every(isTelemetryObject);
},
view: function (domainObject, objectPath) {
let component;
return {
show: function (element) {
component = new Vue({
el: element,
components: {
AlphanumericFormatView: AlphanumericFormatView.default
},
provide: {
openmct,
objectPath
},
template: '<alphanumeric-format-view ref="alphanumericFormatView"></alphanumeric-format-view>'
});
},
getViewContext() {
if (component) {
return component.$refs.alphanumericFormatView.getViewContext();
} else {
return {};
}
},
destroy: function () {
component.$destroy();
component = undefined;
}
};
},
priority: function () {
return 1;
}
};
class AlphanumericFormatView {
constructor(openmct, domainObject, objectPath) {
this.openmct = openmct;
this.domainObject = domainObject;
this.objectPath = objectPath;
this.component = undefined;
}
return AlphanumericFormatViewProvider;
});
show(element) {
this.component = new Vue({
el: element,
name: 'AlphanumericFormat',
components: {
AlphanumericFormat
},
provide: {
openmct: this.openmct,
objectPath: this.objectPath,
currentView: this
},
template: '<alphanumeric-format ref="alphanumericFormat"></alphanumeric-format>'
});
}
getViewContext() {
if (this.component) {
return {};
}
return this.component.$refs.alphanumericFormat.getViewContext();
}
destroy() {
this.component.$destroy();
this.component = undefined;
}
}
export default function AlphanumericFormatViewProvider(openmct, options) {
function isTelemetryObject(selectionPath) {
let selectedObject = selectionPath[0].context.item;
let parentObject = selectionPath[1].context.item;
let selectedLayoutItem = selectionPath[0].context.layoutItem;
return parentObject
&& parentObject.type === 'layout'
&& selectedObject
&& selectedLayoutItem
&& selectedLayoutItem.type === 'telemetry-view'
&& openmct.telemetry.isTelemetryObject(selectedObject)
&& !options.showAsView.includes(selectedObject.type);
}
return {
key: 'alphanumeric-format',
name: 'Alphanumeric Format',
canView: function (selection) {
if (selection.length === 0 || selection[0].length === 1) {
return false;
}
return selection.every(isTelemetryObject);
},
view: function (domainObject, objectPath) {
return new AlphanumericFormatView(openmct, domainObject, objectPath);
},
priority: function () {
return 1;
}
};
}

View File

@@ -14,7 +14,7 @@ export default class CopyToClipboardAction {
invoke(objectPath, view = {}) {
const viewContext = view.getViewContext && view.getViewContext();
const formattedValue = viewContext.formattedValueForCopy();
const formattedValue = viewContext.row.formattedValueForCopy();
clipboard.updateClipboard(formattedValue)
.then(() => {
@@ -26,9 +26,13 @@ export default class CopyToClipboardAction {
}
appliesTo(objectPath, view = {}) {
let viewContext = view.getViewContext && view.getViewContext();
const viewContext = view.getViewContext && view.getViewContext();
const row = viewContext && viewContext.row;
if (!row) {
return false;
}
return viewContext && viewContext.formattedValueForCopy
&& typeof viewContext.formattedValueForCopy === 'function';
return row.formattedValueForCopy
&& typeof row.formattedValueForCopy === 'function';
}
}

View File

@@ -52,7 +52,8 @@
<script>
export default {
inject: ['openmct'],
name: 'AlphanumericFormat',
inject: ['openmct', 'objectPath'],
data() {
return {
isEditing: this.openmct.editor.isEditing(),

View File

@@ -56,6 +56,7 @@
:index="index"
:multi-select="selectedLayoutItems.length > 1"
:is-editing="isEditing"
@contextClick="updateViewContext"
@move="move"
@endMove="endMove"
@endLineResize="endLineResize"
@@ -140,7 +141,7 @@ function getItemDefinition(itemType, ...options) {
export default {
components: components,
inject: ['openmct', 'options', 'objectPath'],
inject: ['openmct', 'objectPath', 'options', 'objectUtils', 'currentView'],
props: {
domainObject: {
type: Object,
@@ -155,7 +156,8 @@ export default {
return {
initSelectIndex: undefined,
selection: [],
showGrid: true
showGrid: true,
viewContext: {}
};
},
computed: {
@@ -819,6 +821,12 @@ export default {
},
toggleGrid() {
this.showGrid = !this.showGrid;
},
updateViewContext(viewContext) {
this.viewContext.row = viewContext;
},
getViewContext() {
return this.viewContext;
}
}
};

View File

@@ -102,7 +102,7 @@ export default {
LayoutFrame
},
mixins: [conditionalStylesMixin],
inject: ['openmct', 'objectPath'],
inject: ['openmct', 'objectPath', 'currentView'],
props: {
item: {
type: Object,
@@ -294,16 +294,6 @@ export default {
this.requestHistoricalData(this.domainObject);
}
},
getView() {
return {
getViewContext: () => {
return {
viewHistoricalData: true,
formattedValueForCopy: this.formattedValueForCopy
};
}
};
},
setObject(domainObject) {
this.domainObject = domainObject;
this.mutablePromise = undefined;
@@ -338,30 +328,38 @@ export default {
this.$emit('formatChanged', this.item, format);
},
updateViewContext() {
this.$emit('contextClick', {
viewHistoricalData: true,
formattedValueForCopy: this.formattedValueForCopy
});
},
async getContextMenuActions() {
const defaultNotebook = getDefaultNotebook();
const domainObject = defaultNotebook && await this.openmct.objects.get(defaultNotebook.notebookMeta.identifier);
const actionCollection = this.openmct.actions.get(this.currentObjectPath, this.getView());
const actionsObject = actionCollection.getActionsObject();
let copyToNotebookAction = actionsObject.copyToNotebook;
let defaultNotebookName;
if (defaultNotebook) {
const defaultPath = domainObject && `${domainObject.name} - ${defaultNotebook.section.name} - ${defaultNotebook.page.name}`;
copyToNotebookAction.name = `Copy to Notebook ${defaultPath}`;
} else {
actionsObject.copyToNotebook = undefined;
delete actionsObject.copyToNotebook;
defaultNotebookName = `Copy to Notebook ${defaultPath}`;
}
return CONTEXT_MENU_ACTIONS.map(actionKey => {
return actionsObject[actionKey];
}).filter(action => action !== undefined);
return CONTEXT_MENU_ACTIONS
.map(actionKey => {
const action = this.openmct.actions.getAction(actionKey);
if (action.key === 'copyToNotebook') {
action.name = defaultNotebookName;
}
return action;
})
.filter(action => action.name !== undefined);
},
async showContextMenu(event) {
this.updateViewContext();
const contextMenuActions = await this.getContextMenuActions();
this.openmct.menus.showMenu(event.x, event.y, contextMenuActions);
const menuItems = this.openmct.menus.actionsToMenuItems(contextMenuActions, this.currentObjectPath, this.currentView);
this.openmct.menus.showMenu(event.x, event.y, menuItems);
},
setStatus(status) {
this.status = status;

View File

@@ -20,13 +20,81 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import Layout from './components/DisplayLayout.vue';
import Vue from 'vue';
import objectUtils from 'objectUtils';
import DisplayLayoutType from './DisplayLayoutType.js';
import DisplayLayoutToolbar from './DisplayLayoutToolbar.js';
import AlphaNumericFormatViewProvider from './AlphanumericFormatViewProvider.js';
import CopyToClipboardAction from './actions/CopyToClipboardAction';
import DisplayLayout from './components/DisplayLayout.vue';
import DisplayLayoutToolbar from './DisplayLayoutToolbar.js';
import DisplayLayoutType from './DisplayLayoutType.js';
import objectUtils from 'objectUtils';
import Vue from 'vue';
class DisplayLayoutView {
constructor(openmct, domainObject, objectPath, options) {
this.openmct = openmct;
this.domainObject = domainObject;
this.objectPath = objectPath;
this.options = options;
this.component = undefined;
}
show(container, isEditing) {
this.component = new Vue({
el: container,
components: {
DisplayLayout
},
provide: {
openmct: this.openmct,
objectPath: this.objectPath,
options: this.options,
objectUtils,
currentView: this
},
data: () => {
return {
domainObject: this.domainObject,
isEditing
};
},
template: '<display-layout ref="displayLayout" :domain-object="domainObject" :is-editing="isEditing"></display-layout>'
});
}
getViewContext() {
if (!this.component) {
return {};
}
return this.component.$refs.displayLayout.getViewContext();
}
getSelectionContext() {
return {
item: this.domainObject,
supportsMultiSelect: true,
addElement: this.component && this.component.$refs.displayLayout.addElement,
removeItem: this.component && this.component.$refs.displayLayout.removeItem,
orderItem: this.component && this.component.$refs.displayLayout.orderItem,
duplicateItem: this.component && this.component.$refs.displayLayout.duplicateItem,
switchViewType: this.component && this.component.$refs.displayLayout.switchViewType,
mergeMultipleTelemetryViews: this.component && this.component.$refs.displayLayout.mergeMultipleTelemetryViews,
mergeMultipleOverlayPlots: this.component && this.component.$refs.displayLayout.mergeMultipleOverlayPlots,
toggleGrid: this.component && this.component.$refs.displayLayout.toggleGrid
};
}
onEditModeChange(isEditing) {
this.component.isEditing = isEditing;
}
destroy() {
this.component.$destroy();
this.component = undefined;
}
}
export default function DisplayLayoutPlugin(options) {
return function (openmct) {
@@ -41,51 +109,7 @@ export default function DisplayLayoutPlugin(options) {
return domainObject.type === 'layout';
},
view: function (domainObject, objectPath) {
let component;
return {
show(container) {
component = new Vue({
el: container,
components: {
Layout
},
provide: {
openmct,
objectUtils,
options,
objectPath
},
data() {
return {
domainObject: domainObject,
isEditing: openmct.editor.isEditing()
};
},
template: '<layout ref="displayLayout" :domain-object="domainObject" :is-editing="isEditing"></layout>'
});
},
getSelectionContext() {
return {
item: domainObject,
supportsMultiSelect: true,
addElement: component && component.$refs.displayLayout.addElement,
removeItem: component && component.$refs.displayLayout.removeItem,
orderItem: component && component.$refs.displayLayout.orderItem,
duplicateItem: component && component.$refs.displayLayout.duplicateItem,
switchViewType: component && component.$refs.displayLayout.switchViewType,
mergeMultipleTelemetryViews: component && component.$refs.displayLayout.mergeMultipleTelemetryViews,
mergeMultipleOverlayPlots: component && component.$refs.displayLayout.mergeMultipleOverlayPlots,
toggleGrid: component && component.$refs.displayLayout.toggleGrid
};
},
onEditModeChange: function (isEditing) {
component.isEditing = isEditing;
},
destroy() {
component.$destroy();
}
};
return new DisplayLayoutView(openmct, domainObject, objectPath, options);
},
priority() {
return 100;

View File

@@ -37,7 +37,15 @@ export default class DuplicateAction {
let duplicationTask = new DuplicateTask(this.openmct);
let originalObject = objectPath[0];
let parent = objectPath[1];
let userInput = await this.getUserInput(originalObject, parent);
let userInput;
try {
userInput = await this.getUserInput(originalObject, parent);
} catch (error) {
// user most likely canceled
return;
}
let newParent = userInput.location;
let inNavigationPath = this.inNavigationPath(originalObject);

View File

@@ -23,6 +23,11 @@
body.mobile & {
flex: 1 0 auto;
}
[class*='l-overlay'] & {
// When this view is in an overlay, prevent navigation
pointer-events: none;
}
}
/******************************* GRID ITEMS */

View File

@@ -22,4 +22,9 @@
@include isAlias();
}
}
[class*='l-overlay'] & {
// When this view is in an overlay, prevent navigation
pointer-events: none;
}
}

View File

@@ -0,0 +1,37 @@
import ImageryViewLayout from './components/ImageryViewLayout.vue';
import Vue from 'vue';
export default class ImageryView {
constructor(openmct, domainObject, objectPath) {
this.openmct = openmct;
this.domainObject = domainObject;
this.objectPath = objectPath;
this.component = undefined;
}
show(element) {
this.component = new Vue({
el: element,
components: {
ImageryViewLayout
},
provide: {
openmct: this.openmct,
domainObject: this.domainObject,
objectPath: this.objectPath,
currentView: this
},
template: '<imagery-view-layout ref="ImageryLayout"></imagery-view-layout>'
});
}
destroy() {
this.component.$destroy();
this.component = undefined;
}
_getInstance() {
return this.component;
}
}

View File

@@ -19,9 +19,7 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import ImageryViewLayout from './components/ImageryViewLayout.vue';
import Vue from 'vue';
import ImageryView from './ImageryView';
export default function ImageryViewProvider(openmct) {
const type = 'example.imagery';
@@ -42,28 +40,8 @@ export default function ImageryViewProvider(openmct) {
canView: function (domainObject) {
return hasImageTelemetry(domainObject);
},
view: function (domainObject) {
let component;
return {
show: function (element) {
component = new Vue({
el: element,
components: {
ImageryViewLayout
},
provide: {
openmct,
domainObject
},
template: '<imagery-view-layout ref="ImageryLayout"></imagery-view-layout>'
});
},
destroy: function () {
component.$destroy();
component = undefined;
}
};
view: function (domainObject, objectPath) {
return new ImageryView(openmct, domainObject, objectPath);
}
};
}

View File

@@ -23,7 +23,7 @@
<template>
<div
class="c-compass"
:style="`width: ${ sizedImageDimensions.width }px; height: ${ sizedImageDimensions.height }px`"
:style="`width: 100%; height: 100%`"
>
<CompassHUD
v-if="hasCameraFieldOfView"
@@ -33,13 +33,12 @@
/>
<CompassRose
v-if="hasCameraFieldOfView"
:heading="heading"
:sized-image-width="sizedImageDimensions.width"
:sun-heading="sunHeading"
:camera-angle-of-view="cameraAngleOfView"
:camera-pan="cameraPan"
:lock-compass="lockCompass"
@toggle-lock-compass="toggleLockCompass"
:compass-rose-sizing-classes="compassRoseSizingClasses"
:heading="heading"
:sized-image-dimensions="sizedImageDimensions"
:sun-heading="sunHeading"
/>
</div>
</template>
@@ -56,42 +55,20 @@ export default {
CompassRose
},
props: {
containerWidth: {
type: Number,
required: true
},
containerHeight: {
type: Number,
required: true
},
naturalAspectRatio: {
type: Number,
compassRoseSizingClasses: {
type: String,
required: true
},
image: {
type: Object,
required: true
},
lockCompass: {
type: Boolean,
sizedImageDimensions: {
type: Object,
required: true
}
},
computed: {
sizedImageDimensions() {
let sizedImageDimensions = {};
if ((this.containerWidth / this.containerHeight) > this.naturalAspectRatio) {
// container is wider than image
sizedImageDimensions.width = this.containerHeight * this.naturalAspectRatio;
sizedImageDimensions.height = this.containerHeight;
} else {
// container is taller than image
sizedImageDimensions.width = this.containerWidth;
sizedImageDimensions.height = this.containerWidth * this.naturalAspectRatio;
}
return sizedImageDimensions;
},
hasCameraFieldOfView() {
return this.cameraPan !== undefined && this.cameraAngleOfView > 0;
},

View File

@@ -21,152 +21,203 @@
*****************************************************************************/
<template>
<div
class="w-direction-rose"
:class="compassRoseSizingClasses"
<div ref="compassRoseWrapper"
class="w-direction-rose"
:class="compassRoseSizingClasses"
@click="toggleLockCompass"
>
<div
class="c-direction-rose"
@click="toggleLockCompass"
<svg ref="compassRoseSvg"
class="c-compass-rose-svg"
viewBox="0 0 100 100"
>
<div
class="c-nsew"
:style="compassRoseStyle"
<mask id="mask0"
mask-type="alpha"
maskUnits="userSpaceOnUse"
x="0"
y="0"
width="100"
height="100"
>
<svg
class="c-nsew__minor-ticks"
viewBox="0 0 100 100"
<circle cx="50"
cy="50"
r="50"
fill="black"
/>
</mask>
<g class="c-cr__compass-wrapper">
<g class="c-cr__compass-main"
mask="url(#mask0)"
>
<rect
class="c-nsew__tick c-tick-ne"
x="49"
y="0"
width="2"
height="5"
<!-- Background and clipped elements -->
<rect class="c-cr__bg"
width="100"
height="100"
fill="black"
/>
<rect
class="c-nsew__tick c-tick-se"
x="95"
y="49"
width="5"
height="2"
<rect class="c-cr__edge"
width="100"
height="100"
fill="url(#paint0_radial)"
/>
<rect
class="c-nsew__tick c-tick-sw"
x="49"
y="95"
width="2"
height="5"
/>
<rect
class="c-nsew__tick c-tick-nw"
x="0"
y="49"
width="5"
height="2"
<rect v-if="hasSunHeading"
class="c-cr__sun"
width="100"
height="100"
fill="url(#paint1_radial)"
:style="sunHeadingStyle"
/>
</svg>
<!-- Camera FOV -->
<mask id="mask2"
class="c-cr__cam-fov-l-mask"
mask-type="alpha"
maskUnits="userSpaceOnUse"
x="0"
y="0"
width="50"
height="100"
>
<rect width="51"
height="100"
/>
</mask>
<mask id="mask1"
class="c-cr__cam-fov-r-mask"
mask-type="alpha"
maskUnits="userSpaceOnUse"
x="50"
y="0"
width="50"
height="100"
>
<rect x="49"
width="51"
height="100"
/>
</mask>
<g class="c-cr__cam-fov"
:style="cameraPanStyle"
>
<g mask="url(#mask2)">
<rect class="c-cr__cam-fov-r"
x="49"
width="51"
height="100"
:style="cameraFOVStyleRightHalf"
/>
</g>
<g mask="url(#mask1)">
<rect class="c-cr__cam-fov-l"
width="51"
height="100"
:style="cameraFOVStyleLeftHalf"
/>
</g>
</g>
</g>
<svg
class="c-nsew__ticks"
viewBox="0 0 100 100"
<!-- Spacecraft body -->
<path v-if="hasHeading"
class="c-cr__spacecraft-body"
fill-rule="evenodd"
clip-rule="evenodd"
d="M37 49C35.3431 49 34 50.3431 34 52V82C34 83.6569 35.3431 85 37 85H63C64.6569 85 66 83.6569 66 82V52C66 50.3431 64.6569 49 63 49H37ZM50 52L58 60H55V67H45V60H42L50 52Z"
:style="headingStyle"
/>
<!-- NSEW and ticks -->
<g class="c-cr__nsew"
:style="compassRoseStyle"
>
<polygon
class="c-nsew__tick c-tick-n"
points="50,0 60,10 40,10"
<g class="c-cr__ticks-major">
<path d="M50 3L43 10H57L50 3Z" />
<path d="M4 51V49H10V51H4Z"
class="--hide-min"
/>
<path d="M49 96V90H51V96H49Z"
class="--hide-min"
/>
<path d="M90 49V51H96V49H90Z"
class="--hide-min"
/>
</g>
<g class="c-cr__ticks-minor --hide-small">
<path d="M4 51V49H10V51H4Z" />
<path d="M90 51V49H96V51H90Z" />
<path d="M51 96H49V90H51V96Z" />
<path d="M51 10L49 10V4L51 4V10Z" />
</g>
<g class="c-cr__nsew-text">
<path :style="cardinalTextRotateW"
class="c-cr__nsew-w --hide-small"
d="M56.7418 45.004H54.1378L52.7238 52.312H52.6958L51.2258 45.004H48.7758L47.3058 52.312H47.2778L45.8638 45.004H43.2598L45.9618 55H48.6078L49.9798 48.112H50.0078L51.3798 55H53.9838L56.7418 45.004Z"
/>
<path :style="cardinalTextRotateE"
class="c-cr__nsew-e --hide-small"
d="M46.104 55H54.21V52.76H48.708V50.856H53.608V48.84H48.708V47.09H54.07V45.004H46.104V55Z"
/>
<path :style="cardinalTextRotateS"
class="c-cr__nsew-s --hide-small"
d="M45.6531 51.64C45.6671 54.202 47.6971 55.21 49.9931 55.21C52.1911 55.21 54.3471 54.398 54.3471 51.864C54.3471 50.058 52.8911 49.386 51.4491 48.98C49.9931 48.574 48.5511 48.434 48.5511 47.664C48.5511 47.006 49.2511 46.81 49.8111 46.81C50.6091 46.81 51.4631 47.104 51.4211 48.014H54.0251C54.0111 45.76 52.0091 44.794 50.0211 44.794C48.1451 44.794 45.9471 45.648 45.9471 47.832C45.9471 49.666 47.4451 50.31 48.8731 50.716C50.3151 51.122 51.7431 51.29 51.7431 52.172C51.7431 52.914 50.9311 53.194 50.1471 53.194C49.0411 53.194 48.3131 52.816 48.2571 51.64H45.6531Z"
/>
<path :style="cardinalTextRotateN"
class="c-cr__nsew-n"
d="M42.5935 60H46.7935V49.32H46.8415L52.7935 60H57.3775V42.864H53.1775V53.424H53.1295L47.1775 42.864H42.5935V60Z"
/>
</g>
</g>
</g>
<defs>
<radialGradient id="paint0_radial"
cx="0"
cy="0"
r="1"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(50 50) rotate(90) scale(50)"
>
<stop offset="0.751387"
stop-opacity="0"
/>
<rect
class="c-nsew__tick c-tick-e"
x="95"
y="49"
width="5"
height="2"
<stop offset="1"
stop-color="white"
/>
<rect
class="c-nsew__tick c-tick-w"
x="0"
y="49"
width="5"
height="2"
</radialGradient>
<radialGradient id="paint1_radial"
cx="0"
cy="0"
r="1"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(50 -7) rotate(-90) scale(18.5)"
>
<stop offset="0.716377"
stop-color="#FFCC00"
/>
<rect
class="c-nsew__tick c-tick-s"
x="49"
y="95"
width="2"
height="5"
<stop offset="1"
stop-color="#FF9900"
stop-opacity="0"
/>
<text
class="c-nsew__label c-label-n"
text-anchor="middle"
:transform="northTextTransform"
>N</text>
<text
class="c-nsew__label c-label-e"
text-anchor="middle"
:transform="eastTextTransform"
>E</text>
<text
class="c-nsew__label c-label-w"
text-anchor="middle"
:transform="southTextTransform"
>W</text>
<text
class="c-nsew__label c-label-s"
text-anchor="middle"
:transform="westTextTransform"
>S</text>
</svg>
</div>
<div
v-if="hasHeading"
class="c-spacecraft-body"
:style="headingStyle"
>
</div>
<div
v-if="hasSunHeading"
class="c-sun"
:style="sunHeadingStyle"
></div>
<div
class="c-cam-field"
:style="cameraPanStyle"
>
<div class="cam-field-half cam-field-half-l">
<div
class="cam-field-area"
:style="cameraFOVStyleLeftHalf"
></div>
</div>
<div class="cam-field-half cam-field-half-r">
<div
class="cam-field-area"
:style="cameraFOVStyleRightHalf"
></div>
</div>
</div>
</div>
</radialGradient>
</defs>
</svg>
</div>
</template>
<script>
import { rotate } from './utils';
import { throttle } from 'lodash';
export default {
props: {
sizedImageWidth: {
type: Number,
compassRoseSizingClasses: {
type: String,
required: true
},
heading: {
type: Number,
required: true
required: true,
default() {
return 0;
}
},
sunHeading: {
type: Number,
@@ -178,58 +229,39 @@ export default {
},
cameraPan: {
type: Number,
required: true
required: true,
default() {
return 0;
}
},
lockCompass: {
type: Boolean,
sizedImageDimensions: {
type: Object,
required: true
}
},
data() {
return {
lockCompass: true
};
},
computed: {
compassRoseSizingClasses() {
let compassRoseSizingClasses = '';
if (this.sizedImageWidth < 300) {
compassRoseSizingClasses = '--rose-small --rose-min';
} else if (this.sizedImageWidth < 500) {
compassRoseSizingClasses = '--rose-small';
} else if (this.sizedImageWidth > 1000) {
compassRoseSizingClasses = '--rose-max';
}
return compassRoseSizingClasses;
},
compassRoseStyle() {
return { transform: `rotate(${ this.north }deg)` };
},
north() {
return this.lockCompass ? rotate(-this.cameraPan) : 0;
},
northTextTransform() {
return this.cardinalPointsTextTransform.north;
cardinalTextRotateN() {
return { transform: `translateY(-27%) rotate(${ -this.north }deg)` };
},
eastTextTransform() {
return this.cardinalPointsTextTransform.east;
cardinalTextRotateS() {
return { transform: `translateY(30%) rotate(${ -this.north }deg)` };
},
southTextTransform() {
return this.cardinalPointsTextTransform.south;
cardinalTextRotateE() {
return { transform: `translateX(30%) rotate(${ -this.north }deg)` };
},
westTextTransform() {
return this.cardinalPointsTextTransform.west;
},
cardinalPointsTextTransform() {
/**
* cardinal points text must be rotated
* in the opposite direction that north is rotated
* to keep text vertically oriented
*/
const rotation = `rotate(${ -this.north })`;
return {
north: `translate(50,23) ${ rotation }`,
east: `translate(82,50) ${ rotation }`,
south: `translate(18,50) ${ rotation }`,
west: `translate(50,82) ${ rotation }`
};
cardinalTextRotateW() {
return { transform: `translateX(-30%) rotate(${ -this.north }deg)` };
},
hasHeading() {
return this.heading !== undefined;
@@ -238,7 +270,7 @@ export default {
const rotation = rotate(this.north, this.heading);
return {
transform: `translateX(-50%) rotate(${ rotation }deg)`
transform: `rotate(${ rotation }deg)`
};
},
hasSunHeading() {
@@ -262,20 +294,37 @@ export default {
// rotated counter-clockwise from camera pan angle
cameraFOVStyleLeftHalf() {
return {
transform: `translateX(50%) rotate(${ -this.cameraAngleOfView / 2 }deg)`
transform: `rotate(${ this.cameraAngleOfView / 2 }deg)`
};
},
// right half of camera field of view
// rotated clockwise from camera pan angle
cameraFOVStyleRightHalf() {
return {
transform: `translateX(-50%) rotate(${ this.cameraAngleOfView / 2 }deg)`
transform: `rotate(${ -this.cameraAngleOfView / 2 }deg)`
};
}
},
watch: {
sizedImageDimensions() {
this.debounceResizeSvg();
}
},
mounted() {
this.debounceResizeSvg = throttle(this.resizeSvg, 100);
this.$nextTick(() => {
this.debounceResizeSvg();
});
},
methods: {
resizeSvg() {
const svg = this.$refs.compassRoseSvg;
svg.setAttribute('width', this.$refs.compassRoseWrapper.clientWidth);
svg.setAttribute('height', this.$refs.compassRoseWrapper.clientHeight);
},
toggleLockCompass() {
this.$emit('toggle-lock-compass');
this.lockCompass = !this.lockCompass;
}
}
};

View File

@@ -12,9 +12,8 @@ $elemBg: rgba(black, 0.7);
.c-compass {
pointer-events: none; // This allows the image element to receive a browser-level context click
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
left: 0;
top: 0;
z-index: 1;
@include userSelectNone;
}
@@ -81,114 +80,55 @@ $elemBg: rgba(black, 0.7);
transform: translateX(-50%);
z-index: 1;
}
}
/***************************** COMPASS DIRECTIONS */
.c-nsew {
/***************************** COMPASS SVG */
.c-compass-rose-svg {
$color: $interfaceKeyColor;
$inset: 5%;
$tickHeightPerc: 15%;
text-shadow: black 0 0 10px;
top: $inset;
right: $inset;
bottom: $inset;
left: $inset;
z-index: 3;
position: absolute;
top: 0; left: 0;
&__tick,
&__label {
fill: $color;
}
&__minor-ticks {
opacity: 0.5;
g, path, rect {
// In an SVG, rotation occurs about the center of the SVG, not the element
transform-origin: center;
transform: rotate(45deg);
}
&__label {
dominant-baseline: central;
font-size: 1.25em;
font-weight: bold;
}
.c-label-n {
font-size: 2em;
}
}
/***************************** CAMERA FIELD ANGLE */
.c-cam-field {
$color: white;
opacity: 0.3;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 2;
.cam-field-half {
top: 0;
right: 0;
bottom: 0;
left: 0;
.cam-field-area {
background: $color;
top: -30%;
right: 0;
bottom: -30%;
left: 0;
.c-cr {
&__bg {
fill: #000;
opacity: 0.8;
}
// clip-paths overlap a bit to avoid a gap between halves
&-l {
clip-path: polygon(0 0, 50.5% 0, 50.5% 100%, 0 100%);
.cam-field-area {
transform-origin: left center;
}
&__edge {
opacity: 0.1;
}
&-r {
clip-path: polygon(49.5% 0, 100% 0, 100% 100%, 49.5% 100%);
.cam-field-area {
transform-origin: right center;
}
&__sun {
opacity: 0.7;
}
}
}
/***************************** SPACECRAFT BODY */
.c-spacecraft-body {
$color: $interfaceKeyColor;
$s: 30%;
background: $color;
border-radius: 3px;
height: $s;
width: $s;
left: 50%;
top: 50%;
opacity: 0.4;
transform-origin: center top;
transform: translateX(-50%); // center by default, overridden by CompassRose.vue / headingStyle()
&__cam-fov-l,
&__cam-fov-r {
// Cam FOV indication
opacity: 0.2;
fill: #fff;
}
&:before {
// Direction arrow
$color: rgba(black, 0.5);
$arwPointerY: 60%;
$arwBodyOffset: 25%;
background: $color;
content: '';
display: block;
position: absolute;
top: 10%;
right: 20%;
bottom: 50%;
left: 20%;
clip-path: polygon(50% 0, 100% $arwPointerY, 100%-$arwBodyOffset $arwPointerY, 100%-$arwBodyOffset 100%, $arwBodyOffset 100%, $arwBodyOffset $arwPointerY, 0 $arwPointerY);
&__nsew-text,
&__spacecraft-body,
&__ticks-major,
&__ticks-minor {
fill: $color;
}
&__ticks-minor {
opacity: 0.5;
transform: rotate(45deg);
}
&__spacecraft-body {
opacity: 0.3;
}
}
}
@@ -196,32 +136,28 @@ $elemBg: rgba(black, 0.7);
.w-direction-rose {
$s: 10%;
$m: 2%;
cursor: pointer;
pointer-events: all;
position: absolute;
bottom: $m;
left: $m;
width: $s;
padding-top: $s;
z-index: 2;
&.--rose-min {
$s: 30px;
width: $s;
padding-top: $s;
.--hide-min {
display: none;
}
}
&.--rose-small {
.c-nsew__minor-ticks,
.c-tick-w,
.c-tick-s,
.c-tick-e,
.c-label-w,
.c-label-s,
.c-label-e {
.--hide-small {
display: none;
}
.c-label-n {
font-size: 2.5em;
}
}
&.--rose-max {
@@ -230,44 +166,3 @@ $elemBg: rgba(black, 0.7);
padding-top: $s;
}
}
.c-direction-rose {
$c2: rgba(white, 0.1);
background: $elemBg;
background-image: radial-gradient(circle closest-side, transparent, transparent 80%, $c2);
transform-origin: 0 0;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
clip-path: circle(50% at 50% 50%);
border-radius: 100%;
pointer-events: all;
svg, div {
position: absolute;
}
// Sun
.c-sun {
top: 0;
right: 0;
bottom: 0;
left: 0;
&:before {
$s: 35%;
@include sun();
content: '';
display: block;
position: absolute;
opacity: 0.7;
top: 0;
left: 50%;
height: $s;
width: $s;
transform: translate(-50%, -60%);
}
}
}

View File

@@ -55,28 +55,34 @@
></a>
</span>
</div>
<div class="c-imagery__main-image__bg"
<div ref="imageBG"
class="c-imagery__main-image__bg"
:class="{'paused unnsynced': isPaused,'stale':false }"
@click="expand"
>
<img
ref="focusedImage"
class="c-imagery__main-image__image js-imageryView-image"
:src="imageUrl"
:style="{
'filter': `brightness(${filters.brightness}%) contrast(${filters.contrast}%)`
}"
:data-openmct-image-timestamp="time"
:data-openmct-object-keystring="keyString"
<div class="image-wrapper"
:style="{
'width': `${sizedImageDimensions.width}px`,
'height': `${sizedImageDimensions.height}px`
}"
>
<Compass
v-if="shouldDisplayCompass"
:container-width="imageContainerWidth"
:container-height="imageContainerHeight"
:natural-aspect-ratio="focusedImageNaturalAspectRatio"
:image="focusedImage"
:lock-compass="lockCompass"
@toggle-lock-compass="toggleLockCompass"
/>
<img ref="focusedImage"
class="c-imagery__main-image__image js-imageryView-image"
:src="imageUrl"
:style="{
'filter': `brightness(${filters.brightness}%) contrast(${filters.contrast}%)`
}"
:data-openmct-image-timestamp="time"
:data-openmct-object-keystring="keyString"
>
<Compass
v-if="shouldDisplayCompass"
:compass-rose-sizing-classes="compassRoseSizingClasses"
:image="focusedImage"
:natural-aspect-ratio="focusedImageNaturalAspectRatio"
:sized-image-dimensions="sizedImageDimensions"
/>
</div>
</div>
<div class="c-local-controls c-local-controls--show-on-hover c-imagery__prev-next-buttons">
<button class="c-nav c-nav--prev"
@@ -124,27 +130,40 @@
</div>
</div>
<div
ref="thumbsWrapper"
class="c-imagery__thumbs-wrapper"
:class="{'is-paused': isPaused}"
@scroll="handleScroll"
:class="[
{ 'is-paused': isPaused },
{ 'is-autoscroll-off': !resizingWindow && !autoScroll && !isPaused }
]"
>
<div v-for="(image, index) in imageHistory"
:key="image.url + image.time"
class="c-imagery__thumb c-thumb"
:class="{ selected: focusedImageIndex === index && isPaused }"
@click="setFocusedImage(index, thumbnailClick)"
<div
ref="thumbsWrapper"
class="c-imagery__thumbs-scroll-area"
@scroll="handleScroll"
>
<a href=""
:download="image.imageDownloadName"
@click.prevent
<div v-for="(image, index) in imageHistory"
:key="image.url + image.time"
class="c-imagery__thumb c-thumb"
:class="{ selected: focusedImageIndex === index && isPaused }"
@click="setFocusedImage(index, thumbnailClick)"
>
<img class="c-thumb__image"
:src="image.url"
<a href=""
:download="image.imageDownloadName"
@click.prevent
>
</a>
<div class="c-thumb__timestamp">{{ image.formattedTime }}</div>
<img class="c-thumb__image"
:src="image.url"
>
</a>
<div class="c-thumb__timestamp">{{ image.formattedTime }}</div>
</div>
</div>
<button
class="c-imagery__auto-scroll-resume-button c-icon-button icon-play"
title="Resume automatic scrolling of image thumbnails"
@click="scrollToRight('reset')"
></button>
</div>
</div>
</template>
@@ -152,8 +171,9 @@
<script>
import _ from 'lodash';
import moment from 'moment';
import Compass from './Compass/Compass.vue';
import RelatedTelemetry from './RelatedTelemetry/RelatedTelemetry';
import Compass from './Compass/Compass.vue';
const DEFAULT_DURATION_FORMATTER = 'duration';
const REFRESH_CSS_MS = 500;
@@ -171,11 +191,13 @@ const TWENTYFOUR_HOURS = EIGHT_HOURS * 3;
const ARROW_RIGHT = 39;
const ARROW_LEFT = 37;
const SCROLL_LATENCY = 250;
export default {
components: {
Compass
},
inject: ['openmct', 'domainObject'],
inject: ['openmct', 'domainObject', 'objectPath', 'currentView'],
data() {
let timeSystem = this.openmct.time.timeSystem();
@@ -204,10 +226,23 @@ export default {
focusedImageNaturalAspectRatio: undefined,
imageContainerWidth: undefined,
imageContainerHeight: undefined,
lockCompass: true
lockCompass: true,
resizingWindow: false
};
},
computed: {
compassRoseSizingClasses() {
let compassRoseSizingClasses = '';
if (this.sizedImageDimensions.width < 300) {
compassRoseSizingClasses = '--rose-small --rose-min';
} else if (this.sizedImageDimensions.width < 500) {
compassRoseSizingClasses = '--rose-small';
} else if (this.sizedImageDimensions.width > 1000) {
compassRoseSizingClasses = '--rose-max';
}
return compassRoseSizingClasses;
},
time() {
return this.formatTime(this.focusedImage);
},
@@ -331,6 +366,20 @@ export default {
}
return isFresh;
},
sizedImageDimensions() {
let sizedImageDimensions = {};
if ((this.imageContainerWidth / this.imageContainerHeight) > this.focusedImageNaturalAspectRatio) {
// container is wider than image
sizedImageDimensions.width = this.imageContainerHeight * this.focusedImageNaturalAspectRatio;
sizedImageDimensions.height = this.imageContainerHeight;
} else {
// container is taller than image
sizedImageDimensions.width = this.imageContainerWidth;
sizedImageDimensions.height = this.imageContainerWidth * this.focusedImageNaturalAspectRatio;
}
return sizedImageDimensions;
}
},
watch: {
@@ -379,10 +428,14 @@ export default {
_.debounce(this.resizeImageContainer, 400);
this.imageContainerResizeObserver = new ResizeObserver(this.resizeImageContainer);
this.imageContainerResizeObserver.observe(this.$refs.focusedImage);
},
updated() {
this.scrollToRight();
this.imageContainerResizeObserver.observe(this.$refs.imageBG);
// For adjusting scroll bar size and position when resizing thumbs wrapper
this.handleScroll = _.debounce(this.handleScroll, SCROLL_LATENCY);
this.handleThumbWindowResizeEnded = _.debounce(this.handleThumbWindowResizeEnded, SCROLL_LATENCY);
this.thumbWrapperResizeObserver = new ResizeObserver(this.handleThumbWindowResizeStart);
this.thumbWrapperResizeObserver.observe(this.$refs.thumbsWrapper);
},
beforeDestroy() {
if (this.unsubscribe) {
@@ -394,6 +447,10 @@ export default {
this.imageContainerResizeObserver.disconnect();
}
if (this.thumbWrapperResizeObserver) {
this.thumbWrapperResizeObserver.disconnect();
}
if (this.relatedTelemetry.hasRelatedTelemetry) {
this.relatedTelemetry.destroy();
}
@@ -413,6 +470,16 @@ export default {
}
},
methods: {
expand() {
const actionCollection = this.openmct.actions.getActionsCollection(this.objectPath, this.currentView);
const visibleActions = actionCollection.getVisibleActions();
const viewLargeAction = visibleActions
&& visibleActions.find(action => action.key === 'large.view');
if (viewLargeAction && viewLargeAction.appliesTo(this.objectPath, this.currentView)) {
viewLargeAction.onItemClicked();
}
},
async initializeRelatedTelemetry() {
this.relatedTelemetry = new RelatedTelemetry(
this.openmct,
@@ -561,17 +628,15 @@ export default {
},
handleScroll() {
const thumbsWrapper = this.$refs.thumbsWrapper;
if (!thumbsWrapper) {
if (!thumbsWrapper || this.resizingWindow) {
return;
}
const { scrollLeft, scrollWidth, clientWidth, scrollTop, scrollHeight, clientHeight } = thumbsWrapper;
const disableScroll = (scrollWidth - scrollLeft) > 2 * clientWidth
|| (scrollHeight - scrollTop) > 2 * clientHeight;
const { scrollLeft, scrollWidth, clientWidth } = thumbsWrapper;
const disableScroll = scrollWidth > Math.ceil(scrollLeft + clientWidth);
this.autoScroll = !disableScroll;
},
paused(state, type) {
this.isPaused = state;
if (type === 'button') {
@@ -584,6 +649,7 @@ export default {
}
this.autoScroll = true;
this.scrollToRight();
},
scrollToFocused() {
const thumbsWrapper = this.$refs.thumbsWrapper;
@@ -600,8 +666,8 @@ export default {
});
}
},
scrollToRight() {
if (this.isPaused || !this.$refs.thumbsWrapper || !this.autoScroll) {
scrollToRight(type) {
if (type !== 'reset' && (this.isPaused || !this.$refs.thumbsWrapper || !this.autoScroll)) {
return;
}
@@ -610,7 +676,9 @@ export default {
return;
}
setTimeout(() => this.$refs.thumbsWrapper.scrollLeft = scrollWidth, 0);
this.$nextTick(() => {
this.$refs.thumbsWrapper.scrollLeft = scrollWidth;
});
},
setFocusedImage(index, thumbnailClick = false) {
if (this.isPaused && !thumbnailClick) {
@@ -678,9 +746,9 @@ export default {
image.imageDownloadName = this.getImageDownloadName(datum);
this.imageHistory.push(image);
if (setFocused) {
this.setFocusedImage(this.imageHistory.length - 1);
this.scrollToRight();
}
},
getFormatter(key) {
@@ -808,16 +876,31 @@ export default {
}, { once: true });
},
resizeImageContainer() {
if (this.$refs.focusedImage.clientWidth !== this.imageContainerWidth) {
this.imageContainerWidth = this.$refs.focusedImage.clientWidth;
if (this.$refs.imageBG.clientWidth !== this.imageContainerWidth) {
this.imageContainerWidth = this.$refs.imageBG.clientWidth;
}
if (this.$refs.focusedImage.clientHeight !== this.imageContainerHeight) {
this.imageContainerHeight = this.$refs.focusedImage.clientHeight;
if (this.$refs.imageBG.clientHeight !== this.imageContainerHeight) {
this.imageContainerHeight = this.$refs.imageBG.clientHeight;
}
},
toggleLockCompass() {
this.lockCompass = !this.lockCompass;
handleThumbWindowResizeStart() {
if (!this.autoScroll) {
return;
}
// To hide resume button while scrolling
this.resizingWindow = true;
this.handleThumbWindowResizeEnded();
},
handleThumbWindowResizeEnded() {
if (!this.isPaused) {
this.scrollToRight('reset');
}
this.$nextTick(() => {
this.resizingWindow = false;
});
}
}
};

View File

@@ -22,6 +22,9 @@
&__bg {
background-color: $colorPlotBg;
border: 1px solid transparent;
display: flex;
align-items: center;
justify-content: center;
flex: 1 1 auto;
height: 0;
@@ -33,7 +36,6 @@
&__image {
height: 100%;
width: 100%;
object-fit: contain;
}
}
@@ -93,24 +95,43 @@
}
&__thumbs-wrapper {
flex: 0 0 auto;
display: flex; // Uses row layout
&.is-autoscroll-off {
background: $colorInteriorBorder;
[class*='__auto-scroll-resume-button'] {
display: block;
}
}
&.is-paused {
background: rgba($colorPausedBg, 0.4);
}
}
&__thumbs-scroll-area {
flex: 0 1 auto;
display: flex;
flex-direction: row;
height: 135px;
overflow-x: auto;
overflow-y: hidden;
margin-bottom: 1px;
padding-bottom: $interiorMarginSm;
&.is-paused {
background: rgba($colorPausedBg, 0.4);
}
.c-thumb:last-child {
// Hilite the lastest thumb
background: $colorBodyFg;
color: $colorBodyBg;
}
}
&__auto-scroll-resume-button {
display: none; // Set to block when __thumbs-wrapper has .is-autoscroll-off
flex: 0 0 auto;
font-size: 0.8em;
margin: $interiorMarginSm;
}
}
/*************************************** THUMBS */
@@ -142,7 +163,7 @@
.l-layout,
.c-fl {
.c-imagery__thumbs-wrapper {
.c-imagery__thumbs-scroll-area {
// When Imagery is in a layout, hide the thumbs area
display: none;
}
@@ -173,6 +194,10 @@
margin-right: $interiorMarginSm;
}
}
.s-status-taking-snapshot & {
display: none;
}
}
&__lc {
@@ -254,6 +279,10 @@
content: $glyph-icon-play;
}
}
.s-status-taking-snapshot & {
display: none;
}
}
.c-imagery__prev-next-buttons {
@@ -268,6 +297,10 @@
.c-nav {
pointer-events: all;
}
.s-status-taking-snapshot & {
display: none;
}
}
.c-nav {

View File

@@ -92,6 +92,7 @@ describe("The Imagery View Layout", () => {
let resolveFunction;
let openmct;
let appHolder;
let parent;
let child;
let imageTelemetry = generateTelemetry(START - TEN_MINUTES, COUNT);
@@ -195,7 +196,7 @@ describe("The Imagery View Layout", () => {
// this setups up the app
beforeEach((done) => {
const appHolder = document.createElement('div');
appHolder = document.createElement('div');
appHolder.style.width = '640px';
appHolder.style.height = '480px';
@@ -209,6 +210,8 @@ describe("The Imagery View Layout", () => {
child = document.createElement('div');
parent.appendChild(child);
// document.querySelector('body').append(parent);
spyOn(window, 'ResizeObserver').and.returnValue({
observe() {},
disconnect() {}
@@ -277,7 +280,7 @@ describe("The Imagery View Layout", () => {
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 1].timeId)).not.toEqual(-1);
});
it("should show the clicked thumbnail as the main image", (done) => {
xit("should show the clicked thumbnail as the main image", (done) => {
const target = imageTelemetry[5].url;
parent.querySelectorAll(`img[src='${target}']`)[0].click();
Vue.nextTick(() => {
@@ -314,7 +317,7 @@ describe("The Imagery View Layout", () => {
});
});
it("should navigate via arrow keys", (done) => {
xit("should navigate via arrow keys", (done) => {
let keyOpts = {
element: parent.querySelector('.c-imagery'),
key: 'ArrowLeft',
@@ -362,5 +365,21 @@ describe("The Imagery View Layout", () => {
done();
});
});
it ('shows an auto scroll button when scroll to left', async () => {
// to mock what a scroll would do
imageryView._getInstance().$refs.ImageryLayout.autoScroll = false;
await Vue.nextTick();
let autoScrollButton = parent.querySelector('.c-imagery__auto-scroll-resume-button');
expect(autoScrollButton).toBeTruthy();
});
it ('scrollToRight is called when clicking on auto scroll button', async () => {
// use spyon to spy the scroll function
spyOn(imageryView._getInstance().$refs.ImageryLayout, 'scrollToRight');
imageryView._getInstance().$refs.ImageryLayout.autoScroll = false;
await Vue.nextTick();
parent.querySelector('.c-imagery__auto-scroll-resume-button').click();
expect(imageryView._getInstance().$refs.ImageryLayout.scrollToRight).toHaveBeenCalledWith('reset');
});
});
});

View File

@@ -25,16 +25,20 @@ export default class CopyToNotebookAction {
});
}
invoke(objectPath, view = {}) {
let viewContext = view.getViewContext && view.getViewContext();
invoke(objectPath, view) {
const formattedValueForCopy = view.getViewContext().row.formattedValueForCopy;
this.copyToNotebook(viewContext.formattedValueForCopy());
this.copyToNotebook(formattedValueForCopy());
}
appliesTo(objectPath, view = {}) {
let viewContext = view.getViewContext && view.getViewContext();
const viewContext = view.getViewContext && view.getViewContext();
const row = viewContext && viewContext.row;
if (!row) {
return;
}
return viewContext && viewContext.formattedValueForCopy
&& typeof viewContext.formattedValueForCopy === 'function';
return row.formattedValueForCopy
&& typeof row.formattedValueForCopy === 'function';
}
}

View File

@@ -31,6 +31,7 @@ import PainterroInstance from '../utils/painterroInstance';
import SnapshotTemplate from './snapshot-template.html';
import { updateNotebookImageDomainObject } from '../utils/notebook-image';
import ImageExporter from '../../../exporters/ImageExporter';
import PopupMenu from './PopupMenu.vue';
import Vue from 'vue';
@@ -71,7 +72,7 @@ export default {
},
mounted() {
this.addPopupMenuItems();
this.exportImageService = this.openmct.$injector.get('exportImageService');
this.imageExporter = new ImageExporter(this.openmct);
},
methods: {
addPopupMenuItems() {
@@ -101,7 +102,6 @@ export default {
buttons: [
{
label: 'Cancel',
emphasis: true,
callback: () => {
painterroInstance.dismiss();
annotateOverlay.dismiss();
@@ -109,6 +109,7 @@ export default {
},
{
label: 'Save',
emphasis: true,
callback: () => {
painterroInstance.save((snapshotObject) => {
annotateOverlay.dismiss();
@@ -234,9 +235,9 @@ export default {
let element = this.snapshot.$refs['snapshot-image'];
if (type === 'png') {
this.exportImageService.exportPNG(element, this.embed.name);
this.imageExporter.exportPNG(element, this.embed.name);
} else {
this.exportImageService.exportJPG(element, this.embed.name);
this.imageExporter.exportJPG(element, this.embed.name);
}
},
previewEmbed() {

View File

@@ -80,7 +80,7 @@ export default {
notebookTypes.push({
cssClass: 'icon-notebook',
name: `Save to Notebook ${defaultPath}`,
callBack: () => {
onItemClicked: () => {
return this.snapshot(NOTEBOOK_DEFAULT);
}
});
@@ -89,7 +89,7 @@ export default {
notebookTypes.push({
cssClass: 'icon-camera',
name: 'Save to Notebook Snapshots',
callBack: () => {
onItemClicked: () => {
return this.snapshot(NOTEBOOK_SNAPSHOT);
}
});

View File

@@ -7,10 +7,10 @@
<div class="c-object-label__type-icon icon-camera"></div>
<div class="c-object-label__name">
Notebook Snapshots
<span v-if="snapshots.length"
class="l-browse-bar__object-details"
>&nbsp;{{ snapshots.length }} of {{ getNotebookSnapshotMaxCount() }}
</span>
</div>
<div v-if="snapshots.length"
class="l-browse-bar__object-details"
>{{ snapshots.length }} of {{ getNotebookSnapshotMaxCount() }}
</div>
</div>
<PopupMenu v-if="snapshots.length > 0"

View File

@@ -4,8 +4,10 @@
<div class="l-browse-bar__start">
<div class="l-browse-bar__object-name--w">
<span class="c-object-label l-browse-bar__object-name"
v-bind:class="cssClass"
>
<span class="c-object-label__type-icon"
v-bind:class="cssClass"
></span>
<span class="c-object-label__name">{{ name }}</span>
</span>
</div>

View File

@@ -117,6 +117,10 @@ export default function NotebookPlugin() {
key: 'notebook-snapshot-indicator'
};
openmct.once('destroy', () => {
snapshotContainer.destroy();
});
openmct.indicators.add(indicator);
openmct.objectViews.addProvider({

View File

@@ -80,4 +80,8 @@ export default class SnapshotContainer extends EventEmitter {
return this.saveSnapshots(updatedSnapshots);
}
destroy() {
delete SnapshotContainer.instance;
}
}

View File

@@ -4,24 +4,24 @@ import { NOTEBOOK_DEFAULT } from '@/plugins/notebook/notebook-constants';
import { createNotebookImageDomainObject, DEFAULT_SIZE } from './utils/notebook-image';
import SnapshotContainer from './snapshot-container';
import ImageExporter from '../../exporters/ImageExporter';
export default class Snapshot {
constructor(openmct) {
this.openmct = openmct;
this.snapshotContainer = new SnapshotContainer(openmct);
this.imageExporter = new ImageExporter(openmct);
this.capture = this.capture.bind(this);
this._saveSnapShot = this._saveSnapShot.bind(this);
}
capture(snapshotMeta, notebookType, domElement) {
const exportImageService = this.openmct.$injector.get('exportImageService');
const options = {
className: 's-status-taking-snapshot',
thumbnailSize: DEFAULT_SIZE
};
exportImageService.exportPNGtoSRC(domElement, options)
this.imageExporter.exportPNGtoSRC(domElement, options)
.then(function ({blob, thumbnail}) {
const reader = new window.FileReader();
reader.readAsDataURL(blob);

View File

@@ -1,3 +1,25 @@
<!--
Open MCT, Copyright (c) 2014-2020, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT is licensed under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
Open MCT includes source code licensed under additional open source
licenses. See the Open Source Licenses file (LICENSES.md) included with
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<template>
<div ref="plan"
class="c-plan c-timeline-holder"
@@ -28,7 +50,6 @@ import SwimLane from "@/ui/components/swim-lane/SwimLane.vue";
import { getValidatedPlan } from "./util";
import Vue from "vue";
//TODO: UI direction needed for the following property values
const PADDING = 1;
const OUTER_TEXT_PADDING = 12;
const INNER_TEXT_PADDING = 17;
@@ -281,7 +302,9 @@ export default {
exceeds: {
start: this.xScale(this.viewBounds.start) > this.xScale(activity.start),
end: this.xScale(this.viewBounds.end) < this.xScale(activity.end)
}
},
start: activity.start,
end: activity.end
},
textLines: textLines,
textStart: textStart,
@@ -339,6 +362,9 @@ export default {
components: {
SwimLane
},
provide: {
openmct: this.openmct
},
data() {
return {
heading,
@@ -376,7 +402,6 @@ export default {
activityRows.forEach((row) => {
const items = activitiesByRow[row];
items.forEach(item => {
//TODO: Don't draw the left-border of the rectangle if the activity started before viewBounds.start
this.plotActivity(item, parseInt(row, 10), groupSVG);
});
});
@@ -399,6 +424,9 @@ export default {
element.setAttributeNS(null, key, attributes[key]);
});
},
getNSAttributesForElement(element, attribute) {
return element.getAttributeNS(null, attribute);
},
// Experimental for now - unused
addForeignElement(svgElement, label, x, y) {
let foreign = document.createElementNS('http://www.w3.org/2000/svg', "foreignObject");
@@ -443,6 +471,10 @@ export default {
fill: activity.color
});
rectElement.addEventListener('click', (event) => {
this.setSelectionForActivity(event.currentTarget, activity, event.metaKey);
});
svgElement.appendChild(rectElement);
item.textLines.forEach((line, index) => {
@@ -456,6 +488,9 @@ export default {
const textNode = document.createTextNode(line);
textElement.appendChild(textNode);
textElement.addEventListener('click', (event) => {
this.setSelectionForActivity(event.currentTarget, activity, event.metaKey);
});
svgElement.appendChild(textElement);
});
// this.addForeignElement(svgElement, activity.name, item.textStart, item.textY - LINE_HEIGHT);
@@ -482,6 +517,22 @@ export default {
const cBrightness = ((hR * 299) + (hG * 587) + (hB * 114)) / 1000;
return cBrightness > cThreshold ? "#000000" : "#ffffff";
},
setSelectionForActivity(element, activity, multiSelect) {
this.openmct.selection.select([{
element: element,
context: {
type: 'activity',
activity: activity
}
}, {
element: this.openmct.layout.$refs.browseObject.$el,
context: {
item: this.domainObject,
supportsMultiSelect: true
}
}], multiSelect);
event.stopPropagation();
}
}
};

View File

@@ -0,0 +1,52 @@
<!--
Open MCT, Copyright (c) 2014-2020, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT is licensed under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
Open MCT includes source code licensed under additional open source
licenses. See the Open Source Licenses file (LICENSES.md) included with
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<template>
<li class="c-inspect-properties__row">
<div class="c-inspect-properties__label">
{{ label }}
</div>
<div class="c-inspect-properties__value">
{{ value }}
</div>
</li>
</template>
<script>
export default {
props: {
label: {
type: String,
default() {
return '';
}
},
value: {
type: String,
default() {
return '';
}
}
}
};
</script>

View File

@@ -0,0 +1,206 @@
<!--
Open MCT, Copyright (c) 2014-2021, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT is licensed under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
Open MCT includes source code licensed under additional open source
licenses. See the Open Source Licenses file (LICENSES.md) included with
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<template>
<div class="c-inspector__properties c-inspect-properties">
<plan-activity-view v-for="activity in activities"
:key="activity.id"
:activity="activity"
:heading="heading"
/>
</div>
</template>
<script>
import PlanActivityView from "./PlanActivityView.vue";
import { getPreciseDuration } from "utils/duration";
import uuid from 'uuid';
const propertyLabels = {
'start': 'Start DateTime',
'end': 'End DateTime',
'duration': 'Duration',
'earliestStart': 'Earliest Start',
'latestEnd': 'Latest End',
'gap': 'Gap',
'overlap': 'Overlap',
'totalTime': 'Total Time'
};
export default {
components: {
PlanActivityView
},
inject: ['openmct', 'selection'],
data() {
return {
name: '',
activities: [],
heading: ''
};
},
mounted() {
this.setFormatters();
this.getPlanData(this.selection);
this.getActivities();
this.openmct.selection.on('change', this.updateSelection);
this.openmct.time.on('timeSystem', this.setFormatters);
},
beforeDestroy() {
this.openmct.selection.off('change', this.updateSelection);
this.openmct.time.off('timeSystem', this.setFormatters);
},
methods: {
setFormatters() {
let timeSystem = this.openmct.time.timeSystem();
this.timeFormatter = this.openmct.telemetry.getValueFormatter({
format: timeSystem.timeFormat
}).formatter;
},
updateSelection(newSelection) {
this.getPlanData(newSelection);
this.getActivities();
},
getPlanData(selection) {
this.selectedActivities = [];
selection.forEach((selectionItem) => {
if (selectionItem[0].context.type === 'activity') {
const activity = selectionItem[0].context.activity;
if (activity) {
this.selectedActivities.push(activity);
}
}
});
},
getActivities() {
if (this.selectedActivities.length <= 1) {
this.heading = 'Time';
this.setSingleActivityProperties();
} else {
this.heading = 'Convex Hull';
this.setMultipleActivityProperties();
}
},
setSingleActivityProperties() {
this.activities.splice(0);
this.selectedActivities.forEach((selectedActivity, index) => {
const activity = {
id: uuid(),
start: {
label: propertyLabels.start,
value: this.formatTime(selectedActivity.start)
},
end: {
label: propertyLabels.end,
value: this.formatTime(selectedActivity.end)
},
duration: {
label: propertyLabels.duration,
value: this.formatDuration(selectedActivity.end - selectedActivity.start)
}
};
this.$set(this.activities, index, activity);
});
},
sortFn(a, b) {
const numA = parseInt(a.start, 10);
const numB = parseInt(b.start, 10);
if (numA > numB) {
return 1;
}
if (numA < numB) {
return -1;
}
return 0;
},
setMultipleActivityProperties() {
this.activities.splice(0);
let earliestStart;
let latestEnd;
let gap;
let overlap;
//Sort by start time
let selectedActivities = this.selectedActivities.sort(this.sortFn);
selectedActivities.forEach((selectedActivity, index) => {
if (selectedActivities.length === 2 && index > 0) {
const previous = selectedActivities[index - 1];
//they're on different rows so there must be overlap
if (previous.end > selectedActivity.start) {
overlap = previous.end - selectedActivity.start;
} else if (previous.end < selectedActivity.start) {
gap = selectedActivity.start - previous.end;
}
}
if (index > 0) {
earliestStart = Math.min(earliestStart, selectedActivity.start);
latestEnd = Math.max(latestEnd, selectedActivity.end);
} else {
earliestStart = selectedActivity.start;
latestEnd = selectedActivity.end;
}
});
let totalTime = latestEnd - earliestStart;
const activity = {
id: uuid(),
'earliestStart': {
label: propertyLabels.earliestStart,
value: this.formatTime(earliestStart)
},
'latestEnd': {
label: propertyLabels.latestEnd,
value: this.formatTime(latestEnd)
}
};
if (gap) {
activity.gap = {
label: propertyLabels.gap,
value: this.formatDuration(gap)
};
} else if (overlap) {
activity.overlap = {
label: propertyLabels.overlap,
value: this.formatDuration(overlap)
};
}
activity.totalTime = {
label: propertyLabels.totalTime,
value: this.formatDuration(totalTime)
};
this.$set(this.activities, 0, activity);
},
formatDuration(duration) {
return getPreciseDuration(duration);
},
formatTime(time) {
return this.timeFormatter.format(time);
}
}
};
</script>

View File

@@ -0,0 +1,84 @@
<!--
Open MCT, Copyright (c) 2014-2020, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT is licensed under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
Open MCT includes source code licensed under additional open source
licenses. See the Open Source Licenses file (LICENSES.md) included with
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<template>
<div v-if="timeProperties.length"
class="u-contents"
>
<div class="c-inspect-properties__header">
{{ heading }}
</div>
<ul v-for="timeProperty in timeProperties"
:key="timeProperty.id"
class="c-inspect-properties__section"
>
<activity-property :label="timeProperty.label"
:value="timeProperty.value"
/>
</ul>
</div>
</template>
<script>
import ActivityProperty from './ActivityProperty.vue';
import uuid from 'uuid';
export default {
components: {
ActivityProperty
},
props: {
activity: {
type: Object,
required: true
},
heading: {
type: String,
required: true
}
},
data() {
return {
timeProperties: []
};
},
mounted() {
this.setProperties();
},
methods: {
setProperties() {
Object.keys(this.activity).forEach((key) => {
if (this.activity[key].label) {
const label = this.activity[key].label;
const value = String(this.activity[key].value);
this.$set(this.timeProperties, this.timeProperties.length, {
id: uuid(),
label,
value
});
}
});
}
}
};
</script>

View File

@@ -0,0 +1,69 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import PlanActivitiesView from "./PlanActivitiesView.vue";
import Vue from 'vue';
export default function PlanInspectorViewProvider(openmct) {
return {
key: 'plan-inspector',
name: 'Plan Inspector View',
canView: function (selection) {
if (selection.length === 0 || selection[0].length === 0) {
return false;
}
let context = selection[0][0].context;
return context
&& context.type === 'activity';
},
view: function (selection) {
let component;
return {
show: function (element) {
component = new Vue({
el: element,
components: {
PlanActivitiesView: PlanActivitiesView
},
provide: {
openmct,
selection: selection
},
template: '<plan-activities-view></plan-activities-view>'
});
},
destroy: function () {
if (component) {
component.$destroy();
component = undefined;
}
}
};
},
priority: function () {
return 1;
}
};
}

View File

@@ -1,3 +1,25 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
.c-plan {
svg {
text-rendering: geometricPrecision;

View File

@@ -21,6 +21,7 @@
*****************************************************************************/
import PlanViewProvider from './PlanViewProvider';
import PlanInspectorViewProvider from "./inspector/PlanInspectorViewProvider";
export default function () {
return function install(openmct) {
@@ -44,6 +45,7 @@ export default function () {
}
});
openmct.objectViews.addProvider(new PlanViewProvider(openmct));
openmct.inspectorViews.addProvider(new PlanInspectorViewProvider(openmct));
};
}

View File

@@ -49,10 +49,6 @@ describe('the plugin', function () {
child.style.height = '480px';
element.appendChild(child);
openmct.time.timeSystem('utc', {
start: 1597160002854,
end: 1597181232854
});
openmct.on('start', done);
openmct.start(appHolder);
});
@@ -105,6 +101,11 @@ describe('the plugin', function () {
let planView;
beforeEach(() => {
openmct.time.timeSystem('utc', {
start: 1597160002854,
end: 1597181232854
});
planDomainObject = {
identifier: {
key: 'test-object',

View File

@@ -1,3 +1,25 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
export function getValidatedPlan(domainObject) {
let body = domainObject.selectFile.body;
let json = {};

View File

@@ -74,7 +74,9 @@
</div>
<div class="gl-plot__local-controls h-local-controls h-local-controls--overlay-content c-local-controls--show-on-hover">
<div class="c-button-set c-button-set--strip-h">
<div v-if="!options.compact"
class="c-button-set c-button-set--strip-h js-zoom"
>
<button class="c-button icon-minus"
title="Zoom out"
@click="zoom('out', 0.2)"
@@ -86,8 +88,8 @@
>
</button>
</div>
<div v-if="plotHistory.length"
class="c-button-set c-button-set--strip-h"
<div v-if="plotHistory.length && !options.compact"
class="c-button-set c-button-set--strip-h js-pan"
>
<button class="c-button icon-arrow-left"
title="Restore previous pan/zoom"
@@ -100,8 +102,8 @@
>
</button>
</div>
<div v-if="isRealTime"
class="c-button-set c-button-set--strip-h"
<div v-if="isRealTime && !options.compact"
class="c-button-set c-button-set--strip-h js-pause"
>
<button v-if="!isFrozen"
class="c-button icon-pause"
@@ -425,9 +427,12 @@ export default {
this.skipReloadOnInteraction = false;
this.loadMoreData(newRange, true);
} else {
// If we're not panning or zooming (time conductor and plot x-axis times are not out of sync)
// Drop any data that is more than 1x (max-min) before min.
// Limit these purges to once a second.
if (!this.nextPurge || this.nextPurge < Date.now()) {
const isPanningOrZooming = this.isTimeOutOfSync;
const purgeRecords = !isPanningOrZooming && (!this.nextPurge || (this.nextPurge < Date.now()));
if (purgeRecords) {
const keepRange = {
min: newRange.min - (newRange.max - newRange.min),
max: newRange.max
@@ -493,10 +498,12 @@ export default {
this.canvas = this.$refs.chartContainer.querySelectorAll('canvas')[1];
this.listenTo(this.canvas, 'mousemove', this.trackMousePosition, this);
this.listenTo(this.canvas, 'mouseleave', this.untrackMousePosition, this);
this.listenTo(this.canvas, 'mousedown', this.onMouseDown, this);
this.listenTo(this.canvas, 'wheel', this.wheelZoom, this);
if (!this.options.compact) {
this.listenTo(this.canvas, 'mousemove', this.trackMousePosition, this);
this.listenTo(this.canvas, 'mouseleave', this.untrackMousePosition, this);
this.listenTo(this.canvas, 'mousedown', this.onMouseDown, this);
this.listenTo(this.canvas, 'wheel', this.wheelZoom, this);
}
},
initialize() {
@@ -514,12 +521,7 @@ export default {
this.chartElementBounds = undefined;
this.tickUpdate = false;
this.canvas = this.$refs.chartContainer.querySelectorAll('canvas')[1];
this.listenTo(this.canvas, 'mousemove', this.trackMousePosition, this);
this.listenTo(this.canvas, 'mouseleave', this.untrackMousePosition, this);
this.listenTo(this.canvas, 'mousedown', this.onMouseDown, this);
this.listenTo(this.canvas, 'wheel', this.wheelZoom, this);
this.initCanvas();
this.config.yAxisLabel = this.config.yAxis.get('label');

View File

@@ -72,7 +72,8 @@
</template>
<script>
import eventHelpers from "./lib/eventHelpers";
import eventHelpers from './lib/eventHelpers';
import ImageExporter from '../../exporters/ImageExporter';
import MctPlot from './MctPlot.vue';
export default {
@@ -102,8 +103,7 @@ export default {
},
mounted() {
eventHelpers.extend(this);
this.exportImageService = this.openmct.$injector.get('exportImageService');
this.imageExporter = new ImageExporter(this.openmct);
},
beforeDestroy() {
this.destroy();
@@ -118,14 +118,12 @@ export default {
exportJPG() {
const plotElement = this.$refs.plotContainer;
this.exportImageService.exportJPG(plotElement, 'plot.jpg', 'export-plot');
this.imageExporter.exportJPG(plotElement, 'plot.jpg', 'export-plot');
},
exportPNG() {
const plotElement = this.$refs.plotContainer;
this.exportImageService.exportPNG(plotElement, 'plot.png', 'export-plot');
this.imageExporter.exportPNG(plotElement, 'plot.png', 'export-plot');
},
toggleCursorGuide() {

View File

@@ -24,19 +24,23 @@ import Plot from './Plot.vue';
import Vue from 'vue';
export default function PlotViewProvider(openmct) {
function hasTelemetry(domainObject) {
function hasNumericTelemetry(domainObject) {
if (!Object.prototype.hasOwnProperty.call(domainObject, 'telemetry')) {
return false;
}
let metadata = openmct.telemetry.getMetadata(domainObject);
return metadata.values().length > 0 && hasDomainAndRange(metadata);
return metadata.values().length > 0 && hasDomainAndNumericRange(metadata);
}
function hasDomainAndRange(metadata) {
return (metadata.valuesForHints(['range']).length > 0
&& metadata.valuesForHints(['domain']).length > 0);
function hasDomainAndNumericRange(metadata) {
const rangeValues = metadata.valuesForHints(['range']);
const domains = metadata.valuesForHints(['domain']);
return domains.length > 0
&& rangeValues.length > 0
&& !rangeValues.every(value => value.format === 'string');
}
function isCompactView(objectPath) {
@@ -44,11 +48,11 @@ export default function PlotViewProvider(openmct) {
}
return {
key: 'plot-simple',
key: 'plot-single',
name: 'Plot',
cssClass: 'icon-telemetry',
canView(domainObject, objectPath) {
return hasTelemetry(domainObject, openmct);
return hasNumericTelemetry(domainObject);
},
view: function (domainObject, objectPath) {

View File

@@ -201,15 +201,57 @@ describe("the plugin", function () {
hints: {
range: 1
}
},
{
key: "yet-another-key",
format: "string",
hints: {
range: 2
}
}]
}
};
const applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath);
let plotView = applicableViews.find((viewProvider) => viewProvider.key === "plot-simple");
const plotView = applicableViews.find((viewProvider) => viewProvider.key === "plot-single");
expect(plotView).toBeDefined();
});
it("does not provide a plot view if the telemetry is entirely non numeric", () => {
const testTelemetryObject = {
id: "test-object",
type: "test-object",
telemetry: {
values: [{
key: "some-key",
hints: {
domain: 1
}
},
{
key: "other-key",
format: "string",
hints: {
range: 1
}
},
{
key: "yet-another-key",
format: "string",
hints: {
range: 1
}
}]
}
};
const applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath);
const plotView = applicableViews.find((viewProvider) => viewProvider.key === "plot-single");
expect(plotView).toBeUndefined();
});
it("provides an overlay plot view for objects with telemetry", () => {
const testTelemetryObject = {
id: "test-object",
@@ -279,14 +321,10 @@ describe("the plugin", function () {
let plotView;
beforeEach(() => {
const getFunc = openmct.$injector.get;
spyOn(openmct.$injector, "get")
.withArgs("exportImageService").and.returnValue({
exportPNG: () => {},
exportJPG: () => {}
})
.and.callFake(getFunc);
openmct.time.timeSystem("utc", {
start: 0,
end: 4
});
testTelemetryObject = {
identifier: {
namespace: "",
@@ -319,7 +357,7 @@ describe("the plugin", function () {
};
applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath);
plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === "plot-simple");
plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === "plot-single");
plotView = plotViewProvider.view(testTelemetryObject, [testTelemetryObject]);
plotView.show(child, true);
@@ -403,6 +441,25 @@ describe("the plugin", function () {
});
});
describe('controls in time strip view', () => {
it('zoom controls are hidden', () => {
let pauseEl = element.querySelectorAll(".c-button-set .js-zoom");
expect(pauseEl.length).toBe(0);
});
it('pan controls are hidden', () => {
let pauseEl = element.querySelectorAll(".c-button-set .js-pan");
expect(pauseEl.length).toBe(0);
});
it('pause/play controls are hidden', () => {
let pauseEl = element.querySelectorAll(".c-button-set .js-pause");
expect(pauseEl.length).toBe(0);
});
});
});
describe("The stacked plot view", () => {
@@ -548,7 +605,7 @@ describe("the plugin", function () {
expect(legend.length).toBe(6);
});
it("Renders X-axis ticks for the telemetry object", () => {
xit("Renders X-axis ticks for the telemetry object", () => {
let xAxisElement = element.querySelectorAll(".gl-plot-axis-area.gl-plot-x .gl-plot-tick-wrapper");
expect(xAxisElement.length).toBe(1);

Some files were not shown because too many files have changed in this diff Show More