mirror of
https://github.com/nasa/openmct.git
synced 2025-06-25 18:50:11 +00:00
Compare commits
40 Commits
trac-code-
...
plot-perfo
Author | SHA1 | Date | |
---|---|---|---|
fd4dcc8513 | |||
9ebd18318b | |||
4a89b81f4f | |||
98e1abd7b1 | |||
56c25762ac | |||
5c8e726b87 | |||
d80f4a1f7d | |||
3fe4c7a954 | |||
676ef60128 | |||
5a90d28450 | |||
2bb6822e6b | |||
383b4c0d8d | |||
404ab720ad | |||
259ab53060 | |||
1db7ac55b4 | |||
82b3383834 | |||
ac240d524c | |||
1b034f6125 | |||
b329ed6ed5 | |||
9b7a0d7e4c | |||
5c15e53abb | |||
f58b3881f2 | |||
071a13b219 | |||
ca66898e51 | |||
94c7b2343a | |||
c397c336ab | |||
eea23f2caf | |||
6665641c02 | |||
c3ebf52dd2 | |||
f8f2e7da9b | |||
240f58b2d0 | |||
7d3baee7b5 | |||
1f5cb7ca42 | |||
4a7ebe326c | |||
10da314a4a | |||
b3ceccd7fb | |||
1bde4c9a0c | |||
4b85360446 | |||
41b860a547 | |||
254b3db966 |
@ -1,36 +1,69 @@
|
||||
version: 2
|
||||
jobs:
|
||||
build:
|
||||
version: 2.1
|
||||
executors:
|
||||
linux:
|
||||
docker:
|
||||
- image: circleci/node:13-browsers
|
||||
environment:
|
||||
CHROME_BIN: "/usr/bin/google-chrome"
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Update npm
|
||||
command: 'sudo npm install -g npm@latest'
|
||||
- restore_cache:
|
||||
key: dependency-cache-{{ checksum "package.json" }}
|
||||
- run:
|
||||
name: Installing dependencies (npm install)
|
||||
command: npm install
|
||||
- save_cache:
|
||||
key: dependency-cache-{{ checksum "package.json" }}
|
||||
paths:
|
||||
- node_modules
|
||||
- run:
|
||||
name: npm run test:coverage
|
||||
command: npm run test:coverage
|
||||
- run:
|
||||
name: npm run lint
|
||||
command: npm run lint
|
||||
- store_artifacts:
|
||||
path: dist
|
||||
prefix: dist
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
- image: cimg/base:stable
|
||||
orbs:
|
||||
node: circleci/node@4.5.1
|
||||
browser-tools: circleci/browser-tools@1.1.3
|
||||
jobs:
|
||||
test:
|
||||
parameters:
|
||||
node-version:
|
||||
type: string
|
||||
browser:
|
||||
type: string
|
||||
always-pass:
|
||||
type: boolean
|
||||
executor: linux
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
key: deps-{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }}
|
||||
- node/install:
|
||||
node-version: << parameters.node-version >>
|
||||
- node/install-packages:
|
||||
override-ci-command: npm install
|
||||
- when: # Just to save time until caching saves the browser bin
|
||||
condition:
|
||||
equal: [ "FirefoxESR", <<parameters.browser>> ]
|
||||
steps:
|
||||
- browser-tools/install-firefox:
|
||||
version: "78.11.0esr" #https://archive.mozilla.org/pub/firefox/releases/
|
||||
- when: # Just to save time until caching saves the browser bin
|
||||
condition:
|
||||
equal: [ "ChromeHeadless", <<parameters.browser>> ]
|
||||
steps:
|
||||
- browser-tools/install-chrome:
|
||||
replace-existing: false
|
||||
- save_cache:
|
||||
key: deps-{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }}
|
||||
paths:
|
||||
- ~/.npm
|
||||
- ~/.cache
|
||||
- node_modules
|
||||
- run: npm run test:coverage -- --browsers=<<parameters.browser>> || <<parameters.always-pass>>
|
||||
- store_test_results:
|
||||
path: dist/reports/tests/
|
||||
- store_artifacts:
|
||||
path: dist/reports/
|
||||
workflows:
|
||||
matrix-tests:
|
||||
jobs:
|
||||
- build
|
||||
- test:
|
||||
name: node10-chrome
|
||||
node-version: lts/dubnium
|
||||
browser: ChromeHeadless
|
||||
always-pass: false
|
||||
- test:
|
||||
name: node12-firefoxESR
|
||||
node-version: lts/erbium
|
||||
browser: FirefoxESR
|
||||
always-pass: true
|
||||
- test:
|
||||
name: node14-chrome
|
||||
node-version: lts/fermium
|
||||
browser: ChromeHeadless
|
||||
always-pass: true
|
||||
|
||||
|
||||
|
11
.github/ISSUE_TEMPLATE/bug_report.md
vendored
11
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -1,11 +1,12 @@
|
||||
<!--- This is for filing bugs. If you have a general question, please -->
|
||||
<!--- visit https://github.com/nasa/openmct/discussions -->
|
||||
|
||||
---
|
||||
name: Bug Report
|
||||
name: Bug report
|
||||
about: File a Bug !
|
||||
title: ''
|
||||
labels: type:bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--- Focus on user impact in the title. Use the Summary Field to -->
|
||||
<!--- describe the problem technically. -->
|
||||
|
||||
@ -35,7 +36,7 @@ about: File a Bug !
|
||||
|
||||
#### Environment
|
||||
* Open MCT Version: <!--- date of build, version, or SHA -->
|
||||
* Deployment Type: <!--- npm dev? VIPER Dev? openmct-yams? -->
|
||||
* Deployment Type: <!--- npm dev? VIPER Dev? openmct-yamcs? -->
|
||||
* OS:
|
||||
* Browser:
|
||||
|
||||
|
6
.github/ISSUE_TEMPLATE/config.yml
vendored
6
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -1 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Discussions
|
||||
url: https://github.com/nasa/openmct/discussions
|
||||
about: Got a question?
|
||||
|
20
.github/ISSUE_TEMPLATE/enhancement-request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/enhancement-request.md
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Enhancement request
|
||||
about: Suggest an enhancement or new improvement for this project
|
||||
title: ''
|
||||
labels: type:enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
4
.github/workflows/lighthouse.yml
vendored
4
.github/workflows/lighthouse.yml
vendored
@ -13,6 +13,8 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.event.inputs.version }}
|
||||
- uses: actions/setup-node@v1
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '14'
|
||||
- run: npm install && npm install -g @lhci/cli #Don't want to include this in our deps
|
||||
- run: lhci autorun
|
@ -49,6 +49,10 @@ define([
|
||||
];
|
||||
const IMAGE_DELAY = 20000;
|
||||
|
||||
function getCompassValues(min, max) {
|
||||
return min + Math.random() * (max - min);
|
||||
}
|
||||
|
||||
function pointForTimestamp(timestamp, name) {
|
||||
const url = IMAGE_SAMPLES[Math.floor(timestamp / IMAGE_DELAY) % IMAGE_SAMPLES.length];
|
||||
const urlItems = url.split('/');
|
||||
@ -59,6 +63,9 @@ define([
|
||||
utc: Math.floor(timestamp / IMAGE_DELAY) * IMAGE_DELAY,
|
||||
local: Math.floor(timestamp / IMAGE_DELAY) * IMAGE_DELAY,
|
||||
url,
|
||||
sunOrientation: getCompassValues(0, 360),
|
||||
cameraPan: getCompassValues(0, 360),
|
||||
heading: getCompassValues(0, 360),
|
||||
imageDownloadName
|
||||
};
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import Vue from 'Vue';
|
||||
import Vue from 'vue';
|
||||
import HelloWorld from './HelloWorld.vue';
|
||||
|
||||
function SimpleVuePlugin() {
|
||||
|
@ -88,6 +88,7 @@
|
||||
openmct.install(openmct.plugins.ExampleImagery());
|
||||
openmct.install(openmct.plugins.PlanLayout());
|
||||
openmct.install(openmct.plugins.Timeline());
|
||||
openmct.install(openmct.plugins.Hyperlink());
|
||||
openmct.install(openmct.plugins.UTCTimeSystem());
|
||||
openmct.install(openmct.plugins.AutoflowView({
|
||||
type: "telemetry.panel"
|
||||
@ -194,6 +195,7 @@
|
||||
['table', 'telemetry.plot.overlay', 'telemetry.plot.stacked'],
|
||||
{indicator: true}
|
||||
));
|
||||
openmct.install(openmct.plugins.Clock({ enableClockIndicator: true }));
|
||||
openmct.start();
|
||||
</script>
|
||||
</html>
|
||||
|
@ -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' : 'ChromeHeadless'];
|
||||
const coverageEnabled = process.env.COVERAGE === 'true';
|
||||
const reporters = ['progress', 'html'];
|
||||
const reporters = ['progress', 'html', 'junit'];
|
||||
|
||||
if (coverageEnabled) {
|
||||
reporters.push('coverage-istanbul');
|
||||
@ -59,7 +59,8 @@ module.exports = (config) => {
|
||||
browsers: browsers,
|
||||
client: {
|
||||
jasmine: {
|
||||
random: false
|
||||
random: false,
|
||||
timeoutInterval: 30000
|
||||
}
|
||||
},
|
||||
customLaunchers: {
|
||||
@ -67,6 +68,10 @@ module.exports = (config) => {
|
||||
base: 'Chrome',
|
||||
flags: ['--remote-debugging-port=9222'],
|
||||
debug: true
|
||||
},
|
||||
FirefoxESR: {
|
||||
base: 'FirefoxHeadless',
|
||||
name: 'FirefoxESR'
|
||||
}
|
||||
},
|
||||
colors: true,
|
||||
@ -78,12 +83,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: {
|
||||
|
19
package.json
19
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openmct",
|
||||
"version": "1.7.4",
|
||||
"version": "1.7.6-SNAPSHOT",
|
||||
"description": "The Open MCT core platform",
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
@ -34,20 +34,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-firefox-launcher": "2.1.1",
|
||||
"karma-cli": "2.0.0",
|
||||
"karma-coverage": "2.0.3",
|
||||
"karma-coverage-istanbul-reporter": "3.0.3",
|
||||
"karma-firefox-launcher": "1.3.0",
|
||||
"karma-junit-reporter": "2.0.1",
|
||||
"karma-html-reporter": "0.2.7",
|
||||
"karma-jasmine": "3.3.1",
|
||||
"karma-sourcemap-loader": "0.3.7",
|
||||
"karma-jasmine": "4.0.1",
|
||||
"karma-sourcemap-loader": "0.3.8",
|
||||
"karma-webpack": "4.0.2",
|
||||
"location-bar": "^3.0.1",
|
||||
"lodash": "^4.17.12",
|
||||
@ -89,6 +90,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 +102,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
|
||||
|
@ -21,32 +21,24 @@
|
||||
*****************************************************************************/
|
||||
|
||||
define([
|
||||
"moment-timezone",
|
||||
"./src/indicators/ClockIndicator",
|
||||
"./src/services/TickerService",
|
||||
"./src/services/TimerService",
|
||||
"./src/controllers/ClockController",
|
||||
"./src/controllers/TimerController",
|
||||
"./src/controllers/RefreshingController",
|
||||
"./src/actions/StartTimerAction",
|
||||
"./src/actions/RestartTimerAction",
|
||||
"./src/actions/StopTimerAction",
|
||||
"./src/actions/PauseTimerAction",
|
||||
"./res/templates/clock.html",
|
||||
"./res/templates/timer.html"
|
||||
], function (
|
||||
MomentTimezone,
|
||||
ClockIndicator,
|
||||
TickerService,
|
||||
TimerService,
|
||||
ClockController,
|
||||
TimerController,
|
||||
RefreshingController,
|
||||
StartTimerAction,
|
||||
RestartTimerAction,
|
||||
StopTimerAction,
|
||||
PauseTimerAction,
|
||||
clockTemplate,
|
||||
timerTemplate
|
||||
) {
|
||||
return {
|
||||
@ -73,16 +65,6 @@ define([
|
||||
"value": "YYYY/MM/DD HH:mm:ss"
|
||||
}
|
||||
],
|
||||
"indicators": [
|
||||
{
|
||||
"implementation": ClockIndicator,
|
||||
"depends": [
|
||||
"tickerService",
|
||||
"CLOCK_INDICATOR_FORMAT"
|
||||
],
|
||||
"priority": "preferred"
|
||||
}
|
||||
],
|
||||
"services": [
|
||||
{
|
||||
"key": "tickerService",
|
||||
@ -99,14 +81,6 @@ define([
|
||||
}
|
||||
],
|
||||
"controllers": [
|
||||
{
|
||||
"key": "ClockController",
|
||||
"implementation": ClockController,
|
||||
"depends": [
|
||||
"$scope",
|
||||
"tickerService"
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "TimerController",
|
||||
"implementation": TimerController,
|
||||
@ -126,12 +100,6 @@ define([
|
||||
}
|
||||
],
|
||||
"views": [
|
||||
{
|
||||
"key": "clock",
|
||||
"type": "clock",
|
||||
"editable": false,
|
||||
"template": clockTemplate
|
||||
},
|
||||
{
|
||||
"key": "timer",
|
||||
"type": "timer",
|
||||
@ -181,75 +149,11 @@ define([
|
||||
],
|
||||
"category": "contextual",
|
||||
"name": "Stop",
|
||||
"cssClass": "icon-box",
|
||||
"cssClass": "icon-box-round-corners",
|
||||
"priority": "preferred"
|
||||
}
|
||||
],
|
||||
"types": [
|
||||
{
|
||||
"key": "clock",
|
||||
"name": "Clock",
|
||||
"cssClass": "icon-clock",
|
||||
"description": "A UTC-based clock that supports a variety of display formats. Clocks can be added to Display Layouts.",
|
||||
"priority": 101,
|
||||
"features": [
|
||||
"creation"
|
||||
],
|
||||
"properties": [
|
||||
{
|
||||
"key": "clockFormat",
|
||||
"name": "Display Format",
|
||||
"control": "composite",
|
||||
"items": [
|
||||
{
|
||||
"control": "select",
|
||||
"options": [
|
||||
{
|
||||
"value": "YYYY/MM/DD hh:mm:ss",
|
||||
"name": "YYYY/MM/DD hh:mm:ss"
|
||||
},
|
||||
{
|
||||
"value": "YYYY/DDD hh:mm:ss",
|
||||
"name": "YYYY/DDD hh:mm:ss"
|
||||
},
|
||||
{
|
||||
"value": "hh:mm:ss",
|
||||
"name": "hh:mm:ss"
|
||||
}
|
||||
],
|
||||
"cssClass": "l-inline"
|
||||
},
|
||||
{
|
||||
"control": "select",
|
||||
"options": [
|
||||
{
|
||||
"value": "clock12",
|
||||
"name": "12hr"
|
||||
},
|
||||
{
|
||||
"value": "clock24",
|
||||
"name": "24hr"
|
||||
}
|
||||
],
|
||||
"cssClass": "l-inline"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "timezone",
|
||||
"name": "Timezone",
|
||||
"control": "autocomplete",
|
||||
"options": MomentTimezone.tz.names()
|
||||
}
|
||||
],
|
||||
"model": {
|
||||
"clockFormat": [
|
||||
"YYYY/MM/DD hh:mm:ss",
|
||||
"clock12"
|
||||
],
|
||||
"timezone": "UTC"
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "timer",
|
||||
"name": "Timer",
|
||||
|
@ -1,32 +0,0 @@
|
||||
<!--
|
||||
Open MCT, Copyright (c) 2009-2016, United States Government
|
||||
as represented by the Administrator of the National Aeronautics and Space
|
||||
Administration. All rights reserved.
|
||||
|
||||
Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0.
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
License for the specific language governing permissions and limitations
|
||||
under the License.
|
||||
|
||||
Open MCT includes source code licensed under additional open source
|
||||
licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
this source code distribution or the Licensing information page available
|
||||
at runtime from the About dialog for additional information.
|
||||
-->
|
||||
<div class="c-clock l-time-display u-style-receiver js-style-receiver" ng-controller="ClockController as clock">
|
||||
<div class="c-clock__timezone">
|
||||
{{clock.zone()}}
|
||||
</div>
|
||||
<div class="c-clock__value">
|
||||
{{clock.text()}}
|
||||
</div>
|
||||
<div class="c-clock__ampm">
|
||||
{{clock.ampm()}}
|
||||
</div>
|
||||
</div>
|
@ -1,110 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2009-2016, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define([
|
||||
'moment',
|
||||
'moment-timezone'
|
||||
],
|
||||
function (
|
||||
moment,
|
||||
momentTimezone
|
||||
) {
|
||||
|
||||
/**
|
||||
* Controller for views of a Clock domain object.
|
||||
*
|
||||
* @constructor
|
||||
* @memberof platform/features/clock
|
||||
* @param {angular.Scope} $scope the Angular scope
|
||||
* @param {platform/features/clock.TickerService} tickerService
|
||||
* a service used to align behavior with clock ticks
|
||||
*/
|
||||
function ClockController($scope, tickerService) {
|
||||
var lastTimestamp,
|
||||
unlisten,
|
||||
timeFormat,
|
||||
zoneName,
|
||||
self = this;
|
||||
|
||||
function update() {
|
||||
var m = zoneName
|
||||
? moment.utc(lastTimestamp).tz(zoneName) : moment.utc(lastTimestamp);
|
||||
self.zoneAbbr = m.zoneAbbr();
|
||||
self.textValue = timeFormat && m.format(timeFormat);
|
||||
self.ampmValue = m.format("A"); // Just the AM or PM part
|
||||
}
|
||||
|
||||
function tick(timestamp) {
|
||||
lastTimestamp = timestamp;
|
||||
update();
|
||||
}
|
||||
|
||||
function updateModel(model) {
|
||||
var baseFormat;
|
||||
if (model !== undefined) {
|
||||
baseFormat = model.clockFormat[0];
|
||||
|
||||
self.use24 = model.clockFormat[1] === 'clock24';
|
||||
timeFormat = self.use24
|
||||
? baseFormat.replace('hh', "HH") : baseFormat;
|
||||
// If wrong timezone is provided, the UTC will be used
|
||||
zoneName = momentTimezone.tz.names().includes(model.timezone)
|
||||
? model.timezone : "UTC";
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
// Pull in the model (clockFormat and timezone) from the domain object model
|
||||
$scope.$watch('model', updateModel);
|
||||
|
||||
// Listen for clock ticks ... and stop listening on destroy
|
||||
unlisten = tickerService.listen(tick);
|
||||
$scope.$on('$destroy', unlisten);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the clock's time zone, as displayable text.
|
||||
* @returns {string}
|
||||
*/
|
||||
ClockController.prototype.zone = function () {
|
||||
return this.zoneAbbr;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the current time, as displayable text.
|
||||
* @returns {string}
|
||||
*/
|
||||
ClockController.prototype.text = function () {
|
||||
return this.textValue;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the text to display to qualify a time as AM or PM.
|
||||
* @returns {string}
|
||||
*/
|
||||
ClockController.prototype.ampm = function () {
|
||||
return this.use24 ? '' : this.ampmValue;
|
||||
};
|
||||
|
||||
return ClockController;
|
||||
}
|
||||
);
|
@ -1,65 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2009-2016, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define(
|
||||
['moment'],
|
||||
function (moment) {
|
||||
|
||||
/**
|
||||
* Indicator that displays the current UTC time in the status area.
|
||||
* @implements {Indicator}
|
||||
* @memberof platform/features/clock
|
||||
* @param {platform/features/clock.TickerService} tickerService
|
||||
* a service used to align behavior with clock ticks
|
||||
* @param {string} indicatorFormat format string for timestamps
|
||||
* shown in this indicator
|
||||
*/
|
||||
function ClockIndicator(tickerService, indicatorFormat) {
|
||||
var self = this;
|
||||
|
||||
this.text = "";
|
||||
|
||||
tickerService.listen(function (timestamp) {
|
||||
self.text = moment.utc(timestamp)
|
||||
.format(indicatorFormat) + " UTC";
|
||||
});
|
||||
}
|
||||
|
||||
ClockIndicator.prototype.getGlyphClass = function () {
|
||||
return "";
|
||||
};
|
||||
|
||||
ClockIndicator.prototype.getCssClass = function () {
|
||||
return "t-indicator-clock icon-clock no-minify c-indicator--not-clickable";
|
||||
};
|
||||
|
||||
ClockIndicator.prototype.getText = function () {
|
||||
return this.text;
|
||||
};
|
||||
|
||||
ClockIndicator.prototype.getDescription = function () {
|
||||
return "";
|
||||
};
|
||||
|
||||
return ClockIndicator;
|
||||
}
|
||||
);
|
@ -1,107 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2009-2017, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define(
|
||||
["../../src/controllers/ClockController"],
|
||||
function (ClockController) {
|
||||
|
||||
// Wed, 03 Jun 2015 17:56:14 GMT
|
||||
var TEST_TIMESTAMP = 1433354174000;
|
||||
|
||||
describe("A clock view's controller", function () {
|
||||
var mockScope,
|
||||
mockTicker,
|
||||
mockUnticker,
|
||||
controller;
|
||||
|
||||
beforeEach(function () {
|
||||
mockScope = jasmine.createSpyObj('$scope', ['$watch', '$on']);
|
||||
mockTicker = jasmine.createSpyObj('ticker', ['listen']);
|
||||
mockUnticker = jasmine.createSpy('unticker');
|
||||
|
||||
mockTicker.listen.and.returnValue(mockUnticker);
|
||||
|
||||
controller = new ClockController(mockScope, mockTicker);
|
||||
});
|
||||
|
||||
it("watches for model (clockFormat and timezone) from the domain object model", function () {
|
||||
expect(mockScope.$watch).toHaveBeenCalledWith(
|
||||
"model",
|
||||
jasmine.any(Function)
|
||||
);
|
||||
});
|
||||
|
||||
it("subscribes to clock ticks", function () {
|
||||
expect(mockTicker.listen)
|
||||
.toHaveBeenCalledWith(jasmine.any(Function));
|
||||
});
|
||||
|
||||
it("unsubscribes to ticks when destroyed", function () {
|
||||
// Make sure $destroy is being listened for...
|
||||
expect(mockScope.$on.calls.mostRecent().args[0]).toEqual('$destroy');
|
||||
expect(mockUnticker).not.toHaveBeenCalled();
|
||||
|
||||
// ...and makes sure that its listener unsubscribes from ticker
|
||||
mockScope.$on.calls.mostRecent().args[1]();
|
||||
expect(mockUnticker).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("formats using the format string from the model", function () {
|
||||
mockTicker.listen.calls.mostRecent().args[0](TEST_TIMESTAMP);
|
||||
mockScope.$watch.calls.mostRecent().args[1]({
|
||||
"clockFormat": [
|
||||
"YYYY-DDD hh:mm:ss",
|
||||
"clock24"
|
||||
],
|
||||
"timezone": "Canada/Eastern"
|
||||
});
|
||||
|
||||
expect(controller.zone()).toEqual("EDT");
|
||||
expect(controller.text()).toEqual("2015-154 13:56:14");
|
||||
expect(controller.ampm()).toEqual("");
|
||||
});
|
||||
|
||||
it("formats 12-hour time", function () {
|
||||
mockTicker.listen.calls.mostRecent().args[0](TEST_TIMESTAMP);
|
||||
mockScope.$watch.calls.mostRecent().args[1]({
|
||||
"clockFormat": [
|
||||
"YYYY-DDD hh:mm:ss",
|
||||
"clock12"
|
||||
],
|
||||
"timezone": ""
|
||||
});
|
||||
|
||||
expect(controller.zone()).toEqual("UTC");
|
||||
expect(controller.text()).toEqual("2015-154 05:56:14");
|
||||
expect(controller.ampm()).toEqual("PM");
|
||||
});
|
||||
|
||||
it("does not throw exceptions when model is undefined", function () {
|
||||
mockTicker.listen.calls.mostRecent().args[0](TEST_TIMESTAMP);
|
||||
expect(function () {
|
||||
mockScope.$watch.calls.mostRecent().args[1](undefined);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
@ -101,7 +101,7 @@ define(
|
||||
name: "Pause"
|
||||
});
|
||||
mockStop.getMetadata.and.returnValue({
|
||||
cssClass: "icon-box",
|
||||
cssClass: "icon-box-round-corners",
|
||||
name: "Stop"
|
||||
});
|
||||
mockScope.domainObject = mockDomainObject;
|
||||
|
@ -1,58 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2009-2016, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define(
|
||||
["../../src/indicators/ClockIndicator"],
|
||||
function (ClockIndicator) {
|
||||
|
||||
// Wed, 03 Jun 2015 17:56:14 GMT
|
||||
var TEST_TIMESTAMP = 1433354174000,
|
||||
TEST_FORMAT = "YYYY-DDD HH:mm:ss";
|
||||
|
||||
describe("The clock indicator", function () {
|
||||
var mockTicker,
|
||||
mockUnticker,
|
||||
indicator;
|
||||
|
||||
beforeEach(function () {
|
||||
mockTicker = jasmine.createSpyObj('ticker', ['listen']);
|
||||
mockUnticker = jasmine.createSpy('unticker');
|
||||
|
||||
mockTicker.listen.and.returnValue(mockUnticker);
|
||||
|
||||
indicator = new ClockIndicator(mockTicker, TEST_FORMAT);
|
||||
});
|
||||
|
||||
it("displays the current time", function () {
|
||||
mockTicker.listen.calls.mostRecent().args[0](TEST_TIMESTAMP);
|
||||
expect(indicator.getText()).toEqual("2015-154 17:56:14 UTC");
|
||||
});
|
||||
|
||||
it("implements the Indicator interface", function () {
|
||||
expect(indicator.getCssClass()).toEqual(jasmine.any(String));
|
||||
expect(indicator.getText()).toEqual(jasmine.any(String));
|
||||
expect(indicator.getDescription()).toEqual(jasmine.any(String));
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
@ -1,120 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2009-2016, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define([
|
||||
'./src/HyperlinkController',
|
||||
'./res/templates/hyperlink.html'
|
||||
], function (
|
||||
HyperlinkController,
|
||||
hyperlinkTemplate
|
||||
) {
|
||||
return {
|
||||
name: "platform/features/hyperlink",
|
||||
definition: {
|
||||
"name": "Hyperlink",
|
||||
"description": "Insert a hyperlink to reference a link",
|
||||
"extensions": {
|
||||
"types": [
|
||||
{
|
||||
"key": "hyperlink",
|
||||
"name": "Hyperlink",
|
||||
"cssClass": "icon-chain-links",
|
||||
"description": "A hyperlink to redirect to a different link",
|
||||
"features": ["creation"],
|
||||
"properties": [
|
||||
{
|
||||
"key": "url",
|
||||
"name": "URL",
|
||||
"control": "textfield",
|
||||
"required": true,
|
||||
"cssClass": "l-input-lg"
|
||||
},
|
||||
|
||||
{
|
||||
"key": "displayText",
|
||||
"name": "Text to Display",
|
||||
"control": "textfield",
|
||||
"required": true,
|
||||
"cssClass": "l-input-lg"
|
||||
},
|
||||
{
|
||||
"key": "displayFormat",
|
||||
"name": "Display Format",
|
||||
"control": "select",
|
||||
"options": [
|
||||
{
|
||||
"name": "Link",
|
||||
"value": "link"
|
||||
},
|
||||
{
|
||||
"value": "button",
|
||||
"name": "Button"
|
||||
}
|
||||
],
|
||||
"cssClass": "l-inline"
|
||||
},
|
||||
{
|
||||
"key": "openNewTab",
|
||||
"name": "Tab to Open Hyperlink",
|
||||
"control": "select",
|
||||
"options": [
|
||||
{
|
||||
"name": "Open in this tab",
|
||||
"value": "thisTab"
|
||||
},
|
||||
{
|
||||
"value": "newTab",
|
||||
"name": "Open in a new tab"
|
||||
}
|
||||
],
|
||||
"cssClass": "l-inline"
|
||||
|
||||
}
|
||||
],
|
||||
"model": {
|
||||
"displayFormat": "link",
|
||||
"openNewTab": "thisTab",
|
||||
"removeTitle": true
|
||||
}
|
||||
|
||||
}
|
||||
],
|
||||
"views": [
|
||||
{
|
||||
"key": "hyperlink",
|
||||
"type": "hyperlink",
|
||||
"name": "Hyperlink Display",
|
||||
"template": hyperlinkTemplate,
|
||||
"editable": false
|
||||
}
|
||||
],
|
||||
"controllers": [
|
||||
{
|
||||
"key": "HyperlinkController",
|
||||
"implementation": HyperlinkController,
|
||||
"depends": ["$scope"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
@ -1,61 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2009-2016, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* This bundle adds the Hyperlink object type, which can be used to add hyperlinks as a domain Object type
|
||||
and into display Layouts as either a button or link that can be chosen to open in either the same tab or
|
||||
create a new tab to open the link in
|
||||
* @namespace platform/features/hyperlink
|
||||
*/
|
||||
define(
|
||||
[],
|
||||
function () {
|
||||
function HyperlinkController($scope) {
|
||||
this.$scope = $scope;
|
||||
}
|
||||
|
||||
/**Function to analyze the location in which to open the hyperlink
|
||||
@returns true if the hyperlink is chosen to open in a different tab, false if the same tab
|
||||
**/
|
||||
HyperlinkController.prototype.openNewTab = function () {
|
||||
if (this.$scope.domainObject.getModel().openNewTab === "thisTab") {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
/**Function to specify the format in which the hyperlink should be created
|
||||
@returns true if the hyperlink is chosen to be created as a button, false if a link
|
||||
**/
|
||||
HyperlinkController.prototype.isButton = function () {
|
||||
if (this.$scope.domainObject.getModel().displayFormat === "link") {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
return HyperlinkController;
|
||||
}
|
||||
|
||||
);
|
@ -1,89 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2009-2016, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define(
|
||||
["../src/HyperlinkController"],
|
||||
function (HyperlinkController) {
|
||||
|
||||
describe("The controller for hyperlinks", function () {
|
||||
var domainObject,
|
||||
controller,
|
||||
scope;
|
||||
beforeEach(function () {
|
||||
scope = jasmine.createSpyObj(
|
||||
"$scope",
|
||||
["domainObject"]
|
||||
);
|
||||
domainObject = jasmine.createSpyObj(
|
||||
"domainObject",
|
||||
["getModel"]
|
||||
);
|
||||
scope.domainObject = domainObject;
|
||||
controller = new HyperlinkController(scope);
|
||||
});
|
||||
it("knows when it should open a new tab", function () {
|
||||
scope.domainObject.getModel.and.returnValue({
|
||||
"displayFormat": "link",
|
||||
"openNewTab": "newTab",
|
||||
"showTitle": false
|
||||
}
|
||||
);
|
||||
controller = new HyperlinkController(scope);
|
||||
expect(controller.openNewTab())
|
||||
.toBe(true);
|
||||
});
|
||||
it("knows when it is a button", function () {
|
||||
scope.domainObject.getModel.and.returnValue({
|
||||
"displayFormat": "button",
|
||||
"openNewTab": "thisTab",
|
||||
"showTitle": false
|
||||
}
|
||||
);
|
||||
controller = new HyperlinkController(scope);
|
||||
expect(controller.isButton())
|
||||
.toEqual(true);
|
||||
});
|
||||
it("knows when it should open in the same tab", function () {
|
||||
scope.domainObject.getModel.and.returnValue({
|
||||
"displayFormat": "link",
|
||||
"openNewTab": "thisTab",
|
||||
"showTitle": false
|
||||
}
|
||||
);
|
||||
controller = new HyperlinkController(scope);
|
||||
expect(controller.openNewTab())
|
||||
.toBe(false);
|
||||
});
|
||||
it("knows when it is a link", function () {
|
||||
scope.domainObject.getModel.and.returnValue({
|
||||
"displayFormat": "link",
|
||||
"openNewTab": "thisTab",
|
||||
"showTitle": false
|
||||
}
|
||||
);
|
||||
controller = new HyperlinkController(scope);
|
||||
expect(controller.openNewTab())
|
||||
.toBe(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
@ -1,70 +0,0 @@
|
||||
This bundle provides the Timeline domain object type, as well
|
||||
as other associated domain object types and relevant views.
|
||||
|
||||
# Implementation notes
|
||||
|
||||
## Model Properties
|
||||
|
||||
The properties below record properties relevant to using and
|
||||
understanding timelines based on their JSON representation.
|
||||
Additional common properties, such as `modified`
|
||||
or `persisted` timestamps, may also be present.
|
||||
|
||||
### Timeline Model
|
||||
|
||||
A timeline's model looks like:
|
||||
|
||||
```
|
||||
{
|
||||
"type": "timeline",
|
||||
"start": {
|
||||
"timestamp": <number> (milliseconds since epoch),
|
||||
"epoch": <string> (currently, always "SET")
|
||||
},
|
||||
"capacity": <number> (optional; battery capacity in watt-hours)
|
||||
"composition": <string[]> (array of identifiers for contained objects)
|
||||
}
|
||||
```
|
||||
|
||||
The identifiers in a timeline's `composition` field should refer to
|
||||
other Timeline objects, or to Activity objects.
|
||||
|
||||
### Activity Model
|
||||
|
||||
An activity's model looks like:
|
||||
|
||||
```
|
||||
{
|
||||
"type": "activity",
|
||||
"start": {
|
||||
"timestamp": <number> (milliseconds since epoch),
|
||||
"epoch": <string> (currently, always "SET")
|
||||
},
|
||||
"duration": {
|
||||
"timestamp": <number> (duration of this activity, in milliseconds)
|
||||
"epoch": "SET" (this is ignored)
|
||||
},
|
||||
"relationships": {
|
||||
"modes": <string[]> (array of applicable Activity Mode ids)
|
||||
},
|
||||
"link": <string> (optional; URL linking to associated external resource)
|
||||
"composition": <string[]> (array of identifiers for contained objects)
|
||||
}
|
||||
```
|
||||
|
||||
The identifiers in a timeline's `composition` field should only refer to
|
||||
other Activity objects.
|
||||
|
||||
### Activity Mode Model
|
||||
|
||||
An activity mode's model looks like:
|
||||
|
||||
```
|
||||
{
|
||||
"type": "mode",
|
||||
"resources": {
|
||||
"comms": <number> (communications utilization, in Kbps)
|
||||
"power": <number> (power utilization, in watts)
|
||||
}
|
||||
}
|
||||
```
|
@ -1,10 +0,0 @@
|
||||
<div>
|
||||
Timeline, Activity and Activity Mode objects have been deprecated and will no longer be supported.
|
||||
</div>
|
||||
<div>
|
||||
Please open an issue in the
|
||||
<a href="https://github.com/nasa/openmct/issues" target="_blank">
|
||||
Open MCT Issue tracker
|
||||
</a>
|
||||
if you have any questions about the timeline plugin.
|
||||
</div>
|
@ -122,6 +122,7 @@ define([
|
||||
}
|
||||
};
|
||||
|
||||
this.destroy = this.destroy.bind(this);
|
||||
/**
|
||||
* Tracks current selection state of the application.
|
||||
* @private
|
||||
@ -262,7 +263,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 +284,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());
|
||||
}
|
||||
@ -434,6 +436,8 @@ define([
|
||||
Browse(this);
|
||||
}
|
||||
|
||||
window.addEventListener('beforeunload', this.destroy);
|
||||
|
||||
this.router.start();
|
||||
this.emit('start');
|
||||
}.bind(this));
|
||||
@ -457,6 +461,7 @@ define([
|
||||
};
|
||||
|
||||
MCT.prototype.destroy = function () {
|
||||
window.removeEventListener('beforeunload', this.destroy);
|
||||
this.emit('destroy');
|
||||
this.router.destroy();
|
||||
};
|
||||
|
@ -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: [
|
||||
|
@ -173,10 +173,11 @@ define([
|
||||
const limitEvaluator = oldObject.getCapability("limit");
|
||||
|
||||
return {
|
||||
limits: function () {
|
||||
return limitEvaluator.limits();
|
||||
limits: () => {
|
||||
return limitEvaluator.limits.then !== undefined
|
||||
? limitEvaluator.limits()
|
||||
: Promise.resolve(limitEvaluator.limits());
|
||||
}
|
||||
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -1,218 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* Module defining ExportImageService. Created by hudsonfoo on 09/02/16
|
||||
*/
|
||||
define(
|
||||
[
|
||||
"html2canvas",
|
||||
"saveAs"
|
||||
],
|
||||
function (
|
||||
html2canvas,
|
||||
{ saveAs }
|
||||
) {
|
||||
|
||||
/**
|
||||
* The export image service will export any HTML node to
|
||||
* JPG, or PNG.
|
||||
* @param {object} dialogService
|
||||
* @constructor
|
||||
*/
|
||||
function ExportImageService(dialogService) {
|
||||
this.dialogService = dialogService;
|
||||
this.exportCount = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an HTML element into a PNG or JPG Blob.
|
||||
* @private
|
||||
* @param {node} element that will be converted to an image
|
||||
* @param {object} options Image options.
|
||||
* @returns {promise}
|
||||
*/
|
||||
ExportImageService.prototype.renderElement = function (element, {imageType, className, thumbnailSize}) {
|
||||
const self = this;
|
||||
const dialogService = this.dialogService;
|
||||
const dialog = dialogService.showBlockingMessage({
|
||||
title: "Capturing...",
|
||||
hint: "Capturing an image",
|
||||
unknownProgress: true,
|
||||
severity: "info",
|
||||
delay: true
|
||||
});
|
||||
|
||||
let mimeType = "image/png";
|
||||
if (imageType === "jpg") {
|
||||
mimeType = "image/jpeg";
|
||||
}
|
||||
|
||||
let exportId = undefined;
|
||||
let oldId = undefined;
|
||||
if (className) {
|
||||
exportId = 'export-element-' + this.exportCount;
|
||||
this.exportCount++;
|
||||
oldId = element.id;
|
||||
element.id = exportId;
|
||||
}
|
||||
|
||||
return html2canvas(element, {
|
||||
onclone: function (document) {
|
||||
if (className) {
|
||||
const clonedElement = document.getElementById(exportId);
|
||||
clonedElement.classList.add(className);
|
||||
}
|
||||
|
||||
element.id = oldId;
|
||||
},
|
||||
removeContainer: true // Set to false to debug what html2canvas renders
|
||||
}).then(function (canvas) {
|
||||
dialog.dismiss();
|
||||
|
||||
return new Promise(function (resolve, reject) {
|
||||
if (thumbnailSize) {
|
||||
const thumbnail = self.getThumbnail(canvas, mimeType, thumbnailSize);
|
||||
|
||||
return canvas.toBlob(blob => resolve({
|
||||
blob,
|
||||
thumbnail
|
||||
}), mimeType);
|
||||
}
|
||||
|
||||
return canvas.toBlob(blob => resolve({ blob }), mimeType);
|
||||
});
|
||||
}, function (error) {
|
||||
console.log('error capturing image', error);
|
||||
dialog.dismiss();
|
||||
const errorDialog = dialogService.showBlockingMessage({
|
||||
title: "Error capturing image",
|
||||
severity: "error",
|
||||
hint: "Image was not captured successfully!",
|
||||
options: [{
|
||||
label: "OK",
|
||||
callback: function () {
|
||||
errorDialog.dismiss();
|
||||
}
|
||||
}]
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
ExportImageService.prototype.getThumbnail = function (canvas, mimeType, size) {
|
||||
const thumbnailCanvas = document.createElement('canvas');
|
||||
thumbnailCanvas.setAttribute('width', size.width);
|
||||
thumbnailCanvas.setAttribute('height', size.height);
|
||||
const ctx = thumbnailCanvas.getContext('2d');
|
||||
ctx.globalCompositeOperation = "copy";
|
||||
ctx.drawImage(canvas, 0, 0, canvas.width, canvas.height, 0, 0, size.width, size.height);
|
||||
|
||||
return thumbnailCanvas.toDataURL(mimeType);
|
||||
};
|
||||
|
||||
/**
|
||||
* Takes a screenshot of a DOM node and exports to JPG.
|
||||
* @param {node} element to be exported
|
||||
* @param {string} filename the exported image
|
||||
* @param {string} className to be added to element before capturing (optional)
|
||||
* @returns {promise}
|
||||
*/
|
||||
ExportImageService.prototype.exportJPG = function (element, filename, className) {
|
||||
const processedFilename = replaceDotsWithUnderscores(filename);
|
||||
|
||||
return this.renderElement(element, {
|
||||
imageType: 'jpg',
|
||||
className
|
||||
})
|
||||
.then(function (img) {
|
||||
saveAs(img.blob, processedFilename);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Takes a screenshot of a DOM node and exports to PNG.
|
||||
* @param {node} element to be exported
|
||||
* @param {string} filename the exported image
|
||||
* @param {string} className to be added to element before capturing (optional)
|
||||
* @returns {promise}
|
||||
*/
|
||||
ExportImageService.prototype.exportPNG = function (element, filename, className) {
|
||||
const processedFilename = replaceDotsWithUnderscores(filename);
|
||||
|
||||
return this.renderElement(element, {
|
||||
imageType: 'png',
|
||||
className
|
||||
})
|
||||
.then(function (img) {
|
||||
saveAs(img.blob, processedFilename);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Takes a screenshot of a DOM node in PNG format.
|
||||
* @param {node} element to be exported
|
||||
* @param {string} filename the exported image
|
||||
* @returns {promise}
|
||||
*/
|
||||
|
||||
ExportImageService.prototype.exportPNGtoSRC = function (element, options) {
|
||||
|
||||
return this.renderElement(element, {
|
||||
imageType: 'png',
|
||||
...options
|
||||
});
|
||||
};
|
||||
|
||||
function replaceDotsWithUnderscores(filename) {
|
||||
const regex = /\./gi;
|
||||
|
||||
return filename.replace(regex, '_');
|
||||
}
|
||||
|
||||
/**
|
||||
* canvas.toBlob() not supported in IE < 10, Opera, and Safari. This polyfill
|
||||
* implements the method in browsers that would not otherwise support it.
|
||||
* https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob
|
||||
*/
|
||||
function polyfillToBlob() {
|
||||
if (!HTMLCanvasElement.prototype.toBlob) {
|
||||
Object.defineProperty(HTMLCanvasElement.prototype, "toBlob", {
|
||||
value: function (callback, mimeType, quality) {
|
||||
const binStr = atob(this.toDataURL(mimeType, quality).split(',')[1]);
|
||||
const len = binStr.length;
|
||||
const arr = new Uint8Array(len);
|
||||
|
||||
for (let i = 0; i < len; i++) {
|
||||
arr[i] = binStr.charCodeAt(i);
|
||||
}
|
||||
|
||||
callback(new Blob([arr], {type: mimeType || "image/png"}));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
polyfillToBlob();
|
||||
|
||||
return ExportImageService;
|
||||
}
|
||||
);
|
@ -46,8 +46,6 @@ class ActionCollection extends EventEmitter {
|
||||
this._observeObjectPath();
|
||||
this.openmct.editor.on('isEditing', this._updateActions);
|
||||
}
|
||||
|
||||
this._initializeActions();
|
||||
}
|
||||
|
||||
disable(actionKeys) {
|
||||
@ -156,19 +154,10 @@ class ActionCollection extends EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
_initializeActions() {
|
||||
Object.keys(this.applicableActions).forEach(key => {
|
||||
this.applicableActions[key].callBack = () => {
|
||||
return this.applicableActions[key].invoke(this.objectPath, this.view);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
_updateActions() {
|
||||
let newApplicableActions = this.openmct.actions._applicableActions(this.objectPath, this.view);
|
||||
|
||||
this.applicableActions = this._mergeOldAndNewActions(this.applicableActions, newApplicableActions);
|
||||
this._initializeActions();
|
||||
this._update();
|
||||
}
|
||||
|
||||
|
@ -34,7 +34,7 @@ class ActionsAPI extends EventEmitter {
|
||||
this._groupOrder = ['windowing', 'undefined', 'view', 'action', 'json'];
|
||||
|
||||
this.register = this.register.bind(this);
|
||||
this.get = this.get.bind(this);
|
||||
this.getActionsCollection = this.getActionsCollection.bind(this);
|
||||
this._applicableActions = this._applicableActions.bind(this);
|
||||
this._updateCachedActionCollections = this._updateCachedActionCollections.bind(this);
|
||||
}
|
||||
@ -43,12 +43,14 @@ class ActionsAPI extends EventEmitter {
|
||||
this._allActions[actionDefinition.key] = actionDefinition;
|
||||
}
|
||||
|
||||
get(objectPath, view) {
|
||||
if (view) {
|
||||
getAction(key) {
|
||||
return this._allActions[key];
|
||||
}
|
||||
|
||||
getActionsCollection(objectPath, view) {
|
||||
if (view) {
|
||||
return this._getCachedActionCollection(objectPath, view) || this._newActionCollection(objectPath, view, true);
|
||||
} else {
|
||||
|
||||
return this._newActionCollection(objectPath, view, true);
|
||||
}
|
||||
}
|
||||
@ -57,15 +59,6 @@ class ActionsAPI extends EventEmitter {
|
||||
this._groupOrder = groupArray;
|
||||
}
|
||||
|
||||
_get(objectPath, view) {
|
||||
let actionCollection = this._newActionCollection(objectPath, view);
|
||||
|
||||
this._actionCollections.set(view, actionCollection);
|
||||
actionCollection.on('destroy', this._updateCachedActionCollections);
|
||||
|
||||
return actionCollection;
|
||||
}
|
||||
|
||||
_getCachedActionCollection(objectPath, view) {
|
||||
let cachedActionCollection = this._actionCollections.get(view);
|
||||
|
||||
@ -75,7 +68,17 @@ class ActionsAPI extends EventEmitter {
|
||||
_newActionCollection(objectPath, view, skipEnvironmentObservers) {
|
||||
let applicableActions = this._applicableActions(objectPath, view);
|
||||
|
||||
return new ActionCollection(applicableActions, objectPath, view, this._openmct, skipEnvironmentObservers);
|
||||
const actionCollection = new ActionCollection(applicableActions, objectPath, view, this._openmct, skipEnvironmentObservers);
|
||||
if (view) {
|
||||
this._cacheActionCollection(view, actionCollection);
|
||||
}
|
||||
|
||||
return actionCollection;
|
||||
}
|
||||
|
||||
_cacheActionCollection(view, actionCollection) {
|
||||
this._actionCollections.set(view, actionCollection);
|
||||
actionCollection.on('destroy', this._updateCachedActionCollections);
|
||||
}
|
||||
|
||||
_updateCachedActionCollections(key) {
|
||||
|
@ -106,7 +106,7 @@ describe('The Actions API', () => {
|
||||
it("adds action to ActionsAPI", () => {
|
||||
actionsAPI.register(mockAction);
|
||||
|
||||
let actionCollection = actionsAPI.get(mockObjectPath, mockViewContext1);
|
||||
let actionCollection = actionsAPI.getActionsCollection(mockObjectPath, mockViewContext1);
|
||||
let action = actionCollection.getActionsObject()[mockAction.key];
|
||||
|
||||
expect(action.key).toEqual(mockAction.key);
|
||||
@ -121,21 +121,21 @@ describe('The Actions API', () => {
|
||||
});
|
||||
|
||||
it("returns an ActionCollection when invoked with an objectPath only", () => {
|
||||
let actionCollection = actionsAPI.get(mockObjectPath);
|
||||
let actionCollection = actionsAPI.getActionsCollection(mockObjectPath);
|
||||
let instanceOfActionCollection = actionCollection instanceof ActionCollection;
|
||||
|
||||
expect(instanceOfActionCollection).toBeTrue();
|
||||
});
|
||||
|
||||
it("returns an ActionCollection when invoked with an objectPath and view", () => {
|
||||
let actionCollection = actionsAPI.get(mockObjectPath, mockViewContext1);
|
||||
let actionCollection = actionsAPI.getActionsCollection(mockObjectPath, mockViewContext1);
|
||||
let instanceOfActionCollection = actionCollection instanceof ActionCollection;
|
||||
|
||||
expect(instanceOfActionCollection).toBeTrue();
|
||||
});
|
||||
|
||||
it("returns relevant actions when invoked with objectPath only", () => {
|
||||
let actionCollection = actionsAPI.get(mockObjectPath);
|
||||
let actionCollection = actionsAPI.getActionsCollection(mockObjectPath);
|
||||
let action = actionCollection.getActionsObject()[mockObjectPathAction.key];
|
||||
|
||||
expect(action.key).toEqual(mockObjectPathAction.key);
|
||||
@ -143,7 +143,7 @@ describe('The Actions API', () => {
|
||||
});
|
||||
|
||||
it("returns relevant actions when invoked with objectPath and view", () => {
|
||||
let actionCollection = actionsAPI.get(mockObjectPath, mockViewContext1);
|
||||
let actionCollection = actionsAPI.getActionsCollection(mockObjectPath, mockViewContext1);
|
||||
let action = actionCollection.getActionsObject()[mockAction.key];
|
||||
|
||||
expect(action.key).toEqual(mockAction.key);
|
||||
|
@ -37,7 +37,7 @@ import Menu, { MENU_PLACEMENT } from './menu.js';
|
||||
* @property {Boolean} isDisabled adds disable class if true
|
||||
* @property {String} name Menu item text
|
||||
* @property {String} description Menu item description
|
||||
* @property {Function} callBack callback function: invoked when item is clicked
|
||||
* @property {Function} onItemClicked callback function: invoked when item is clicked
|
||||
*/
|
||||
|
||||
/**
|
||||
@ -66,12 +66,27 @@ class MenuAPI {
|
||||
* @param {Array.<Action>|Array.<Array.<Action>>} actions collection of actions{@link Action} or collection of groups of actions {@link Action}
|
||||
* @param {MenuOptions} [menuOptions] [Optional] The {@link MenuOptions} options for Menu
|
||||
*/
|
||||
showMenu(x, y, actions, menuOptions) {
|
||||
this._createMenuComponent(x, y, actions, menuOptions);
|
||||
showMenu(x, y, items, menuOptions) {
|
||||
this._createMenuComponent(x, y, items, menuOptions);
|
||||
|
||||
this.menuComponent.showMenu();
|
||||
}
|
||||
|
||||
actionsToMenuItems(actions, objectPath, view) {
|
||||
return actions.map(action => {
|
||||
const isActionGroup = Array.isArray(action);
|
||||
if (isActionGroup) {
|
||||
action = this.actionsToMenuItems(action, objectPath, view);
|
||||
} else {
|
||||
action.onItemClicked = () => {
|
||||
action.invoke(objectPath, view);
|
||||
};
|
||||
}
|
||||
|
||||
return action;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show popup menu with description of item on hover
|
||||
* @param {number} x x-coordinates for popup
|
||||
|
@ -57,7 +57,7 @@ describe ('The Menu API', () => {
|
||||
name: 'Test Action 1',
|
||||
cssClass: 'icon-clock',
|
||||
description: 'This is a test action',
|
||||
callBack: () => {
|
||||
onItemClicked: () => {
|
||||
result = 'Test Action 1 Invoked';
|
||||
}
|
||||
},
|
||||
@ -66,7 +66,7 @@ describe ('The Menu API', () => {
|
||||
name: 'Test Action 2',
|
||||
cssClass: 'icon-clock',
|
||||
description: 'This is a test action',
|
||||
callBack: () => {
|
||||
onItemClicked: () => {
|
||||
result = 'Test Action 2 Invoked';
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,7 @@
|
||||
:key="action.name"
|
||||
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
|
||||
:title="action.description"
|
||||
@click="action.callBack"
|
||||
@click="action.onItemClicked"
|
||||
>
|
||||
{{ action.name }}
|
||||
</li>
|
||||
@ -36,7 +36,7 @@
|
||||
:key="action.name"
|
||||
:class="action.cssClass"
|
||||
:title="action.description"
|
||||
@click="action.callBack"
|
||||
@click="action.onItemClicked"
|
||||
>
|
||||
{{ action.name }}
|
||||
</li>
|
||||
|
@ -13,7 +13,7 @@
|
||||
:key="action.name"
|
||||
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
|
||||
:title="action.description"
|
||||
@click="action.callBack"
|
||||
@click="action.onItemClicked"
|
||||
@mouseover="toggleItemDescription(action)"
|
||||
@mouseleave="toggleItemDescription()"
|
||||
>
|
||||
@ -42,7 +42,7 @@
|
||||
:key="action.name"
|
||||
:class="action.cssClass"
|
||||
:title="action.description"
|
||||
@click="action.callBack"
|
||||
@click="action.onItemClicked"
|
||||
@mouseover="toggleItemDescription(action)"
|
||||
@mouseleave="toggleItemDescription()"
|
||||
>
|
||||
|
@ -71,12 +71,12 @@ class Menu extends EventEmitter {
|
||||
|
||||
showMenu() {
|
||||
this.component = new Vue({
|
||||
provide: {
|
||||
options: this.options
|
||||
},
|
||||
components: {
|
||||
MenuComponent
|
||||
},
|
||||
provide: {
|
||||
options: this.options
|
||||
},
|
||||
template: '<menu-component />'
|
||||
});
|
||||
|
||||
@ -85,12 +85,12 @@ class Menu extends EventEmitter {
|
||||
|
||||
showSuperMenu() {
|
||||
this.component = new Vue({
|
||||
provide: {
|
||||
options: this.options
|
||||
},
|
||||
components: {
|
||||
SuperMenuComponent
|
||||
},
|
||||
provide: {
|
||||
options: this.options
|
||||
},
|
||||
template: '<super-menu-component />'
|
||||
});
|
||||
|
||||
|
185
src/exporters/ImageExporter.js
Normal file
185
src/exporters/ImageExporter.js
Normal file
@ -0,0 +1,185 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* Class defining an image exporter for JPG/PNG output.
|
||||
* Originally created by hudsonfoo on 09/02/16
|
||||
*/
|
||||
|
||||
function replaceDotsWithUnderscores(filename) {
|
||||
const regex = /\./gi;
|
||||
|
||||
return filename.replace(regex, '_');
|
||||
}
|
||||
|
||||
import {saveAs} from 'file-saver/FileSaver';
|
||||
import html2canvas from 'html2canvas';
|
||||
import uuid from 'uuid';
|
||||
|
||||
class ImageExporter {
|
||||
constructor(openmct) {
|
||||
this.openmct = openmct;
|
||||
}
|
||||
/**
|
||||
* Converts an HTML element into a PNG or JPG Blob.
|
||||
* @private
|
||||
* @param {node} element that will be converted to an image
|
||||
* @param {object} options Image options.
|
||||
* @returns {promise}
|
||||
*/
|
||||
renderElement(element, { imageType, className, thumbnailSize }) {
|
||||
const self = this;
|
||||
const overlays = this.openmct.overlays;
|
||||
const dialog = overlays.dialog({
|
||||
iconClass: 'info',
|
||||
message: 'Caputuring an image',
|
||||
buttons: [
|
||||
{
|
||||
label: 'Cancel',
|
||||
emphasis: true,
|
||||
callback: function () {
|
||||
dialog.dismiss();
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
let mimeType = 'image/png';
|
||||
if (imageType === 'jpg') {
|
||||
mimeType = 'image/jpeg';
|
||||
}
|
||||
|
||||
let exportId = undefined;
|
||||
let oldId = undefined;
|
||||
if (className) {
|
||||
const newUUID = uuid();
|
||||
exportId = `$export-element-${newUUID}`;
|
||||
oldId = element.id;
|
||||
element.id = exportId;
|
||||
}
|
||||
|
||||
return html2canvas(element, {
|
||||
onclone: function (document) {
|
||||
if (className) {
|
||||
const clonedElement = document.getElementById(exportId);
|
||||
clonedElement.classList.add(className);
|
||||
}
|
||||
|
||||
element.id = oldId;
|
||||
},
|
||||
removeContainer: true // Set to false to debug what html2canvas renders
|
||||
}).then(function (canvas) {
|
||||
dialog.dismiss();
|
||||
|
||||
return new Promise(function (resolve, reject) {
|
||||
if (thumbnailSize) {
|
||||
const thumbnail = self.getThumbnail(canvas, mimeType, thumbnailSize);
|
||||
|
||||
return canvas.toBlob(blob => resolve({
|
||||
blob,
|
||||
thumbnail
|
||||
}), mimeType);
|
||||
}
|
||||
|
||||
return canvas.toBlob(blob => resolve({ blob }), mimeType);
|
||||
});
|
||||
}, function (error) {
|
||||
console.log('error capturing image', error);
|
||||
dialog.dismiss();
|
||||
const errorDialog = overlays.dialog({
|
||||
iconClass: 'error',
|
||||
message: 'Image was not captured successfully!',
|
||||
buttons: [
|
||||
{
|
||||
label: "OK",
|
||||
emphasis: true,
|
||||
callback: function () {
|
||||
errorDialog.dismiss();
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getThumbnail(canvas, mimeType, size) {
|
||||
const thumbnailCanvas = document.createElement('canvas');
|
||||
thumbnailCanvas.setAttribute('width', size.width);
|
||||
thumbnailCanvas.setAttribute('height', size.height);
|
||||
const ctx = thumbnailCanvas.getContext('2d');
|
||||
ctx.globalCompositeOperation = "copy";
|
||||
ctx.drawImage(canvas, 0, 0, canvas.width, canvas.height, 0, 0, size.width, size.height);
|
||||
|
||||
return thumbnailCanvas.toDataURL(mimeType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a screenshot of a DOM node and exports to JPG.
|
||||
* @param {node} element to be exported
|
||||
* @param {string} filename the exported image
|
||||
* @param {string} className to be added to element before capturing (optional)
|
||||
* @returns {promise}
|
||||
*/
|
||||
async exportJPG(element, filename, className) {
|
||||
const processedFilename = replaceDotsWithUnderscores(filename);
|
||||
|
||||
const img = await this.renderElement(element, {
|
||||
imageType: 'jpg',
|
||||
className
|
||||
});
|
||||
saveAs(img.blob, processedFilename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a screenshot of a DOM node and exports to PNG.
|
||||
* @param {node} element to be exported
|
||||
* @param {string} filename the exported image
|
||||
* @param {string} className to be added to element before capturing (optional)
|
||||
* @returns {promise}
|
||||
*/
|
||||
async exportPNG(element, filename, className) {
|
||||
const processedFilename = replaceDotsWithUnderscores(filename);
|
||||
|
||||
const img = await this.renderElement(element, {
|
||||
imageType: 'png',
|
||||
className
|
||||
});
|
||||
saveAs(img.blob, processedFilename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a screenshot of a DOM node in PNG format.
|
||||
* @param {node} element to be exported
|
||||
* @param {string} filename the exported image
|
||||
* @returns {promise}
|
||||
*/
|
||||
|
||||
exportPNGtoSRC(element, options) {
|
||||
return this.renderElement(element, {
|
||||
imageType: 'png',
|
||||
...options
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default ImageExporter;
|
||||
|
58
src/exporters/ImageExporterSpec.js
Normal file
58
src/exporters/ImageExporterSpec.js
Normal file
@ -0,0 +1,58 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import ImageExporter from './ImageExporter';
|
||||
import { createOpenMct, resetApplicationState } from '../utils/testing';
|
||||
|
||||
describe('The Image Exporter', () => {
|
||||
let openmct;
|
||||
let imageExporter;
|
||||
|
||||
beforeEach(() => {
|
||||
openmct = createOpenMct();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
return resetApplicationState(openmct);
|
||||
});
|
||||
|
||||
describe("basic instatation", () => {
|
||||
it("can be instatiated", () => {
|
||||
imageExporter = new ImageExporter(openmct);
|
||||
|
||||
expect(imageExporter).not.toEqual(null);
|
||||
});
|
||||
it("can render an element to a blob", async () => {
|
||||
const mockHeadElement = document.createElement("h1");
|
||||
const mockTextNode = document.createTextNode('foo bar');
|
||||
mockHeadElement.appendChild(mockTextNode);
|
||||
document.body.appendChild(mockHeadElement);
|
||||
imageExporter = new ImageExporter(openmct);
|
||||
const returnedBlob = await imageExporter.renderElement(document.body, {
|
||||
imageType: 'png'
|
||||
});
|
||||
expect(returnedBlob).not.toEqual(null);
|
||||
expect(returnedBlob.blob).not.toEqual(null);
|
||||
expect(returnedBlob.blob).toBeInstanceOf(Blob);
|
||||
});
|
||||
});
|
||||
});
|
@ -38,8 +38,6 @@ const DEFAULTS = [
|
||||
'platform/exporters',
|
||||
'platform/telemetry',
|
||||
'platform/features/clock',
|
||||
'platform/features/hyperlink',
|
||||
'platform/features/timeline',
|
||||
'platform/forms',
|
||||
'platform/identity',
|
||||
'platform/persistence/aggregator',
|
||||
@ -82,9 +80,7 @@ define([
|
||||
'../platform/exporters/bundle',
|
||||
'../platform/features/clock/bundle',
|
||||
'../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',
|
||||
|
@ -19,8 +19,8 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import LadTableSet from './components/LadTableSet.vue';
|
||||
import Vue from 'vue';
|
||||
|
||||
import LadTableSetView from './LadTableSetView';
|
||||
|
||||
export default function LADTableSetViewProvider(openmct) {
|
||||
return {
|
||||
@ -34,32 +34,7 @@ export default function LADTableSetViewProvider(openmct) {
|
||||
return domainObject.type === 'LadTableSet';
|
||||
},
|
||||
view: function (domainObject, objectPath) {
|
||||
let component;
|
||||
|
||||
return {
|
||||
show: function (element) {
|
||||
component = new Vue({
|
||||
el: element,
|
||||
components: {
|
||||
LadTableSet: LadTableSet
|
||||
},
|
||||
provide: {
|
||||
openmct,
|
||||
objectPath
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
domainObject
|
||||
};
|
||||
},
|
||||
template: '<lad-table-set :domain-object="domainObject"></lad-table-set>'
|
||||
});
|
||||
},
|
||||
destroy: function (element) {
|
||||
component.$destroy();
|
||||
component = undefined;
|
||||
}
|
||||
};
|
||||
return new LadTableSetView(openmct, domainObject, objectPath);
|
||||
},
|
||||
priority: function () {
|
||||
return 1;
|
||||
|
45
src/plugins/LADTable/LADTableView.js
Normal file
45
src/plugins/LADTable/LADTableView.js
Normal file
@ -0,0 +1,45 @@
|
||||
import LadTable from './components/LADTable.vue';
|
||||
|
||||
import Vue from 'vue';
|
||||
|
||||
export default class LADTableView {
|
||||
constructor(openmct, domainObject, objectPath) {
|
||||
this.openmct = openmct;
|
||||
this.domainObject = domainObject;
|
||||
this.objectPath = objectPath;
|
||||
this.component = undefined;
|
||||
}
|
||||
|
||||
show(element) {
|
||||
this.component = new Vue({
|
||||
el: element,
|
||||
components: {
|
||||
LadTable
|
||||
},
|
||||
provide: {
|
||||
openmct: this.openmct,
|
||||
currentView: this
|
||||
},
|
||||
data: () => {
|
||||
return {
|
||||
domainObject: this.domainObject,
|
||||
objectPath: this.objectPath
|
||||
};
|
||||
},
|
||||
template: '<lad-table ref="ladTable" :domain-object="domainObject" :object-path="objectPath"></lad-table>'
|
||||
});
|
||||
}
|
||||
|
||||
getViewContext() {
|
||||
if (!this.component) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return this.component.$refs.ladTable.getViewContext();
|
||||
}
|
||||
|
||||
destroy(element) {
|
||||
this.component.$destroy();
|
||||
this.component = undefined;
|
||||
}
|
||||
}
|
@ -19,50 +19,30 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import LadTable from './components/LADTable.vue';
|
||||
import Vue from 'vue';
|
||||
|
||||
export default function LADTableViewProvider(openmct) {
|
||||
return {
|
||||
key: 'LadTable',
|
||||
name: 'LAD Table',
|
||||
cssClass: 'icon-tabular-lad',
|
||||
canView: function (domainObject) {
|
||||
return domainObject.type === 'LadTable';
|
||||
},
|
||||
canEdit: function (domainObject) {
|
||||
return domainObject.type === 'LadTable';
|
||||
},
|
||||
view: function (domainObject, objectPath) {
|
||||
let component;
|
||||
import LADTableView from './LADTableView';
|
||||
|
||||
return {
|
||||
show: function (element) {
|
||||
component = new Vue({
|
||||
el: element,
|
||||
components: {
|
||||
LadTableComponent: LadTable
|
||||
},
|
||||
provide: {
|
||||
openmct
|
||||
},
|
||||
data: () => {
|
||||
return {
|
||||
domainObject,
|
||||
objectPath
|
||||
};
|
||||
},
|
||||
template: '<lad-table-component :domain-object="domainObject" :object-path="objectPath"></lad-table-component>'
|
||||
});
|
||||
},
|
||||
destroy: function (element) {
|
||||
component.$destroy();
|
||||
component = undefined;
|
||||
}
|
||||
};
|
||||
},
|
||||
priority: function () {
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
export default class LADTableViewProvider {
|
||||
constructor(openmct) {
|
||||
this.openmct = openmct;
|
||||
this.name = 'LAD Table';
|
||||
this.key = 'LadTable';
|
||||
this.cssClass = 'icon-tabular-lad';
|
||||
}
|
||||
|
||||
canView(domainObject) {
|
||||
return domainObject.type === 'LadTable';
|
||||
}
|
||||
|
||||
canEdit(domainObject) {
|
||||
return domainObject.type === 'LadTable';
|
||||
}
|
||||
|
||||
view(domainObject, objectPath) {
|
||||
return new LADTableView(this.openmct, domainObject, objectPath);
|
||||
}
|
||||
|
||||
priority(domainObject) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
45
src/plugins/LADTable/LadTableSetView.js
Normal file
45
src/plugins/LADTable/LadTableSetView.js
Normal file
@ -0,0 +1,45 @@
|
||||
import LadTableSet from './components/LadTableSet.vue';
|
||||
|
||||
import Vue from 'vue';
|
||||
|
||||
export default class LadTableSetView {
|
||||
constructor(openmct, domainObject, objectPath) {
|
||||
this.openmct = openmct;
|
||||
this.domainObject = domainObject;
|
||||
this.objectPath = objectPath;
|
||||
this.component = undefined;
|
||||
}
|
||||
|
||||
show(element) {
|
||||
this.component = new Vue({
|
||||
el: element,
|
||||
components: {
|
||||
LadTableSet
|
||||
},
|
||||
provide: {
|
||||
openmct: this.openmct,
|
||||
objectPath: this.objectPath,
|
||||
currentView: this
|
||||
},
|
||||
data: () => {
|
||||
return {
|
||||
domainObject: this.domainObject
|
||||
};
|
||||
},
|
||||
template: '<lad-table-set ref="ladTableSet" :domain-object="domainObject"></lad-table-set>'
|
||||
});
|
||||
}
|
||||
|
||||
getViewContext() {
|
||||
if (!this.component) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return this.component.$refs.ladTableSet.getViewContext();
|
||||
}
|
||||
|
||||
destroy(element) {
|
||||
this.component.$destroy();
|
||||
this.component = undefined;
|
||||
}
|
||||
}
|
@ -50,7 +50,7 @@ const CONTEXT_MENU_ACTIONS = [
|
||||
];
|
||||
|
||||
export default {
|
||||
inject: ['openmct'],
|
||||
inject: ['openmct', 'currentView'],
|
||||
props: {
|
||||
domainObject: {
|
||||
type: Object,
|
||||
@ -167,25 +167,23 @@ export default {
|
||||
this.resetValues();
|
||||
this.timestampKey = timeSystem.key;
|
||||
},
|
||||
getView() {
|
||||
return {
|
||||
getViewContext: () => {
|
||||
return {
|
||||
viewHistoricalData: true,
|
||||
viewDatumAction: true,
|
||||
getDatum: () => {
|
||||
return this.datum;
|
||||
}
|
||||
};
|
||||
updateViewContext() {
|
||||
this.$emit('rowContextClick', {
|
||||
viewHistoricalData: true,
|
||||
viewDatumAction: true,
|
||||
getDatum: () => {
|
||||
return this.datum;
|
||||
}
|
||||
};
|
||||
});
|
||||
},
|
||||
showContextMenu(event) {
|
||||
let actionCollection = this.openmct.actions.get(this.objectPath, this.getView());
|
||||
let allActions = actionCollection.getActionsObject();
|
||||
let applicableActions = CONTEXT_MENU_ACTIONS.map(key => allActions[key]);
|
||||
this.updateViewContext();
|
||||
|
||||
this.openmct.menus.showMenu(event.x, event.y, applicableActions);
|
||||
const actions = CONTEXT_MENU_ACTIONS.map(key => this.openmct.actions.getAction(key));
|
||||
const menuItems = this.openmct.menus.actionsToMenuItems(actions, this.objectPath, this.currentView);
|
||||
if (menuItems.length) {
|
||||
this.openmct.menus.showMenu(event.x, event.y, menuItems);
|
||||
}
|
||||
},
|
||||
resetValues() {
|
||||
this.value = '---';
|
||||
|
@ -38,6 +38,7 @@
|
||||
:domain-object="ladRow.domainObject"
|
||||
:path-to-table="objectPath"
|
||||
:has-units="hasUnits"
|
||||
@rowContextClick="updateViewContext"
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
@ -51,7 +52,7 @@ export default {
|
||||
components: {
|
||||
LadRow
|
||||
},
|
||||
inject: ['openmct'],
|
||||
inject: ['openmct', 'currentView'],
|
||||
props: {
|
||||
domainObject: {
|
||||
type: Object,
|
||||
@ -64,7 +65,8 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
items: []
|
||||
items: [],
|
||||
viewContext: {}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@ -114,6 +116,12 @@ export default {
|
||||
let metadataWithUnits = valueMetadatas.filter(metadatum => metadatum.unit);
|
||||
|
||||
return metadataWithUnits.length > 0;
|
||||
},
|
||||
updateViewContext(rowContext) {
|
||||
this.viewContext.row = rowContext;
|
||||
},
|
||||
getViewContext() {
|
||||
return this.viewContext;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -48,6 +48,7 @@
|
||||
:domain-object="ladRow.domainObject"
|
||||
:path-to-table="ladTable.objectPath"
|
||||
:has-units="hasUnits"
|
||||
@rowContextClick="updateViewContext"
|
||||
/>
|
||||
</template>
|
||||
</tbody>
|
||||
@ -61,7 +62,7 @@ export default {
|
||||
components: {
|
||||
LadRow
|
||||
},
|
||||
inject: ['openmct', 'objectPath'],
|
||||
inject: ['openmct', 'objectPath', 'currentView'],
|
||||
props: {
|
||||
domainObject: {
|
||||
type: Object,
|
||||
@ -72,7 +73,8 @@ export default {
|
||||
return {
|
||||
ladTableObjects: [],
|
||||
ladTelemetryObjects: {},
|
||||
compositions: []
|
||||
compositions: [],
|
||||
viewContext: {}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@ -166,6 +168,12 @@ export default {
|
||||
|
||||
this.$set(this.ladTelemetryObjects, ladTable.key, telemetryObjects);
|
||||
};
|
||||
},
|
||||
updateViewContext(rowContext) {
|
||||
this.viewContext.row = rowContext;
|
||||
},
|
||||
getViewContext() {
|
||||
return this.viewContext;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -45,6 +45,7 @@ export default class URLTimeSettingsSynchronizer {
|
||||
}
|
||||
|
||||
initialize() {
|
||||
this.updateTimeSettings();
|
||||
this.openmct.router.on('change:params', this.updateTimeSettings);
|
||||
|
||||
TIME_EVENTS.forEach(event => {
|
||||
|
59
src/plugins/clock/ClockViewProvider.js
Normal file
59
src/plugins/clock/ClockViewProvider.js
Normal file
@ -0,0 +1,59 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import Clock from './components/Clock.vue';
|
||||
import Vue from 'vue';
|
||||
|
||||
export default function ClockViewProvider(openmct) {
|
||||
return {
|
||||
key: 'clock.view',
|
||||
name: 'Clock',
|
||||
cssClass: 'icon-clock',
|
||||
canView(domainObject) {
|
||||
return domainObject.type === 'clock';
|
||||
},
|
||||
|
||||
view: function (domainObject) {
|
||||
let component;
|
||||
|
||||
return {
|
||||
show: function (element) {
|
||||
component = new Vue({
|
||||
el: element,
|
||||
components: {
|
||||
Clock
|
||||
},
|
||||
provide: {
|
||||
openmct,
|
||||
domainObject
|
||||
},
|
||||
template: '<clock></clock>'
|
||||
});
|
||||
},
|
||||
destroy: function () {
|
||||
component.$destroy();
|
||||
component = undefined;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
99
src/plugins/clock/components/Clock.vue
Normal file
99
src/plugins/clock/components/Clock.vue
Normal file
@ -0,0 +1,99 @@
|
||||
<!--
|
||||
Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
as represented by the Administrator of the National Aeronautics and Space
|
||||
Administration. All rights reserved.
|
||||
|
||||
Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0.
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
License for the specific language governing permissions and limitations
|
||||
under the License.
|
||||
|
||||
Open MCT includes source code licensed under additional open source
|
||||
licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
this source code distribution or the Licensing information page available
|
||||
at runtime from the About dialog for additional information.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="l-angular-ov-wrapper">
|
||||
<div class="u-contents">
|
||||
<div class="c-clock l-time-display u-style-receiver js-style-receiver">
|
||||
<div class="c-clock__timezone">
|
||||
{{ timeZoneAbbr }}
|
||||
</div>
|
||||
<div class="c-clock__value">
|
||||
{{ timeTextValue }}
|
||||
</div>
|
||||
<div class="c-clock__ampm">
|
||||
{{ timeAmPm }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import moment from 'moment';
|
||||
import momentTimezone from 'moment-timezone';
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'domainObject'],
|
||||
data() {
|
||||
return {
|
||||
lastTimestamp: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
configuration() {
|
||||
return this.domainObject.configuration;
|
||||
},
|
||||
baseFormat() {
|
||||
return this.configuration.baseFormat;
|
||||
},
|
||||
use24() {
|
||||
return this.configuration.use24 === 'clock24';
|
||||
},
|
||||
timezone() {
|
||||
return this.configuration.timezone;
|
||||
},
|
||||
timeFormat() {
|
||||
return this.use24 ? this.baseFormat.replace('hh', "HH") : this.baseFormat;
|
||||
},
|
||||
zoneName() {
|
||||
return momentTimezone.tz.names().includes(this.timezone) ? this.timezone : "UTC";
|
||||
},
|
||||
momentTime() {
|
||||
return this.zoneName ? moment.utc(this.lastTimestamp).tz(this.zoneName) : moment.utc(this.lastTimestamp);
|
||||
},
|
||||
timeZoneAbbr() {
|
||||
return this.momentTime.zoneAbbr();
|
||||
},
|
||||
timeTextValue() {
|
||||
return this.timeFormat && this.momentTime.format(this.timeFormat);
|
||||
},
|
||||
timeAmPm() {
|
||||
return this.use24 ? '' : this.momentTime.format("A");
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
const TickerService = this.openmct.$injector.get('tickerService');
|
||||
this.unlisten = TickerService.listen(this.tick);
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.unlisten) {
|
||||
this.unlisten();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
tick(timestamp) {
|
||||
this.lastTimestamp = timestamp;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
@ -19,10 +19,46 @@
|
||||
this source code distribution or the Licensing information page available
|
||||
at runtime from the About dialog for additional information.
|
||||
-->
|
||||
<a class="c-hyperlink u-links" ng-controller="HyperlinkController as hyperlink" href="{{domainObject.getModel().url}}"
|
||||
ng-attr-target="{{hyperlink.openNewTab() ? '_blank' : undefined}}"
|
||||
ng-class="{
|
||||
'c-hyperlink--button u-fills-container' : hyperlink.isButton(),
|
||||
'c-hyperlink--link' : !hyperlink.isButton() }">
|
||||
<span class="c-hyperlink__label">{{domainObject.getModel().displayText}}</span>
|
||||
</a>
|
||||
|
||||
<template>
|
||||
<div class="c-indicator t-indicator-clock icon-clock no-minify c-indicator--not-clickable">
|
||||
<span class="label c-indicator__label">
|
||||
{{ timeTextValue }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import moment from 'moment';
|
||||
|
||||
export default {
|
||||
inject: ['openmct'],
|
||||
props: {
|
||||
indicatorFormat: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
timeTextValue: null
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.openmct.on('start', () => {
|
||||
const TickerService = this.openmct.$injector.get('tickerService');
|
||||
this.unlisten = TickerService.listen(this.tick);
|
||||
});
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.unlisten) {
|
||||
this.unlisten();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
tick(timestamp) {
|
||||
this.timeTextValue = `${moment.utc(timestamp).format(this.indicatorFormat)} UTC`;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
154
src/plugins/clock/plugin.js
Normal file
154
src/plugins/clock/plugin.js
Normal file
@ -0,0 +1,154 @@
|
||||
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import ClockViewProvider from './ClockViewProvider';
|
||||
import ClockIndicator from './components/ClockIndicator.vue';
|
||||
|
||||
import momentTimezone from 'moment-timezone';
|
||||
import Vue from 'vue';
|
||||
|
||||
export default function ClockPlugin(options) {
|
||||
return function install(openmct) {
|
||||
const CLOCK_INDICATOR_FORMAT = 'YYYY/MM/DD HH:mm:ss';
|
||||
openmct.types.addType('clock', {
|
||||
name: 'Clock',
|
||||
description: 'A UTC-based clock that supports a variety of display formats. Clocks can be added to Display Layouts.',
|
||||
creatable: true,
|
||||
cssClass: 'icon-clock',
|
||||
initialize: function (domainObject) {
|
||||
domainObject.configuration = {
|
||||
baseFormat: 'YYYY/MM/DD hh:mm:ss',
|
||||
use24: 'clock12',
|
||||
timezone: 'UTC'
|
||||
};
|
||||
},
|
||||
"form": [
|
||||
{
|
||||
"key": "displayFormat",
|
||||
"name": "Display Format",
|
||||
control: 'select',
|
||||
options: [
|
||||
{
|
||||
value: 'YYYY/MM/DD hh:mm:ss',
|
||||
name: 'YYYY/MM/DD hh:mm:ss'
|
||||
},
|
||||
{
|
||||
value: 'YYYY/DDD hh:mm:ss',
|
||||
name: 'YYYY/DDD hh:mm:ss'
|
||||
},
|
||||
{
|
||||
value: 'hh:mm:ss',
|
||||
name: 'hh:mm:ss'
|
||||
}
|
||||
],
|
||||
cssClass: 'l-inline',
|
||||
property: [
|
||||
'configuration',
|
||||
'baseFormat'
|
||||
]
|
||||
},
|
||||
{
|
||||
control: 'select',
|
||||
options: [
|
||||
{
|
||||
value: 'clock12',
|
||||
name: '12hr'
|
||||
},
|
||||
{
|
||||
value: 'clock24',
|
||||
name: '24hr'
|
||||
}
|
||||
],
|
||||
cssClass: 'l-inline',
|
||||
property: [
|
||||
'configuration',
|
||||
'use24'
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "timezone",
|
||||
"name": "Timezone",
|
||||
"control": "autocomplete",
|
||||
"options": momentTimezone.tz.names(),
|
||||
property: [
|
||||
'configuration',
|
||||
'timezone'
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
openmct.objectViews.addProvider(new ClockViewProvider(openmct));
|
||||
|
||||
if (options && options.enableClockIndicator) {
|
||||
const clockIndicator = new Vue ({
|
||||
components: {
|
||||
ClockIndicator
|
||||
},
|
||||
provide: {
|
||||
openmct
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
indicatorFormat: CLOCK_INDICATOR_FORMAT
|
||||
};
|
||||
},
|
||||
template: '<ClockIndicator :indicator-format="indicatorFormat"></ClockIndicator>'
|
||||
});
|
||||
const indicator = {
|
||||
element: clockIndicator.$mount().$el,
|
||||
key: 'clock-indicator'
|
||||
};
|
||||
|
||||
openmct.indicators.add(indicator);
|
||||
}
|
||||
|
||||
openmct.objects.addGetInterceptor({
|
||||
appliesTo: (identifier, domainObject) => {
|
||||
return domainObject && domainObject.type === 'clock';
|
||||
},
|
||||
invoke: (identifier, domainObject) => {
|
||||
if (domainObject.configuration) {
|
||||
return domainObject;
|
||||
}
|
||||
|
||||
if (domainObject.clockFormat
|
||||
&& domainObject.timezone) {
|
||||
const baseFormat = domainObject.clockFormat[0];
|
||||
const use24 = domainObject.clockFormat[1];
|
||||
const timezone = domainObject.timezone;
|
||||
|
||||
domainObject.configuration = {
|
||||
baseFormat,
|
||||
use24,
|
||||
timezone
|
||||
};
|
||||
|
||||
openmct.objects.mutate(domainObject, 'configuration', domainObject.configuration);
|
||||
}
|
||||
|
||||
return domainObject;
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
}
|
@ -41,7 +41,7 @@ export default class ConditionManager extends EventEmitter {
|
||||
this.subscriptions = {};
|
||||
this.telemetryObjects = {};
|
||||
this.testData = {
|
||||
conditionTestData: [],
|
||||
conditionTestInputs: this.conditionSetDomainObject.configuration.conditionTestData,
|
||||
applied: false
|
||||
};
|
||||
this.initialize();
|
||||
@ -154,8 +154,10 @@ export default class ConditionManager extends EventEmitter {
|
||||
|
||||
updateConditionDescription(condition) {
|
||||
const found = this.conditionSetDomainObject.configuration.conditionCollection.find(conditionConfiguration => (conditionConfiguration.id === condition.id));
|
||||
found.summary = condition.description;
|
||||
this.persistConditions();
|
||||
if (found.summary !== condition.description) {
|
||||
found.summary = condition.description;
|
||||
this.persistConditions();
|
||||
}
|
||||
}
|
||||
|
||||
initCondition(conditionConfiguration, index) {
|
||||
@ -414,8 +416,10 @@ export default class ConditionManager extends EventEmitter {
|
||||
}
|
||||
|
||||
updateTestData(testData) {
|
||||
this.testData = testData;
|
||||
this.openmct.objects.mutate(this.conditionSetDomainObject, 'configuration.conditionTestData', this.testData.conditionTestInputs);
|
||||
if (!_.isEqual(testData, this.testData)) {
|
||||
this.testData = testData;
|
||||
this.openmct.objects.mutate(this.conditionSetDomainObject, 'configuration.conditionTestData', this.testData.conditionTestInputs);
|
||||
}
|
||||
}
|
||||
|
||||
persistConditions() {
|
||||
|
@ -215,7 +215,8 @@ export default {
|
||||
},
|
||||
isEditing: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
required: true,
|
||||
default: false
|
||||
},
|
||||
telemetry: {
|
||||
type: Array,
|
||||
|
@ -20,71 +20,78 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define([
|
||||
'./components/AlphanumericFormatView.vue',
|
||||
'vue'
|
||||
], function (AlphanumericFormatView, Vue) {
|
||||
import AlphanumericFormat from './components/AlphanumericFormat.vue';
|
||||
|
||||
function AlphanumericFormatViewProvider(openmct, options) {
|
||||
function isTelemetryObject(selectionPath) {
|
||||
let selectedObject = selectionPath[0].context.item;
|
||||
let parentObject = selectionPath[1].context.item;
|
||||
let selectedLayoutItem = selectionPath[0].context.layoutItem;
|
||||
import Vue from 'vue';
|
||||
|
||||
return parentObject
|
||||
&& parentObject.type === 'layout'
|
||||
&& selectedObject
|
||||
&& selectedLayoutItem
|
||||
&& selectedLayoutItem.type === 'telemetry-view'
|
||||
&& openmct.telemetry.isTelemetryObject(selectedObject)
|
||||
&& !options.showAsView.includes(selectedObject.type);
|
||||
}
|
||||
|
||||
return {
|
||||
key: 'alphanumeric-format',
|
||||
name: 'Alphanumeric Format',
|
||||
canView: function (selection) {
|
||||
if (selection.length === 0 || selection[0].length === 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return selection.every(isTelemetryObject);
|
||||
},
|
||||
view: function (domainObject, objectPath) {
|
||||
let component;
|
||||
|
||||
return {
|
||||
show: function (element) {
|
||||
component = new Vue({
|
||||
el: element,
|
||||
components: {
|
||||
AlphanumericFormatView: AlphanumericFormatView.default
|
||||
},
|
||||
provide: {
|
||||
openmct,
|
||||
objectPath
|
||||
},
|
||||
template: '<alphanumeric-format-view ref="alphanumericFormatView"></alphanumeric-format-view>'
|
||||
});
|
||||
},
|
||||
getViewContext() {
|
||||
if (component) {
|
||||
return component.$refs.alphanumericFormatView.getViewContext();
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
destroy: function () {
|
||||
component.$destroy();
|
||||
component = undefined;
|
||||
}
|
||||
};
|
||||
},
|
||||
priority: function () {
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
class AlphanumericFormatView {
|
||||
constructor(openmct, domainObject, objectPath) {
|
||||
this.openmct = openmct;
|
||||
this.domainObject = domainObject;
|
||||
this.objectPath = objectPath;
|
||||
this.component = undefined;
|
||||
}
|
||||
|
||||
return AlphanumericFormatViewProvider;
|
||||
});
|
||||
show(element) {
|
||||
this.component = new Vue({
|
||||
el: element,
|
||||
name: 'AlphanumericFormat',
|
||||
components: {
|
||||
AlphanumericFormat
|
||||
},
|
||||
provide: {
|
||||
openmct: this.openmct,
|
||||
objectPath: this.objectPath,
|
||||
currentView: this
|
||||
},
|
||||
template: '<alphanumeric-format ref="alphanumericFormat"></alphanumeric-format>'
|
||||
});
|
||||
}
|
||||
|
||||
getViewContext() {
|
||||
if (this.component) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return this.component.$refs.alphanumericFormat.getViewContext();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.component.$destroy();
|
||||
this.component = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export default function AlphanumericFormatViewProvider(openmct, options) {
|
||||
function isTelemetryObject(selectionPath) {
|
||||
let selectedObject = selectionPath[0].context.item;
|
||||
let parentObject = selectionPath[1].context.item;
|
||||
let selectedLayoutItem = selectionPath[0].context.layoutItem;
|
||||
|
||||
return parentObject
|
||||
&& parentObject.type === 'layout'
|
||||
&& selectedObject
|
||||
&& selectedLayoutItem
|
||||
&& selectedLayoutItem.type === 'telemetry-view'
|
||||
&& openmct.telemetry.isTelemetryObject(selectedObject)
|
||||
&& !options.showAsView.includes(selectedObject.type);
|
||||
}
|
||||
|
||||
return {
|
||||
key: 'alphanumeric-format',
|
||||
name: 'Alphanumeric Format',
|
||||
canView: function (selection) {
|
||||
if (selection.length === 0 || selection[0].length === 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return selection.every(isTelemetryObject);
|
||||
},
|
||||
view: function (domainObject, objectPath) {
|
||||
return new AlphanumericFormatView(openmct, domainObject, objectPath);
|
||||
},
|
||||
priority: function () {
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ export default class CopyToClipboardAction {
|
||||
|
||||
invoke(objectPath, view = {}) {
|
||||
const viewContext = view.getViewContext && view.getViewContext();
|
||||
const formattedValue = viewContext.formattedValueForCopy();
|
||||
const formattedValue = viewContext.row.formattedValueForCopy();
|
||||
|
||||
clipboard.updateClipboard(formattedValue)
|
||||
.then(() => {
|
||||
@ -26,9 +26,13 @@ export default class CopyToClipboardAction {
|
||||
}
|
||||
|
||||
appliesTo(objectPath, view = {}) {
|
||||
let viewContext = view.getViewContext && view.getViewContext();
|
||||
const viewContext = view.getViewContext && view.getViewContext();
|
||||
const row = viewContext && viewContext.row;
|
||||
if (!row) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return viewContext && viewContext.formattedValueForCopy
|
||||
&& typeof viewContext.formattedValueForCopy === 'function';
|
||||
return row.formattedValueForCopy
|
||||
&& typeof row.formattedValueForCopy === 'function';
|
||||
}
|
||||
}
|
||||
|
@ -52,7 +52,8 @@
|
||||
|
||||
<script>
|
||||
export default {
|
||||
inject: ['openmct'],
|
||||
name: 'AlphanumericFormat',
|
||||
inject: ['openmct', 'objectPath'],
|
||||
data() {
|
||||
return {
|
||||
isEditing: this.openmct.editor.isEditing(),
|
@ -56,6 +56,7 @@
|
||||
:index="index"
|
||||
:multi-select="selectedLayoutItems.length > 1"
|
||||
:is-editing="isEditing"
|
||||
@contextClick="updateViewContext"
|
||||
@move="move"
|
||||
@endMove="endMove"
|
||||
@endLineResize="endLineResize"
|
||||
@ -140,7 +141,7 @@ function getItemDefinition(itemType, ...options) {
|
||||
|
||||
export default {
|
||||
components: components,
|
||||
inject: ['openmct', 'options', 'objectPath'],
|
||||
inject: ['openmct', 'objectPath', 'options', 'objectUtils', 'currentView'],
|
||||
props: {
|
||||
domainObject: {
|
||||
type: Object,
|
||||
@ -155,7 +156,8 @@ export default {
|
||||
return {
|
||||
initSelectIndex: undefined,
|
||||
selection: [],
|
||||
showGrid: true
|
||||
showGrid: true,
|
||||
viewContext: {}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@ -819,6 +821,12 @@ export default {
|
||||
},
|
||||
toggleGrid() {
|
||||
this.showGrid = !this.showGrid;
|
||||
},
|
||||
updateViewContext(viewContext) {
|
||||
this.viewContext.row = viewContext;
|
||||
},
|
||||
getViewContext() {
|
||||
return this.viewContext;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -102,7 +102,7 @@ export default {
|
||||
LayoutFrame
|
||||
},
|
||||
mixins: [conditionalStylesMixin],
|
||||
inject: ['openmct', 'objectPath'],
|
||||
inject: ['openmct', 'objectPath', 'currentView'],
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
@ -294,16 +294,6 @@ export default {
|
||||
this.requestHistoricalData(this.domainObject);
|
||||
}
|
||||
},
|
||||
getView() {
|
||||
return {
|
||||
getViewContext: () => {
|
||||
return {
|
||||
viewHistoricalData: true,
|
||||
formattedValueForCopy: this.formattedValueForCopy
|
||||
};
|
||||
}
|
||||
};
|
||||
},
|
||||
setObject(domainObject) {
|
||||
this.domainObject = domainObject;
|
||||
this.mutablePromise = undefined;
|
||||
@ -338,30 +328,38 @@ export default {
|
||||
|
||||
this.$emit('formatChanged', this.item, format);
|
||||
},
|
||||
updateViewContext() {
|
||||
this.$emit('contextClick', {
|
||||
viewHistoricalData: true,
|
||||
formattedValueForCopy: this.formattedValueForCopy
|
||||
});
|
||||
},
|
||||
async getContextMenuActions() {
|
||||
const defaultNotebook = getDefaultNotebook();
|
||||
const domainObject = defaultNotebook && await this.openmct.objects.get(defaultNotebook.notebookMeta.identifier);
|
||||
const actionCollection = this.openmct.actions.get(this.currentObjectPath, this.getView());
|
||||
const actionsObject = actionCollection.getActionsObject();
|
||||
|
||||
let copyToNotebookAction = actionsObject.copyToNotebook;
|
||||
|
||||
let defaultNotebookName;
|
||||
if (defaultNotebook) {
|
||||
const defaultPath = domainObject && `${domainObject.name} - ${defaultNotebook.section.name} - ${defaultNotebook.page.name}`;
|
||||
copyToNotebookAction.name = `Copy to Notebook ${defaultPath}`;
|
||||
} else {
|
||||
actionsObject.copyToNotebook = undefined;
|
||||
delete actionsObject.copyToNotebook;
|
||||
defaultNotebookName = `Copy to Notebook ${defaultPath}`;
|
||||
}
|
||||
|
||||
return CONTEXT_MENU_ACTIONS.map(actionKey => {
|
||||
return actionsObject[actionKey];
|
||||
}).filter(action => action !== undefined);
|
||||
return CONTEXT_MENU_ACTIONS
|
||||
.map(actionKey => {
|
||||
const action = this.openmct.actions.getAction(actionKey);
|
||||
if (action.key === 'copyToNotebook') {
|
||||
action.name = defaultNotebookName;
|
||||
}
|
||||
|
||||
return action;
|
||||
})
|
||||
.filter(action => action.name !== undefined);
|
||||
},
|
||||
async showContextMenu(event) {
|
||||
this.updateViewContext();
|
||||
const contextMenuActions = await this.getContextMenuActions();
|
||||
|
||||
this.openmct.menus.showMenu(event.x, event.y, contextMenuActions);
|
||||
const menuItems = this.openmct.menus.actionsToMenuItems(contextMenuActions, this.currentObjectPath, this.currentView);
|
||||
this.openmct.menus.showMenu(event.x, event.y, menuItems);
|
||||
},
|
||||
setStatus(status) {
|
||||
this.status = status;
|
||||
|
@ -20,13 +20,81 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import Layout from './components/DisplayLayout.vue';
|
||||
import Vue from 'vue';
|
||||
import objectUtils from 'objectUtils';
|
||||
import DisplayLayoutType from './DisplayLayoutType.js';
|
||||
import DisplayLayoutToolbar from './DisplayLayoutToolbar.js';
|
||||
import AlphaNumericFormatViewProvider from './AlphanumericFormatViewProvider.js';
|
||||
import CopyToClipboardAction from './actions/CopyToClipboardAction';
|
||||
import DisplayLayout from './components/DisplayLayout.vue';
|
||||
import DisplayLayoutToolbar from './DisplayLayoutToolbar.js';
|
||||
import DisplayLayoutType from './DisplayLayoutType.js';
|
||||
|
||||
import objectUtils from 'objectUtils';
|
||||
|
||||
import Vue from 'vue';
|
||||
|
||||
class DisplayLayoutView {
|
||||
constructor(openmct, domainObject, objectPath, options) {
|
||||
this.openmct = openmct;
|
||||
this.domainObject = domainObject;
|
||||
this.objectPath = objectPath;
|
||||
this.options = options;
|
||||
|
||||
this.component = undefined;
|
||||
}
|
||||
|
||||
show(container, isEditing) {
|
||||
this.component = new Vue({
|
||||
el: container,
|
||||
components: {
|
||||
DisplayLayout
|
||||
},
|
||||
provide: {
|
||||
openmct: this.openmct,
|
||||
objectPath: this.objectPath,
|
||||
options: this.options,
|
||||
objectUtils,
|
||||
currentView: this
|
||||
},
|
||||
data: () => {
|
||||
return {
|
||||
domainObject: this.domainObject,
|
||||
isEditing
|
||||
};
|
||||
},
|
||||
template: '<display-layout ref="displayLayout" :domain-object="domainObject" :is-editing="isEditing"></display-layout>'
|
||||
});
|
||||
}
|
||||
|
||||
getViewContext() {
|
||||
if (!this.component) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return this.component.$refs.displayLayout.getViewContext();
|
||||
}
|
||||
|
||||
getSelectionContext() {
|
||||
return {
|
||||
item: this.domainObject,
|
||||
supportsMultiSelect: true,
|
||||
addElement: this.component && this.component.$refs.displayLayout.addElement,
|
||||
removeItem: this.component && this.component.$refs.displayLayout.removeItem,
|
||||
orderItem: this.component && this.component.$refs.displayLayout.orderItem,
|
||||
duplicateItem: this.component && this.component.$refs.displayLayout.duplicateItem,
|
||||
switchViewType: this.component && this.component.$refs.displayLayout.switchViewType,
|
||||
mergeMultipleTelemetryViews: this.component && this.component.$refs.displayLayout.mergeMultipleTelemetryViews,
|
||||
mergeMultipleOverlayPlots: this.component && this.component.$refs.displayLayout.mergeMultipleOverlayPlots,
|
||||
toggleGrid: this.component && this.component.$refs.displayLayout.toggleGrid
|
||||
};
|
||||
}
|
||||
|
||||
onEditModeChange(isEditing) {
|
||||
this.component.isEditing = isEditing;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.component.$destroy();
|
||||
this.component = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export default function DisplayLayoutPlugin(options) {
|
||||
return function (openmct) {
|
||||
@ -41,51 +109,7 @@ export default function DisplayLayoutPlugin(options) {
|
||||
return domainObject.type === 'layout';
|
||||
},
|
||||
view: function (domainObject, objectPath) {
|
||||
let component;
|
||||
|
||||
return {
|
||||
show(container) {
|
||||
component = new Vue({
|
||||
el: container,
|
||||
components: {
|
||||
Layout
|
||||
},
|
||||
provide: {
|
||||
openmct,
|
||||
objectUtils,
|
||||
options,
|
||||
objectPath
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
domainObject: domainObject,
|
||||
isEditing: openmct.editor.isEditing()
|
||||
};
|
||||
},
|
||||
template: '<layout ref="displayLayout" :domain-object="domainObject" :is-editing="isEditing"></layout>'
|
||||
});
|
||||
},
|
||||
getSelectionContext() {
|
||||
return {
|
||||
item: domainObject,
|
||||
supportsMultiSelect: true,
|
||||
addElement: component && component.$refs.displayLayout.addElement,
|
||||
removeItem: component && component.$refs.displayLayout.removeItem,
|
||||
orderItem: component && component.$refs.displayLayout.orderItem,
|
||||
duplicateItem: component && component.$refs.displayLayout.duplicateItem,
|
||||
switchViewType: component && component.$refs.displayLayout.switchViewType,
|
||||
mergeMultipleTelemetryViews: component && component.$refs.displayLayout.mergeMultipleTelemetryViews,
|
||||
mergeMultipleOverlayPlots: component && component.$refs.displayLayout.mergeMultipleOverlayPlots,
|
||||
toggleGrid: component && component.$refs.displayLayout.toggleGrid
|
||||
};
|
||||
},
|
||||
onEditModeChange: function (isEditing) {
|
||||
component.isEditing = isEditing;
|
||||
},
|
||||
destroy() {
|
||||
component.$destroy();
|
||||
}
|
||||
};
|
||||
return new DisplayLayoutView(openmct, domainObject, objectPath, options);
|
||||
},
|
||||
priority() {
|
||||
return 100;
|
||||
|
51
src/plugins/hyperlink/HyperlinkLayout.vue
Normal file
51
src/plugins/hyperlink/HyperlinkLayout.vue
Normal file
@ -0,0 +1,51 @@
|
||||
/*****************************************************************************
|
||||
* 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>
|
||||
|
||||
<a class="c-hyperlink"
|
||||
:class="{
|
||||
'c-hyperlink--button' : isButton
|
||||
}"
|
||||
:target="domainObject.linkTarget"
|
||||
:href="domainObject.url"
|
||||
>
|
||||
<span class="c-hyperlink__label">{{ domainObject.displayText }}</span>
|
||||
</a>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
inject: ['domainObject'],
|
||||
computed: {
|
||||
isButton() {
|
||||
if (this.domainObject.displayFormat === "link") {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
59
src/plugins/hyperlink/HyperlinkProvider.js
Normal file
59
src/plugins/hyperlink/HyperlinkProvider.js
Normal file
@ -0,0 +1,59 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import HyperlinkLayout from './HyperlinkLayout.vue';
|
||||
import Vue from 'vue';
|
||||
|
||||
export default function HyperlinkProvider(openmct) {
|
||||
|
||||
return {
|
||||
key: 'hyperlink.view',
|
||||
name: 'Hyperlink',
|
||||
cssClass: 'icon-chain-links',
|
||||
canView(domainObject) {
|
||||
return domainObject.type === 'hyperlink';
|
||||
},
|
||||
|
||||
view: function (domainObject) {
|
||||
let component;
|
||||
|
||||
return {
|
||||
show: function (element) {
|
||||
component = new Vue({
|
||||
el: element,
|
||||
components: {
|
||||
HyperlinkLayout
|
||||
},
|
||||
provide: {
|
||||
domainObject
|
||||
},
|
||||
template: '<hyperlink-layout></hyperlink-layout>'
|
||||
});
|
||||
},
|
||||
destroy: function () {
|
||||
component.$destroy();
|
||||
component = undefined;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
89
src/plugins/hyperlink/plugin.js
Normal file
89
src/plugins/hyperlink/plugin.js
Normal file
@ -0,0 +1,89 @@
|
||||
/*****************************************************************************
|
||||
* 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 HyperlinkProvider from './HyperlinkProvider';
|
||||
|
||||
export default function () {
|
||||
return function install(openmct) {
|
||||
openmct.types.addType('hyperlink', {
|
||||
name: 'Hyperlink',
|
||||
key: 'hyperlink',
|
||||
description: 'A hyperlink to redirect to a different link',
|
||||
creatable: true,
|
||||
cssClass: 'icon-chain-links',
|
||||
initialize: function (domainObject) {
|
||||
domainObject.displayFormat = "link";
|
||||
domainObject.linkTarget = "_self";
|
||||
},
|
||||
form: [
|
||||
{
|
||||
"key": "url",
|
||||
"name": "URL",
|
||||
"control": "textfield",
|
||||
"required": true,
|
||||
"cssClass": "l-input-lg"
|
||||
},
|
||||
{
|
||||
"key": "displayText",
|
||||
"name": "Text to Display",
|
||||
"control": "textfield",
|
||||
"required": true,
|
||||
"cssClass": "l-input-lg"
|
||||
},
|
||||
{
|
||||
"key": "displayFormat",
|
||||
"name": "Display Format",
|
||||
"control": "select",
|
||||
"options": [
|
||||
{
|
||||
"name": "Link",
|
||||
"value": "link"
|
||||
},
|
||||
{
|
||||
"name": "Button",
|
||||
"value": "button"
|
||||
}
|
||||
],
|
||||
"cssClass": "l-inline"
|
||||
},
|
||||
{
|
||||
"key": "linkTarget",
|
||||
"name": "Tab to Open Hyperlink",
|
||||
"control": "select",
|
||||
"options": [
|
||||
{
|
||||
"name": "Open in this tab",
|
||||
"value": "_self"
|
||||
},
|
||||
{
|
||||
"name": "Open in a new tab",
|
||||
"value": "_blank"
|
||||
}
|
||||
],
|
||||
"cssClass": "l-inline"
|
||||
|
||||
}
|
||||
]
|
||||
});
|
||||
openmct.objectViews.addProvider(new HyperlinkProvider(openmct));
|
||||
};
|
||||
}
|
130
src/plugins/hyperlink/pluginSpec.js
Normal file
130
src/plugins/hyperlink/pluginSpec.js
Normal file
@ -0,0 +1,130 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2009-2016, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import { createOpenMct, resetApplicationState } from "utils/testing";
|
||||
import HyperlinkPlugin from "./plugin";
|
||||
|
||||
function getView(openmct, domainObj, objectPath) {
|
||||
const applicableViews = openmct.objectViews.get(domainObj, objectPath);
|
||||
const hyperLinkView = applicableViews.find((viewProvider) => viewProvider.key === 'hyperlink.view');
|
||||
|
||||
return hyperLinkView.view(domainObj);
|
||||
}
|
||||
|
||||
function destroyView(view) {
|
||||
return view.destroy();
|
||||
}
|
||||
|
||||
describe("The controller for hyperlinks", function () {
|
||||
let mockDomainObject;
|
||||
let mockObjectPath;
|
||||
let openmct;
|
||||
let element;
|
||||
let child;
|
||||
let view;
|
||||
|
||||
beforeEach((done) => {
|
||||
mockObjectPath = [
|
||||
{
|
||||
name: 'mock hyperlink',
|
||||
type: 'hyperlink',
|
||||
identifier: {
|
||||
key: 'mock-hyperlink',
|
||||
namespace: ''
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
mockDomainObject = {
|
||||
displayFormat: "",
|
||||
linkTarget: "",
|
||||
name: "Unnamed HyperLink",
|
||||
type: "hyperlink",
|
||||
location: "f69c21ac-24ef-450c-8e2f-3d527087d285",
|
||||
modified: 1627483839783,
|
||||
url: "123",
|
||||
displayText: "123",
|
||||
persisted: 1627483839783,
|
||||
id: "3d9c243d-dffb-446b-8474-d9931a99d679",
|
||||
identifier: {
|
||||
namespace: "",
|
||||
key: "3d9c243d-dffb-446b-8474-d9931a99d679"
|
||||
}
|
||||
};
|
||||
|
||||
openmct = createOpenMct();
|
||||
openmct.install(new HyperlinkPlugin());
|
||||
|
||||
element = document.createElement('div');
|
||||
element.style.width = '640px';
|
||||
element.style.height = '480px';
|
||||
child = document.createElement('div');
|
||||
child.style.width = '640px';
|
||||
child.style.height = '480px';
|
||||
element.appendChild(child);
|
||||
|
||||
openmct.on('start', done);
|
||||
openmct.startHeadless();
|
||||
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
destroyView(view);
|
||||
|
||||
return resetApplicationState(openmct);
|
||||
});
|
||||
it("knows when it should open a new tab", () => {
|
||||
mockDomainObject.displayFormat = "link";
|
||||
mockDomainObject.linkTarget = "_blank";
|
||||
|
||||
view = getView(openmct, mockDomainObject, mockObjectPath);
|
||||
view.show(child, true);
|
||||
|
||||
expect(element.querySelector('.c-hyperlink').target).toBe('_blank');
|
||||
});
|
||||
it("knows when it should open in the same tab", function () {
|
||||
mockDomainObject.displayFormat = "button";
|
||||
mockDomainObject.linkTarget = "_self";
|
||||
|
||||
view = getView(openmct, mockDomainObject, mockObjectPath);
|
||||
view.show(child, true);
|
||||
|
||||
expect(element.querySelector('.c-hyperlink').target).toBe('_self');
|
||||
});
|
||||
|
||||
it("knows when it is a button", function () {
|
||||
mockDomainObject.displayFormat = "button";
|
||||
|
||||
view = getView(openmct, mockDomainObject, mockObjectPath);
|
||||
view.show(child, true);
|
||||
|
||||
expect(element.querySelector('.c-hyperlink--button')).toBeDefined();
|
||||
});
|
||||
it("knows when it is a link", function () {
|
||||
mockDomainObject.displayFormat = "link";
|
||||
|
||||
view = getView(openmct, mockDomainObject, mockObjectPath);
|
||||
view.show(child, true);
|
||||
|
||||
expect(element.querySelector('.c-hyperlink')).not.toHaveClass('c-hyperlink--button');
|
||||
});
|
||||
});
|
37
src/plugins/imagery/ImageryView.js
Normal file
37
src/plugins/imagery/ImageryView.js
Normal file
@ -0,0 +1,37 @@
|
||||
import ImageryViewLayout from './components/ImageryViewLayout.vue';
|
||||
|
||||
import Vue from 'vue';
|
||||
|
||||
export default class ImageryView {
|
||||
constructor(openmct, domainObject, objectPath) {
|
||||
this.openmct = openmct;
|
||||
this.domainObject = domainObject;
|
||||
this.objectPath = objectPath;
|
||||
this.component = undefined;
|
||||
}
|
||||
|
||||
show(element) {
|
||||
this.component = new Vue({
|
||||
el: element,
|
||||
components: {
|
||||
ImageryViewLayout
|
||||
},
|
||||
provide: {
|
||||
openmct: this.openmct,
|
||||
domainObject: this.domainObject,
|
||||
objectPath: this.objectPath,
|
||||
currentView: this
|
||||
},
|
||||
template: '<imagery-view-layout ref="ImageryLayout"></imagery-view-layout>'
|
||||
});
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.component.$destroy();
|
||||
this.component = undefined;
|
||||
}
|
||||
|
||||
_getInstance() {
|
||||
return this.component;
|
||||
}
|
||||
}
|
@ -19,9 +19,7 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import ImageryViewLayout from './components/ImageryViewLayout.vue';
|
||||
import Vue from 'vue';
|
||||
import ImageryView from './ImageryView';
|
||||
|
||||
export default function ImageryViewProvider(openmct) {
|
||||
const type = 'example.imagery';
|
||||
@ -42,31 +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;
|
||||
},
|
||||
_getInstance: function () {
|
||||
return component;
|
||||
}
|
||||
};
|
||||
view: function (domainObject, objectPath) {
|
||||
return new ImageryView(openmct, domainObject, objectPath);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -23,7 +23,7 @@
|
||||
<template>
|
||||
<div
|
||||
class="c-compass"
|
||||
:style="`width: ${ sizedImageDimensions.width }px; height: ${ sizedImageDimensions.height }px`"
|
||||
:style="`width: 100%; height: 100%`"
|
||||
>
|
||||
<CompassHUD
|
||||
v-if="hasCameraFieldOfView"
|
||||
@ -33,13 +33,12 @@
|
||||
/>
|
||||
<CompassRose
|
||||
v-if="hasCameraFieldOfView"
|
||||
:heading="heading"
|
||||
:sized-image-width="sizedImageDimensions.width"
|
||||
:sun-heading="sunHeading"
|
||||
:camera-angle-of-view="cameraAngleOfView"
|
||||
:camera-pan="cameraPan"
|
||||
:lock-compass="lockCompass"
|
||||
@toggle-lock-compass="toggleLockCompass"
|
||||
:compass-rose-sizing-classes="compassRoseSizingClasses"
|
||||
:heading="heading"
|
||||
:sized-image-dimensions="sizedImageDimensions"
|
||||
:sun-heading="sunHeading"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@ -56,42 +55,20 @@ export default {
|
||||
CompassRose
|
||||
},
|
||||
props: {
|
||||
containerWidth: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
containerHeight: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
naturalAspectRatio: {
|
||||
type: Number,
|
||||
compassRoseSizingClasses: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
image: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
lockCompass: {
|
||||
type: Boolean,
|
||||
sizedImageDimensions: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
sizedImageDimensions() {
|
||||
let sizedImageDimensions = {};
|
||||
if ((this.containerWidth / this.containerHeight) > this.naturalAspectRatio) {
|
||||
// container is wider than image
|
||||
sizedImageDimensions.width = this.containerHeight * this.naturalAspectRatio;
|
||||
sizedImageDimensions.height = this.containerHeight;
|
||||
} else {
|
||||
// container is taller than image
|
||||
sizedImageDimensions.width = this.containerWidth;
|
||||
sizedImageDimensions.height = this.containerWidth * this.naturalAspectRatio;
|
||||
}
|
||||
|
||||
return sizedImageDimensions;
|
||||
},
|
||||
hasCameraFieldOfView() {
|
||||
return this.cameraPan !== undefined && this.cameraAngleOfView > 0;
|
||||
},
|
||||
|
@ -21,152 +21,203 @@
|
||||
*****************************************************************************/
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="w-direction-rose"
|
||||
:class="compassRoseSizingClasses"
|
||||
<div ref="compassRoseWrapper"
|
||||
class="w-direction-rose"
|
||||
:class="compassRoseSizingClasses"
|
||||
@click="toggleLockCompass"
|
||||
>
|
||||
<div
|
||||
class="c-direction-rose"
|
||||
@click="toggleLockCompass"
|
||||
<svg ref="compassRoseSvg"
|
||||
class="c-compass-rose-svg"
|
||||
viewBox="0 0 100 100"
|
||||
>
|
||||
<div
|
||||
class="c-nsew"
|
||||
:style="compassRoseStyle"
|
||||
<mask id="mask0"
|
||||
mask-type="alpha"
|
||||
maskUnits="userSpaceOnUse"
|
||||
x="0"
|
||||
y="0"
|
||||
width="100"
|
||||
height="100"
|
||||
>
|
||||
<svg
|
||||
class="c-nsew__minor-ticks"
|
||||
viewBox="0 0 100 100"
|
||||
<circle cx="50"
|
||||
cy="50"
|
||||
r="50"
|
||||
fill="black"
|
||||
/>
|
||||
</mask>
|
||||
<g class="c-cr__compass-wrapper">
|
||||
<g class="c-cr__compass-main"
|
||||
mask="url(#mask0)"
|
||||
>
|
||||
<rect
|
||||
class="c-nsew__tick c-tick-ne"
|
||||
x="49"
|
||||
y="0"
|
||||
width="2"
|
||||
height="5"
|
||||
<!-- Background and clipped elements -->
|
||||
<rect class="c-cr__bg"
|
||||
width="100"
|
||||
height="100"
|
||||
fill="black"
|
||||
/>
|
||||
<rect
|
||||
class="c-nsew__tick c-tick-se"
|
||||
x="95"
|
||||
y="49"
|
||||
width="5"
|
||||
height="2"
|
||||
<rect class="c-cr__edge"
|
||||
width="100"
|
||||
height="100"
|
||||
fill="url(#paint0_radial)"
|
||||
/>
|
||||
<rect
|
||||
class="c-nsew__tick c-tick-sw"
|
||||
x="49"
|
||||
y="95"
|
||||
width="2"
|
||||
height="5"
|
||||
/>
|
||||
<rect
|
||||
class="c-nsew__tick c-tick-nw"
|
||||
x="0"
|
||||
y="49"
|
||||
width="5"
|
||||
height="2"
|
||||
<rect v-if="hasSunHeading"
|
||||
class="c-cr__sun"
|
||||
width="100"
|
||||
height="100"
|
||||
fill="url(#paint1_radial)"
|
||||
:style="sunHeadingStyle"
|
||||
/>
|
||||
|
||||
</svg>
|
||||
<!-- Camera FOV -->
|
||||
<mask id="mask2"
|
||||
class="c-cr__cam-fov-l-mask"
|
||||
mask-type="alpha"
|
||||
maskUnits="userSpaceOnUse"
|
||||
x="0"
|
||||
y="0"
|
||||
width="50"
|
||||
height="100"
|
||||
>
|
||||
<rect width="51"
|
||||
height="100"
|
||||
/>
|
||||
</mask>
|
||||
<mask id="mask1"
|
||||
class="c-cr__cam-fov-r-mask"
|
||||
mask-type="alpha"
|
||||
maskUnits="userSpaceOnUse"
|
||||
x="50"
|
||||
y="0"
|
||||
width="50"
|
||||
height="100"
|
||||
>
|
||||
<rect x="49"
|
||||
width="51"
|
||||
height="100"
|
||||
/>
|
||||
</mask>
|
||||
<g class="c-cr__cam-fov"
|
||||
:style="cameraPanStyle"
|
||||
>
|
||||
<g mask="url(#mask2)">
|
||||
<rect class="c-cr__cam-fov-r"
|
||||
x="49"
|
||||
width="51"
|
||||
height="100"
|
||||
:style="cameraFOVStyleRightHalf"
|
||||
/>
|
||||
</g>
|
||||
<g mask="url(#mask1)">
|
||||
<rect class="c-cr__cam-fov-l"
|
||||
width="51"
|
||||
height="100"
|
||||
:style="cameraFOVStyleLeftHalf"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<svg
|
||||
class="c-nsew__ticks"
|
||||
viewBox="0 0 100 100"
|
||||
<!-- Spacecraft body -->
|
||||
<path v-if="hasHeading"
|
||||
class="c-cr__spacecraft-body"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M37 49C35.3431 49 34 50.3431 34 52V82C34 83.6569 35.3431 85 37 85H63C64.6569 85 66 83.6569 66 82V52C66 50.3431 64.6569 49 63 49H37ZM50 52L58 60H55V67H45V60H42L50 52Z"
|
||||
:style="headingStyle"
|
||||
/>
|
||||
|
||||
<!-- NSEW and ticks -->
|
||||
<g class="c-cr__nsew"
|
||||
:style="compassRoseStyle"
|
||||
>
|
||||
<polygon
|
||||
class="c-nsew__tick c-tick-n"
|
||||
points="50,0 60,10 40,10"
|
||||
<g class="c-cr__ticks-major">
|
||||
<path d="M50 3L43 10H57L50 3Z" />
|
||||
<path d="M4 51V49H10V51H4Z"
|
||||
class="--hide-min"
|
||||
/>
|
||||
<path d="M49 96V90H51V96H49Z"
|
||||
class="--hide-min"
|
||||
/>
|
||||
<path d="M90 49V51H96V49H90Z"
|
||||
class="--hide-min"
|
||||
/>
|
||||
</g>
|
||||
<g class="c-cr__ticks-minor --hide-small">
|
||||
<path d="M4 51V49H10V51H4Z" />
|
||||
<path d="M90 51V49H96V51H90Z" />
|
||||
<path d="M51 96H49V90H51V96Z" />
|
||||
<path d="M51 10L49 10V4L51 4V10Z" />
|
||||
</g>
|
||||
<g class="c-cr__nsew-text">
|
||||
<path :style="cardinalTextRotateW"
|
||||
class="c-cr__nsew-w --hide-small"
|
||||
d="M56.7418 45.004H54.1378L52.7238 52.312H52.6958L51.2258 45.004H48.7758L47.3058 52.312H47.2778L45.8638 45.004H43.2598L45.9618 55H48.6078L49.9798 48.112H50.0078L51.3798 55H53.9838L56.7418 45.004Z"
|
||||
/>
|
||||
<path :style="cardinalTextRotateE"
|
||||
class="c-cr__nsew-e --hide-small"
|
||||
d="M46.104 55H54.21V52.76H48.708V50.856H53.608V48.84H48.708V47.09H54.07V45.004H46.104V55Z"
|
||||
/>
|
||||
<path :style="cardinalTextRotateS"
|
||||
class="c-cr__nsew-s --hide-small"
|
||||
d="M45.6531 51.64C45.6671 54.202 47.6971 55.21 49.9931 55.21C52.1911 55.21 54.3471 54.398 54.3471 51.864C54.3471 50.058 52.8911 49.386 51.4491 48.98C49.9931 48.574 48.5511 48.434 48.5511 47.664C48.5511 47.006 49.2511 46.81 49.8111 46.81C50.6091 46.81 51.4631 47.104 51.4211 48.014H54.0251C54.0111 45.76 52.0091 44.794 50.0211 44.794C48.1451 44.794 45.9471 45.648 45.9471 47.832C45.9471 49.666 47.4451 50.31 48.8731 50.716C50.3151 51.122 51.7431 51.29 51.7431 52.172C51.7431 52.914 50.9311 53.194 50.1471 53.194C49.0411 53.194 48.3131 52.816 48.2571 51.64H45.6531Z"
|
||||
/>
|
||||
<path :style="cardinalTextRotateN"
|
||||
class="c-cr__nsew-n"
|
||||
d="M42.5935 60H46.7935V49.32H46.8415L52.7935 60H57.3775V42.864H53.1775V53.424H53.1295L47.1775 42.864H42.5935V60Z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<radialGradient id="paint0_radial"
|
||||
cx="0"
|
||||
cy="0"
|
||||
r="1"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(50 50) rotate(90) scale(50)"
|
||||
>
|
||||
<stop offset="0.751387"
|
||||
stop-opacity="0"
|
||||
/>
|
||||
<rect
|
||||
class="c-nsew__tick c-tick-e"
|
||||
x="95"
|
||||
y="49"
|
||||
width="5"
|
||||
height="2"
|
||||
<stop offset="1"
|
||||
stop-color="white"
|
||||
/>
|
||||
<rect
|
||||
class="c-nsew__tick c-tick-w"
|
||||
x="0"
|
||||
y="49"
|
||||
width="5"
|
||||
height="2"
|
||||
</radialGradient>
|
||||
<radialGradient id="paint1_radial"
|
||||
cx="0"
|
||||
cy="0"
|
||||
r="1"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(50 -7) rotate(-90) scale(18.5)"
|
||||
>
|
||||
<stop offset="0.716377"
|
||||
stop-color="#FFCC00"
|
||||
/>
|
||||
<rect
|
||||
class="c-nsew__tick c-tick-s"
|
||||
x="49"
|
||||
y="95"
|
||||
width="2"
|
||||
height="5"
|
||||
<stop offset="1"
|
||||
stop-color="#FF9900"
|
||||
stop-opacity="0"
|
||||
/>
|
||||
|
||||
<text
|
||||
class="c-nsew__label c-label-n"
|
||||
text-anchor="middle"
|
||||
:transform="northTextTransform"
|
||||
>N</text>
|
||||
<text
|
||||
class="c-nsew__label c-label-e"
|
||||
text-anchor="middle"
|
||||
:transform="eastTextTransform"
|
||||
>E</text>
|
||||
<text
|
||||
class="c-nsew__label c-label-w"
|
||||
text-anchor="middle"
|
||||
:transform="southTextTransform"
|
||||
>W</text>
|
||||
<text
|
||||
class="c-nsew__label c-label-s"
|
||||
text-anchor="middle"
|
||||
:transform="westTextTransform"
|
||||
>S</text>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="hasHeading"
|
||||
class="c-spacecraft-body"
|
||||
:style="headingStyle"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="hasSunHeading"
|
||||
class="c-sun"
|
||||
:style="sunHeadingStyle"
|
||||
></div>
|
||||
|
||||
<div
|
||||
class="c-cam-field"
|
||||
:style="cameraPanStyle"
|
||||
>
|
||||
<div class="cam-field-half cam-field-half-l">
|
||||
<div
|
||||
class="cam-field-area"
|
||||
:style="cameraFOVStyleLeftHalf"
|
||||
></div>
|
||||
</div>
|
||||
<div class="cam-field-half cam-field-half-r">
|
||||
<div
|
||||
class="cam-field-area"
|
||||
:style="cameraFOVStyleRightHalf"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { rotate } from './utils';
|
||||
import { throttle } from 'lodash';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
sizedImageWidth: {
|
||||
type: Number,
|
||||
compassRoseSizingClasses: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
heading: {
|
||||
type: Number,
|
||||
required: true
|
||||
required: true,
|
||||
default() {
|
||||
return 0;
|
||||
}
|
||||
},
|
||||
sunHeading: {
|
||||
type: Number,
|
||||
@ -178,58 +229,39 @@ export default {
|
||||
},
|
||||
cameraPan: {
|
||||
type: Number,
|
||||
required: true
|
||||
required: true,
|
||||
default() {
|
||||
return 0;
|
||||
}
|
||||
},
|
||||
lockCompass: {
|
||||
type: Boolean,
|
||||
sizedImageDimensions: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
lockCompass: true
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
compassRoseSizingClasses() {
|
||||
let compassRoseSizingClasses = '';
|
||||
if (this.sizedImageWidth < 300) {
|
||||
compassRoseSizingClasses = '--rose-small --rose-min';
|
||||
} else if (this.sizedImageWidth < 500) {
|
||||
compassRoseSizingClasses = '--rose-small';
|
||||
} else if (this.sizedImageWidth > 1000) {
|
||||
compassRoseSizingClasses = '--rose-max';
|
||||
}
|
||||
|
||||
return compassRoseSizingClasses;
|
||||
},
|
||||
compassRoseStyle() {
|
||||
return { transform: `rotate(${ this.north }deg)` };
|
||||
},
|
||||
north() {
|
||||
return this.lockCompass ? rotate(-this.cameraPan) : 0;
|
||||
},
|
||||
northTextTransform() {
|
||||
return this.cardinalPointsTextTransform.north;
|
||||
cardinalTextRotateN() {
|
||||
return { transform: `translateY(-27%) rotate(${ -this.north }deg)` };
|
||||
},
|
||||
eastTextTransform() {
|
||||
return this.cardinalPointsTextTransform.east;
|
||||
cardinalTextRotateS() {
|
||||
return { transform: `translateY(30%) rotate(${ -this.north }deg)` };
|
||||
},
|
||||
southTextTransform() {
|
||||
return this.cardinalPointsTextTransform.south;
|
||||
cardinalTextRotateE() {
|
||||
return { transform: `translateX(30%) rotate(${ -this.north }deg)` };
|
||||
},
|
||||
westTextTransform() {
|
||||
return this.cardinalPointsTextTransform.west;
|
||||
},
|
||||
cardinalPointsTextTransform() {
|
||||
/**
|
||||
* cardinal points text must be rotated
|
||||
* in the opposite direction that north is rotated
|
||||
* to keep text vertically oriented
|
||||
*/
|
||||
const rotation = `rotate(${ -this.north })`;
|
||||
|
||||
return {
|
||||
north: `translate(50,23) ${ rotation }`,
|
||||
east: `translate(82,50) ${ rotation }`,
|
||||
south: `translate(18,50) ${ rotation }`,
|
||||
west: `translate(50,82) ${ rotation }`
|
||||
};
|
||||
cardinalTextRotateW() {
|
||||
return { transform: `translateX(-30%) rotate(${ -this.north }deg)` };
|
||||
},
|
||||
hasHeading() {
|
||||
return this.heading !== undefined;
|
||||
@ -238,7 +270,7 @@ export default {
|
||||
const rotation = rotate(this.north, this.heading);
|
||||
|
||||
return {
|
||||
transform: `translateX(-50%) rotate(${ rotation }deg)`
|
||||
transform: `rotate(${ rotation }deg)`
|
||||
};
|
||||
},
|
||||
hasSunHeading() {
|
||||
@ -262,20 +294,37 @@ export default {
|
||||
// rotated counter-clockwise from camera pan angle
|
||||
cameraFOVStyleLeftHalf() {
|
||||
return {
|
||||
transform: `translateX(50%) rotate(${ -this.cameraAngleOfView / 2 }deg)`
|
||||
transform: `rotate(${ this.cameraAngleOfView / 2 }deg)`
|
||||
};
|
||||
},
|
||||
// right half of camera field of view
|
||||
// rotated clockwise from camera pan angle
|
||||
cameraFOVStyleRightHalf() {
|
||||
return {
|
||||
transform: `translateX(-50%) rotate(${ this.cameraAngleOfView / 2 }deg)`
|
||||
transform: `rotate(${ -this.cameraAngleOfView / 2 }deg)`
|
||||
};
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
sizedImageDimensions() {
|
||||
this.debounceResizeSvg();
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.debounceResizeSvg = throttle(this.resizeSvg, 100);
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.debounceResizeSvg();
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
resizeSvg() {
|
||||
const svg = this.$refs.compassRoseSvg;
|
||||
svg.setAttribute('width', this.$refs.compassRoseWrapper.clientWidth);
|
||||
svg.setAttribute('height', this.$refs.compassRoseWrapper.clientHeight);
|
||||
},
|
||||
toggleLockCompass() {
|
||||
this.$emit('toggle-lock-compass');
|
||||
this.lockCompass = !this.lockCompass;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -12,9 +12,8 @@ $elemBg: rgba(black, 0.7);
|
||||
.c-compass {
|
||||
pointer-events: none; // This allows the image element to receive a browser-level context click
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
@include userSelectNone;
|
||||
}
|
||||
@ -81,114 +80,55 @@ $elemBg: rgba(black, 0.7);
|
||||
transform: translateX(-50%);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/***************************** COMPASS DIRECTIONS */
|
||||
.c-nsew {
|
||||
/***************************** COMPASS SVG */
|
||||
.c-compass-rose-svg {
|
||||
$color: $interfaceKeyColor;
|
||||
$inset: 5%;
|
||||
$tickHeightPerc: 15%;
|
||||
text-shadow: black 0 0 10px;
|
||||
top: $inset;
|
||||
right: $inset;
|
||||
bottom: $inset;
|
||||
left: $inset;
|
||||
z-index: 3;
|
||||
position: absolute;
|
||||
top: 0; left: 0;
|
||||
|
||||
&__tick,
|
||||
&__label {
|
||||
fill: $color;
|
||||
}
|
||||
|
||||
&__minor-ticks {
|
||||
opacity: 0.5;
|
||||
g, path, rect {
|
||||
// In an SVG, rotation occurs about the center of the SVG, not the element
|
||||
transform-origin: center;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
&__label {
|
||||
dominant-baseline: central;
|
||||
font-size: 1.25em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.c-label-n {
|
||||
font-size: 2em;
|
||||
}
|
||||
}
|
||||
|
||||
/***************************** CAMERA FIELD ANGLE */
|
||||
.c-cam-field {
|
||||
$color: white;
|
||||
opacity: 0.3;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
|
||||
.cam-field-half {
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
|
||||
.cam-field-area {
|
||||
background: $color;
|
||||
top: -30%;
|
||||
right: 0;
|
||||
bottom: -30%;
|
||||
left: 0;
|
||||
.c-cr {
|
||||
&__bg {
|
||||
fill: #000;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
// clip-paths overlap a bit to avoid a gap between halves
|
||||
&-l {
|
||||
clip-path: polygon(0 0, 50.5% 0, 50.5% 100%, 0 100%);
|
||||
|
||||
.cam-field-area {
|
||||
transform-origin: left center;
|
||||
}
|
||||
&__edge {
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
&-r {
|
||||
clip-path: polygon(49.5% 0, 100% 0, 100% 100%, 49.5% 100%);
|
||||
|
||||
.cam-field-area {
|
||||
transform-origin: right center;
|
||||
}
|
||||
&__sun {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/***************************** SPACECRAFT BODY */
|
||||
.c-spacecraft-body {
|
||||
$color: $interfaceKeyColor;
|
||||
$s: 30%;
|
||||
background: $color;
|
||||
border-radius: 3px;
|
||||
height: $s;
|
||||
width: $s;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
opacity: 0.4;
|
||||
transform-origin: center top;
|
||||
transform: translateX(-50%); // center by default, overridden by CompassRose.vue / headingStyle()
|
||||
&__cam-fov-l,
|
||||
&__cam-fov-r {
|
||||
// Cam FOV indication
|
||||
opacity: 0.2;
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
&:before {
|
||||
// Direction arrow
|
||||
$color: rgba(black, 0.5);
|
||||
$arwPointerY: 60%;
|
||||
$arwBodyOffset: 25%;
|
||||
background: $color;
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 10%;
|
||||
right: 20%;
|
||||
bottom: 50%;
|
||||
left: 20%;
|
||||
clip-path: polygon(50% 0, 100% $arwPointerY, 100%-$arwBodyOffset $arwPointerY, 100%-$arwBodyOffset 100%, $arwBodyOffset 100%, $arwBodyOffset $arwPointerY, 0 $arwPointerY);
|
||||
&__nsew-text,
|
||||
&__spacecraft-body,
|
||||
&__ticks-major,
|
||||
&__ticks-minor {
|
||||
fill: $color;
|
||||
}
|
||||
|
||||
&__ticks-minor {
|
||||
opacity: 0.5;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
&__spacecraft-body {
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -196,32 +136,28 @@ $elemBg: rgba(black, 0.7);
|
||||
.w-direction-rose {
|
||||
$s: 10%;
|
||||
$m: 2%;
|
||||
cursor: pointer;
|
||||
pointer-events: all;
|
||||
position: absolute;
|
||||
bottom: $m;
|
||||
left: $m;
|
||||
width: $s;
|
||||
padding-top: $s;
|
||||
z-index: 2;
|
||||
|
||||
&.--rose-min {
|
||||
$s: 30px;
|
||||
width: $s;
|
||||
padding-top: $s;
|
||||
.--hide-min {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.--rose-small {
|
||||
.c-nsew__minor-ticks,
|
||||
.c-tick-w,
|
||||
.c-tick-s,
|
||||
.c-tick-e,
|
||||
.c-label-w,
|
||||
.c-label-s,
|
||||
.c-label-e {
|
||||
.--hide-small {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.c-label-n {
|
||||
font-size: 2.5em;
|
||||
}
|
||||
}
|
||||
|
||||
&.--rose-max {
|
||||
@ -230,44 +166,3 @@ $elemBg: rgba(black, 0.7);
|
||||
padding-top: $s;
|
||||
}
|
||||
}
|
||||
|
||||
.c-direction-rose {
|
||||
$c2: rgba(white, 0.1);
|
||||
background: $elemBg;
|
||||
background-image: radial-gradient(circle closest-side, transparent, transparent 80%, $c2);
|
||||
transform-origin: 0 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
clip-path: circle(50% at 50% 50%);
|
||||
border-radius: 100%;
|
||||
pointer-events: all;
|
||||
|
||||
svg, div {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
// Sun
|
||||
.c-sun {
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
|
||||
&:before {
|
||||
$s: 35%;
|
||||
@include sun();
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
opacity: 0.7;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
height: $s;
|
||||
width: $s;
|
||||
transform: translate(-50%, -60%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -55,28 +55,34 @@
|
||||
></a>
|
||||
</span>
|
||||
</div>
|
||||
<div class="c-imagery__main-image__bg"
|
||||
<div ref="imageBG"
|
||||
class="c-imagery__main-image__bg"
|
||||
:class="{'paused unnsynced': isPaused,'stale':false }"
|
||||
@click="expand"
|
||||
>
|
||||
<img
|
||||
ref="focusedImage"
|
||||
class="c-imagery__main-image__image js-imageryView-image"
|
||||
:src="imageUrl"
|
||||
:style="{
|
||||
'filter': `brightness(${filters.brightness}%) contrast(${filters.contrast}%)`
|
||||
}"
|
||||
:data-openmct-image-timestamp="time"
|
||||
:data-openmct-object-keystring="keyString"
|
||||
<div class="image-wrapper"
|
||||
:style="{
|
||||
'width': `${sizedImageDimensions.width}px`,
|
||||
'height': `${sizedImageDimensions.height}px`
|
||||
}"
|
||||
>
|
||||
<Compass
|
||||
v-if="shouldDisplayCompass"
|
||||
:container-width="imageContainerWidth"
|
||||
:container-height="imageContainerHeight"
|
||||
:natural-aspect-ratio="focusedImageNaturalAspectRatio"
|
||||
:image="focusedImage"
|
||||
:lock-compass="lockCompass"
|
||||
@toggle-lock-compass="toggleLockCompass"
|
||||
/>
|
||||
<img ref="focusedImage"
|
||||
class="c-imagery__main-image__image js-imageryView-image"
|
||||
:src="imageUrl"
|
||||
:style="{
|
||||
'filter': `brightness(${filters.brightness}%) contrast(${filters.contrast}%)`
|
||||
}"
|
||||
:data-openmct-image-timestamp="time"
|
||||
:data-openmct-object-keystring="keyString"
|
||||
>
|
||||
<Compass
|
||||
v-if="shouldDisplayCompass"
|
||||
:compass-rose-sizing-classes="compassRoseSizingClasses"
|
||||
:image="focusedImage"
|
||||
:natural-aspect-ratio="focusedImageNaturalAspectRatio"
|
||||
:sized-image-dimensions="sizedImageDimensions"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="c-local-controls c-local-controls--show-on-hover c-imagery__prev-next-buttons">
|
||||
<button class="c-nav c-nav--prev"
|
||||
@ -165,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;
|
||||
@ -190,7 +197,7 @@ export default {
|
||||
components: {
|
||||
Compass
|
||||
},
|
||||
inject: ['openmct', 'domainObject'],
|
||||
inject: ['openmct', 'domainObject', 'objectPath', 'currentView'],
|
||||
data() {
|
||||
let timeSystem = this.openmct.time.timeSystem();
|
||||
|
||||
@ -224,6 +231,18 @@ export default {
|
||||
};
|
||||
},
|
||||
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);
|
||||
},
|
||||
@ -347,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: {
|
||||
@ -395,7 +428,7 @@ export default {
|
||||
_.debounce(this.resizeImageContainer, 400);
|
||||
|
||||
this.imageContainerResizeObserver = new ResizeObserver(this.resizeImageContainer);
|
||||
this.imageContainerResizeObserver.observe(this.$refs.focusedImage);
|
||||
this.imageContainerResizeObserver.observe(this.$refs.imageBG);
|
||||
|
||||
// For adjusting scroll bar size and position when resizing thumbs wrapper
|
||||
this.handleScroll = _.debounce(this.handleScroll, SCROLL_LATENCY);
|
||||
@ -437,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,
|
||||
@ -833,12 +876,12 @@ 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;
|
||||
}
|
||||
},
|
||||
handleThumbWindowResizeStart() {
|
||||
@ -858,9 +901,6 @@ export default {
|
||||
this.$nextTick(() => {
|
||||
this.resizingWindow = false;
|
||||
});
|
||||
},
|
||||
toggleLockCompass() {
|
||||
this.lockCompass = !this.lockCompass;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -1,7 +1,7 @@
|
||||
.c-imagery {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
&:focus {
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -192,6 +194,10 @@
|
||||
margin-right: $interiorMarginSm;
|
||||
}
|
||||
}
|
||||
|
||||
.s-status-taking-snapshot & {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__lc {
|
||||
@ -273,6 +279,10 @@
|
||||
content: $glyph-icon-play;
|
||||
}
|
||||
}
|
||||
|
||||
.s-status-taking-snapshot & {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.c-imagery__prev-next-buttons {
|
||||
@ -287,6 +297,10 @@
|
||||
.c-nav {
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.s-status-taking-snapshot & {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.c-nav {
|
||||
|
@ -280,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(() => {
|
||||
@ -317,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',
|
||||
|
@ -25,16 +25,20 @@ export default class CopyToNotebookAction {
|
||||
});
|
||||
}
|
||||
|
||||
invoke(objectPath, view = {}) {
|
||||
let viewContext = view.getViewContext && view.getViewContext();
|
||||
invoke(objectPath, view) {
|
||||
const formattedValueForCopy = view.getViewContext().row.formattedValueForCopy;
|
||||
|
||||
this.copyToNotebook(viewContext.formattedValueForCopy());
|
||||
this.copyToNotebook(formattedValueForCopy());
|
||||
}
|
||||
|
||||
appliesTo(objectPath, view = {}) {
|
||||
let viewContext = view.getViewContext && view.getViewContext();
|
||||
const viewContext = view.getViewContext && view.getViewContext();
|
||||
const row = viewContext && viewContext.row;
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
|
||||
return viewContext && viewContext.formattedValueForCopy
|
||||
&& typeof viewContext.formattedValueForCopy === 'function';
|
||||
return row.formattedValueForCopy
|
||||
&& typeof row.formattedValueForCopy === 'function';
|
||||
}
|
||||
}
|
||||
|
@ -31,6 +31,7 @@ import PainterroInstance from '../utils/painterroInstance';
|
||||
import SnapshotTemplate from './snapshot-template.html';
|
||||
|
||||
import { updateNotebookImageDomainObject } from '../utils/notebook-image';
|
||||
import ImageExporter from '../../../exporters/ImageExporter';
|
||||
|
||||
import PopupMenu from './PopupMenu.vue';
|
||||
import Vue from 'vue';
|
||||
@ -71,7 +72,7 @@ export default {
|
||||
},
|
||||
mounted() {
|
||||
this.addPopupMenuItems();
|
||||
this.exportImageService = this.openmct.$injector.get('exportImageService');
|
||||
this.imageExporter = new ImageExporter(this.openmct);
|
||||
},
|
||||
methods: {
|
||||
addPopupMenuItems() {
|
||||
@ -101,7 +102,6 @@ export default {
|
||||
buttons: [
|
||||
{
|
||||
label: 'Cancel',
|
||||
emphasis: true,
|
||||
callback: () => {
|
||||
painterroInstance.dismiss();
|
||||
annotateOverlay.dismiss();
|
||||
@ -109,6 +109,7 @@ export default {
|
||||
},
|
||||
{
|
||||
label: 'Save',
|
||||
emphasis: true,
|
||||
callback: () => {
|
||||
painterroInstance.save((snapshotObject) => {
|
||||
annotateOverlay.dismiss();
|
||||
@ -234,9 +235,9 @@ export default {
|
||||
let element = this.snapshot.$refs['snapshot-image'];
|
||||
|
||||
if (type === 'png') {
|
||||
this.exportImageService.exportPNG(element, this.embed.name);
|
||||
this.imageExporter.exportPNG(element, this.embed.name);
|
||||
} else {
|
||||
this.exportImageService.exportJPG(element, this.embed.name);
|
||||
this.imageExporter.exportJPG(element, this.embed.name);
|
||||
}
|
||||
},
|
||||
previewEmbed() {
|
||||
|
@ -80,7 +80,7 @@ export default {
|
||||
notebookTypes.push({
|
||||
cssClass: 'icon-notebook',
|
||||
name: `Save to Notebook ${defaultPath}`,
|
||||
callBack: () => {
|
||||
onItemClicked: () => {
|
||||
return this.snapshot(NOTEBOOK_DEFAULT);
|
||||
}
|
||||
});
|
||||
@ -89,7 +89,7 @@ export default {
|
||||
notebookTypes.push({
|
||||
cssClass: 'icon-camera',
|
||||
name: 'Save to Notebook Snapshots',
|
||||
callBack: () => {
|
||||
onItemClicked: () => {
|
||||
return this.snapshot(NOTEBOOK_SNAPSHOT);
|
||||
}
|
||||
});
|
||||
|
@ -7,10 +7,10 @@
|
||||
<div class="c-object-label__type-icon icon-camera"></div>
|
||||
<div class="c-object-label__name">
|
||||
Notebook Snapshots
|
||||
<span v-if="snapshots.length"
|
||||
class="l-browse-bar__object-details"
|
||||
> {{ snapshots.length }} of {{ getNotebookSnapshotMaxCount() }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="snapshots.length"
|
||||
class="l-browse-bar__object-details"
|
||||
>{{ snapshots.length }} of {{ getNotebookSnapshotMaxCount() }}
|
||||
</div>
|
||||
</div>
|
||||
<PopupMenu v-if="snapshots.length > 0"
|
||||
|
@ -4,8 +4,10 @@
|
||||
<div class="l-browse-bar__start">
|
||||
<div class="l-browse-bar__object-name--w">
|
||||
<span class="c-object-label l-browse-bar__object-name"
|
||||
v-bind:class="cssClass"
|
||||
>
|
||||
<span class="c-object-label__type-icon"
|
||||
v-bind:class="cssClass"
|
||||
></span>
|
||||
<span class="c-object-label__name">{{ name }}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
@ -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);
|
||||
|
106
src/plugins/persistence/couch/CouchChangesFeed.js
Normal file
106
src/plugins/persistence/couch/CouchChangesFeed.js
Normal file
@ -0,0 +1,106 @@
|
||||
(function () {
|
||||
const connections = [];
|
||||
let connected = false;
|
||||
const controller = new AbortController();
|
||||
const signal = controller.signal;
|
||||
|
||||
self.onconnect = function (e) {
|
||||
let port = e.ports[0];
|
||||
connections.push(port);
|
||||
|
||||
port.postMessage({
|
||||
type: 'connection',
|
||||
connectionId: connections.length
|
||||
});
|
||||
|
||||
port.onmessage = async function (event) {
|
||||
if (event.data.request === 'close') {
|
||||
connections.splice(event.data.connectionId - 1, 1);
|
||||
if (connections.length <= 0) {
|
||||
// abort any outstanding requests if there's nobody listening to it.
|
||||
controller.abort();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.data.request === 'changes') {
|
||||
if (connected === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
connected = true;
|
||||
|
||||
let url = event.data.url;
|
||||
let body = event.data.body;
|
||||
let error = false;
|
||||
// feed=continuous maintains an indefinitely open connection with a keep-alive of HEARTBEAT milliseconds until this client closes the connection
|
||||
// style=main_only returns only the current winning revision of the document
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": 'application/json'
|
||||
},
|
||||
signal,
|
||||
body
|
||||
});
|
||||
|
||||
let reader;
|
||||
|
||||
if (response.body === undefined) {
|
||||
error = true;
|
||||
} else {
|
||||
reader = response.body.getReader();
|
||||
}
|
||||
|
||||
while (!error) {
|
||||
const {done, value} = await reader.read();
|
||||
//done is true when we lose connection with the provider
|
||||
if (done) {
|
||||
error = true;
|
||||
}
|
||||
|
||||
if (value) {
|
||||
let chunk = new Uint8Array(value.length);
|
||||
chunk.set(value, 0);
|
||||
const decodedChunk = new TextDecoder("utf-8").decode(chunk).split('\n');
|
||||
if (decodedChunk.length && decodedChunk[decodedChunk.length - 1] === '') {
|
||||
decodedChunk.forEach((doc, index) => {
|
||||
try {
|
||||
if (doc) {
|
||||
const objectChanges = JSON.parse(doc);
|
||||
connections.forEach(function (connection) {
|
||||
connection.postMessage({
|
||||
objectChanges
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch (decodeError) {
|
||||
//do nothing;
|
||||
console.log(decodeError);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (error) {
|
||||
port.postMessage({
|
||||
error
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
port.start();
|
||||
|
||||
};
|
||||
|
||||
self.onerror = function () {
|
||||
//do nothing
|
||||
console.log('Error on feed');
|
||||
};
|
||||
|
||||
}());
|
@ -40,6 +40,64 @@ export default class CouchObjectProvider {
|
||||
this.batchIds = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
startSharedWorker() {
|
||||
let provider = this;
|
||||
let sharedWorker;
|
||||
|
||||
const sharedWorkerURL = `${this.openmct.getAssetPath()}${__OPENMCT_ROOT_RELATIVE__}couchDBChangesFeed.js`;
|
||||
|
||||
sharedWorker = new SharedWorker(sharedWorkerURL);
|
||||
sharedWorker.port.onmessage = provider.onSharedWorkerMessage.bind(this);
|
||||
sharedWorker.port.onmessageerror = provider.onSharedWorkerMessageError.bind(this);
|
||||
sharedWorker.port.start();
|
||||
|
||||
this.openmct.on('destroy', () => {
|
||||
this.changesFeedSharedWorker.port.postMessage({
|
||||
request: 'close',
|
||||
connectionId: this.changesFeedSharedWorkerConnectionId
|
||||
});
|
||||
this.changesFeedSharedWorker.port.close();
|
||||
});
|
||||
|
||||
return sharedWorker;
|
||||
}
|
||||
|
||||
onSharedWorkerMessageError(event) {
|
||||
console.log('Error', event);
|
||||
}
|
||||
|
||||
onSharedWorkerMessage(event) {
|
||||
if (event.data.type === 'connection') {
|
||||
this.changesFeedSharedWorkerConnectionId = event.data.connectionId;
|
||||
} else {
|
||||
const error = event.data.error;
|
||||
if (error && Object.keys(this.observers).length > 0) {
|
||||
this.observeObjectChanges();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let objectChanges = event.data.objectChanges;
|
||||
objectChanges.identifier = {
|
||||
namespace: this.namespace,
|
||||
key: objectChanges.id
|
||||
};
|
||||
let keyString = this.openmct.objects.makeKeyString(objectChanges.identifier);
|
||||
//TODO: Optimize this so that we don't 'get' the object if it's current revision (from this.objectQueue) is the same as the one we already have.
|
||||
let observersForObject = this.observers[keyString];
|
||||
|
||||
if (observersForObject) {
|
||||
observersForObject.forEach(async (observer) => {
|
||||
const updatedObject = await this.get(objectChanges.identifier);
|
||||
observer(updatedObject);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//backwards compatibility, options used to be a url. Now it's an object
|
||||
_normalize(options) {
|
||||
if (typeof options === 'string') {
|
||||
@ -320,7 +378,7 @@ export default class CouchObjectProvider {
|
||||
this.observers[keyString] = this.observers[keyString].filter(observer => observer !== callback);
|
||||
if (this.observers[keyString].length === 0) {
|
||||
delete this.observers[keyString];
|
||||
if (Object.keys(this.observers).length === 0) {
|
||||
if (Object.keys(this.observers).length === 0 && this.isObservingObjectChanges()) {
|
||||
this.stopObservingObjectChanges();
|
||||
}
|
||||
}
|
||||
@ -334,9 +392,8 @@ export default class CouchObjectProvider {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
async observeObjectChanges() {
|
||||
const controller = new AbortController();
|
||||
const signal = controller.signal;
|
||||
observeObjectChanges() {
|
||||
|
||||
let filter = {selector: {}};
|
||||
|
||||
if (this.openmct.objects.SYNCHRONIZED_OBJECT_TYPES.length > 1) {
|
||||
@ -354,17 +411,6 @@ export default class CouchObjectProvider {
|
||||
};
|
||||
}
|
||||
|
||||
let error = false;
|
||||
|
||||
if (typeof this.stopObservingObjectChanges === 'function') {
|
||||
this.stopObservingObjectChanges();
|
||||
}
|
||||
|
||||
this.stopObservingObjectChanges = () => {
|
||||
controller.abort();
|
||||
delete this.stopObservingObjectChanges;
|
||||
};
|
||||
|
||||
// feed=continuous maintains an indefinitely open connection with a keep-alive of HEARTBEAT milliseconds until this client closes the connection
|
||||
// style=main_only returns only the current winning revision of the document
|
||||
let url = `${this.url}/_changes?feed=continuous&style=main_only&heartbeat=${HEARTBEAT}`;
|
||||
@ -375,6 +421,52 @@ export default class CouchObjectProvider {
|
||||
body = JSON.stringify(filter);
|
||||
}
|
||||
|
||||
if (typeof SharedWorker === 'undefined') {
|
||||
this.fetchChanges(url, body);
|
||||
} else {
|
||||
this.initiateSharedWorkerFetchChanges(url, body);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
initiateSharedWorkerFetchChanges(url, body) {
|
||||
if (!this.changesFeedSharedWorker) {
|
||||
this.changesFeedSharedWorker = this.startSharedWorker();
|
||||
|
||||
if (this.isObservingObjectChanges()) {
|
||||
this.stopObservingObjectChanges();
|
||||
}
|
||||
|
||||
this.stopObservingObjectChanges = () => {
|
||||
delete this.stopObservingObjectChanges;
|
||||
};
|
||||
|
||||
this.changesFeedSharedWorker.port.postMessage({
|
||||
request: 'changes',
|
||||
body,
|
||||
url
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async fetchChanges(url, body) {
|
||||
const controller = new AbortController();
|
||||
const signal = controller.signal;
|
||||
|
||||
let error = false;
|
||||
|
||||
if (this.isObservingObjectChanges()) {
|
||||
this.stopObservingObjectChanges();
|
||||
}
|
||||
|
||||
this.stopObservingObjectChanges = () => {
|
||||
controller.abort();
|
||||
delete this.stopObservingObjectChanges;
|
||||
};
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
signal,
|
||||
|
@ -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',
|
||||
|
@ -25,16 +25,16 @@
|
||||
:class="[plotLegendExpandedStateClass, plotLegendPositionClass]"
|
||||
>
|
||||
<plot-legend :cursor-locked="!!lockHighlightPoint"
|
||||
:series="config.series.models"
|
||||
:series="seriesModels"
|
||||
:highlights="highlights"
|
||||
:legend="config.legend"
|
||||
:legend="legend"
|
||||
@legendHoverChanged="legendHoverChanged"
|
||||
/>
|
||||
<div class="plot-wrapper-axis-and-display-area flex-elem grows">
|
||||
<y-axis v-if="config.series.models.length > 0"
|
||||
<y-axis v-if="seriesModels.length > 0"
|
||||
:tick-width="tickWidth"
|
||||
:single-series="config.series.models.length === 1"
|
||||
:series-model="config.series.models[0]"
|
||||
:single-series="seriesModels.length === 1"
|
||||
:series-model="seriesModels[0]"
|
||||
@yKeyChanged="setYAxisKey"
|
||||
@tickWidthChanged="onTickWidthChange"
|
||||
/>
|
||||
@ -141,8 +141,8 @@
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<x-axis v-if="config.series.models.length > 0 && !options.compact"
|
||||
:series-model="config.series.models[0]"
|
||||
<x-axis v-if="seriesModels.length > 0 && !options.compact"
|
||||
:series-model="seriesModels[0]"
|
||||
/>
|
||||
|
||||
</div>
|
||||
@ -213,7 +213,8 @@ export default {
|
||||
plotHistory: [],
|
||||
selectedXKeyOption: {},
|
||||
xKeyOptions: [],
|
||||
config: {},
|
||||
seriesModels: [],
|
||||
legend: {},
|
||||
pending: 0,
|
||||
isRealTime: this.openmct.time.clock() !== undefined,
|
||||
loaded: false,
|
||||
@ -239,18 +240,13 @@ export default {
|
||||
watch: {
|
||||
plotTickWidth(newTickWidth) {
|
||||
this.onTickWidthChange(newTickWidth, true);
|
||||
},
|
||||
gridLines(newGridLines) {
|
||||
this.setGridLinesVisibility(newGridLines);
|
||||
},
|
||||
cursorGuide(newCursorGuide) {
|
||||
this.setCursorGuideVisibility(newCursorGuide);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
eventHelpers.extend(this);
|
||||
|
||||
this.config = this.getConfig();
|
||||
this.legend = this.config.legend;
|
||||
|
||||
this.listenTo(this.config.series, 'add', this.addSeries, this);
|
||||
this.listenTo(this.config.series, 'remove', this.removeSeries, this);
|
||||
@ -290,14 +286,18 @@ export default {
|
||||
config = new PlotConfigurationModel({
|
||||
id: configId,
|
||||
domainObject: this.domainObject,
|
||||
openmct: this.openmct
|
||||
openmct: this.openmct,
|
||||
callback: (data) => {
|
||||
this.data = data;
|
||||
}
|
||||
});
|
||||
configStore.add(configId, config);
|
||||
}
|
||||
|
||||
return config;
|
||||
},
|
||||
addSeries(series) {
|
||||
addSeries(series, index) {
|
||||
this.$set(this.seriesModels, index, series);
|
||||
this.listenTo(series, 'change:xKey', (xKey) => {
|
||||
this.setDisplayRange(series, xKey);
|
||||
}, this);
|
||||
@ -377,11 +377,8 @@ export default {
|
||||
},
|
||||
|
||||
stopLoading() {
|
||||
//TODO: Is Vue.$nextTick ok to replace $scope.$evalAsync?
|
||||
this.$nextTick().then(() => {
|
||||
this.pending -= 1;
|
||||
this.updateLoading();
|
||||
});
|
||||
this.pending -= 1;
|
||||
this.updateLoading();
|
||||
},
|
||||
|
||||
updateLoading() {
|
||||
@ -427,9 +424,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
|
||||
@ -504,7 +504,7 @@ export default {
|
||||
},
|
||||
|
||||
initialize() {
|
||||
_.debounce(this.handleWindowResize, 400);
|
||||
this.handleWindowResize = _.debounce(this.handleWindowResize, 500);
|
||||
this.plotContainerResizeObserver = new ResizeObserver(this.handleWindowResize);
|
||||
this.plotContainerResizeObserver.observe(this.$parent.$refs.plotWrapper);
|
||||
|
||||
@ -620,7 +620,7 @@ export default {
|
||||
this.config.series.models.forEach(series => delete series.closest);
|
||||
} else {
|
||||
this.highlights = this.config.series.models
|
||||
.filter(series => series.data.length > 0)
|
||||
.filter(series => series.getSeriesData().length > 0)
|
||||
.map(series => {
|
||||
series.closest = series.nearestPoint(point);
|
||||
|
||||
@ -924,16 +924,8 @@ export default {
|
||||
this.userViewportChangeEnd();
|
||||
},
|
||||
|
||||
setCursorGuideVisibility(cursorGuide) {
|
||||
this.cursorGuide = cursorGuide === true;
|
||||
},
|
||||
|
||||
setGridLinesVisibility(gridLines) {
|
||||
this.gridLines = gridLines === true;
|
||||
},
|
||||
|
||||
setYAxisKey(yKey) {
|
||||
this.config.series.models[0].emit('change:yKey', yKey);
|
||||
this.config.series.models[0].set('yKey', yKey);
|
||||
},
|
||||
|
||||
pause() {
|
||||
|
@ -72,7 +72,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import eventHelpers from "./lib/eventHelpers";
|
||||
import eventHelpers from './lib/eventHelpers';
|
||||
import ImageExporter from '../../exporters/ImageExporter';
|
||||
import MctPlot from './MctPlot.vue';
|
||||
|
||||
export default {
|
||||
@ -102,8 +103,7 @@ export default {
|
||||
},
|
||||
mounted() {
|
||||
eventHelpers.extend(this);
|
||||
|
||||
this.exportImageService = this.openmct.$injector.get('exportImageService');
|
||||
this.imageExporter = new ImageExporter(this.openmct);
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.destroy();
|
||||
@ -118,14 +118,12 @@ export default {
|
||||
|
||||
exportJPG() {
|
||||
const plotElement = this.$refs.plotContainer;
|
||||
|
||||
this.exportImageService.exportJPG(plotElement, 'plot.jpg', 'export-plot');
|
||||
this.imageExporter.exportJPG(plotElement, 'plot.jpg', 'export-plot');
|
||||
},
|
||||
|
||||
exportPNG() {
|
||||
const plotElement = this.$refs.plotContainer;
|
||||
|
||||
this.exportImageService.exportPNG(plotElement, 'plot.png', 'export-plot');
|
||||
this.imageExporter.exportPNG(plotElement, 'plot.png', 'export-plot');
|
||||
},
|
||||
|
||||
toggleCursorGuide() {
|
||||
|
@ -24,19 +24,23 @@ import Plot from './Plot.vue';
|
||||
import Vue from 'vue';
|
||||
|
||||
export default function PlotViewProvider(openmct) {
|
||||
function hasTelemetry(domainObject) {
|
||||
function hasNumericTelemetry(domainObject) {
|
||||
if (!Object.prototype.hasOwnProperty.call(domainObject, 'telemetry')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let metadata = openmct.telemetry.getMetadata(domainObject);
|
||||
|
||||
return metadata.values().length > 0 && hasDomainAndRange(metadata);
|
||||
return metadata.values().length > 0 && hasDomainAndNumericRange(metadata);
|
||||
}
|
||||
|
||||
function hasDomainAndRange(metadata) {
|
||||
return (metadata.valuesForHints(['range']).length > 0
|
||||
&& metadata.valuesForHints(['domain']).length > 0);
|
||||
function hasDomainAndNumericRange(metadata) {
|
||||
const rangeValues = metadata.valuesForHints(['range']);
|
||||
const domains = metadata.valuesForHints(['domain']);
|
||||
|
||||
return domains.length > 0
|
||||
&& rangeValues.length > 0
|
||||
&& !rangeValues.every(value => value.format === 'string');
|
||||
}
|
||||
|
||||
function isCompactView(objectPath) {
|
||||
@ -44,11 +48,11 @@ export default function PlotViewProvider(openmct) {
|
||||
}
|
||||
|
||||
return {
|
||||
key: 'plot-simple',
|
||||
key: 'plot-single',
|
||||
name: 'Plot',
|
||||
cssClass: 'icon-telemetry',
|
||||
canView(domainObject, objectPath) {
|
||||
return hasTelemetry(domainObject, openmct);
|
||||
return hasNumericTelemetry(domainObject);
|
||||
},
|
||||
|
||||
view: function (domainObject, objectPath) {
|
||||
|
@ -106,8 +106,9 @@ export default {
|
||||
},
|
||||
toggleXKeyOption() {
|
||||
const selectedXKey = this.selectedXKeyOptionKey;
|
||||
const dataForSelectedXKey = this.seriesModel.data
|
||||
? this.seriesModel.data[0][selectedXKey]
|
||||
const seriesData = this.seriesModel.getSeriesData();
|
||||
const dataForSelectedXKey = seriesData
|
||||
? seriesData[0][selectedXKey]
|
||||
: undefined;
|
||||
|
||||
if (dataForSelectedXKey !== undefined) {
|
||||
|
@ -36,7 +36,7 @@ export default class MCTChartAlarmPointSet {
|
||||
this.listenTo(series, 'reset', this.reset, this);
|
||||
this.listenTo(series, 'destroy', this.destroy, this);
|
||||
|
||||
series.data.forEach(function (point, index) {
|
||||
this.series.getSeriesData().forEach(function (point, index) {
|
||||
this.append(point, index, series);
|
||||
}, this);
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ export default class MCTChartSeriesElement {
|
||||
this.listenTo(series, 'remove', this.remove, this);
|
||||
this.listenTo(series, 'reset', this.reset, this);
|
||||
this.listenTo(series, 'destroy', this.destroy, this);
|
||||
series.data.forEach(function (point, index) {
|
||||
this.series.getSeriesData().forEach(function (point, index) {
|
||||
this.append(point, index, series);
|
||||
}, this);
|
||||
}
|
||||
@ -133,7 +133,7 @@ export default class MCTChartSeriesElement {
|
||||
this.buffer = new Float32Array(20000);
|
||||
this.count = 0;
|
||||
if (this.offset.x) {
|
||||
this.series.data.forEach(function (point, index) {
|
||||
this.series.getSeriesData().forEach(function (point, index) {
|
||||
this.append(point, index, this.series);
|
||||
}, this);
|
||||
}
|
||||
|
@ -107,6 +107,7 @@ export default class PlotConfigurationModel extends Model {
|
||||
updateDomainObject(domainObject) {
|
||||
this.set('domainObject', domainObject);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up all objects and remove all listeners.
|
||||
*/
|
||||
|
@ -22,6 +22,7 @@
|
||||
import _ from 'lodash';
|
||||
import Model from "./Model";
|
||||
import { MARKER_SHAPES } from '../draw/MarkerShapes';
|
||||
import configStore from "../configuration/configStore";
|
||||
|
||||
/**
|
||||
* Plot series handle interpreting telemetry metadata for a single telemetry
|
||||
@ -62,7 +63,6 @@ import { MARKER_SHAPES } from '../draw/MarkerShapes';
|
||||
export default class PlotSeries extends Model {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
this.data = [];
|
||||
|
||||
this.listenTo(this, 'change:xKey', this.onXKeyChange, this);
|
||||
this.listenTo(this, 'change:yKey', this.onYKeyChange, this);
|
||||
@ -115,6 +115,8 @@ export default class PlotSeries extends Model {
|
||||
this.openmct = options.openmct;
|
||||
this.domainObject = options.domainObject;
|
||||
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
this.dataStoreId = `data-${options.collection.plot.id}-${this.keyString}`;
|
||||
this.updateSeriesData([]);
|
||||
this.limitEvaluator = this.openmct.telemetry.limitEvaluator(options.domainObject);
|
||||
this.limitDefinition = this.openmct.telemetry.limitDefinition(options.domainObject);
|
||||
this.limits = [];
|
||||
@ -182,7 +184,8 @@ export default class PlotSeries extends Model {
|
||||
.telemetry
|
||||
.request(this.domainObject, options)
|
||||
.then(function (points) {
|
||||
const newPoints = _(this.data)
|
||||
const data = this.getSeriesData();
|
||||
const newPoints = _(data)
|
||||
.concat(points)
|
||||
.sortBy(this.getXVal)
|
||||
.uniq(true, point => [this.getXVal(point), this.getYVal(point)].join())
|
||||
@ -236,7 +239,7 @@ export default class PlotSeries extends Model {
|
||||
*/
|
||||
resetStats() {
|
||||
this.unset('stats');
|
||||
this.data.forEach(this.updateStats, this);
|
||||
this.getSeriesData().forEach(this.updateStats, this);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -244,7 +247,7 @@ export default class PlotSeries extends Model {
|
||||
* data to series after reset.
|
||||
*/
|
||||
reset(newData) {
|
||||
this.data = [];
|
||||
this.updateSeriesData([]);
|
||||
this.resetStats();
|
||||
this.emit('reset');
|
||||
if (newData) {
|
||||
@ -258,8 +261,9 @@ export default class PlotSeries extends Model {
|
||||
*/
|
||||
nearestPoint(xValue) {
|
||||
const insertIndex = this.sortedIndex(xValue);
|
||||
const lowPoint = this.data[insertIndex - 1];
|
||||
const highPoint = this.data[insertIndex];
|
||||
const data = this.getSeriesData();
|
||||
const lowPoint = data[insertIndex - 1];
|
||||
const highPoint = data[insertIndex];
|
||||
const indexVal = this.getXVal(xValue);
|
||||
const lowDistance = lowPoint
|
||||
? indexVal - this.getXVal(lowPoint)
|
||||
@ -292,7 +296,7 @@ export default class PlotSeries extends Model {
|
||||
* @private
|
||||
*/
|
||||
sortedIndex(point) {
|
||||
return _.sortedIndexBy(this.data, point, this.getXVal);
|
||||
return _.sortedIndexBy(this.getSeriesData(), point, this.getXVal);
|
||||
}
|
||||
/**
|
||||
* Update min/max stats for the series.
|
||||
@ -346,9 +350,10 @@ export default class PlotSeries extends Model {
|
||||
* a point to the end without dupe checking.
|
||||
*/
|
||||
add(point, appendOnly) {
|
||||
let insertIndex = this.data.length;
|
||||
let data = this.getSeriesData();
|
||||
let insertIndex = data.length;
|
||||
const currentYVal = this.getYVal(point);
|
||||
const lastYVal = this.getYVal(this.data[insertIndex - 1]);
|
||||
const lastYVal = this.getYVal(data[insertIndex - 1]);
|
||||
|
||||
if (this.isValueInvalid(currentYVal) && this.isValueInvalid(lastYVal)) {
|
||||
console.warn('[Plot] Invalid Y Values detected');
|
||||
@ -358,18 +363,19 @@ export default class PlotSeries extends Model {
|
||||
|
||||
if (!appendOnly) {
|
||||
insertIndex = this.sortedIndex(point);
|
||||
if (this.getXVal(this.data[insertIndex]) === this.getXVal(point)) {
|
||||
if (this.getXVal(data[insertIndex]) === this.getXVal(point)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.getXVal(this.data[insertIndex - 1]) === this.getXVal(point)) {
|
||||
if (this.getXVal(data[insertIndex - 1]) === this.getXVal(point)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.updateStats(point);
|
||||
point.mctLimitState = this.evaluate(point);
|
||||
this.data.splice(insertIndex, 0, point);
|
||||
data.splice(insertIndex, 0, point);
|
||||
this.updateSeriesData(data);
|
||||
this.emit('add', point, insertIndex, this);
|
||||
}
|
||||
|
||||
@ -386,8 +392,10 @@ export default class PlotSeries extends Model {
|
||||
* @private
|
||||
*/
|
||||
remove(point) {
|
||||
const index = this.data.indexOf(point);
|
||||
this.data.splice(index, 1);
|
||||
let data = this.getSeriesData();
|
||||
const index = data.indexOf(point);
|
||||
data.splice(index, 1);
|
||||
this.updateSeriesData(data);
|
||||
this.emit('remove', point, index, this);
|
||||
}
|
||||
/**
|
||||
@ -403,14 +411,16 @@ export default class PlotSeries extends Model {
|
||||
purgeRecordsOutsideRange(range) {
|
||||
const startIndex = this.sortedIndex(range.min);
|
||||
const endIndex = this.sortedIndex(range.max) + 1;
|
||||
const pointsToRemove = startIndex + (this.data.length - endIndex + 1);
|
||||
let data = this.getSeriesData();
|
||||
const pointsToRemove = startIndex + (data.length - endIndex + 1);
|
||||
if (pointsToRemove > 0) {
|
||||
if (pointsToRemove < 1000) {
|
||||
this.data.slice(0, startIndex).forEach(this.remove, this);
|
||||
this.data.slice(endIndex, this.data.length).forEach(this.remove, this);
|
||||
data.slice(0, startIndex).forEach(this.remove, this);
|
||||
data.slice(endIndex, data.length).forEach(this.remove, this);
|
||||
this.updateSeriesData(data);
|
||||
this.resetStats();
|
||||
} else {
|
||||
const newData = this.data.slice(startIndex, endIndex);
|
||||
const newData = this.getSeriesData().slice(startIndex, endIndex);
|
||||
this.reset(newData);
|
||||
}
|
||||
}
|
||||
@ -441,12 +451,13 @@ export default class PlotSeries extends Model {
|
||||
}
|
||||
}
|
||||
getDisplayRange(xKey) {
|
||||
const unsortedData = this.data;
|
||||
this.data = [];
|
||||
const unsortedData = this.getSeriesData();
|
||||
this.updateSeriesData([]);
|
||||
unsortedData.forEach(point => this.add(point, false));
|
||||
|
||||
const minValue = this.getXVal(this.data[0]);
|
||||
const maxValue = this.getXVal(this.data[this.data.length - 1]);
|
||||
let data = this.getSeriesData();
|
||||
const minValue = this.getXVal(data[0]);
|
||||
const maxValue = this.getXVal(data[data.length - 1]);
|
||||
|
||||
return {
|
||||
min: minValue,
|
||||
@ -470,4 +481,18 @@ export default class PlotSeries extends Model {
|
||||
|
||||
return this.get('name') + (unit ? ' ' + unit : '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the series data with the given value.
|
||||
*/
|
||||
updateSeriesData(data) {
|
||||
configStore.add(this.dataStoreId, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the series data with the given value.
|
||||
*/
|
||||
getSeriesData() {
|
||||
return configStore.get(this.dataStoreId) || [];
|
||||
}
|
||||
}
|
||||
|
@ -25,7 +25,10 @@ function ConfigStore() {
|
||||
|
||||
ConfigStore.prototype.deleteStore = function (id) {
|
||||
if (this.store[id]) {
|
||||
this.store[id].destroy();
|
||||
if (this.store[id].destroy) {
|
||||
this.store[id].destroy();
|
||||
}
|
||||
|
||||
delete this.store[id];
|
||||
}
|
||||
};
|
||||
|
@ -176,7 +176,9 @@ DrawWebGL.prototype.doDraw = function (drawType, buf, color, points, shape) {
|
||||
this.gl.vertexAttribPointer(this.aVertexPosition, 2, this.gl.FLOAT, false, 0, 0);
|
||||
this.gl.uniform4fv(this.uColor, color);
|
||||
this.gl.uniform1i(this.uMarkerShape, shapeCode);
|
||||
this.gl.drawArrays(drawType, 0, points);
|
||||
if (points !== 0) {
|
||||
this.gl.drawArrays(drawType, 0, points);
|
||||
}
|
||||
};
|
||||
|
||||
DrawWebGL.prototype.clear = function () {
|
||||
|
@ -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);
|
||||
|
||||
@ -567,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);
|
||||
|
||||
@ -677,14 +715,15 @@ describe("the plugin", function () {
|
||||
});
|
||||
|
||||
it("Adds a new point to the plot", (done) => {
|
||||
let originalLength = config.series.models[0].data.length;
|
||||
let originalLength = config.series.models[0].getSeriesData().length;
|
||||
config.series.models[0].add({
|
||||
utc: 2,
|
||||
'some-key': 1,
|
||||
'some-other-key': 2
|
||||
});
|
||||
Vue.nextTick(() => {
|
||||
expect(config.series.models[0].data.length).toEqual(originalLength + 1);
|
||||
const seriesData = config.series.models[0].getSeriesData();
|
||||
expect(seriesData.length).toEqual(originalLength + 1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
@ -67,8 +67,9 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import eventHelpers from "../lib/eventHelpers";
|
||||
import StackedPlotItem from "./StackedPlotItem.vue";
|
||||
import eventHelpers from '../lib/eventHelpers';
|
||||
import StackedPlotItem from './StackedPlotItem.vue';
|
||||
import ImageExporter from '../../../exporters/ImageExporter';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -103,7 +104,7 @@ export default {
|
||||
mounted() {
|
||||
eventHelpers.extend(this);
|
||||
|
||||
this.exportImageService = this.openmct.$injector.get('exportImageService');
|
||||
this.imageExporter = new ImageExporter(this.openmct);
|
||||
|
||||
this.tickWidthMap = {};
|
||||
|
||||
@ -159,9 +160,9 @@ export default {
|
||||
|
||||
exportJPG() {
|
||||
this.hideExportButtons = true;
|
||||
const plotElement = this.$refs.plotContainer;
|
||||
const plotElement = this.$el;
|
||||
|
||||
this.exportImageService.exportJPG(plotElement, 'stacked-plot.jpg', 'export-plot')
|
||||
this.imageExporter.exportJPG(plotElement, 'stacked-plot.jpg', 'export-plot')
|
||||
.finally(function () {
|
||||
this.hideExportButtons = false;
|
||||
}.bind(this));
|
||||
@ -170,9 +171,9 @@ export default {
|
||||
exportPNG() {
|
||||
this.hideExportButtons = true;
|
||||
|
||||
const plotElement = this.$refs.plotContainer;
|
||||
const plotElement = this.$el;
|
||||
|
||||
this.exportImageService.exportPNG(plotElement, 'stacked-plot.png', 'export-plot')
|
||||
this.imageExporter.exportPNG(plotElement, 'stacked-plot.png', 'export-plot')
|
||||
.finally(function () {
|
||||
this.hideExportButtons = false;
|
||||
}.bind(this));
|
||||
|
@ -23,6 +23,7 @@
|
||||
define([
|
||||
'lodash',
|
||||
'./utcTimeSystem/plugin',
|
||||
'./remoteClock/plugin',
|
||||
'./localTimeSystem/plugin',
|
||||
'./ISOTimeFormat/plugin',
|
||||
'../../example/generator/plugin',
|
||||
@ -62,13 +63,17 @@ define([
|
||||
'./defaultRootName/plugin',
|
||||
'./plan/plugin',
|
||||
'./viewDatumAction/plugin',
|
||||
'./viewLargeAction/plugin',
|
||||
'./interceptors/plugin',
|
||||
'./performanceIndicator/plugin',
|
||||
'./CouchDBSearchFolder/plugin',
|
||||
'./timeline/plugin'
|
||||
'./timeline/plugin',
|
||||
'./hyperlink/plugin',
|
||||
'./clock/plugin'
|
||||
], function (
|
||||
_,
|
||||
UTCTimeSystem,
|
||||
RemoteClock,
|
||||
LocalTimeSystem,
|
||||
ISOTimeFormat,
|
||||
GeneratorPlugin,
|
||||
@ -108,10 +113,13 @@ define([
|
||||
DefaultRootName,
|
||||
PlanLayout,
|
||||
ViewDatumAction,
|
||||
ViewLargeAction,
|
||||
ObjectInterceptors,
|
||||
PerformanceIndicator,
|
||||
CouchDBSearchFolder,
|
||||
Timeline
|
||||
Timeline,
|
||||
Hyperlink,
|
||||
Clock
|
||||
) {
|
||||
const bundleMap = {
|
||||
LocalStorage: 'platform/persistence/local',
|
||||
@ -129,6 +137,7 @@ define([
|
||||
|
||||
plugins.UTCTimeSystem = UTCTimeSystem;
|
||||
plugins.LocalTimeSystem = LocalTimeSystem;
|
||||
plugins.RemoteClock = RemoteClock.default;
|
||||
|
||||
plugins.ImportExport = ImportExport;
|
||||
|
||||
@ -208,10 +217,13 @@ define([
|
||||
plugins.DefaultRootName = DefaultRootName.default;
|
||||
plugins.PlanLayout = PlanLayout.default;
|
||||
plugins.ViewDatumAction = ViewDatumAction.default;
|
||||
plugins.ViewLargeAction = ViewLargeAction.default;
|
||||
plugins.ObjectInterceptors = ObjectInterceptors.default;
|
||||
plugins.PerformanceIndicator = PerformanceIndicator.default;
|
||||
plugins.CouchDBSearchFolder = CouchDBSearchFolder.default;
|
||||
plugins.Timeline = Timeline.default;
|
||||
plugins.Hyperlink = Hyperlink.default;
|
||||
plugins.Clock = Clock.default;
|
||||
|
||||
return plugins;
|
||||
});
|
||||
|
132
src/plugins/remoteClock/RemoteClock.js
Normal file
132
src/plugins/remoteClock/RemoteClock.js
Normal file
@ -0,0 +1,132 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT Web, Copyright (c) 2014-2021, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT Web includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import DefaultClock from '../../utils/clock/DefaultClock';
|
||||
|
||||
/**
|
||||
* A {@link openmct.TimeAPI.Clock} that updates the temporal bounds of the
|
||||
* application based on a time providing telemetry domainObject.
|
||||
*
|
||||
* @param {openmct} Object Instance of OpenMCT
|
||||
* @param {module:openmct.ObjectAPI~Identifier} identifier An object identifier for
|
||||
* the time providing telemetry domainObject
|
||||
* @constructor
|
||||
*/
|
||||
|
||||
export default class RemoteClock extends DefaultClock {
|
||||
constructor(openmct, identifier) {
|
||||
super();
|
||||
|
||||
this.key = 'remote-clock';
|
||||
|
||||
this.openmct = openmct;
|
||||
this.identifier = identifier;
|
||||
|
||||
this.name = 'Remote Clock';
|
||||
this.description = "Provides telemetry based timestamps from a configurable source.";
|
||||
|
||||
this.timeTelemetryObject = undefined;
|
||||
this.parseTime = undefined;
|
||||
this.metadata = undefined;
|
||||
|
||||
this.lastTick = 0;
|
||||
|
||||
this._processDatum = this._processDatum.bind(this);
|
||||
}
|
||||
|
||||
start() {
|
||||
this.openmct.time.on('timeSystem', this._timeSystemChange);
|
||||
this.openmct.objects.get(this.identifier).then((domainObject) => {
|
||||
this.timeTelemetryObject = domainObject;
|
||||
this.metadata = this.openmct.telemetry.getMetadata(domainObject);
|
||||
this._timeSystemChange();
|
||||
this._requestLatest();
|
||||
this._subscribe();
|
||||
}).catch((error) => {
|
||||
throw new Error(error);
|
||||
});
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.openmct.time.off('timeSystem', this._timeSystemChange);
|
||||
if (this._unsubscribe) {
|
||||
this._unsubscribe();
|
||||
}
|
||||
|
||||
this.removeAllListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Will start a subscription to the timeTelemetryObject as well
|
||||
* handle the unsubscribe callback
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_subscribe() {
|
||||
this._unsubscribe = this.openmct.telemetry.subscribe(
|
||||
this.timeTelemetryObject,
|
||||
this._processDatum
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Will request the latest data for the timeTelemetryObject
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_requestLatest() {
|
||||
this.openmct.telemetry.request(this.timeTelemetryObject, {
|
||||
size: 1,
|
||||
strategy: 'latest'
|
||||
}).then(data => {
|
||||
this._processDatum(data[data.length - 1]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to parse the datum from the timeTelemetryObject as well
|
||||
* as check if it's valid, calls "tick"
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_processDatum(datum) {
|
||||
let time = this.parseTime(datum);
|
||||
|
||||
if (time > this.lastTick) {
|
||||
this.tick(time);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback function for timeSystem change events
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_timeSystemChange() {
|
||||
let timeSystem = this.openmct.time.timeSystem();
|
||||
let timeKey = timeSystem.key;
|
||||
let metadataValue = this.metadata.value(timeKey);
|
||||
let timeFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);
|
||||
this.parseTime = (datum) => {
|
||||
return timeFormatter.parse(datum);
|
||||
};
|
||||
}
|
||||
}
|
152
src/plugins/remoteClock/RemoteClockSpec.js
Normal file
152
src/plugins/remoteClock/RemoteClockSpec.js
Normal file
@ -0,0 +1,152 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT Web, Copyright (c) 2014-2015, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT Web includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import {
|
||||
createOpenMct,
|
||||
resetApplicationState
|
||||
} from 'utils/testing';
|
||||
|
||||
const REMOTE_CLOCK_KEY = 'remote-clock';
|
||||
const TIME_TELEMETRY_ID = {
|
||||
namespace: 'remote',
|
||||
key: 'telemetry'
|
||||
};
|
||||
const TIME_VALUE = 12345;
|
||||
const REQ_OPTIONS = {
|
||||
size: 1,
|
||||
strategy: 'latest'
|
||||
};
|
||||
const OFFSET_START = -10;
|
||||
const OFFSET_END = 1;
|
||||
|
||||
describe("the RemoteClock plugin", () => {
|
||||
let openmct;
|
||||
let object = {
|
||||
name: 'remote-telemetry',
|
||||
identifier: TIME_TELEMETRY_ID
|
||||
};
|
||||
|
||||
beforeEach((done) => {
|
||||
openmct = createOpenMct();
|
||||
openmct.on('start', done);
|
||||
openmct.startHeadless();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
return resetApplicationState(openmct);
|
||||
});
|
||||
|
||||
describe('once installed', () => {
|
||||
let remoteClock;
|
||||
let boundsCallback;
|
||||
let metadataValue = { some: 'value' };
|
||||
let timeSystem = { key: 'utc' };
|
||||
let metadata = {
|
||||
value: () => metadataValue
|
||||
};
|
||||
let reqDatum = {
|
||||
key: TIME_VALUE
|
||||
};
|
||||
|
||||
let formatter = {
|
||||
parse: (datum) => datum.key
|
||||
};
|
||||
|
||||
beforeEach((done) => {
|
||||
openmct.install(openmct.plugins.RemoteClock(TIME_TELEMETRY_ID));
|
||||
|
||||
let clocks = openmct.time.getAllClocks();
|
||||
remoteClock = clocks.filter(clock => clock.key === REMOTE_CLOCK_KEY)[0];
|
||||
|
||||
boundsCallback = jasmine.createSpy("boundsCallback");
|
||||
openmct.time.on('bounds', boundsCallback);
|
||||
|
||||
spyOn(remoteClock, '_timeSystemChange').and.callThrough();
|
||||
spyOn(openmct.telemetry, 'getMetadata').and.returnValue(metadata);
|
||||
spyOn(openmct.telemetry, 'getValueFormatter').and.returnValue(formatter);
|
||||
spyOn(openmct.telemetry, 'subscribe').and.callThrough();
|
||||
spyOn(openmct.time, 'on').and.callThrough();
|
||||
spyOn(openmct.time, 'timeSystem').and.returnValue(timeSystem);
|
||||
spyOn(metadata, 'value').and.callThrough();
|
||||
|
||||
let requestPromiseResolve;
|
||||
let requestPromise = new Promise((resolve) => {
|
||||
requestPromiseResolve = resolve;
|
||||
});
|
||||
spyOn(openmct.telemetry, 'request').and.callFake(() => {
|
||||
requestPromiseResolve([reqDatum]);
|
||||
|
||||
return requestPromise;
|
||||
});
|
||||
|
||||
let objectPromiseResolve;
|
||||
let objectPromise = new Promise((resolve) => {
|
||||
objectPromiseResolve = resolve;
|
||||
});
|
||||
spyOn(openmct.objects, 'get').and.callFake(() => {
|
||||
objectPromiseResolve(object);
|
||||
|
||||
return objectPromise;
|
||||
});
|
||||
|
||||
openmct.time.clock(REMOTE_CLOCK_KEY, {
|
||||
start: OFFSET_START,
|
||||
end: OFFSET_END
|
||||
});
|
||||
|
||||
Promise.all([objectPromiseResolve, requestPromise])
|
||||
.then(done)
|
||||
.catch(done);
|
||||
});
|
||||
|
||||
it('is available and sets up initial values and listeners', () => {
|
||||
expect(remoteClock.key).toEqual(REMOTE_CLOCK_KEY);
|
||||
expect(remoteClock.identifier).toEqual(TIME_TELEMETRY_ID);
|
||||
expect(openmct.time.on).toHaveBeenCalledWith('timeSystem', remoteClock._timeSystemChange);
|
||||
expect(remoteClock._timeSystemChange).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('will request/store the object based on the identifier passed in', () => {
|
||||
expect(remoteClock.timeTelemetryObject).toEqual(object);
|
||||
});
|
||||
|
||||
it('will request metadata and set up formatters', () => {
|
||||
expect(remoteClock.metadata).toEqual(metadata);
|
||||
expect(metadata.value).toHaveBeenCalled();
|
||||
expect(openmct.telemetry.getValueFormatter).toHaveBeenCalledWith(metadataValue);
|
||||
});
|
||||
|
||||
it('will request the latest datum for the object it received and process the datum returned', () => {
|
||||
expect(openmct.telemetry.request).toHaveBeenCalledWith(remoteClock.timeTelemetryObject, REQ_OPTIONS);
|
||||
expect(boundsCallback).toHaveBeenCalledWith({
|
||||
start: TIME_VALUE + OFFSET_START,
|
||||
end: TIME_VALUE + OFFSET_END
|
||||
}, true);
|
||||
});
|
||||
|
||||
it('will set up subscriptions correctly', () => {
|
||||
expect(remoteClock._unsubscribe).toBeDefined();
|
||||
expect(openmct.telemetry.subscribe).toHaveBeenCalledWith(remoteClock.timeTelemetryObject, remoteClock._processDatum);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
32
src/plugins/remoteClock/plugin.js
Normal file
32
src/plugins/remoteClock/plugin.js
Normal file
@ -0,0 +1,32 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT Web, Copyright (c) 2014-2021, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT Web includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import RemoteClock from "./RemoteClock";
|
||||
/**
|
||||
* Install a clock that uses a configurable telemetry endpoint.
|
||||
*/
|
||||
|
||||
export default function (identifier) {
|
||||
return function (openmct) {
|
||||
openmct.time.addClock(new RemoteClock(openmct, identifier));
|
||||
};
|
||||
}
|
@ -35,8 +35,8 @@ define([
|
||||
|
||||
this.removeColumnsForObject = this.removeColumnsForObject.bind(this);
|
||||
this.objectMutated = this.objectMutated.bind(this);
|
||||
//Make copy of configuration, otherwise change detection is impossible if shared instance is being modified.
|
||||
this.oldConfiguration = JSON.parse(JSON.stringify(this.getConfiguration()));
|
||||
|
||||
this.unlistenFromMutation = openmct.objects.observe(domainObject, 'configuration', this.objectMutated);
|
||||
}
|
||||
|
||||
getConfiguration() {
|
||||
@ -58,14 +58,9 @@ define([
|
||||
* @private
|
||||
* @param {*} object
|
||||
*/
|
||||
objectMutated(object) {
|
||||
//Synchronize domain object reference. Duplicate object otherwise change detection becomes impossible.
|
||||
this.domainObject = object;
|
||||
//Was it the configuration that changed?
|
||||
if (object.configuration !== undefined && !_.eq(object.configuration, this.oldConfiguration)) {
|
||||
//Make copy of configuration, otherwise change detection is impossible if shared instance is being modified.
|
||||
this.oldConfiguration = JSON.parse(JSON.stringify(this.getConfiguration()));
|
||||
this.emit('change', object.configuration);
|
||||
objectMutated(configuration) {
|
||||
if (configuration !== undefined) {
|
||||
this.emit('change', configuration);
|
||||
}
|
||||
}
|
||||
|
||||
@ -162,7 +157,9 @@ define([
|
||||
this.updateConfiguration(configuration);
|
||||
}
|
||||
|
||||
destroy() {}
|
||||
destroy() {
|
||||
this.unlistenFromMutation();
|
||||
}
|
||||
}
|
||||
|
||||
return TelemetryTableConfiguration;
|
||||
|
67
src/plugins/telemetryTable/TelemetryTableView.js
Normal file
67
src/plugins/telemetryTable/TelemetryTableView.js
Normal file
@ -0,0 +1,67 @@
|
||||
import TableComponent from './components/table.vue';
|
||||
import TelemetryTable from './TelemetryTable';
|
||||
import Vue from 'vue';
|
||||
|
||||
export default class TelemetryTableView {
|
||||
constructor(openmct, domainObject, objectPath) {
|
||||
this.openmct = openmct;
|
||||
this.domainObject = domainObject;
|
||||
this.objectPath = objectPath;
|
||||
this.component = undefined;
|
||||
|
||||
this.table = new TelemetryTable(domainObject, openmct);
|
||||
}
|
||||
|
||||
getViewContext() {
|
||||
if (!this.component) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return this.component.$refs.tableComponent.getViewContext();
|
||||
}
|
||||
|
||||
onEditModeChange(editMode) {
|
||||
this.component.isEditing = editMode;
|
||||
}
|
||||
|
||||
onClearData() {
|
||||
this.table.clearData();
|
||||
}
|
||||
|
||||
getTable() {
|
||||
return this.table;
|
||||
}
|
||||
|
||||
destroy(element) {
|
||||
this.component.$destroy();
|
||||
this.component = undefined;
|
||||
}
|
||||
|
||||
show(element, editMode) {
|
||||
this.component = new Vue({
|
||||
el: element,
|
||||
components: {
|
||||
TableComponent
|
||||
},
|
||||
provide: {
|
||||
openmct: this.openmct,
|
||||
objectPath: this.objectPath,
|
||||
table: this.table,
|
||||
currentView: this
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isEditing: editMode,
|
||||
marking: {
|
||||
disableMultiSelect: false,
|
||||
enable: true,
|
||||
rowName: '',
|
||||
rowNamePlural: '',
|
||||
useAlternateControlBar: false
|
||||
}
|
||||
};
|
||||
},
|
||||
template: '<table-component ref="tableComponent" :is-editing="isEditing" :marking="marking"></table-component>'
|
||||
});
|
||||
}
|
||||
}
|
@ -20,99 +20,35 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define([
|
||||
'./components/table.vue',
|
||||
'./TelemetryTable',
|
||||
'vue'
|
||||
], function (
|
||||
TableComponent,
|
||||
TelemetryTable,
|
||||
Vue
|
||||
) {
|
||||
function TelemetryTableViewProvider(openmct) {
|
||||
function hasTelemetry(domainObject) {
|
||||
if (!Object.prototype.hasOwnProperty.call(domainObject, 'telemetry')) {
|
||||
return false;
|
||||
}
|
||||
import TelemetryTableView from './TelemetryTableView';
|
||||
|
||||
let metadata = openmct.telemetry.getMetadata(domainObject);
|
||||
|
||||
return metadata.values().length > 0;
|
||||
export default function TelemetryTableViewProvider(openmct) {
|
||||
function hasTelemetry(domainObject) {
|
||||
if (!Object.prototype.hasOwnProperty.call(domainObject, 'telemetry')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return {
|
||||
key: 'table',
|
||||
name: 'Telemetry Table',
|
||||
cssClass: 'icon-tabular-realtime',
|
||||
canView(domainObject) {
|
||||
return domainObject.type === 'table'
|
||||
|| hasTelemetry(domainObject);
|
||||
},
|
||||
canEdit(domainObject) {
|
||||
return domainObject.type === 'table';
|
||||
},
|
||||
view(domainObject, objectPath) {
|
||||
let table = new TelemetryTable(domainObject, openmct);
|
||||
let component;
|
||||
let markingProp = {
|
||||
enable: true,
|
||||
useAlternateControlBar: false,
|
||||
rowName: '',
|
||||
rowNamePlural: ''
|
||||
};
|
||||
const view = {
|
||||
show: function (element, editMode) {
|
||||
component = new Vue({
|
||||
el: element,
|
||||
components: {
|
||||
TableComponent: TableComponent.default
|
||||
},
|
||||
provide: {
|
||||
openmct,
|
||||
table,
|
||||
objectPath
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isEditing: editMode,
|
||||
markingProp,
|
||||
view
|
||||
};
|
||||
},
|
||||
template: '<table-component ref="tableComponent" :isEditing="isEditing" :marking="markingProp" :view="view"/>'
|
||||
});
|
||||
},
|
||||
onEditModeChange(editMode) {
|
||||
component.isEditing = editMode;
|
||||
},
|
||||
onClearData() {
|
||||
table.clearData();
|
||||
},
|
||||
getViewContext() {
|
||||
if (component) {
|
||||
return component.$refs.tableComponent.getViewContext();
|
||||
} else {
|
||||
return {
|
||||
type: 'telemetry-table'
|
||||
};
|
||||
}
|
||||
},
|
||||
destroy: function (element) {
|
||||
component.$destroy();
|
||||
component = undefined;
|
||||
},
|
||||
_getTable: function () {
|
||||
return table;
|
||||
}
|
||||
};
|
||||
let metadata = openmct.telemetry.getMetadata(domainObject);
|
||||
|
||||
return view;
|
||||
},
|
||||
priority() {
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
return metadata.values().length > 0;
|
||||
}
|
||||
|
||||
return TelemetryTableViewProvider;
|
||||
});
|
||||
return {
|
||||
key: 'table',
|
||||
name: 'Telemetry Table',
|
||||
cssClass: 'icon-tabular-realtime',
|
||||
canView(domainObject) {
|
||||
return domainObject.type === 'table'
|
||||
|| hasTelemetry(domainObject);
|
||||
},
|
||||
canEdit(domainObject) {
|
||||
return domainObject.type === 'table';
|
||||
},
|
||||
view(domainObject, objectPath) {
|
||||
return new TelemetryTableView(openmct, domainObject, objectPath);
|
||||
},
|
||||
priority() {
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -20,83 +20,89 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
let exportCSV = {
|
||||
const exportCSV = {
|
||||
name: 'Export Table Data',
|
||||
key: 'export-csv-all',
|
||||
description: "Export this view's data",
|
||||
cssClass: 'icon-download labeled',
|
||||
invoke: (objectPath, viewProvider) => {
|
||||
viewProvider.getViewContext().exportAllDataAsCSV();
|
||||
invoke: (objectPath, view) => {
|
||||
view.getViewContext().exportAllDataAsCSV();
|
||||
},
|
||||
group: 'view'
|
||||
};
|
||||
let exportMarkedDataAsCSV = {
|
||||
|
||||
const exportMarkedDataAsCSV = {
|
||||
name: 'Export Marked Rows',
|
||||
key: 'export-csv-marked',
|
||||
description: "Export marked rows as CSV",
|
||||
cssClass: 'icon-download labeled',
|
||||
invoke: (objectPath, viewProvider) => {
|
||||
viewProvider.getViewContext().exportMarkedDataAsCSV();
|
||||
invoke: (objectPath, view) => {
|
||||
view.getViewContext().exportMarkedDataAsCSV();
|
||||
},
|
||||
group: 'view'
|
||||
};
|
||||
let unmarkAllRows = {
|
||||
|
||||
const unmarkAllRows = {
|
||||
name: 'Unmark All Rows',
|
||||
key: 'unmark-all-rows',
|
||||
description: 'Unmark all rows',
|
||||
cssClass: 'icon-x labeled',
|
||||
invoke: (objectPath, viewProvider) => {
|
||||
viewProvider.getViewContext().unmarkAllRows();
|
||||
},
|
||||
showInStatusBar: true,
|
||||
group: 'view'
|
||||
};
|
||||
let pause = {
|
||||
name: 'Pause',
|
||||
key: 'pause-data',
|
||||
description: 'Pause real-time data flow',
|
||||
cssClass: 'icon-pause',
|
||||
invoke: (objectPath, viewProvider) => {
|
||||
viewProvider.getViewContext().togglePauseByButton();
|
||||
},
|
||||
showInStatusBar: true,
|
||||
group: 'view'
|
||||
};
|
||||
let play = {
|
||||
name: 'Play',
|
||||
key: 'play-data',
|
||||
description: 'Continue real-time data flow',
|
||||
cssClass: 'c-button pause-play is-paused',
|
||||
invoke: (objectPath, viewProvider) => {
|
||||
viewProvider.getViewContext().togglePauseByButton();
|
||||
},
|
||||
showInStatusBar: true,
|
||||
group: 'view'
|
||||
};
|
||||
let expandColumns = {
|
||||
name: 'Expand Columns',
|
||||
key: 'expand-columns',
|
||||
description: "Increase column widths to fit currently available data.",
|
||||
cssClass: 'icon-arrows-right-left labeled',
|
||||
invoke: (objectPath, viewProvider) => {
|
||||
viewProvider.getViewContext().expandColumns();
|
||||
},
|
||||
showInStatusBar: true,
|
||||
group: 'view'
|
||||
};
|
||||
let autosizeColumns = {
|
||||
name: 'Autosize Columns',
|
||||
key: 'autosize-columns',
|
||||
description: "Automatically size columns to fit the table into the available space.",
|
||||
cssClass: 'icon-expand labeled',
|
||||
invoke: (objectPath, viewProvider) => {
|
||||
viewProvider.getViewContext().autosizeColumns();
|
||||
invoke: (objectPath, view) => {
|
||||
view.getViewContext().unmarkAllRows();
|
||||
},
|
||||
showInStatusBar: true,
|
||||
group: 'view'
|
||||
};
|
||||
|
||||
let viewActions = [
|
||||
const pause = {
|
||||
name: 'Pause',
|
||||
key: 'pause-data',
|
||||
description: 'Pause real-time data flow',
|
||||
cssClass: 'icon-pause',
|
||||
invoke: (objectPath, view) => {
|
||||
view.getViewContext().togglePauseByButton();
|
||||
},
|
||||
showInStatusBar: true,
|
||||
group: 'view'
|
||||
};
|
||||
|
||||
const play = {
|
||||
name: 'Play',
|
||||
key: 'play-data',
|
||||
description: 'Continue real-time data flow',
|
||||
cssClass: 'c-button pause-play is-paused',
|
||||
invoke: (objectPath, view) => {
|
||||
view.getViewContext().togglePauseByButton();
|
||||
},
|
||||
showInStatusBar: true,
|
||||
group: 'view'
|
||||
};
|
||||
|
||||
const expandColumns = {
|
||||
name: 'Expand Columns',
|
||||
key: 'expand-columns',
|
||||
description: "Increase column widths to fit currently available data.",
|
||||
cssClass: 'icon-arrows-right-left labeled',
|
||||
invoke: (objectPath, view) => {
|
||||
view.getViewContext().expandColumns();
|
||||
},
|
||||
showInStatusBar: true,
|
||||
group: 'view'
|
||||
};
|
||||
|
||||
const autosizeColumns = {
|
||||
name: 'Autosize Columns',
|
||||
key: 'autosize-columns',
|
||||
description: "Automatically size columns to fit the table into the available space.",
|
||||
cssClass: 'icon-expand labeled',
|
||||
invoke: (objectPath, view) => {
|
||||
view.getViewContext().autosizeColumns();
|
||||
},
|
||||
showInStatusBar: true,
|
||||
group: 'view'
|
||||
};
|
||||
|
||||
const viewActions = [
|
||||
exportCSV,
|
||||
exportMarkedDataAsCSV,
|
||||
unmarkAllRows,
|
||||
@ -107,16 +113,13 @@ let viewActions = [
|
||||
];
|
||||
|
||||
viewActions.forEach(action => {
|
||||
action.appliesTo = (objectPath, viewProvider = {}) => {
|
||||
let viewContext = viewProvider.getViewContext && viewProvider.getViewContext();
|
||||
|
||||
if (viewContext) {
|
||||
let type = viewContext.type;
|
||||
|
||||
return type === 'telemetry-table';
|
||||
action.appliesTo = (objectPath, view = {}) => {
|
||||
const viewContext = view.getViewContext && view.getViewContext();
|
||||
if (!viewContext) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
return viewContext.type === 'telemetry-table';
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -49,7 +49,7 @@ export default {
|
||||
components: {
|
||||
TableCell
|
||||
},
|
||||
inject: ['openmct'],
|
||||
inject: ['openmct', 'currentView'],
|
||||
props: {
|
||||
headers: {
|
||||
type: Object,
|
||||
@ -93,25 +93,11 @@ export default {
|
||||
rowTop: (this.rowOffset + this.rowIndex) * this.rowHeight + 'px',
|
||||
rowClass: this.row.getRowClass(),
|
||||
cellLimitClasses: this.row.getCellLimitClasses(),
|
||||
componentList: Object.keys(this.headers).reduce((components, header) => {
|
||||
components[header] = this.row.getCellComponentName(header) || 'table-cell';
|
||||
|
||||
return components;
|
||||
}, {}),
|
||||
selectableColumns: Object.keys(this.row.columns).reduce((selectable, columnKeys) => {
|
||||
selectable[columnKeys] = this.row.columns[columnKeys].selectable;
|
||||
|
||||
return selectable;
|
||||
}, {}),
|
||||
actionsViewContext: {
|
||||
getViewContext: () => {
|
||||
return {
|
||||
viewHistoricalData: true,
|
||||
viewDatumAction: true,
|
||||
getDatum: this.getDatum
|
||||
};
|
||||
}
|
||||
}
|
||||
}, {})
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@ -125,6 +111,13 @@ export default {
|
||||
}
|
||||
|
||||
return listenersObject;
|
||||
},
|
||||
componentList() {
|
||||
return Object.keys(this.headers).reduce((components, header) => {
|
||||
components[header] = this.row.getCellComponentName(header) || 'table-cell';
|
||||
|
||||
return components;
|
||||
}, {});
|
||||
}
|
||||
},
|
||||
// TODO: use computed properties
|
||||
@ -185,19 +178,20 @@ export default {
|
||||
showContextMenu: function (event) {
|
||||
event.preventDefault();
|
||||
|
||||
this.updateViewContext();
|
||||
this.markRow(event);
|
||||
|
||||
this.row.getContextualDomainObject(this.openmct, this.row.objectKeyString).then(domainObject => {
|
||||
let contextualObjectPath = this.objectPath.slice();
|
||||
contextualObjectPath.unshift(domainObject);
|
||||
|
||||
let actionsCollection = this.openmct.actions.get(contextualObjectPath, this.actionsViewContext);
|
||||
let allActions = actionsCollection.getActionsObject();
|
||||
let applicableActions = this.row.getContextMenuActions().map(key => allActions[key]);
|
||||
|
||||
if (applicableActions.length) {
|
||||
this.openmct.menus.showMenu(event.x, event.y, applicableActions);
|
||||
}
|
||||
const actions = this.row.getContextMenuActions().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);
|
||||
}
|
||||
},
|
||||
updateViewContext() {
|
||||
this.$emit('rowContextClick', {
|
||||
viewHistoricalData: true,
|
||||
viewDatumAction: true,
|
||||
getDatum: this.getDatum
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -233,6 +233,7 @@
|
||||
@mark="markRow"
|
||||
@unmark="unmarkRow"
|
||||
@markMultipleConcurrent="markMultipleConcurrentRows"
|
||||
@rowContextClick="updateViewContext"
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
@ -263,6 +264,7 @@
|
||||
:column-widths="configuredColumnWidths"
|
||||
:row="sizingRowData"
|
||||
:object-path="objectPath"
|
||||
@rowContextClick="updateViewContext"
|
||||
/>
|
||||
</table>
|
||||
<table-footer-indicator
|
||||
@ -298,12 +300,25 @@ export default {
|
||||
ToggleSwitch,
|
||||
SizingRow
|
||||
},
|
||||
inject: ['table', 'openmct', 'objectPath'],
|
||||
inject: ['openmct', 'objectPath', 'table', 'currentView'],
|
||||
props: {
|
||||
isEditing: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
marking: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default() {
|
||||
return {
|
||||
enable: false,
|
||||
disableMultiSelect: false,
|
||||
useAlternateControlBar: false,
|
||||
rowName: '',
|
||||
rowNamePlural: ''
|
||||
};
|
||||
}
|
||||
},
|
||||
allowExport: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
@ -316,28 +331,9 @@ export default {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
marking: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {
|
||||
enable: false,
|
||||
disableMultiSelect: false,
|
||||
useAlternateControlBar: false,
|
||||
rowName: '',
|
||||
rowNamePlural: ""
|
||||
};
|
||||
}
|
||||
},
|
||||
enableLegacyToolbar: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
view: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default() {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@ -373,7 +369,8 @@ export default {
|
||||
isShowingMarkedRowsOnly: false,
|
||||
enableRegexSearch: {},
|
||||
hideHeaders: configuration.hideHeaders,
|
||||
totalNumberOfRows: 0
|
||||
totalNumberOfRows: 0,
|
||||
rowContext: {}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@ -461,8 +458,10 @@ export default {
|
||||
this.scroll = _.throttle(this.scroll, 100);
|
||||
|
||||
if (!this.marking.useAlternateControlBar && !this.enableLegacyToolbar) {
|
||||
this.viewActionsCollection = this.openmct.actions.get(this.objectPath, this.view);
|
||||
this.initializeViewActions();
|
||||
this.$nextTick(() => {
|
||||
this.viewActionsCollection = this.openmct.actions.getActionsCollection(this.objectPath, this.currentView);
|
||||
this.initializeViewActions();
|
||||
});
|
||||
}
|
||||
|
||||
this.table.on('object-added', this.addObject);
|
||||
@ -996,7 +995,8 @@ export default {
|
||||
unmarkAllRows: this.unmarkAllRows,
|
||||
togglePauseByButton: this.togglePauseByButton,
|
||||
expandColumns: this.recalculateColumnWidths,
|
||||
autosizeColumns: this.autosizeColumns
|
||||
autosizeColumns: this.autosizeColumns,
|
||||
row: this.rowContext
|
||||
};
|
||||
},
|
||||
initializeViewActions() {
|
||||
@ -1027,6 +1027,9 @@ export default {
|
||||
this.setHeight();
|
||||
this.calculateTableSize();
|
||||
this.clearRowsAndRerender();
|
||||
},
|
||||
updateViewContext(rowContext) {
|
||||
this.rowContext = rowContext;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user