Compare commits
131 Commits
tabs-wrong
...
imagery-en
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb97e94cd6 | ||
|
|
e7b8c42f02 | ||
|
|
3810b6c441 | ||
|
|
1499286bee | ||
|
|
2c62b4c1bc | ||
|
|
ca47fb6f2d | ||
|
|
5d4d87cd89 | ||
|
|
6226763c37 | ||
|
|
7623a0648f | ||
|
|
f32602343d | ||
|
|
bf82abd464 | ||
|
|
ab31581ea4 | ||
|
|
35bad9cb82 | ||
|
|
03a104c9f5 | ||
|
|
b7085f7f62 | ||
|
|
99ace5ec9b | ||
|
|
55c851873c | ||
|
|
2b143dfc0f | ||
|
|
93785544f1 | ||
|
|
9405272f3b | ||
|
|
a9be9f1827 | ||
|
|
abb1a5c75b | ||
|
|
ed0095fc00 | ||
|
|
5e2fe7dc42 | ||
|
|
63cf6e8156 | ||
|
|
e8600d23e1 | ||
|
|
36e720ad85 | ||
|
|
46e926aa08 | ||
|
|
429a628c92 | ||
|
|
03e1229576 | ||
|
|
05c8a8a2f0 | ||
|
|
7a7ec7c9b7 | ||
|
|
b5fcda3107 | ||
|
|
ab4e770b79 | ||
|
|
3f140de03a | ||
|
|
9ee6cca07d | ||
|
|
54182e400a | ||
|
|
863533910e | ||
|
|
edbdf432d1 | ||
|
|
35256b6e96 | ||
|
|
375bbd244e | ||
|
|
8090e27b7b | ||
|
|
275410f99c | ||
|
|
31ab08c9d3 | ||
|
|
082a89440e | ||
|
|
c729732541 | ||
|
|
8d3737912b | ||
|
|
d6a71adb7f | ||
|
|
8397b13c57 | ||
|
|
6a4ceb5219 | ||
|
|
c25b196b8f | ||
|
|
cd5cc4c76c | ||
|
|
f8b818e78b | ||
|
|
6cea1a77e5 | ||
|
|
88ff09857b | ||
|
|
b409d3cb1e | ||
|
|
a64e3e5ca0 | ||
|
|
d6ba2f8b4c | ||
|
|
4f1642a8d6 | ||
|
|
9b73b45ba9 | ||
|
|
98a048062f | ||
|
|
9b114c49df | ||
|
|
2c0c998e29 | ||
|
|
a001e07600 | ||
|
|
157564487d | ||
|
|
fcbd8c682a | ||
|
|
4b40233bf3 | ||
|
|
fa2197f9c1 | ||
|
|
f3f833a337 | ||
|
|
e6e8b8e048 | ||
|
|
6a2c079336 | ||
|
|
0d23fe3d14 | ||
|
|
1d645a8472 | ||
|
|
334aeb42ae | ||
|
|
5900bb0d98 | ||
|
|
a2c350b105 | ||
|
|
174f212328 | ||
|
|
a28ec45f71 | ||
|
|
fcc6bb9873 | ||
|
|
45578b113f | ||
|
|
5ef14b0975 | ||
|
|
2be429a04f | ||
|
|
c4ce405b1e | ||
|
|
b95f844a4e | ||
|
|
3804fe1a1e | ||
|
|
7576673e77 | ||
|
|
63e04caab6 | ||
|
|
956cfbd01f | ||
|
|
6c77be32c7 | ||
|
|
af4c7c9ca0 | ||
|
|
1697362994 | ||
|
|
e69911385f | ||
|
|
6478267cbe | ||
|
|
22d53c1ccd | ||
|
|
633a95dd27 | ||
|
|
f732167e02 | ||
|
|
50ff26ad5d | ||
|
|
f056e8e57b | ||
|
|
1d56fd98dc | ||
|
|
320217f8c4 | ||
|
|
b43fef6e21 | ||
|
|
d04c29345b | ||
|
|
49afec5cdd | ||
|
|
24b96cdb47 | ||
|
|
14ce4a1aa0 | ||
|
|
43a8901c34 | ||
|
|
28d97be60e | ||
|
|
4bb2b35124 | ||
|
|
1f6e91c6b5 | ||
|
|
0b078497f1 | ||
|
|
060a1b17db | ||
|
|
db50b8b732 | ||
|
|
62de05808e | ||
|
|
417f81b7fd | ||
|
|
f219394abd | ||
|
|
4e5c74ecef | ||
|
|
218530e436 | ||
|
|
0890499a2b | ||
|
|
d9dad09dfd | ||
|
|
9af5df0f20 | ||
|
|
1580a61092 | ||
|
|
c236444a05 | ||
|
|
39c1eb1d5b | ||
|
|
95caab944d | ||
|
|
ac89e51d1b | ||
|
|
85d9ed8287 | ||
|
|
aedc24a2da | ||
|
|
823eda4465 | ||
|
|
7eaa1d3e2b | ||
|
|
4633436cbd | ||
|
|
b68a7e27c9 |
20
.eslintrc.js
20
.eslintrc.js
@@ -54,7 +54,7 @@ module.exports = {
|
||||
{
|
||||
"anonymous": "always",
|
||||
"asyncArrow": "always",
|
||||
"named": "never",
|
||||
"named": "never"
|
||||
}
|
||||
],
|
||||
"array-bracket-spacing": "error",
|
||||
@@ -178,7 +178,10 @@ module.exports = {
|
||||
//https://eslint.org/docs/rules/no-whitespace-before-property
|
||||
"no-whitespace-before-property": "error",
|
||||
// https://eslint.org/docs/rules/object-curly-newline
|
||||
"object-curly-newline": ["error", {"consistent": true, "multiline": true}],
|
||||
"object-curly-newline": ["error", {
|
||||
"consistent": true,
|
||||
"multiline": true
|
||||
}],
|
||||
// https://eslint.org/docs/rules/object-property-newline
|
||||
"object-property-newline": "error",
|
||||
// https://eslint.org/docs/rules/brace-style
|
||||
@@ -188,7 +191,7 @@ module.exports = {
|
||||
// https://eslint.org/docs/rules/operator-linebreak
|
||||
"operator-linebreak": ["error", "before", {"overrides": {"=": "after"}}],
|
||||
// https://eslint.org/docs/rules/padding-line-between-statements
|
||||
"padding-line-between-statements":["error", {
|
||||
"padding-line-between-statements": ["error", {
|
||||
"blankLine": "always",
|
||||
"prev": "multiline-block-like",
|
||||
"next": "*"
|
||||
@@ -200,11 +203,17 @@ module.exports = {
|
||||
// https://eslint.org/docs/rules/space-infix-ops
|
||||
"space-infix-ops": "error",
|
||||
// https://eslint.org/docs/rules/space-unary-ops
|
||||
"space-unary-ops": ["error", {"words": true, "nonwords": false}],
|
||||
"space-unary-ops": ["error", {
|
||||
"words": true,
|
||||
"nonwords": false
|
||||
}],
|
||||
// https://eslint.org/docs/rules/arrow-spacing
|
||||
"arrow-spacing": "error",
|
||||
// https://eslint.org/docs/rules/semi-spacing
|
||||
"semi-spacing": ["error", {"before": false, "after": true}],
|
||||
"semi-spacing": ["error", {
|
||||
"before": false,
|
||||
"after": true
|
||||
}],
|
||||
|
||||
"vue/html-indent": [
|
||||
"error",
|
||||
@@ -237,6 +246,7 @@ module.exports = {
|
||||
}],
|
||||
"vue/multiline-html-element-content-newline": "off",
|
||||
"vue/singleline-html-element-content-newline": "off",
|
||||
"vue/no-mutating-props": "off"
|
||||
|
||||
},
|
||||
"overrides": [
|
||||
|
||||
@@ -138,7 +138,7 @@ define([
|
||||
"id": "styleguide:home",
|
||||
"priority": "preferred",
|
||||
"model": {
|
||||
"type": "folder",
|
||||
"type": "noneditable.folder",
|
||||
"name": "Style Guide Home",
|
||||
"location": "ROOT",
|
||||
"composition": [
|
||||
@@ -155,7 +155,7 @@ define([
|
||||
"id": "styleguide:ui-elements",
|
||||
"priority": "preferred",
|
||||
"model": {
|
||||
"type": "folder",
|
||||
"type": "noneditable.folder",
|
||||
"name": "UI Elements",
|
||||
"location": "styleguide:home",
|
||||
"composition": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openmct",
|
||||
"version": "1.6.1-SNAPSHOT",
|
||||
"version": "1.6.2-SNAPSHOT",
|
||||
"description": "The Open MCT core platform",
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
@@ -23,7 +23,7 @@
|
||||
"d3-time": "1.0.x",
|
||||
"d3-time-format": "2.1.x",
|
||||
"eslint": "7.0.0",
|
||||
"eslint-plugin-vue": "^6.0.0",
|
||||
"eslint-plugin-vue": "^7.5.0",
|
||||
"eslint-plugin-you-dont-need-lodash-underscore": "^6.10.0",
|
||||
"eventemitter3": "^1.2.0",
|
||||
"exports-loader": "^0.7.0",
|
||||
|
||||
@@ -146,10 +146,15 @@ define([
|
||||
* @param {String} id to be indexed.
|
||||
*/
|
||||
GenericSearchProvider.prototype.scheduleForIndexing = function (id) {
|
||||
if (!this.indexedIds[id] && !this.pendingIndex[id]) {
|
||||
this.indexedIds[id] = true;
|
||||
this.pendingIndex[id] = true;
|
||||
this.idsToIndex.push(id);
|
||||
const identifier = objectUtils.parseKeyString(id);
|
||||
const objectProvider = this.openmct.objects.getProvider(identifier);
|
||||
|
||||
if (objectProvider === undefined || objectProvider.search === undefined) {
|
||||
if (!this.indexedIds[id] && !this.pendingIndex[id]) {
|
||||
this.indexedIds[id] = true;
|
||||
this.pendingIndex[id] = true;
|
||||
this.idsToIndex.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
this.keepIndexing();
|
||||
|
||||
@@ -283,6 +283,7 @@ define([
|
||||
this.install(this.plugins.NewFolderAction());
|
||||
this.install(this.plugins.ViewDatumAction());
|
||||
this.install(this.plugins.ObjectInterceptors());
|
||||
this.install(this.plugins.NonEditableFolder());
|
||||
}
|
||||
|
||||
MCT.prototype = Object.create(EventEmitter.prototype);
|
||||
|
||||
@@ -139,6 +139,12 @@ define([
|
||||
});
|
||||
};
|
||||
|
||||
ObjectServiceProvider.prototype.superSecretFallbackSearch = function (query, options) {
|
||||
const searchService = this.$injector.get('searchService');
|
||||
|
||||
return searchService.query(query);
|
||||
};
|
||||
|
||||
// Injects new object API as a decorator so that it hijacks all requests.
|
||||
// Object providers implemented on new API should just work, old API should just work, many things may break.
|
||||
function LegacyObjectAPIInterceptor(openmct, ROOTS, instantiate, topic, objectService) {
|
||||
|
||||
@@ -30,12 +30,12 @@ class Menu extends EventEmitter {
|
||||
this.options = options;
|
||||
|
||||
this.component = new Vue({
|
||||
provide: {
|
||||
actions: options.actions
|
||||
},
|
||||
components: {
|
||||
MenuComponent
|
||||
},
|
||||
provide: {
|
||||
actions: options.actions
|
||||
},
|
||||
template: '<menu-component />'
|
||||
});
|
||||
|
||||
|
||||
@@ -75,13 +75,20 @@ export default class NotificationAPI extends EventEmitter {
|
||||
* Info notifications are low priority informational messages for the user. They will be auto-destroy after a brief
|
||||
* period of time.
|
||||
* @param {string} message The message to display to the user
|
||||
* @param {Object} [options] object with following properties
|
||||
* autoDismissTimeout: {number} in miliseconds to automatically dismisses notification
|
||||
* link: {Object} Add a link to notifications for navigation
|
||||
* onClick: callback function
|
||||
* cssClass: css class name to add style on link
|
||||
* text: text to display for link
|
||||
* @returns {InfoNotification}
|
||||
*/
|
||||
info(message) {
|
||||
info(message, options = {}) {
|
||||
let notificationModel = {
|
||||
message: message,
|
||||
autoDismiss: true,
|
||||
severity: "info"
|
||||
severity: "info",
|
||||
options
|
||||
};
|
||||
|
||||
return this._notify(notificationModel);
|
||||
@@ -90,12 +97,19 @@ export default class NotificationAPI extends EventEmitter {
|
||||
/**
|
||||
* Present an alert to the user.
|
||||
* @param {string} message The message to display to the user.
|
||||
* @param {Object} [options] object with following properties
|
||||
* autoDismissTimeout: {number} in miliseconds to automatically dismisses notification
|
||||
* link: {Object} Add a link to notifications for navigation
|
||||
* onClick: callback function
|
||||
* cssClass: css class name to add style on link
|
||||
* text: text to display for link
|
||||
* @returns {Notification}
|
||||
*/
|
||||
alert(message) {
|
||||
alert(message, options = {}) {
|
||||
let notificationModel = {
|
||||
message: message,
|
||||
severity: "alert"
|
||||
severity: "alert",
|
||||
options
|
||||
};
|
||||
|
||||
return this._notify(notificationModel);
|
||||
@@ -104,12 +118,19 @@ export default class NotificationAPI extends EventEmitter {
|
||||
/**
|
||||
* Present an error message to the user
|
||||
* @param {string} message
|
||||
* @param {Object} [options] object with following properties
|
||||
* autoDismissTimeout: {number} in miliseconds to automatically dismisses notification
|
||||
* link: {Object} Add a link to notifications for navigation
|
||||
* onClick: callback function
|
||||
* cssClass: css class name to add style on link
|
||||
* text: text to display for link
|
||||
* @returns {Notification}
|
||||
*/
|
||||
error(message) {
|
||||
error(message, options = {}) {
|
||||
let notificationModel = {
|
||||
message: message,
|
||||
severity: "error"
|
||||
severity: "error",
|
||||
options
|
||||
};
|
||||
|
||||
return this._notify(notificationModel);
|
||||
@@ -325,9 +346,11 @@ export default class NotificationAPI extends EventEmitter {
|
||||
this.emit('notification', notification);
|
||||
|
||||
if (notification.model.autoDismiss || this._selectNextNotification()) {
|
||||
const autoDismissTimeout = notification.model.options.autoDismissTimeout
|
||||
|| DEFAULT_AUTO_DISMISS_TIMEOUT;
|
||||
this.activeTimeout = setTimeout(() => {
|
||||
this._dismissOrMinimize(notification);
|
||||
}, DEFAULT_AUTO_DISMISS_TIMEOUT);
|
||||
}, autoDismissTimeout);
|
||||
} else {
|
||||
delete this.activeTimeout;
|
||||
}
|
||||
|
||||
@@ -168,6 +168,34 @@ ObjectAPI.prototype.get = function (identifier) {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Search for domain objects.
|
||||
*
|
||||
* Object providersSearches and combines results of each object provider search.
|
||||
* Objects without search provided will have been indexed
|
||||
* and will be searched using the fallback indexed search.
|
||||
* Search results are asynchronous and resolve in parallel.
|
||||
*
|
||||
* @method search
|
||||
* @memberof module:openmct.ObjectAPI#
|
||||
* @param {string} query the term to search for
|
||||
* @param {Object} options search options
|
||||
* @returns {Array.<Promise.<module:openmct.DomainObject>>}
|
||||
* an array of promises returned from each object provider's search function
|
||||
* each resolving to domain objects matching provided search query and options.
|
||||
*/
|
||||
ObjectAPI.prototype.search = function (query, options) {
|
||||
const searchPromises = Object.values(this.providers)
|
||||
.filter(provider => provider.search !== undefined)
|
||||
.map(provider => provider.search(query, options));
|
||||
|
||||
searchPromises.push(this.fallbackProvider.superSecretFallbackSearch(query, options)
|
||||
.then(results => results.hits
|
||||
.map(hit => utils.toNewFormat(hit.object.getModel(), hit.object.getId()))));
|
||||
|
||||
return searchPromises;
|
||||
};
|
||||
|
||||
/**
|
||||
* Will fetch object for the given identifier, returning a version of the object that will automatically keep
|
||||
* itself updated as it is mutated. Before using this function, you should ask yourself whether you really need it.
|
||||
|
||||
119
src/api/objects/ObjectAPISearchSpec.js
Normal file
119
src/api/objects/ObjectAPISearchSpec.js
Normal file
@@ -0,0 +1,119 @@
|
||||
import ObjectAPI from './ObjectAPI.js';
|
||||
|
||||
describe("The Object API Search Function", () => {
|
||||
const MOCK_PROVIDER_KEY = 'mockProvider';
|
||||
const ANOTHER_MOCK_PROVIDER_KEY = 'anotherMockProvider';
|
||||
const MOCK_PROVIDER_SEARCH_DELAY = 15000;
|
||||
const ANOTHER_MOCK_PROVIDER_SEARCH_DELAY = 20000;
|
||||
const TOTAL_TIME_ELAPSED = 21000;
|
||||
const BASE_TIME = new Date(2021, 0, 1);
|
||||
|
||||
let objectAPI;
|
||||
let mockObjectProvider;
|
||||
let anotherMockObjectProvider;
|
||||
let mockFallbackProvider;
|
||||
let fallbackProviderSearchResults;
|
||||
let resultsPromises;
|
||||
|
||||
beforeEach(() => {
|
||||
jasmine.clock().install();
|
||||
jasmine.clock().mockDate(BASE_TIME);
|
||||
|
||||
resultsPromises = [];
|
||||
fallbackProviderSearchResults = {
|
||||
hits: []
|
||||
};
|
||||
|
||||
objectAPI = new ObjectAPI();
|
||||
|
||||
mockObjectProvider = jasmine.createSpyObj("mock object provider", [
|
||||
"search"
|
||||
]);
|
||||
anotherMockObjectProvider = jasmine.createSpyObj("another mock object provider", [
|
||||
"search"
|
||||
]);
|
||||
mockFallbackProvider = jasmine.createSpyObj("super secret fallback provider", [
|
||||
"superSecretFallbackSearch"
|
||||
]);
|
||||
objectAPI.addProvider('objects', mockObjectProvider);
|
||||
objectAPI.addProvider('other-objects', anotherMockObjectProvider);
|
||||
objectAPI.supersecretSetFallbackProvider(mockFallbackProvider);
|
||||
|
||||
mockObjectProvider.search.and.callFake(() => {
|
||||
return new Promise(resolve => {
|
||||
const mockProviderSearch = {
|
||||
name: MOCK_PROVIDER_KEY,
|
||||
start: new Date()
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
mockProviderSearch.end = new Date();
|
||||
|
||||
return resolve(mockProviderSearch);
|
||||
}, MOCK_PROVIDER_SEARCH_DELAY);
|
||||
});
|
||||
});
|
||||
anotherMockObjectProvider.search.and.callFake(() => {
|
||||
return new Promise(resolve => {
|
||||
const anotherMockProviderSearch = {
|
||||
name: ANOTHER_MOCK_PROVIDER_KEY,
|
||||
start: new Date()
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
anotherMockProviderSearch.end = new Date();
|
||||
|
||||
return resolve(anotherMockProviderSearch);
|
||||
}, ANOTHER_MOCK_PROVIDER_SEARCH_DELAY);
|
||||
});
|
||||
});
|
||||
mockFallbackProvider.superSecretFallbackSearch.and.callFake(
|
||||
() => new Promise(
|
||||
resolve => setTimeout(
|
||||
() => resolve(fallbackProviderSearchResults),
|
||||
50
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
resultsPromises = objectAPI.search('foo');
|
||||
|
||||
jasmine.clock().tick(TOTAL_TIME_ELAPSED);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jasmine.clock().uninstall();
|
||||
});
|
||||
|
||||
it("uses each objects given provider's search function", () => {
|
||||
expect(mockObjectProvider.search).toHaveBeenCalled();
|
||||
expect(anotherMockObjectProvider.search).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses the fallback indexed search for objects without a search function provided", () => {
|
||||
expect(mockFallbackProvider.superSecretFallbackSearch).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("provides each providers results as promises that resolve in parallel", async () => {
|
||||
const results = await Promise.all(resultsPromises);
|
||||
const mockProviderResults = results.find(
|
||||
result => result.name === MOCK_PROVIDER_KEY
|
||||
);
|
||||
const anotherMockProviderResults = results.find(
|
||||
result => result.name === ANOTHER_MOCK_PROVIDER_KEY
|
||||
);
|
||||
const mockProviderStart = mockProviderResults.start.getTime();
|
||||
const mockProviderEnd = mockProviderResults.end.getTime();
|
||||
const anotherMockProviderStart = anotherMockProviderResults.start.getTime();
|
||||
const anotherMockProviderEnd = anotherMockProviderResults.end.getTime();
|
||||
const searchElapsedTime = Math.max(mockProviderEnd, anotherMockProviderEnd)
|
||||
- Math.min(mockProviderEnd, anotherMockProviderEnd);
|
||||
|
||||
expect(mockProviderStart).toBeLessThan(anotherMockProviderEnd);
|
||||
expect(anotherMockProviderStart).toBeLessThan(mockProviderEnd);
|
||||
expect(searchElapsedTime).toBeLessThan(
|
||||
MOCK_PROVIDER_SEARCH_DELAY
|
||||
+ ANOTHER_MOCK_PROVIDER_SEARCH_DELAY
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -48,7 +48,7 @@ define([
|
||||
this.providers.push(function () {
|
||||
return key;
|
||||
});
|
||||
} else if (_.isFunction(key)) {
|
||||
} else if (typeof key === "function") {
|
||||
this.providers.push(key);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -6,6 +6,9 @@ class Dialog extends Overlay {
|
||||
constructor({iconClass, message, title, hint, timestamp, ...options}) {
|
||||
|
||||
let component = new Vue({
|
||||
components: {
|
||||
DialogComponent: DialogComponent
|
||||
},
|
||||
provide: {
|
||||
iconClass,
|
||||
message,
|
||||
@@ -13,9 +16,6 @@ class Dialog extends Overlay {
|
||||
hint,
|
||||
timestamp
|
||||
},
|
||||
components: {
|
||||
DialogComponent: DialogComponent
|
||||
},
|
||||
template: '<dialog-component></dialog-component>'
|
||||
}).$mount();
|
||||
|
||||
|
||||
@@ -7,6 +7,9 @@ let component;
|
||||
class ProgressDialog extends Overlay {
|
||||
constructor({progressPerc, progressText, iconClass, message, title, hint, timestamp, ...options}) {
|
||||
component = new Vue({
|
||||
components: {
|
||||
ProgressDialogComponent: ProgressDialogComponent
|
||||
},
|
||||
provide: {
|
||||
iconClass,
|
||||
message,
|
||||
@@ -14,9 +17,6 @@ class ProgressDialog extends Overlay {
|
||||
hint,
|
||||
timestamp
|
||||
},
|
||||
components: {
|
||||
ProgressDialogComponent: ProgressDialogComponent
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
model: {
|
||||
|
||||
@@ -38,12 +38,12 @@
|
||||
|
||||
<script>
|
||||
export default {
|
||||
inject: ['dismiss', 'element', 'buttons', 'dismissable'],
|
||||
data: function () {
|
||||
return {
|
||||
focusIndex: -1
|
||||
};
|
||||
},
|
||||
inject: ['dismiss', 'element', 'buttons', 'dismissable'],
|
||||
mounted() {
|
||||
const element = this.$refs.element;
|
||||
element.appendChild(this.element);
|
||||
|
||||
@@ -43,15 +43,15 @@ export default function LADTableViewProvider(openmct) {
|
||||
components: {
|
||||
LadTableComponent: LadTable
|
||||
},
|
||||
provide: {
|
||||
openmct
|
||||
},
|
||||
data: () => {
|
||||
return {
|
||||
domainObject,
|
||||
objectPath
|
||||
};
|
||||
},
|
||||
provide: {
|
||||
openmct
|
||||
},
|
||||
template: '<lad-table-component :domain-object="domainObject" :object-path="objectPath"></lad-table-component>'
|
||||
});
|
||||
},
|
||||
|
||||
@@ -48,10 +48,10 @@
|
||||
import LadRow from './LADRow.vue';
|
||||
|
||||
export default {
|
||||
inject: ['openmct'],
|
||||
components: {
|
||||
LadRow
|
||||
},
|
||||
inject: ['openmct'],
|
||||
props: {
|
||||
domainObject: {
|
||||
type: Object,
|
||||
|
||||
@@ -57,10 +57,10 @@
|
||||
import LadRow from './LADRow.vue';
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'domainObject'],
|
||||
components: {
|
||||
LadRow
|
||||
},
|
||||
inject: ['openmct', 'domainObject'],
|
||||
data() {
|
||||
return {
|
||||
ladTableObjects: [],
|
||||
|
||||
@@ -37,12 +37,12 @@ define([
|
||||
return function install(openmct) {
|
||||
if (installIndicator) {
|
||||
let component = new Vue ({
|
||||
provide: {
|
||||
openmct
|
||||
},
|
||||
components: {
|
||||
GlobalClearIndicator: GlobaClearIndicator.default
|
||||
},
|
||||
provide: {
|
||||
openmct
|
||||
},
|
||||
template: '<GlobalClearIndicator></GlobalClearIndicator>'
|
||||
});
|
||||
|
||||
|
||||
@@ -195,11 +195,11 @@ import { TRIGGER, TRIGGER_LABEL } from "@/plugins/condition/utils/constants";
|
||||
import uuid from 'uuid';
|
||||
|
||||
export default {
|
||||
inject: ['openmct'],
|
||||
components: {
|
||||
Criterion,
|
||||
ConditionDescription
|
||||
},
|
||||
inject: ['openmct'],
|
||||
props: {
|
||||
currentConditionId: {
|
||||
type: String,
|
||||
|
||||
@@ -81,10 +81,10 @@ import Condition from './Condition.vue';
|
||||
import ConditionManager from '../ConditionManager';
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'domainObject'],
|
||||
components: {
|
||||
Condition
|
||||
},
|
||||
inject: ['openmct', 'domainObject'],
|
||||
props: {
|
||||
isEditing: Boolean,
|
||||
testData: {
|
||||
|
||||
@@ -58,11 +58,11 @@ import TestData from './TestData.vue';
|
||||
import ConditionCollection from './ConditionCollection.vue';
|
||||
|
||||
export default {
|
||||
inject: ["openmct", "domainObject"],
|
||||
components: {
|
||||
TestData,
|
||||
ConditionCollection
|
||||
},
|
||||
inject: ["openmct", "domainObject"],
|
||||
props: {
|
||||
isEditing: Boolean
|
||||
},
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<ul
|
||||
v-if="expanded"
|
||||
v-if="expanded && !isLoading"
|
||||
class="c-tree"
|
||||
>
|
||||
<li
|
||||
@@ -68,10 +68,10 @@ import viewControl from '@/ui/components/viewControl.vue';
|
||||
|
||||
export default {
|
||||
name: 'ConditionSetDialogTreeItem',
|
||||
inject: ['openmct'],
|
||||
components: {
|
||||
viewControl
|
||||
},
|
||||
inject: ['openmct'],
|
||||
props: {
|
||||
node: {
|
||||
type: Object,
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
></div>
|
||||
<!-- end loading -->
|
||||
|
||||
<div v-if="(allTreeItems.length === 0) || (searchValue && filteredTreeItems.length === 0)"
|
||||
<div v-if="shouldDisplayNoResultsText"
|
||||
class="c-tree-and-search__no-results"
|
||||
>
|
||||
No results found
|
||||
@@ -63,7 +63,7 @@
|
||||
<!-- end main tree -->
|
||||
|
||||
<!-- search tree -->
|
||||
<ul v-if="searchValue"
|
||||
<ul v-if="searchValue && !isLoading"
|
||||
class="c-tree-and-search__tree c-tree"
|
||||
>
|
||||
<condition-set-dialog-tree-item
|
||||
@@ -80,16 +80,17 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import debounce from 'lodash/debounce';
|
||||
import search from '@/ui/components/search.vue';
|
||||
import ConditionSetDialogTreeItem from './ConditionSetDialogTreeItem.vue';
|
||||
|
||||
export default {
|
||||
inject: ['openmct'],
|
||||
name: 'ConditionSetSelectorDialog',
|
||||
components: {
|
||||
search,
|
||||
ConditionSetDialogTreeItem
|
||||
},
|
||||
inject: ['openmct'],
|
||||
data() {
|
||||
return {
|
||||
expanded: false,
|
||||
@@ -100,8 +101,20 @@ export default {
|
||||
selectedItem: undefined
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
shouldDisplayNoResultsText() {
|
||||
if (this.isLoading) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.allTreeItems.length === 0
|
||||
|| (this.searchValue && this.filteredTreeItems.length === 0);
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getDebouncedFilteredChildren = debounce(this.getFilteredChildren, 400);
|
||||
},
|
||||
mounted() {
|
||||
this.searchService = this.openmct.$injector.get('searchService');
|
||||
this.getAllChildren();
|
||||
},
|
||||
methods: {
|
||||
@@ -124,37 +137,44 @@ export default {
|
||||
});
|
||||
},
|
||||
getFilteredChildren() {
|
||||
this.searchService.query(this.searchValue).then(children => {
|
||||
this.filteredTreeItems = children.hits.map(child => {
|
||||
// clear any previous search results
|
||||
this.filteredTreeItems = [];
|
||||
|
||||
let context = child.object.getCapability('context');
|
||||
let object = child.object.useCapability('adapter');
|
||||
let objectPath = [];
|
||||
let navigateToParent;
|
||||
const promises = this.openmct.objects.search(this.searchValue)
|
||||
.map(promise => promise
|
||||
.then(results => this.aggregateFilteredChildren(results)));
|
||||
|
||||
if (context) {
|
||||
objectPath = context.getPath().slice(1)
|
||||
.map(oldObject => oldObject.useCapability('adapter'))
|
||||
.reverse();
|
||||
navigateToParent = '/browse/' + objectPath.slice(1)
|
||||
.map((parent) => this.openmct.objects.makeKeyString(parent.identifier))
|
||||
.join('/');
|
||||
}
|
||||
|
||||
return {
|
||||
id: this.openmct.objects.makeKeyString(object.identifier),
|
||||
object,
|
||||
objectPath,
|
||||
navigateToParent
|
||||
};
|
||||
});
|
||||
Promise.all(promises).then(() => {
|
||||
this.isLoading = false;
|
||||
});
|
||||
},
|
||||
async aggregateFilteredChildren(results) {
|
||||
for (const object of results) {
|
||||
const objectPath = await this.openmct.objects.getOriginalPath(object.identifier);
|
||||
|
||||
const navigateToParent = '/browse/'
|
||||
+ objectPath.slice(1)
|
||||
.map(parent => this.openmct.objects.makeKeyString(parent.identifier))
|
||||
.join('/');
|
||||
|
||||
const filteredChild = {
|
||||
id: this.openmct.objects.makeKeyString(object.identifier),
|
||||
object,
|
||||
objectPath,
|
||||
navigateToParent
|
||||
};
|
||||
|
||||
this.filteredTreeItems.push(filteredChild);
|
||||
}
|
||||
},
|
||||
searchTree(value) {
|
||||
this.searchValue = value;
|
||||
this.isLoading = true;
|
||||
|
||||
if (this.searchValue !== '') {
|
||||
this.getFilteredChildren();
|
||||
this.getDebouncedFilteredChildren();
|
||||
} else {
|
||||
this.isLoading = false;
|
||||
}
|
||||
},
|
||||
handleItemSelection(item, node) {
|
||||
|
||||
@@ -401,15 +401,15 @@ describe('the plugin', function () {
|
||||
let viewContainer = document.createElement('div');
|
||||
child.append(viewContainer);
|
||||
component = new Vue({
|
||||
el: viewContainer,
|
||||
components: {
|
||||
StylesView
|
||||
},
|
||||
provide: {
|
||||
openmct: openmct,
|
||||
selection: selection,
|
||||
stylesManager
|
||||
},
|
||||
el: viewContainer,
|
||||
components: {
|
||||
StylesView
|
||||
},
|
||||
template: '<styles-view/>'
|
||||
});
|
||||
|
||||
|
||||
@@ -56,14 +56,14 @@ define([
|
||||
return {
|
||||
show: function (element) {
|
||||
component = new Vue({
|
||||
provide: {
|
||||
openmct,
|
||||
objectPath
|
||||
},
|
||||
el: element,
|
||||
components: {
|
||||
AlphanumericFormatView: AlphanumericFormatView.default
|
||||
},
|
||||
provide: {
|
||||
openmct,
|
||||
objectPath
|
||||
},
|
||||
template: '<alphanumeric-format-view ref="alphanumericFormatView"></alphanumeric-format-view>'
|
||||
});
|
||||
},
|
||||
|
||||
@@ -51,11 +51,11 @@ export default {
|
||||
height: 5
|
||||
};
|
||||
},
|
||||
inject: ['openmct'],
|
||||
components: {
|
||||
LayoutFrame
|
||||
},
|
||||
mixins: [conditionalStylesMixin],
|
||||
inject: ['openmct'],
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
|
||||
@@ -51,11 +51,11 @@ export default {
|
||||
url: element.url
|
||||
};
|
||||
},
|
||||
inject: ['openmct'],
|
||||
components: {
|
||||
LayoutFrame
|
||||
},
|
||||
mixins: [conditionalStylesMixin],
|
||||
inject: ['openmct'],
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
|
||||
@@ -99,8 +99,8 @@ export default {
|
||||
stroke: '#717171'
|
||||
};
|
||||
},
|
||||
inject: ['openmct'],
|
||||
mixins: [conditionalStylesMixin],
|
||||
inject: ['openmct'],
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
|
||||
@@ -80,11 +80,11 @@ export default {
|
||||
viewKey
|
||||
};
|
||||
},
|
||||
inject: ['openmct', 'objectPath'],
|
||||
components: {
|
||||
ObjectFrame,
|
||||
LayoutFrame
|
||||
},
|
||||
inject: ['openmct', 'objectPath'],
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
|
||||
@@ -98,11 +98,11 @@ export default {
|
||||
font: 'default'
|
||||
};
|
||||
},
|
||||
inject: ['openmct', 'objectPath'],
|
||||
components: {
|
||||
LayoutFrame
|
||||
},
|
||||
mixins: [conditionalStylesMixin],
|
||||
inject: ['openmct', 'objectPath'],
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
|
||||
@@ -59,11 +59,11 @@ export default {
|
||||
font: 'default'
|
||||
};
|
||||
},
|
||||
inject: ['openmct'],
|
||||
components: {
|
||||
LayoutFrame
|
||||
},
|
||||
mixins: [conditionalStylesMixin],
|
||||
inject: ['openmct'],
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
|
||||
@@ -47,13 +47,13 @@ define([
|
||||
return {
|
||||
show: function (element) {
|
||||
component = new Vue({
|
||||
provide: {
|
||||
openmct
|
||||
},
|
||||
el: element,
|
||||
components: {
|
||||
FiltersView: FiltersView.default
|
||||
},
|
||||
provide: {
|
||||
openmct
|
||||
},
|
||||
template: '<filters-view></filters-view>'
|
||||
});
|
||||
},
|
||||
|
||||
@@ -65,11 +65,11 @@ import ToggleSwitch from '../../../ui/components/ToggleSwitch.vue';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
|
||||
export default {
|
||||
inject: ['openmct'],
|
||||
components: {
|
||||
FilterField,
|
||||
ToggleSwitch
|
||||
},
|
||||
inject: ['openmct'],
|
||||
props: {
|
||||
filterObject: {
|
||||
type: Object,
|
||||
|
||||
@@ -41,10 +41,10 @@
|
||||
import FilterField from './FilterField.vue';
|
||||
|
||||
export default {
|
||||
inject: ['openmct'],
|
||||
components: {
|
||||
FilterField
|
||||
},
|
||||
inject: ['openmct'],
|
||||
props: {
|
||||
globalMetadata: {
|
||||
type: Object,
|
||||
|
||||
@@ -87,12 +87,12 @@ import DropHint from './dropHint.vue';
|
||||
const MIN_FRAME_SIZE = 5;
|
||||
|
||||
export default {
|
||||
inject: ['openmct'],
|
||||
components: {
|
||||
FrameComponent,
|
||||
ResizeHandle,
|
||||
DropHint
|
||||
},
|
||||
inject: ['openmct'],
|
||||
props: {
|
||||
container: {
|
||||
type: Object,
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
></div>
|
||||
|
||||
<div
|
||||
v-if="areAllContainersEmpty()"
|
||||
v-if="allContainersAreEmpty"
|
||||
class="c-fl__empty"
|
||||
>
|
||||
<span class="c-fl__empty-message">This Flexible Layout is currently empty</span>
|
||||
@@ -94,7 +94,6 @@ import Container from '../utils/container';
|
||||
import Frame from '../utils/frame';
|
||||
import ResizeHandle from './resizeHandle.vue';
|
||||
import DropHint from './dropHint.vue';
|
||||
import RemoveAction from '../../remove/RemoveAction.js';
|
||||
|
||||
const MIN_CONTAINER_SIZE = 5;
|
||||
|
||||
@@ -140,19 +139,20 @@ function sizeToFill(items) {
|
||||
}
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'objectPath', 'layoutObject'],
|
||||
components: {
|
||||
ContainerComponent,
|
||||
ResizeHandle,
|
||||
DropHint
|
||||
},
|
||||
inject: ['openmct', 'objectPath', 'layoutObject'],
|
||||
props: {
|
||||
isEditing: Boolean
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
domainObject: this.layoutObject,
|
||||
newFrameLocation: []
|
||||
newFrameLocation: [],
|
||||
identifierMap: {}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -168,26 +168,30 @@ export default {
|
||||
},
|
||||
rowsLayout() {
|
||||
return this.domainObject.configuration.rowsLayout;
|
||||
},
|
||||
allContainersAreEmpty() {
|
||||
return this.containers.every(container => container.frames.length === 0);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.buildIdentifierMap();
|
||||
this.composition = this.openmct.composition.get(this.domainObject);
|
||||
this.composition.on('remove', this.removeChildObject);
|
||||
this.composition.on('add', this.addFrame);
|
||||
|
||||
this.RemoveAction = new RemoveAction(this.openmct);
|
||||
|
||||
this.unobserve = this.openmct.objects.observe(this.domainObject, '*', this.updateDomainObject);
|
||||
this.composition.load();
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.composition.off('remove', this.removeChildObject);
|
||||
this.composition.off('add', this.addFrame);
|
||||
|
||||
this.unobserve();
|
||||
},
|
||||
methods: {
|
||||
areAllContainersEmpty() {
|
||||
return !this.containers.filter(container => container.frames.length).length;
|
||||
buildIdentifierMap() {
|
||||
this.containers.forEach(container => {
|
||||
container.frames.forEach(frame => {
|
||||
let keystring = this.openmct.objects.makeKeyString(frame.domainObjectIdentifier);
|
||||
this.identifierMap[keystring] = true;
|
||||
});
|
||||
});
|
||||
},
|
||||
addContainer() {
|
||||
let container = new Container();
|
||||
@@ -236,16 +240,21 @@ export default {
|
||||
this.newFrameLocation = [containerIndex, insertFrameIndex];
|
||||
},
|
||||
addFrame(domainObject) {
|
||||
let containerIndex = this.newFrameLocation.length ? this.newFrameLocation[0] : 0;
|
||||
let container = this.containers[containerIndex];
|
||||
let frameIndex = this.newFrameLocation.length ? this.newFrameLocation[1] : container.frames.length;
|
||||
let frame = new Frame(domainObject.identifier);
|
||||
let keystring = this.openmct.objects.makeKeyString(domainObject.identifier);
|
||||
|
||||
container.frames.splice(frameIndex + 1, 0, frame);
|
||||
sizeItems(container.frames, frame);
|
||||
if (!this.identifierMap[keystring]) {
|
||||
let containerIndex = this.newFrameLocation.length ? this.newFrameLocation[0] : 0;
|
||||
let container = this.containers[containerIndex];
|
||||
let frameIndex = this.newFrameLocation.length ? this.newFrameLocation[1] : container.frames.length;
|
||||
let frame = new Frame(domainObject.identifier);
|
||||
|
||||
this.newFrameLocation = [];
|
||||
this.persist(containerIndex);
|
||||
container.frames.splice(frameIndex + 1, 0, frame);
|
||||
sizeItems(container.frames, frame);
|
||||
|
||||
this.newFrameLocation = [];
|
||||
this.persist(containerIndex);
|
||||
this.identifierMap[keystring] = true;
|
||||
}
|
||||
},
|
||||
deleteFrame(frameId) {
|
||||
let container = this.containers
|
||||
@@ -254,16 +263,20 @@ export default {
|
||||
.frames
|
||||
.filter((f => f.id === frameId))[0];
|
||||
|
||||
this.removeFromComposition(frame.domainObjectIdentifier)
|
||||
.then(() => {
|
||||
sizeToFill(container.frames);
|
||||
this.setSelectionToParent();
|
||||
});
|
||||
this.removeFromComposition(frame.domainObjectIdentifier);
|
||||
|
||||
this.$nextTick().then(() => {
|
||||
sizeToFill(container.frames);
|
||||
this.setSelectionToParent();
|
||||
});
|
||||
},
|
||||
removeFromComposition(identifier) {
|
||||
return this.openmct.objects.get(identifier).then((childDomainObject) => {
|
||||
this.RemoveAction.removeFromComposition(this.domainObject, childDomainObject);
|
||||
});
|
||||
let keystring = this.openmct.objects.makeKeyString(identifier);
|
||||
|
||||
this.identifierMap[keystring] = undefined;
|
||||
delete this.identifierMap[keystring];
|
||||
|
||||
this.composition.remove({identifier});
|
||||
},
|
||||
setSelectionToParent() {
|
||||
this.$el.click();
|
||||
|
||||
@@ -58,10 +58,10 @@
|
||||
import ObjectFrame from '../../../ui/components/ObjectFrame.vue';
|
||||
|
||||
export default {
|
||||
inject: ['openmct'],
|
||||
components: {
|
||||
ObjectFrame
|
||||
},
|
||||
inject: ['openmct'],
|
||||
props: {
|
||||
frame: {
|
||||
type: Object,
|
||||
|
||||
@@ -44,15 +44,15 @@ define([
|
||||
return {
|
||||
show: function (element, isEditing) {
|
||||
component = new Vue({
|
||||
el: element,
|
||||
components: {
|
||||
FlexibleLayoutComponent: FlexibleLayoutComponent.default
|
||||
},
|
||||
provide: {
|
||||
openmct,
|
||||
objectPath,
|
||||
layoutObject: domainObject
|
||||
},
|
||||
el: element,
|
||||
components: {
|
||||
FlexibleLayoutComponent: FlexibleLayoutComponent.default
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isEditing: isEditing
|
||||
|
||||
73
src/plugins/goToOriginalAction/pluginSpec.js
Normal file
73
src/plugins/goToOriginalAction/pluginSpec.js
Normal file
@@ -0,0 +1,73 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import {
|
||||
createOpenMct,
|
||||
resetApplicationState
|
||||
} from 'utils/testing';
|
||||
|
||||
describe("the plugin", () => {
|
||||
let openmct;
|
||||
let goToFolderAction;
|
||||
let mockObjectPath;
|
||||
|
||||
beforeEach((done) => {
|
||||
openmct = createOpenMct();
|
||||
|
||||
openmct.on('start', done);
|
||||
openmct.startHeadless();
|
||||
|
||||
goToFolderAction = openmct.actions._allActions.goToOriginal;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
return resetApplicationState(openmct);
|
||||
});
|
||||
|
||||
it('installs the go to folder action', () => {
|
||||
expect(goToFolderAction).toBeDefined();
|
||||
});
|
||||
|
||||
describe('when invoked', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
mockObjectPath = [{
|
||||
name: 'mock folder',
|
||||
type: 'folder',
|
||||
identifier: {
|
||||
key: 'mock-folder',
|
||||
namespace: ''
|
||||
}
|
||||
}];
|
||||
spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({
|
||||
identifier: {
|
||||
namespace: '',
|
||||
key: 'test'
|
||||
}
|
||||
}));
|
||||
goToFolderAction.invoke(mockObjectPath);
|
||||
});
|
||||
|
||||
it('goes to the original location', () => {
|
||||
expect(window.location.href).toContain('context.html#/browse/?tc.mode=fixed&tc.startBound=0&tc.endBound=1&tc.timeSystem=utc');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,25 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import ImageryViewLayout from './components/ImageryViewLayout.vue';
|
||||
import Vue from 'vue';
|
||||
|
||||
|
||||
127
src/plugins/imagery/components/Compass/Compass.vue
Normal file
127
src/plugins/imagery/components/Compass/Compass.vue
Normal file
@@ -0,0 +1,127 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="c-compass"
|
||||
:style="compassDimensionsStyle"
|
||||
>
|
||||
<CompassHUD
|
||||
v-if="shouldDisplayCompassHUD"
|
||||
:heading="heading"
|
||||
:roll="roll"
|
||||
:sun-heading="sunHeading"
|
||||
:camera-field-of-view="cameraFieldOfView"
|
||||
:camera-pan="cameraPan"
|
||||
/>
|
||||
<CompassRose
|
||||
v-if="shouldDisplayCompassRose"
|
||||
:heading="heading"
|
||||
:sun-heading="sunHeading"
|
||||
:camera-field-of-view="cameraFieldOfView"
|
||||
:camera-pan="cameraPan"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CompassHUD from './CompassHUD.vue';
|
||||
import CompassRose from './CompassRose.vue';
|
||||
|
||||
const CAM_FIELD_OF_VIEW = 70;
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CompassHUD,
|
||||
CompassRose
|
||||
},
|
||||
props: {
|
||||
containerWidth: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
containerHeight: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
naturalAspectRatio: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
image: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
shouldDisplayCompassRose() {
|
||||
return this.heading !== undefined;
|
||||
},
|
||||
shouldDisplayCompassHUD() {
|
||||
return this.heading !== undefined;
|
||||
},
|
||||
// degrees from north heading
|
||||
heading() {
|
||||
return this.image.heading;
|
||||
},
|
||||
roll() {
|
||||
return this.image.roll;
|
||||
},
|
||||
pitch() {
|
||||
return this.image.pitch;
|
||||
},
|
||||
// degrees from north heading
|
||||
sunHeading() {
|
||||
return this.image.sunOrientation;
|
||||
},
|
||||
// degrees from spacecraft heading
|
||||
cameraPan() {
|
||||
return this.image.cameraPan;
|
||||
},
|
||||
cameraTilt() {
|
||||
return this.image.cameraTilt;
|
||||
},
|
||||
cameraFieldOfView() {
|
||||
return CAM_FIELD_OF_VIEW;
|
||||
},
|
||||
compassDimensionsStyle() {
|
||||
const containerAspectRatio = this.containerWidth / this.containerHeight;
|
||||
|
||||
let width;
|
||||
let height;
|
||||
|
||||
if (containerAspectRatio < this.naturalAspectRatio) {
|
||||
width = '100%';
|
||||
height = `${ this.containerWidth / this.naturalAspectRatio }px`;
|
||||
} else {
|
||||
width = `${ this.containerHeight * this.naturalAspectRatio }px`;
|
||||
height = '100%';
|
||||
}
|
||||
|
||||
return {
|
||||
width: width,
|
||||
height: height
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
171
src/plugins/imagery/components/Compass/CompassHUD.vue
Normal file
171
src/plugins/imagery/components/Compass/CompassHUD.vue
Normal file
@@ -0,0 +1,171 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="c-compass__hud c-hud"
|
||||
:style="skewCompassHUDStyle"
|
||||
>
|
||||
<div
|
||||
v-for="point in visibleCompassPoints"
|
||||
:key="point.direction"
|
||||
:class="point.class"
|
||||
:style="point.style"
|
||||
>
|
||||
{{ point.direction }}
|
||||
</div>
|
||||
<div
|
||||
v-if="isSunInRange"
|
||||
ref="sun"
|
||||
class="c-hud__sun"
|
||||
:style="sunPositionStyle"
|
||||
></div>
|
||||
<div class="c-hud__range"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
normalizeDegrees,
|
||||
inRange,
|
||||
percentOfRange
|
||||
} from './utils';
|
||||
|
||||
const COMPASS_POINTS = [
|
||||
{
|
||||
direction: 'N',
|
||||
class: 'c-hud__dir',
|
||||
degrees: 0
|
||||
},
|
||||
{
|
||||
direction: 'NE',
|
||||
class: 'c-hud__dir--sub',
|
||||
degrees: 45
|
||||
},
|
||||
{
|
||||
direction: 'E',
|
||||
class: 'c-hud__dir',
|
||||
degrees: 90
|
||||
},
|
||||
{
|
||||
direction: 'SE',
|
||||
class: 'c-hud__dir--sub',
|
||||
degrees: 135
|
||||
},
|
||||
{
|
||||
direction: 'S',
|
||||
class: 'c-hud__dir',
|
||||
degrees: 180
|
||||
},
|
||||
{
|
||||
direction: 'SW',
|
||||
class: 'c-hud__dir--sub',
|
||||
degrees: 225
|
||||
},
|
||||
{
|
||||
direction: 'W',
|
||||
class: 'c-hud__dir',
|
||||
degrees: 270
|
||||
},
|
||||
{
|
||||
direction: 'NW',
|
||||
class: 'c-hud__dir--sub',
|
||||
degrees: 315
|
||||
}
|
||||
];
|
||||
|
||||
export default {
|
||||
props: {
|
||||
heading: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
roll: {
|
||||
type: Number,
|
||||
default: undefined
|
||||
},
|
||||
sunHeading: {
|
||||
type: Number,
|
||||
default: undefined
|
||||
},
|
||||
cameraFieldOfView: {
|
||||
type: Number,
|
||||
default: undefined
|
||||
},
|
||||
cameraPan: {
|
||||
type: Number,
|
||||
default: undefined
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
skewCompassHUDStyle() {
|
||||
if (this.roll === undefined || this.roll === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const origin = this.roll > 0 ? 'left bottom' : 'right top';
|
||||
|
||||
return {
|
||||
'transform-origin': origin,
|
||||
transform: `skew(0, ${ this.roll }deg`
|
||||
};
|
||||
},
|
||||
visibleCompassPoints() {
|
||||
return COMPASS_POINTS
|
||||
.filter(point => inRange(point.degrees, this.visibleRange))
|
||||
.map(point => {
|
||||
const percentage = percentOfRange(point.degrees, this.visibleRange);
|
||||
point.style = Object.assign(
|
||||
{ left: `${ percentage * 100 }%` }
|
||||
);
|
||||
|
||||
return point;
|
||||
});
|
||||
},
|
||||
isSunInRange() {
|
||||
return inRange(this.normalizedSunHeading, this.visibleRange);
|
||||
},
|
||||
sunPositionStyle() {
|
||||
const percentage = percentOfRange(this.normalizedSunHeading, this.visibleRange);
|
||||
|
||||
return {
|
||||
left: `${ percentage * 100 }%`
|
||||
};
|
||||
},
|
||||
normalizedSunHeading() {
|
||||
return normalizeDegrees(this.sunHeading);
|
||||
},
|
||||
normalizedHeading() {
|
||||
return normalizeDegrees(this.heading);
|
||||
},
|
||||
visibleRange() {
|
||||
const min = normalizeDegrees(this.normalizedHeading + this.cameraPan - this.cameraFieldOfView / 2);
|
||||
const max = normalizeDegrees(this.normalizedHeading + this.cameraPan + this.cameraFieldOfView / 2);
|
||||
|
||||
return [
|
||||
min,
|
||||
max
|
||||
];
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
262
src/plugins/imagery/components/Compass/CompassRose.vue
Normal file
262
src/plugins/imagery/components/Compass/CompassRose.vue
Normal file
@@ -0,0 +1,262 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="c-direction-rose"
|
||||
@click="toggleBezelLock"
|
||||
>
|
||||
<div
|
||||
class="c-nsew"
|
||||
:style="rotateFrameStyle"
|
||||
>
|
||||
<svg
|
||||
class="c-nsew__minor-ticks"
|
||||
viewBox="0 0 100 100"
|
||||
>
|
||||
<rect
|
||||
class="c-nsew__tick c-tick-ne"
|
||||
x="49"
|
||||
y="0"
|
||||
width="2"
|
||||
height="5"
|
||||
/>
|
||||
<rect
|
||||
class="c-nsew__tick c-tick-se"
|
||||
x="95"
|
||||
y="49"
|
||||
width="5"
|
||||
height="2"
|
||||
/>
|
||||
<rect
|
||||
class="c-nsew__tick c-tick-sw"
|
||||
x="49"
|
||||
y="95"
|
||||
width="2"
|
||||
height="5"
|
||||
/>
|
||||
<rect
|
||||
class="c-nsew__tick c-tick-nw"
|
||||
x="0"
|
||||
y="49"
|
||||
width="5"
|
||||
height="2"
|
||||
/>
|
||||
|
||||
</svg>
|
||||
|
||||
<svg
|
||||
class="c-nsew__ticks"
|
||||
viewBox="0 0 100 100"
|
||||
>
|
||||
<polygon
|
||||
class="c-nsew__tick c-tick-n"
|
||||
points="50,0 57,5 43,5"
|
||||
/>
|
||||
<rect
|
||||
class="c-nsew__tick c-tick-e"
|
||||
x="95"
|
||||
y="49"
|
||||
width="5"
|
||||
height="2"
|
||||
/>
|
||||
<rect
|
||||
class="c-nsew__tick c-tick-w"
|
||||
x="0"
|
||||
y="49"
|
||||
width="5"
|
||||
height="2"
|
||||
/>
|
||||
<rect
|
||||
class="c-nsew__tick c-tick-s"
|
||||
x="49"
|
||||
y="95"
|
||||
width="2"
|
||||
height="5"
|
||||
/>
|
||||
|
||||
<text
|
||||
class="c-nsew__label c-label-n"
|
||||
text-anchor="middle"
|
||||
:transform="northTextTransform"
|
||||
>N</text>
|
||||
<text
|
||||
class="c-nsew__label c-label-e"
|
||||
text-anchor="middle"
|
||||
:transform="eastTextTransform"
|
||||
>E</text>
|
||||
<text
|
||||
class="c-nsew__label c-label-w"
|
||||
text-anchor="middle"
|
||||
:transform="southTextTransform"
|
||||
>W</text>
|
||||
<text
|
||||
class="c-nsew__label c-label-s"
|
||||
text-anchor="middle"
|
||||
:transform="westTextTransform"
|
||||
>S</text>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="c-spacecraft-body"
|
||||
:style="headingStyle"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="c-sun"
|
||||
:style="sunHeadingStyle"
|
||||
></div>
|
||||
|
||||
<div
|
||||
v-if="showCameraFOV"
|
||||
class="c-cam-field"
|
||||
:style="cameraFOVHeadingStyle"
|
||||
>
|
||||
<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>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { normalizeDegrees } from './utils';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
heading: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
sunHeading: {
|
||||
type: Number,
|
||||
default: undefined
|
||||
},
|
||||
cameraFieldOfView: {
|
||||
type: Number,
|
||||
default: undefined
|
||||
},
|
||||
cameraPan: {
|
||||
type: Number,
|
||||
default: undefined
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
lockBezel: true
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
compassHeading() {
|
||||
return this.lockBezel ? normalizeDegrees(this.heading) : 0;
|
||||
},
|
||||
north() {
|
||||
return normalizeDegrees(this.compassHeading - this.heading);
|
||||
},
|
||||
rotateFrameStyle() {
|
||||
return { transform: `rotate(${ this.north }deg)` };
|
||||
},
|
||||
northTextTransform() {
|
||||
return this.cardinalPointsTextTransform.north;
|
||||
},
|
||||
eastTextTransform() {
|
||||
return this.cardinalPointsTextTransform.east;
|
||||
},
|
||||
southTextTransform() {
|
||||
return this.cardinalPointsTextTransform.south;
|
||||
},
|
||||
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,15) ${ rotation }`,
|
||||
east: `translate(87,50) ${ rotation }`,
|
||||
south: `translate(13,50) ${ rotation }`,
|
||||
west: `translate(50,87) ${ rotation }`
|
||||
};
|
||||
},
|
||||
headingStyle() {
|
||||
return {
|
||||
transform: `translateX(-50%) rotate(${ this.compassHeading }deg)`
|
||||
};
|
||||
},
|
||||
cameraFOVHeading() {
|
||||
return this.compassHeading + this.cameraPan;
|
||||
},
|
||||
cameraFOVHeadingStyle() {
|
||||
return {
|
||||
transform: `rotate(${ this.cameraFOVHeading }deg)`
|
||||
};
|
||||
},
|
||||
sunHeadingStyle() {
|
||||
const rotation = normalizeDegrees(this.north + this.sunHeading);
|
||||
|
||||
return {
|
||||
transform: `rotate(${ rotation }deg)`
|
||||
};
|
||||
},
|
||||
showCameraFOV() {
|
||||
return this.cameraPan !== undefined && this.cameraFieldOfView > 0;
|
||||
},
|
||||
// left half of camera field of view
|
||||
// rotated counter-clockwise from camera field of view heading
|
||||
cameraFOVStyleLeftHalf() {
|
||||
return {
|
||||
transform: `translateX(50%) rotate(${ -this.cameraFieldOfView / 2 }deg)`
|
||||
};
|
||||
},
|
||||
// right half of camera field of view
|
||||
// rotated clockwise from camera field of view heading
|
||||
cameraFOVStyleRightHalf() {
|
||||
return {
|
||||
transform: `translateX(-50%) rotate(${ this.cameraFieldOfView / 2 }deg)`
|
||||
};
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleBezelLock() {
|
||||
this.lockBezel = !this.lockBezel;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
216
src/plugins/imagery/components/Compass/compass.scss
Normal file
216
src/plugins/imagery/components/Compass/compass.scss
Normal file
@@ -0,0 +1,216 @@
|
||||
/***************************** THEME/UI CONSTANTS AND MIXINS */
|
||||
$interfaceKeyColor: #00B9C5;
|
||||
$elemBg: rgba(black, 0.7);
|
||||
|
||||
@mixin sun($position: 'circle closest-side') {
|
||||
$color: #ff9900;
|
||||
$gradEdgePerc: 60%;
|
||||
background: radial-gradient(#{$position}, $color, $color $gradEdgePerc, rgba($color, 0.4) $gradEdgePerc + 5%, transparent);
|
||||
|
||||
}
|
||||
|
||||
.c-compass {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 1;
|
||||
@include userSelectNone;
|
||||
}
|
||||
|
||||
/***************************** COMPASS HUD */
|
||||
.c-hud {
|
||||
// To be placed within a imagery view, in the bounding box of the image
|
||||
$m: 1px;
|
||||
$padTB: 2px;
|
||||
$padLR: $padTB;
|
||||
background: $elemBg;
|
||||
border-radius: 3px;
|
||||
color: $interfaceKeyColor;
|
||||
font-size: 0.8em;
|
||||
position: absolute;
|
||||
top: $m; right: $m; left: $m;
|
||||
height: 18px;
|
||||
|
||||
svg, div {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
&__display {
|
||||
height: 30px;
|
||||
pointer-events: all;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
&__range {
|
||||
border: 1px solid $interfaceKeyColor;
|
||||
border-top-color: transparent;
|
||||
position: absolute;
|
||||
top: 50%; right: $padLR; bottom: $padTB; left: $padLR;
|
||||
}
|
||||
|
||||
[class*="__dir"] {
|
||||
// NSEW
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
text-shadow: black 0 0 3px;
|
||||
top: 50%;
|
||||
transform: translate(-50%,-50%);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
[class*="__dir--sub"] {
|
||||
font-weight: normal;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&__sun {
|
||||
$s: 10px;
|
||||
@include sun('circle farthest-side at bottom');
|
||||
bottom: $padTB + 2px;
|
||||
height: $s; width: $s*2;
|
||||
opacity: 0.8;
|
||||
transform: translateX(-50%);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/***************************** COMPASS DIRECTIONS */
|
||||
.c-nsew {
|
||||
$color: $interfaceKeyColor;
|
||||
$inset: 7%;
|
||||
$tickHeightPerc: 15%;
|
||||
text-shadow: black 0 0 10px;
|
||||
top: $inset; right: $inset; bottom: $inset; left: $inset;
|
||||
z-index: 3;
|
||||
|
||||
&__tick,
|
||||
&__label {
|
||||
fill: $color;
|
||||
}
|
||||
|
||||
&__minor-ticks {
|
||||
opacity: 0.5;
|
||||
transform-origin: center;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
&__label {
|
||||
dominant-baseline: central;
|
||||
font-size: 0.8em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.c-label-n {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
}
|
||||
|
||||
/***************************** CAMERA FIELD ANGLE */
|
||||
.c-cam-field {
|
||||
$color: white;
|
||||
opacity: 0.2;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
|
||||
.cam-field-half {
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
|
||||
.cam-field-area {
|
||||
background: $color;
|
||||
top: -30%;
|
||||
right: 0;
|
||||
bottom: -30%;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
// clip-paths overlap a bit to avoid a gap between halves
|
||||
&-l {
|
||||
clip-path: polygon(0 0, 50.5% 0, 50.5% 100%, 0 100%);
|
||||
.cam-field-area {
|
||||
transform-origin: left center;
|
||||
}
|
||||
}
|
||||
|
||||
&-r {
|
||||
clip-path: polygon(49.5% 0, 100% 0, 100% 100%, 49.5% 100%);
|
||||
.cam-field-area {
|
||||
transform-origin: right center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/***************************** SPACECRAFT BODY */
|
||||
.c-spacecraft-body {
|
||||
$color: $interfaceKeyColor;
|
||||
$s: 30%;
|
||||
background: $color;
|
||||
border-radius: 3px;
|
||||
height: $s; width: $s;
|
||||
left: 50%; top: 50%;
|
||||
opacity: 0.4;
|
||||
transform-origin: center top;
|
||||
|
||||
&:before {
|
||||
// Direction arrow
|
||||
$color: rgba(black, 0.5);
|
||||
$arwPointerY: 60%;
|
||||
$arwBodyOffset: 25%;
|
||||
background: $color;
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 10%; right: 20%; bottom: 50%; left: 20%;
|
||||
clip-path: polygon(50% 0, 100% $arwPointerY, 100%-$arwBodyOffset $arwPointerY, 100%-$arwBodyOffset 100%, $arwBodyOffset 100%, $arwBodyOffset $arwPointerY, 0 $arwPointerY);
|
||||
}
|
||||
}
|
||||
|
||||
/***************************** DIRECTION ROSE */
|
||||
.c-direction-rose {
|
||||
$d: 100px;
|
||||
$c2: rgba(white, 0.1);
|
||||
background: $elemBg;
|
||||
background-image: radial-gradient(circle closest-side, transparent, transparent 80%, $c2);
|
||||
width: $d;
|
||||
height: $d;
|
||||
transform-origin: 0 0;
|
||||
position: absolute;
|
||||
bottom: 10px; left: 10px;
|
||||
clip-path: circle(50% at 50% 50%);
|
||||
border-radius: 100%;
|
||||
|
||||
svg, div {
|
||||
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%);
|
||||
}
|
||||
}
|
||||
}
|
||||
44
src/plugins/imagery/components/Compass/utils.js
Normal file
44
src/plugins/imagery/components/Compass/utils.js
Normal file
@@ -0,0 +1,44 @@
|
||||
export function normalizeDegrees(degrees) {
|
||||
const base = degrees % 360;
|
||||
|
||||
return base >= 0 ? base : 360 + base;
|
||||
}
|
||||
|
||||
export function inRange(degrees, [min, max]) {
|
||||
return min > max
|
||||
? (degrees >= min && degrees < 360) || (degrees <= max && degrees >= 0)
|
||||
: degrees >= min && degrees <= max;
|
||||
}
|
||||
|
||||
export function percentOfRange(degrees, [min, max]) {
|
||||
let distance = degrees;
|
||||
let minRange = min;
|
||||
let maxRange = max;
|
||||
|
||||
if (min > max) {
|
||||
if (distance < max) {
|
||||
distance += 360;
|
||||
}
|
||||
|
||||
maxRange += 360;
|
||||
}
|
||||
|
||||
return (distance - minRange) / (maxRange - minRange);
|
||||
}
|
||||
|
||||
export function normalizeSemiCircleDegrees(rawDegrees) {
|
||||
// in case tony hawk is providing us degrees
|
||||
let degrees = rawDegrees % 360;
|
||||
|
||||
// westward degrees are between 0 and -180 exclusively
|
||||
if (degrees > 180) {
|
||||
degrees = degrees - 360;
|
||||
}
|
||||
|
||||
// eastward degrees are between 0 and 180 inclusively
|
||||
if (degrees <= -180) {
|
||||
degrees = 360 - degrees;
|
||||
}
|
||||
|
||||
return degrees;
|
||||
}
|
||||
@@ -1,3 +1,25 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
<template>
|
||||
<div
|
||||
tabindex="0"
|
||||
@@ -36,14 +58,23 @@
|
||||
<div class="c-imagery__main-image__bg"
|
||||
:class="{'paused unnsynced': isPaused,'stale':false }"
|
||||
>
|
||||
<div class="c-imagery__main-image__image js-imageryView-image"
|
||||
:style="{
|
||||
'background-image': imageUrl ? `url(${imageUrl})` : 'none',
|
||||
'filter': `brightness(${filters.brightness}%) contrast(${filters.contrast}%)`
|
||||
}"
|
||||
:data-openmct-image-timestamp="time"
|
||||
:data-openmct-object-keystring="keyString"
|
||||
></div>
|
||||
<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"
|
||||
:container-width="imageContainerWidth"
|
||||
:container-height="imageContainerHeight"
|
||||
:natural-aspect-ratio="focusedImageNaturalAspectRatio"
|
||||
:image="focusedImage"
|
||||
/>
|
||||
</div>
|
||||
<div class="c-local-controls c-local-controls--show-on-hover c-imagery__prev-next-buttons">
|
||||
<button class="c-nav c-nav--prev"
|
||||
@@ -61,11 +92,25 @@
|
||||
<div class="c-imagery__control-bar">
|
||||
<div class="c-imagery__time">
|
||||
<div class="c-imagery__timestamp u-style-receiver js-style-receiver">{{ time }}</div>
|
||||
|
||||
<!-- image fresh -->
|
||||
<div
|
||||
v-if="canTrackDuration"
|
||||
:class="{'c-imagery--new': isImageNew && !refreshCSS}"
|
||||
class="c-imagery__age icon-timer"
|
||||
>{{ formattedDuration }}</div>
|
||||
|
||||
<!-- spacecraft position fresh -->
|
||||
<div
|
||||
v-if="relatedTelemetry.hasRelatedTelemetry && isSpacecraftPositionFresh"
|
||||
class="c-imagery__age icon-check c-imagery--new"
|
||||
>POS</div>
|
||||
|
||||
<!-- camera position fresh -->
|
||||
<div
|
||||
v-if="relatedTelemetry.hasRelatedTelemetry && isCameraPositionFresh"
|
||||
class="c-imagery__age icon-check c-imagery--new"
|
||||
>CAM</div>
|
||||
</div>
|
||||
<div class="h-local-controls">
|
||||
<button
|
||||
@@ -76,13 +121,14 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div ref="thumbsWrapper"
|
||||
class="c-imagery__thumbs-wrapper"
|
||||
:class="{'is-paused': isPaused}"
|
||||
@scroll="handleScroll"
|
||||
<div
|
||||
ref="thumbsWrapper"
|
||||
class="c-imagery__thumbs-wrapper"
|
||||
:class="{'is-paused': isPaused}"
|
||||
@scroll="handleScroll"
|
||||
>
|
||||
<div v-for="(datum, index) in imageHistory"
|
||||
:key="datum.url"
|
||||
:key="datum.url + datum[timeKey]"
|
||||
class="c-imagery__thumb c-thumb"
|
||||
:class="{ selected: focusedImageIndex === index && isPaused }"
|
||||
@click="setFocusedImage(index, thumbnailClick)"
|
||||
@@ -97,7 +143,10 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
import Compass from './Compass/Compass.vue';
|
||||
import RelatedTelemetry from './RelatedTelemetry/RelatedTelemetry';
|
||||
|
||||
const DEFAULT_DURATION_FORMATTER = 'duration';
|
||||
const REFRESH_CSS_MS = 500;
|
||||
@@ -116,6 +165,9 @@ const ARROW_RIGHT = 39;
|
||||
const ARROW_LEFT = 37;
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Compass
|
||||
},
|
||||
inject: ['openmct', 'domainObject'],
|
||||
data() {
|
||||
let timeSystem = this.openmct.time.timeSystem();
|
||||
@@ -137,7 +189,14 @@ export default {
|
||||
refreshCSS: false,
|
||||
keyString: undefined,
|
||||
focusedImageIndex: undefined,
|
||||
numericDuration: undefined
|
||||
focusedImageRelatedTelemetry: {},
|
||||
numericDuration: undefined,
|
||||
metadataEndpoints: {},
|
||||
relatedTelemetry: {},
|
||||
latestRelatedTelemetry: {},
|
||||
focusedImageNaturalAspectRatio: undefined,
|
||||
imageContainerWidth: undefined,
|
||||
imageContainerHeight: undefined
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -195,15 +254,69 @@ export default {
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
shouldDisplayCompass() {
|
||||
return this.focusedImage !== undefined
|
||||
&& this.focusedImageNaturalAspectRatio !== undefined
|
||||
&& this.imageContainerWidth !== undefined
|
||||
&& this.imageContainerHeight !== undefined;
|
||||
},
|
||||
isSpacecraftPositionFresh() {
|
||||
let isFresh = undefined;
|
||||
let latest = this.latestRelatedTelemetry;
|
||||
let focused = this.focusedImageRelatedTelemetry;
|
||||
|
||||
if (this.relatedTelemetry.hasRelatedTelemetry) {
|
||||
isFresh = true;
|
||||
for (let key of this.spacecraftKeys) {
|
||||
if (this.relatedTelemetry[key] && latest[key] && focused[key]) {
|
||||
if (!this.relatedTelemetry[key].comparisonFunction(latest[key], focused[key])) {
|
||||
isFresh = false;
|
||||
}
|
||||
} else {
|
||||
isFresh = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return isFresh;
|
||||
},
|
||||
isCameraPositionFresh() {
|
||||
let isFresh = undefined;
|
||||
let latest = this.latestRelatedTelemetry;
|
||||
let focused = this.focusedImageRelatedTelemetry;
|
||||
|
||||
if (this.relatedTelemetry.hasRelatedTelemetry) {
|
||||
isFresh = true;
|
||||
|
||||
// camera freshness relies on spacecraft position freshness
|
||||
if (this.isSpacecraftPositionFresh) {
|
||||
for (let key of this.cameraKeys) {
|
||||
if (this.relatedTelemetry[key] && latest[key] && focused[key]) {
|
||||
if (!this.relatedTelemetry[key].comparisonFunction(latest[key], focused[key])) {
|
||||
isFresh = false;
|
||||
}
|
||||
} else {
|
||||
isFresh = false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
isFresh = false;
|
||||
}
|
||||
}
|
||||
|
||||
return isFresh;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
focusedImageIndex() {
|
||||
this.trackDuration();
|
||||
this.resetAgeCSS();
|
||||
this.updateRelatedTelemetryForFocusedImage();
|
||||
this.getImageNaturalDimensions();
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
async mounted() {
|
||||
// listen
|
||||
this.openmct.time.on('bounds', this.boundsChange);
|
||||
this.openmct.time.on('timeSystem', this.timeSystemChange);
|
||||
@@ -212,8 +325,14 @@ export default {
|
||||
// set
|
||||
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
|
||||
this.imageHints = { ...this.metadata.valuesForHints(['image'])[0] };
|
||||
this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
|
||||
this.imageFormatter = this.openmct.telemetry.getValueFormatter(this.metadata.valuesForHints(['image'])[0]);
|
||||
this.imageFormatter = this.openmct.telemetry.getValueFormatter(this.imageHints);
|
||||
|
||||
// related telemetry keys
|
||||
this.spacecraftKeys = ['heading', 'roll', 'pitch'];
|
||||
this.cameraKeys = ['cameraPan', 'cameraTilt'];
|
||||
this.sunKeys = ['sunOrientation'];
|
||||
|
||||
// initialize
|
||||
this.timeKey = this.timeSystem.key;
|
||||
@@ -222,6 +341,18 @@ export default {
|
||||
// kickoff
|
||||
this.subscribe();
|
||||
this.requestHistory();
|
||||
|
||||
// related telemetry
|
||||
await this.initializeRelatedTelemetry();
|
||||
this.updateRelatedTelemetryForFocusedImage();
|
||||
this.trackLatestRelatedTelemetry();
|
||||
|
||||
// for scrolling through images quickly and resizing the object view
|
||||
_.debounce(this.updateRelatedTelemetryForFocusedImage, 400);
|
||||
_.debounce(this.resizeImageContainer, 400);
|
||||
|
||||
this.imageContainerResizeObserver = new ResizeObserver(this.resizeImageContainer);
|
||||
this.imageContainerResizeObserver.observe(this.$refs.focusedImage);
|
||||
},
|
||||
updated() {
|
||||
this.scrollToRight();
|
||||
@@ -232,12 +363,115 @@ export default {
|
||||
delete this.unsubscribe;
|
||||
}
|
||||
|
||||
this.imageContainerResizeObserver.disconnect();
|
||||
|
||||
if (this.relatedTelemetry.hasRelatedTelemetry) {
|
||||
this.relatedTelemetry.destroy();
|
||||
}
|
||||
|
||||
this.stopDurationTracking();
|
||||
this.openmct.time.off('bounds', this.boundsChange);
|
||||
this.openmct.time.off('timeSystem', this.timeSystemChange);
|
||||
this.openmct.time.off('clock', this.clockChange);
|
||||
|
||||
// unsubscribe from related telemetry
|
||||
if (this.relatedTelemetry.hasRelatedTelemetry) {
|
||||
for (let key of this.relatedTelemetry.keys) {
|
||||
if (this.relatedTelemetry[key].unsubscribe) {
|
||||
this.relatedTelemetry[key].unsubscribe();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async initializeRelatedTelemetry() {
|
||||
this.relatedTelemetry = new RelatedTelemetry(
|
||||
this.openmct,
|
||||
this.domainObject,
|
||||
[...this.spacecraftKeys, ...this.cameraKeys, ...this.sunKeys]
|
||||
);
|
||||
|
||||
if (this.relatedTelemetry.hasRelatedTelemetry) {
|
||||
await this.relatedTelemetry.load();
|
||||
}
|
||||
},
|
||||
async getMostRecentRelatedTelemetry(key, targetDatum) {
|
||||
if (!this.relatedTelemetry.hasRelatedTelemetry) {
|
||||
throw new Error(`${this.domainObject.name} does not have any related telemetry`);
|
||||
}
|
||||
|
||||
if (!this.relatedTelemetry[key]) {
|
||||
throw new Error(`${key} does not exist on related telemetry`);
|
||||
}
|
||||
|
||||
let mostRecent;
|
||||
let valueKey = this.relatedTelemetry[key].historical.valueKey;
|
||||
let valuesOnTelemetry = this.relatedTelemetry[key].hasTelemetryOnDatum;
|
||||
|
||||
if (valuesOnTelemetry) {
|
||||
mostRecent = targetDatum[valueKey];
|
||||
|
||||
if (mostRecent) {
|
||||
return mostRecent;
|
||||
} else {
|
||||
console.warn(`Related Telemetry for ${key} does NOT exist on this telemetry datum as configuration implied.`);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
mostRecent = await this.relatedTelemetry[key].requestLatestFor(targetDatum);
|
||||
|
||||
return mostRecent[valueKey];
|
||||
},
|
||||
// will subscribe to data for this key if not already done
|
||||
subscribeToDataForKey(key) {
|
||||
if (this.relatedTelemetry[key].isSubscribed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.relatedTelemetry[key].realtimeDomainObject) {
|
||||
this.relatedTelemetry[key].unsubscribe = this.openmct.telemetry.subscribe(
|
||||
this.relatedTelemetry[key].realtimeDomainObject, datum => {
|
||||
this.relatedTelemetry[key].listeners.forEach(callback => {
|
||||
callback(datum);
|
||||
});
|
||||
|
||||
}
|
||||
);
|
||||
|
||||
this.relatedTelemetry[key].isSubscribed = true;
|
||||
}
|
||||
},
|
||||
async updateRelatedTelemetryForFocusedImage() {
|
||||
if (!this.relatedTelemetry.hasRelatedTelemetry || !this.focusedImage) {
|
||||
return;
|
||||
}
|
||||
|
||||
// set data ON image telemetry as well as in focusedImageRelatedTelemetry
|
||||
for (let key of this.relatedTelemetry.keys) {
|
||||
if (this.relatedTelemetry[key] && this.relatedTelemetry[key].historical) {
|
||||
let valuesOnTelemetry = this.relatedTelemetry[key].hasTelemetryOnDatum;
|
||||
let value = await this.getMostRecentRelatedTelemetry(key, this.focusedImage);
|
||||
|
||||
if (!valuesOnTelemetry) {
|
||||
this.$set(this.imageHistory[this.focusedImageIndex], key, value); // manually add to telemetry
|
||||
}
|
||||
|
||||
this.$set(this.focusedImageRelatedTelemetry, key, value);
|
||||
}
|
||||
}
|
||||
},
|
||||
trackLatestRelatedTelemetry() {
|
||||
[...this.spacecraftKeys, ...this.cameraKeys, ...this.sunKeys].forEach(key => {
|
||||
if (this.relatedTelemetry[key] && this.relatedTelemetry[key].subscribe) {
|
||||
this.relatedTelemetry[key].subscribe((datum) => {
|
||||
let valueKey = this.relatedTelemetry[key].realtime.valueKey;
|
||||
this.$set(this.latestRelatedTelemetry, key, datum[valueKey]);
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
focusElement() {
|
||||
this.$el.focus();
|
||||
},
|
||||
@@ -358,6 +592,7 @@ export default {
|
||||
this.requestCount++;
|
||||
const requestId = this.requestCount;
|
||||
this.imageHistory = [];
|
||||
|
||||
let data = await this.openmct.telemetry
|
||||
.request(this.domainObject, bounds) || [];
|
||||
|
||||
@@ -509,6 +744,25 @@ export default {
|
||||
},
|
||||
isLeftOrRightArrowKey(keyCode) {
|
||||
return [ARROW_RIGHT, ARROW_LEFT].includes(keyCode);
|
||||
},
|
||||
getImageNaturalDimensions() {
|
||||
this.focusedImageNaturalAspectRatio = undefined;
|
||||
|
||||
const img = this.$refs.focusedImage;
|
||||
|
||||
// TODO - should probably cache this
|
||||
img.addEventListener('load', () => {
|
||||
this.focusedImageNaturalAspectRatio = img.naturalWidth / img.naturalHeight;
|
||||
}, { once: true });
|
||||
},
|
||||
resizeImageContainer() {
|
||||
if (this.$refs.focusedImage.clientWidth !== this.imageContainerWidth) {
|
||||
this.imageContainerWidth = this.$refs.focusedImage.clientWidth;
|
||||
}
|
||||
|
||||
if (this.$refs.focusedImage.clientHeight !== this.imageContainerHeight) {
|
||||
this.imageContainerHeight = this.$refs.focusedImage.clientHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
function copyRelatedMetadata(metadata) {
|
||||
let compare = metadata.comparisonFunction;
|
||||
let copiedMetadata = JSON.parse(JSON.stringify(metadata));
|
||||
copiedMetadata.comparisonFunction = compare;
|
||||
|
||||
return copiedMetadata;
|
||||
}
|
||||
|
||||
export default class RelatedTelemetry {
|
||||
|
||||
constructor(openmct, domainObject, telemetryKeys) {
|
||||
this._openmct = openmct;
|
||||
this._domainObject = domainObject;
|
||||
|
||||
let metadata = this._openmct.telemetry.getMetadata(this._domainObject);
|
||||
let imageHints = metadata.valuesForHints(['image'])[0];
|
||||
|
||||
this.hasRelatedTelemetry = imageHints.relatedTelemetry !== undefined;
|
||||
|
||||
if (this.hasRelatedTelemetry) {
|
||||
this.keys = telemetryKeys;
|
||||
|
||||
this._timeFormatter = undefined;
|
||||
this._timeSystemChange(this._openmct.time.timeSystem());
|
||||
|
||||
// grab related telemetry metadata
|
||||
for (let key of this.keys) {
|
||||
if (imageHints.relatedTelemetry[key]) {
|
||||
this[key] = copyRelatedMetadata(imageHints.relatedTelemetry[key]);
|
||||
}
|
||||
}
|
||||
|
||||
this.load = this.load.bind(this);
|
||||
this._parseTime = this._parseTime.bind(this);
|
||||
this._timeSystemChange = this._timeSystemChange.bind(this);
|
||||
this.destroy = this.destroy.bind(this);
|
||||
|
||||
this._openmct.time.on('timeSystem', this._timeSystemChange);
|
||||
}
|
||||
}
|
||||
|
||||
async load() {
|
||||
if (!this.hasRelatedTelemetry) {
|
||||
throw new Error('This domain object does not have related telemetry, use "hasRelatedTelemetry" to check before loading.');
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
this.keys.map(async (key) => {
|
||||
if (this[key].historical) {
|
||||
await this._initializeHistorical(key);
|
||||
}
|
||||
|
||||
if (this[key].realtime && this[key].realtime.telemetryObjectId) {
|
||||
await this._intializeRealtime(key);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async _initializeHistorical(key) {
|
||||
if (this[key].historical.telemetryObjectId) {
|
||||
this[key].historicalDomainObject = await this._openmct.objects.get(this[key].historical.telemetryObjectId);
|
||||
|
||||
this[key].requestLatestFor = async (datum) => {
|
||||
const options = {
|
||||
start: this._openmct.time.bounds().start,
|
||||
end: this._parseTime(datum),
|
||||
strategy: 'latest'
|
||||
};
|
||||
let results = await this._openmct.telemetry
|
||||
.request(this[key].historicalDomainObject, options);
|
||||
|
||||
return results[results.length - 1];
|
||||
};
|
||||
} else {
|
||||
this[key].historical.hasTelemetryOnDatum = true;
|
||||
}
|
||||
}
|
||||
|
||||
async _intializeRealtime(key) {
|
||||
this[key].realtimeDomainObject = await this._openmct.objects.get(this[key].realtime.telemetryObjectId);
|
||||
this[key].listeners = [];
|
||||
this[key].subscribe = (callback) => {
|
||||
|
||||
if (!this[key].isSubscribed) {
|
||||
this._subscribeToDataForKey(key);
|
||||
}
|
||||
|
||||
if (!this[key].listeners.includes(callback)) {
|
||||
this[key].listeners.push(callback);
|
||||
|
||||
return () => {
|
||||
this[key].listeners.remove(callback);
|
||||
};
|
||||
} else {
|
||||
return () => {};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
_subscribeToDataForKey(key) {
|
||||
if (this[key].isSubscribed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this[key].realtimeDomainObject) {
|
||||
this[key].unsubscribe = this._openmct.telemetry.subscribe(
|
||||
this[key].realtimeDomainObject, datum => {
|
||||
this[key].listeners.forEach(callback => {
|
||||
callback(datum);
|
||||
});
|
||||
|
||||
}
|
||||
);
|
||||
|
||||
this[key].isSubscribed = true;
|
||||
}
|
||||
}
|
||||
|
||||
_parseTime(datum) {
|
||||
return this._timeFormatter.parse(datum);
|
||||
}
|
||||
|
||||
_timeSystemChange(system) {
|
||||
let key = system.key;
|
||||
let metadata = this._openmct.telemetry.getMetadata(this._domainObject);
|
||||
let metadataValue = metadata.value(key) || { format: key };
|
||||
this._timeFormatter = this._openmct.telemetry.getValueFormatter(metadataValue);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this._openmct.time.off('timeSystem', this._timeSystemChange);
|
||||
for (let key of this.keys) {
|
||||
if (this[key].unsubscribe) {
|
||||
this[key].unsubscribe();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -23,6 +23,7 @@
|
||||
background-color: $colorPlotBg;
|
||||
border: 1px solid transparent;
|
||||
flex: 1 1 auto;
|
||||
height: 0;
|
||||
|
||||
&.unnsynced{
|
||||
@include sUnsynced();
|
||||
@@ -30,10 +31,9 @@
|
||||
}
|
||||
|
||||
&__image {
|
||||
@include abs(); // Safari fix
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,13 +71,14 @@
|
||||
}
|
||||
|
||||
&__age {
|
||||
border-radius: $controlCr;
|
||||
border-radius: $smallCr;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: baseline;
|
||||
padding: 1px $interiorMarginSm;
|
||||
align-items: center;
|
||||
padding: 2px $interiorMarginSm;
|
||||
|
||||
&:before {
|
||||
font-size: 0.9em;
|
||||
opacity: 0.5;
|
||||
margin-right: $interiorMarginSm;
|
||||
}
|
||||
@@ -86,8 +87,9 @@
|
||||
&--new {
|
||||
// New imagery
|
||||
$bgColor: $colorOk;
|
||||
color: $colorOkFg;
|
||||
background: rgba($bgColor, 0.5);
|
||||
@include flash($animName: flashImageAge, $dur: 250ms, $valStart: rgba($colorOk, 0.7), $valEnd: rgba($colorOk, 0));
|
||||
@include flash($animName: flashImageAge, $iter: 2, $dur: 250ms, $valStart: rgba($colorOk, 0.7), $valEnd: rgba($colorOk, 0));
|
||||
}
|
||||
|
||||
&__thumbs-wrapper {
|
||||
|
||||
@@ -1,3 +1,25 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import ImageryViewProvider from './ImageryViewProvider';
|
||||
|
||||
export default function () {
|
||||
|
||||
@@ -37,7 +37,7 @@ function getImageInfo(doc) {
|
||||
let imageElement = doc.querySelectorAll(MAIN_IMAGE_CLASS)[0];
|
||||
let timestamp = imageElement.dataset.openmctImageTimestamp;
|
||||
let identifier = imageElement.dataset.openmctObjectKeystring;
|
||||
let url = imageElement.style.backgroundImage;
|
||||
let url = imageElement.src;
|
||||
|
||||
return {
|
||||
timestamp,
|
||||
@@ -70,7 +70,7 @@ function generateTelemetry(start, count) {
|
||||
return telemetry;
|
||||
}
|
||||
|
||||
describe("The Imagery View Layout", () => {
|
||||
fdescribe("The Imagery View Layout", () => {
|
||||
const imageryKey = 'example.imagery';
|
||||
const START = Date.now();
|
||||
const COUNT = 10;
|
||||
|
||||
99
src/plugins/interceptors/pluginSpec.js
Normal file
99
src/plugins/interceptors/pluginSpec.js
Normal file
@@ -0,0 +1,99 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import { createOpenMct, resetApplicationState } from "utils/testing";
|
||||
import InterceptorPlugin from "./plugin";
|
||||
|
||||
describe('the plugin', function () {
|
||||
let element;
|
||||
let child;
|
||||
let openmct;
|
||||
const TEST_NAMESPACE = 'test';
|
||||
|
||||
beforeEach((done) => {
|
||||
const appHolder = document.createElement('div');
|
||||
appHolder.style.width = '640px';
|
||||
appHolder.style.height = '480px';
|
||||
|
||||
openmct = createOpenMct();
|
||||
openmct.install(new InterceptorPlugin(openmct));
|
||||
|
||||
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(appHolder);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
return resetApplicationState(openmct);
|
||||
});
|
||||
|
||||
describe('the missingObjectInterceptor', () => {
|
||||
let mockProvider;
|
||||
beforeEach(() => {
|
||||
mockProvider = jasmine.createSpyObj("mock provider", [
|
||||
"get"
|
||||
]);
|
||||
mockProvider.get.and.returnValue(Promise.resolve(undefined));
|
||||
openmct.objects.addProvider(TEST_NAMESPACE, mockProvider);
|
||||
});
|
||||
|
||||
it('returns missing objects', (done) => {
|
||||
const identifier = {
|
||||
namespace: TEST_NAMESPACE,
|
||||
key: 'hello'
|
||||
};
|
||||
openmct.objects.get(identifier).then((testObject) => {
|
||||
expect(testObject).toEqual({
|
||||
identifier,
|
||||
type: 'unknown',
|
||||
name: 'Missing: test:hello'
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the My items object if not found', (done) => {
|
||||
const identifier = {
|
||||
namespace: TEST_NAMESPACE,
|
||||
key: 'mine'
|
||||
};
|
||||
openmct.objects.get(identifier).then((testObject) => {
|
||||
expect(testObject).toEqual({
|
||||
identifier,
|
||||
"name": "My Items",
|
||||
"type": "folder",
|
||||
"composition": [],
|
||||
"location": "ROOT"
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
@@ -97,7 +97,8 @@
|
||||
:selected-page="getSelectedPage()"
|
||||
:selected-section="getSelectedSection()"
|
||||
:read-only="false"
|
||||
@updateEntries="updateEntries"
|
||||
@deleteEntry="deleteEntry"
|
||||
@updateEntry="updateEntry"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -111,19 +112,20 @@ import Search from '@/ui/components/search.vue';
|
||||
import SearchResults from './SearchResults.vue';
|
||||
import Sidebar from './Sidebar.vue';
|
||||
import { clearDefaultNotebook, getDefaultNotebook, setDefaultNotebook, setDefaultNotebookSection, setDefaultNotebookPage } from '../utils/notebook-storage';
|
||||
import { addNotebookEntry, createNewEmbed, getNotebookEntries, mutateObject } from '../utils/notebook-entries';
|
||||
import { addNotebookEntry, createNewEmbed, getEntryPosById, getNotebookEntries, mutateObject } from '../utils/notebook-entries';
|
||||
import objectUtils from 'objectUtils';
|
||||
|
||||
import { throttle } from 'lodash';
|
||||
import objectLink from '../../../ui/mixins/object-link';
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'domainObject', 'snapshotContainer'],
|
||||
components: {
|
||||
NotebookEntry,
|
||||
Search,
|
||||
SearchResults,
|
||||
Sidebar
|
||||
},
|
||||
inject: ['openmct', 'domainObject', 'snapshotContainer'],
|
||||
data() {
|
||||
return {
|
||||
defaultPageId: getDefaultNotebook() ? getDefaultNotebook().page.id : '',
|
||||
@@ -182,7 +184,9 @@ export default {
|
||||
mounted() {
|
||||
this.unlisten = this.openmct.objects.observe(this.internalDomainObject, '*', this.updateInternalDomainObject);
|
||||
this.formatSidebar();
|
||||
|
||||
window.addEventListener('orientationchange', this.formatSidebar);
|
||||
window.addEventListener("hashchange", this.navigateToSectionPage, false);
|
||||
|
||||
this.navigateToSectionPage();
|
||||
},
|
||||
@@ -190,6 +194,9 @@ export default {
|
||||
if (this.unlisten) {
|
||||
this.unlisten();
|
||||
}
|
||||
|
||||
window.removeEventListener('orientationchange', this.formatSidebar);
|
||||
window.removeEventListener("hashchange", this.navigateToSectionPage);
|
||||
},
|
||||
updated: function () {
|
||||
this.$nextTick(() => {
|
||||
@@ -225,17 +232,50 @@ export default {
|
||||
},
|
||||
createNotebookStorageObject() {
|
||||
const notebookMeta = {
|
||||
identifier: this.internalDomainObject.identifier
|
||||
name: this.internalDomainObject.name,
|
||||
identifier: this.internalDomainObject.identifier,
|
||||
link: this.getLinktoNotebook()
|
||||
};
|
||||
const page = this.getSelectedPage();
|
||||
const section = this.getSelectedSection();
|
||||
|
||||
return {
|
||||
notebookMeta,
|
||||
section,
|
||||
page
|
||||
page,
|
||||
section
|
||||
};
|
||||
},
|
||||
deleteEntry(entryId) {
|
||||
const self = this;
|
||||
const entryPos = getEntryPosById(entryId, this.internalDomainObject, this.selectedSection, this.selectedPage);
|
||||
if (entryPos === -1) {
|
||||
this.openmct.notifications.alert('Warning: unable to delete entry');
|
||||
console.error(`unable to delete entry ${entryId} from section ${this.selectedSection}, page ${this.selectedPage}`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const dialog = this.openmct.overlays.dialog({
|
||||
iconClass: 'alert',
|
||||
message: 'This action will permanently delete this entry. Do you wish to continue?',
|
||||
buttons: [
|
||||
{
|
||||
label: "Ok",
|
||||
emphasis: true,
|
||||
callback: () => {
|
||||
const entries = getNotebookEntries(self.internalDomainObject, self.selectedSection, self.selectedPage);
|
||||
entries.splice(entryPos, 1);
|
||||
self.updateEntries(entries);
|
||||
dialog.dismiss();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "Cancel",
|
||||
callback: () => dialog.dismiss()
|
||||
}
|
||||
]
|
||||
});
|
||||
},
|
||||
dragOver(event) {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = "copy";
|
||||
@@ -309,6 +349,20 @@ export default {
|
||||
|
||||
return this.openmct.objects.get(oldNotebookStorage.notebookMeta.identifier);
|
||||
},
|
||||
getLinktoNotebook() {
|
||||
const objectPath = this.openmct.router.path;
|
||||
const link = objectLink.computed.objectLink.call({
|
||||
objectPath,
|
||||
openmct: this.openmct
|
||||
});
|
||||
|
||||
const selectedSection = this.selectedSection;
|
||||
const selectedPage = this.selectedPage;
|
||||
const sectionId = selectedSection ? selectedSection.id : '';
|
||||
const pageId = selectedPage ? selectedPage.id : '';
|
||||
|
||||
return `${link}?sectionId=${sectionId}&pageId=${pageId}`;
|
||||
},
|
||||
getPage(section, id) {
|
||||
return section.pages.find(p => p.id === id);
|
||||
},
|
||||
@@ -393,6 +447,12 @@ export default {
|
||||
return s;
|
||||
});
|
||||
|
||||
const selectedSectionId = this.selectedSection && this.selectedSection.id;
|
||||
const selectedPageId = this.selectedPage && this.selectedPage.id;
|
||||
if (selectedPageId === pageId && selectedSectionId === sectionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.sectionsChanged({ sections });
|
||||
},
|
||||
newEntry(embed = null) {
|
||||
@@ -512,6 +572,13 @@ export default {
|
||||
|
||||
setDefaultNotebookSection(section);
|
||||
},
|
||||
updateEntry(entry) {
|
||||
const entries = getNotebookEntries(this.internalDomainObject, this.selectedSection, this.selectedPage);
|
||||
const entryPos = getEntryPosById(entry.id, this.internalDomainObject, this.selectedSection, this.selectedPage);
|
||||
entries[entryPos] = entry;
|
||||
|
||||
this.updateEntries(entries);
|
||||
},
|
||||
updateEntries(entries) {
|
||||
const configuration = this.internalDomainObject.configuration;
|
||||
const notebookEntries = configuration.entries || {};
|
||||
|
||||
@@ -33,10 +33,10 @@ import SnapshotTemplate from './snapshot-template.html';
|
||||
import Vue from 'vue';
|
||||
|
||||
export default {
|
||||
inject: ['openmct'],
|
||||
components: {
|
||||
PopupMenu
|
||||
},
|
||||
inject: ['openmct'],
|
||||
props: {
|
||||
embed: {
|
||||
type: Object,
|
||||
|
||||
@@ -12,11 +12,15 @@
|
||||
<div class="c-ne__content">
|
||||
<div :id="entry.id"
|
||||
class="c-ne__text"
|
||||
:class="{'c-ne__input' : !readOnly }"
|
||||
tabindex="0"
|
||||
:class="{ 'c-ne__input' : !readOnly }"
|
||||
:contenteditable="!readOnly"
|
||||
@blur="updateEntryValue($event, entry.id)"
|
||||
@focus="updateCurrentEntryValue($event, entry.id)"
|
||||
>{{ entry.text }}</div>
|
||||
@blur="updateEntryValue($event)"
|
||||
@keydown.enter.exact.prevent
|
||||
@keyup.enter.exact.prevent="forceBlur($event)"
|
||||
v-text="entry.text"
|
||||
>
|
||||
</div>
|
||||
<div class="c-snapshots c-ne__embeds">
|
||||
<NotebookEmbed v-for="embed in entry.embeds"
|
||||
:key="embed.id"
|
||||
@@ -33,6 +37,7 @@
|
||||
>
|
||||
<button class="c-icon-button c-icon-button--major icon-trash"
|
||||
title="Delete this entry"
|
||||
tabindex="-1"
|
||||
@click="deleteEntry"
|
||||
>
|
||||
</button>
|
||||
@@ -57,14 +62,14 @@
|
||||
|
||||
<script>
|
||||
import NotebookEmbed from './NotebookEmbed.vue';
|
||||
import { createNewEmbed, getEntryPosById, getNotebookEntries } from '../utils/notebook-entries';
|
||||
import { createNewEmbed } from '../utils/notebook-entries';
|
||||
import Moment from 'moment';
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'snapshotContainer'],
|
||||
components: {
|
||||
NotebookEmbed
|
||||
},
|
||||
inject: ['openmct', 'snapshotContainer'],
|
||||
props: {
|
||||
domainObject: {
|
||||
type: Object,
|
||||
@@ -103,11 +108,6 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentEntryValue: ''
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
createdOnDate() {
|
||||
return this.formatTime(this.entry.createdOn, 'YYYY-MM-DD');
|
||||
@@ -117,10 +117,20 @@ export default {
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.updateEntries = this.updateEntries.bind(this);
|
||||
this.dropOnEntry = this.dropOnEntry.bind(this);
|
||||
},
|
||||
methods: {
|
||||
addNewEmbed(objectPath) {
|
||||
const bounds = this.openmct.time.bounds();
|
||||
const snapshotMeta = {
|
||||
bounds,
|
||||
link: null,
|
||||
objectPath,
|
||||
openmct: this.openmct
|
||||
};
|
||||
const newEmbed = createNewEmbed(snapshotMeta);
|
||||
this.entry.embeds.push(newEmbed);
|
||||
},
|
||||
cancelEditMode(event) {
|
||||
const isEditing = this.openmct.editor.isEditing();
|
||||
if (isEditing) {
|
||||
@@ -132,63 +142,23 @@ export default {
|
||||
event.dataTransfer.dropEffect = "copy";
|
||||
},
|
||||
deleteEntry() {
|
||||
const self = this;
|
||||
const entryPosById = self.entryPosById(self.entry.id);
|
||||
if (entryPosById === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dialog = this.openmct.overlays.dialog({
|
||||
iconClass: 'alert',
|
||||
message: 'This action will permanently delete this entry. Do you wish to continue?',
|
||||
buttons: [
|
||||
{
|
||||
label: "Ok",
|
||||
emphasis: true,
|
||||
callback: () => {
|
||||
const entries = getNotebookEntries(self.domainObject, self.selectedSection, self.selectedPage);
|
||||
entries.splice(entryPosById, 1);
|
||||
self.updateEntries(entries);
|
||||
dialog.dismiss();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "Cancel",
|
||||
callback: () => {
|
||||
dialog.dismiss();
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
this.$emit('deleteEntry', this.entry.id);
|
||||
},
|
||||
dropOnEntry($event) {
|
||||
event.stopImmediatePropagation();
|
||||
|
||||
const snapshotId = $event.dataTransfer.getData('openmct/snapshot/id');
|
||||
if (snapshotId.length) {
|
||||
this.moveSnapshot(snapshotId);
|
||||
|
||||
return;
|
||||
const snapshot = this.snapshotContainer.getSnapshot(snapshotId);
|
||||
this.snapshotContainer.removeSnapshot(snapshotId);
|
||||
this.entry.embeds.push(snapshot);
|
||||
} else {
|
||||
const data = $event.dataTransfer.getData('openmct/domain-object-path');
|
||||
const objectPath = JSON.parse(data);
|
||||
this.addNewEmbed(objectPath);
|
||||
}
|
||||
|
||||
const data = $event.dataTransfer.getData('openmct/domain-object-path');
|
||||
const objectPath = JSON.parse(data);
|
||||
const entryPos = this.entryPosById(this.entry.id);
|
||||
const bounds = this.openmct.time.bounds();
|
||||
const snapshotMeta = {
|
||||
bounds,
|
||||
link: null,
|
||||
objectPath,
|
||||
openmct: this.openmct
|
||||
};
|
||||
const newEmbed = createNewEmbed(snapshotMeta);
|
||||
const entries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage);
|
||||
const currentEntryEmbeds = entries[entryPos].embeds;
|
||||
currentEntryEmbeds.push(newEmbed);
|
||||
this.updateEntries(entries);
|
||||
},
|
||||
entryPosById(entryId) {
|
||||
return getEntryPosById(entryId, this.domainObject, this.selectedSection, this.selectedPage);
|
||||
this.$emit('updateEntry', this.entry);
|
||||
},
|
||||
findPositionInArray(array, id) {
|
||||
let position = -1;
|
||||
@@ -203,15 +173,12 @@ export default {
|
||||
|
||||
return position;
|
||||
},
|
||||
forceBlur(event) {
|
||||
event.target.blur();
|
||||
},
|
||||
formatTime(unixTime, timeFormat) {
|
||||
return Moment.utc(unixTime).format(timeFormat);
|
||||
},
|
||||
moveSnapshot(snapshotId) {
|
||||
const snapshot = this.snapshotContainer.getSnapshot(snapshotId);
|
||||
this.entry.embeds.push(snapshot);
|
||||
this.updateEntry(this.entry);
|
||||
this.snapshotContainer.removeSnapshot(snapshotId);
|
||||
},
|
||||
navigateToPage() {
|
||||
this.$emit('changeSectionPage', {
|
||||
sectionId: this.result.section.id,
|
||||
@@ -227,15 +194,8 @@ export default {
|
||||
removeEmbed(id) {
|
||||
const embedPosition = this.findPositionInArray(this.entry.embeds, id);
|
||||
this.entry.embeds.splice(embedPosition, 1);
|
||||
this.updateEntry(this.entry);
|
||||
},
|
||||
updateCurrentEntryValue($event) {
|
||||
if (this.readOnly) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = $event.target;
|
||||
this.currentEntryValue = target ? target.textContent : '';
|
||||
this.$emit('updateEntry', this.entry);
|
||||
},
|
||||
updateEmbed(newEmbed) {
|
||||
this.entry.embeds.some(e => {
|
||||
@@ -247,44 +207,14 @@ export default {
|
||||
return found;
|
||||
});
|
||||
|
||||
this.updateEntry(this.entry);
|
||||
this.$emit('updateEntry', this.entry);
|
||||
},
|
||||
updateEntry(newEntry) {
|
||||
const entries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage);
|
||||
entries.some(entry => {
|
||||
const found = (entry.id === newEntry.id);
|
||||
if (found) {
|
||||
entry = newEntry;
|
||||
}
|
||||
|
||||
return found;
|
||||
});
|
||||
|
||||
this.updateEntries(entries);
|
||||
},
|
||||
updateEntryValue($event, entryId) {
|
||||
if (!this.domainObject || !this.selectedSection || !this.selectedPage) {
|
||||
return;
|
||||
updateEntryValue($event) {
|
||||
const value = $event.target.innerText;
|
||||
if (value !== this.entry.text && value.match(/\S/)) {
|
||||
this.entry.text = value;
|
||||
this.$emit('updateEntry', this.entry);
|
||||
}
|
||||
|
||||
const target = $event.target;
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entryPos = this.entryPosById(entryId);
|
||||
const value = target.textContent.trim();
|
||||
if (this.currentEntryValue !== value) {
|
||||
target.textContent = value;
|
||||
|
||||
const entries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage);
|
||||
entries[entryPos].text = value;
|
||||
|
||||
this.updateEntries(entries);
|
||||
}
|
||||
},
|
||||
updateEntries(entries) {
|
||||
this.$emit('updateEntries', entries);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -56,11 +56,11 @@ import { NOTEBOOK_SNAPSHOT_MAX_COUNT } from '../snapshot-container';
|
||||
import { EVENT_SNAPSHOTS_UPDATED } from '../notebook-constants';
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'snapshotContainer'],
|
||||
components: {
|
||||
NotebookEmbed,
|
||||
PopupMenu
|
||||
},
|
||||
inject: ['openmct', 'snapshotContainer'],
|
||||
props: {
|
||||
toggleSnapshot: {
|
||||
type: Function,
|
||||
|
||||
@@ -69,14 +69,14 @@ export default {
|
||||
const divElement = document.querySelector('.l-shell__drawer div');
|
||||
|
||||
this.component = new Vue({
|
||||
provide: {
|
||||
openmct,
|
||||
snapshotContainer
|
||||
},
|
||||
el: divElement,
|
||||
components: {
|
||||
SnapshotContainerComponent
|
||||
},
|
||||
provide: {
|
||||
openmct,
|
||||
snapshotContainer
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
toggleSnapshot
|
||||
|
||||
@@ -22,10 +22,10 @@ import { getDefaultNotebook } from '../utils/notebook-storage';
|
||||
import Page from './PageComponent.vue';
|
||||
|
||||
export default {
|
||||
inject: ['openmct'],
|
||||
components: {
|
||||
Page
|
||||
},
|
||||
inject: ['openmct'],
|
||||
props: {
|
||||
defaultPageId: {
|
||||
type: String,
|
||||
|
||||
@@ -18,10 +18,10 @@ import PopupMenu from './PopupMenu.vue';
|
||||
import RemoveDialog from '../utils/removeDialog';
|
||||
|
||||
export default {
|
||||
inject: ['openmct'],
|
||||
components: {
|
||||
PopupMenu
|
||||
},
|
||||
inject: ['openmct'],
|
||||
props: {
|
||||
defaultPageId: {
|
||||
type: String,
|
||||
|
||||
@@ -21,10 +21,10 @@
|
||||
import NotebookEntry from './NotebookEntry.vue';
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'snapshotContainer'],
|
||||
components: {
|
||||
NotebookEntry
|
||||
},
|
||||
inject: ['openmct', 'snapshotContainer'],
|
||||
props: {
|
||||
domainObject: {
|
||||
type: Object,
|
||||
|
||||
@@ -22,10 +22,10 @@ import { getDefaultNotebook } from '../utils/notebook-storage';
|
||||
import sectionComponent from './SectionComponent.vue';
|
||||
|
||||
export default {
|
||||
inject: ['openmct'],
|
||||
components: {
|
||||
sectionComponent
|
||||
},
|
||||
inject: ['openmct'],
|
||||
props: {
|
||||
defaultSectionId: {
|
||||
type: String,
|
||||
|
||||
@@ -21,10 +21,10 @@ import PopupMenu from './PopupMenu.vue';
|
||||
import RemoveDialog from '../utils/removeDialog';
|
||||
|
||||
export default {
|
||||
inject: ['openmct'],
|
||||
components: {
|
||||
PopupMenu
|
||||
},
|
||||
inject: ['openmct'],
|
||||
props: {
|
||||
defaultSectionId: {
|
||||
type: String,
|
||||
|
||||
@@ -61,11 +61,11 @@ import PageCollection from './PageCollection.vue';
|
||||
import uuid from 'uuid';
|
||||
|
||||
export default {
|
||||
inject: ['openmct'],
|
||||
components: {
|
||||
SectionCollection,
|
||||
PageCollection
|
||||
},
|
||||
inject: ['openmct'],
|
||||
props: {
|
||||
defaultPageId: {
|
||||
type: String,
|
||||
|
||||
@@ -88,13 +88,13 @@ export default function NotebookPlugin() {
|
||||
|
||||
const snapshotContainer = new SnapshotContainer(openmct);
|
||||
const notebookSnapshotIndicator = new Vue ({
|
||||
components: {
|
||||
NotebookSnapshotIndicator
|
||||
},
|
||||
provide: {
|
||||
openmct,
|
||||
snapshotContainer
|
||||
},
|
||||
components: {
|
||||
NotebookSnapshotIndicator
|
||||
},
|
||||
template: '<NotebookSnapshotIndicator></NotebookSnapshotIndicator>'
|
||||
});
|
||||
const indicator = {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { addNotebookEntry, createNewEmbed } from './utils/notebook-entries';
|
||||
import { getDefaultNotebook } from './utils/notebook-storage';
|
||||
import { getDefaultNotebook, getDefaultNotebookLink, setDefaultNotebook } from './utils/notebook-storage';
|
||||
import { NOTEBOOK_DEFAULT } from '@/plugins/notebook/notebook-constants';
|
||||
import SnapshotContainer from './snapshot-container';
|
||||
|
||||
@@ -45,12 +45,21 @@ export default class Snapshot {
|
||||
_saveToDefaultNoteBook(embed) {
|
||||
const notebookStorage = getDefaultNotebook();
|
||||
this.openmct.objects.get(notebookStorage.notebookMeta.identifier)
|
||||
.then(domainObject => {
|
||||
.then(async (domainObject) => {
|
||||
addNotebookEntry(this.openmct, domainObject, notebookStorage, embed);
|
||||
|
||||
let link = notebookStorage.notebookMeta.link;
|
||||
|
||||
// Backwards compatibility fix (old notebook model without link)
|
||||
if (!link) {
|
||||
link = await getDefaultNotebookLink(this.openmct, domainObject);
|
||||
notebookStorage.notebookMeta.link = link;
|
||||
setDefaultNotebook(this.openmct, notebookStorage);
|
||||
}
|
||||
|
||||
const defaultPath = `${domainObject.name} - ${notebookStorage.section.name} - ${notebookStorage.page.name}`;
|
||||
const msg = `Saved to Notebook ${defaultPath}`;
|
||||
this._showNotification(msg);
|
||||
this._showNotification(msg, link);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -58,16 +67,29 @@ export default class Snapshot {
|
||||
* @private
|
||||
*/
|
||||
_saveToNotebookSnapshots(embed) {
|
||||
const saved = this.snapshotContainer.addSnapshot(embed);
|
||||
if (!saved) {
|
||||
return;
|
||||
this.snapshotContainer.addSnapshot(embed);
|
||||
}
|
||||
|
||||
_showNotification(msg, url) {
|
||||
const options = {
|
||||
autoDismissTimeout: 30000,
|
||||
link: {
|
||||
cssClass: '',
|
||||
text: 'click to view',
|
||||
onClick: this._navigateToNotebook(url)
|
||||
}
|
||||
};
|
||||
|
||||
this.openmct.notifications.info(msg, options);
|
||||
}
|
||||
|
||||
_navigateToNotebook(url = null) {
|
||||
if (!url) {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
const msg = 'Saved to Notebook Snapshots - click to view.';
|
||||
this._showNotification(msg);
|
||||
}
|
||||
|
||||
_showNotification(msg) {
|
||||
this.openmct.notifications.info(msg);
|
||||
return () => {
|
||||
window.location.href = window.location.origin + url;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,14 +48,29 @@ export function getDefaultNotebook() {
|
||||
return JSON.parse(notebookStorage);
|
||||
}
|
||||
|
||||
export async function getDefaultNotebookLink(openmct, domainObject = null) {
|
||||
if (!domainObject) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const path = await openmct.objects.getOriginalPath(domainObject.identifier)
|
||||
.then(objectPath => objectPath
|
||||
.map(o => o && openmct.objects.makeKeyString(o.identifier))
|
||||
.reverse()
|
||||
.join('/')
|
||||
);
|
||||
const { page, section } = getDefaultNotebook();
|
||||
|
||||
return `#/browse/${path}?sectionId=${section.id}&pageId=${page.id}`;
|
||||
}
|
||||
|
||||
export function setDefaultNotebook(openmct, notebookStorage, domainObject) {
|
||||
observeDefaultNotebookObject(openmct, notebookStorage.notebookMeta, domainObject);
|
||||
observeDefaultNotebookObject(openmct, notebookStorage, domainObject);
|
||||
saveDefaultNotebook(notebookStorage);
|
||||
}
|
||||
|
||||
export function setDefaultNotebookSection(section) {
|
||||
const notebookStorage = getDefaultNotebook();
|
||||
|
||||
notebookStorage.section = section;
|
||||
saveDefaultNotebook(notebookStorage);
|
||||
}
|
||||
|
||||
@@ -27,10 +27,10 @@
|
||||
import NotificationsList from './NotificationsList.vue';
|
||||
|
||||
export default {
|
||||
inject: ['openmct'],
|
||||
components: {
|
||||
NotificationsList
|
||||
},
|
||||
inject: ['openmct'],
|
||||
data() {
|
||||
return {
|
||||
notifications: this.openmct.notifications.notifications,
|
||||
|
||||
@@ -25,12 +25,12 @@ import NotificationIndicator from './components/NotificationIndicator.vue';
|
||||
export default function plugin() {
|
||||
return function install(openmct) {
|
||||
let component = new Vue ({
|
||||
provide: {
|
||||
openmct
|
||||
},
|
||||
components: {
|
||||
NotificationIndicator: NotificationIndicator
|
||||
},
|
||||
provide: {
|
||||
openmct
|
||||
},
|
||||
template: '<NotificationIndicator></NotificationIndicator>'
|
||||
});
|
||||
|
||||
|
||||
@@ -136,7 +136,7 @@ define([
|
||||
};
|
||||
}
|
||||
|
||||
if (objectPath && !_.isFunction(objectPath)) {
|
||||
if (objectPath && (typeof objectPath !== "function")) {
|
||||
const staticObjectPath = objectPath;
|
||||
objectPath = function () {
|
||||
return staticObjectPath;
|
||||
|
||||
63
src/plugins/plot/vue/overlayPlot/OverlayPlotViewProvider.js
Normal file
63
src/plugins/plot/vue/overlayPlot/OverlayPlotViewProvider.js
Normal file
@@ -0,0 +1,63 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import Plot from '../single/Plot.vue';
|
||||
import Vue from 'vue';
|
||||
|
||||
export default function OverlayPlotViewProvider(openmct) {
|
||||
return {
|
||||
key: 'plot-overlay',
|
||||
name: 'Overlay Plot',
|
||||
cssClass: 'icon-telemetry',
|
||||
canView(domainObject) {
|
||||
return domainObject.type === 'telemetry.plot.overlay';
|
||||
},
|
||||
|
||||
canEdit(domainObject) {
|
||||
return domainObject.type === 'telemetry.plot.overlay';
|
||||
},
|
||||
|
||||
view: function (domainObject) {
|
||||
let component;
|
||||
|
||||
return {
|
||||
show: function (element) {
|
||||
component = new Vue({
|
||||
el: element,
|
||||
components: {
|
||||
Plot
|
||||
},
|
||||
provide: {
|
||||
openmct,
|
||||
domainObject
|
||||
},
|
||||
template: '<plot></plot>'
|
||||
});
|
||||
},
|
||||
destroy: function () {
|
||||
component.$destroy();
|
||||
component = undefined;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
83
src/plugins/plot/vue/single/LinearScale.js
Normal file
83
src/plugins/plot/vue/single/LinearScale.js
Normal file
@@ -0,0 +1,83 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
/*jscs:disable disallowDanglingUnderscores */
|
||||
/**
|
||||
* A scale has an input domain and an output range. It provides functions
|
||||
* `scale` return the range value associated with a domain value.
|
||||
* `invert` return the domain value associated with range value.
|
||||
*/
|
||||
|
||||
class LinearScale {
|
||||
constructor(domain) {
|
||||
this.domain(domain);
|
||||
}
|
||||
|
||||
domain(newDomain) {
|
||||
if (newDomain) {
|
||||
this._domain = newDomain;
|
||||
this._domainDenominator = newDomain.max - newDomain.min;
|
||||
}
|
||||
|
||||
return this._domain;
|
||||
}
|
||||
|
||||
range(newRange) {
|
||||
if (newRange) {
|
||||
this._range = newRange;
|
||||
this._rangeDenominator = newRange.max - newRange.min;
|
||||
}
|
||||
|
||||
return this._range;
|
||||
}
|
||||
|
||||
scale(domainValue) {
|
||||
if (!this._domain || !this._range) {
|
||||
return;
|
||||
}
|
||||
|
||||
const domainOffset = domainValue - this._domain.min;
|
||||
const rangeFraction = domainOffset - this._domainDenominator;
|
||||
const rangeOffset = rangeFraction * this._rangeDenominator;
|
||||
const rangeValue = rangeOffset + this._range.min;
|
||||
|
||||
return rangeValue;
|
||||
}
|
||||
|
||||
invert(rangeValue) {
|
||||
if (!this._domain || !this._range) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rangeOffset = rangeValue - this._range.min;
|
||||
const domainFraction = rangeOffset / this._rangeDenominator;
|
||||
const domainOffset = domainFraction * this._domainDenominator;
|
||||
const domainValue = domainOffset + this._domain.min;
|
||||
|
||||
return domainValue;
|
||||
}
|
||||
}
|
||||
|
||||
export default LinearScale;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
892
src/plugins/plot/vue/single/MctPlot.vue
Normal file
892
src/plugins/plot/vue/single/MctPlot.vue
Normal file
@@ -0,0 +1,892 @@
|
||||
<!--
|
||||
Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
as represented by the Administrator of the National Aeronautics and Space
|
||||
Administration. All rights reserved.
|
||||
|
||||
Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0.
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
License for the specific language governing permissions and limitations
|
||||
under the License.
|
||||
|
||||
Open MCT includes source code licensed under additional open source
|
||||
licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
this source code distribution or the Licensing information page available
|
||||
at runtime from the About dialog for additional information.
|
||||
-->
|
||||
<template>
|
||||
<div v-if="loaded"
|
||||
class="gl-plot"
|
||||
:class="[plotLegendExpandedStateClass, plotLegendPositionClass]"
|
||||
>
|
||||
<plot-legend :cursor-locked="!!lockHighlightPoint"
|
||||
:series="config.series.models"
|
||||
:highlights="highlights"
|
||||
:legend="config.legend"
|
||||
/>
|
||||
<div class="plot-wrapper-axis-and-display-area flex-elem grows">
|
||||
<y-axis v-if="config.series.models.length > 0"
|
||||
:tick-width="tickWidth"
|
||||
:single-series="config.series.models.length === 1"
|
||||
:series-model="config.series.models[0]"
|
||||
@yKeyChanged="setYAxisKey"
|
||||
@tickWidthChanged="onTickWidthChange"
|
||||
/>
|
||||
<div class="gl-plot-wrapper-display-area-and-x-axis"
|
||||
:style="{
|
||||
left: (tickWidth + 20) + 'px'
|
||||
}"
|
||||
>
|
||||
|
||||
<div class="gl-plot-display-area has-local-controls has-cursor-guides">
|
||||
<div class="l-state-indicators">
|
||||
<span class="l-state-indicators__alert-no-lad t-object-alert t-alert-unsynced icon-alert-triangle"
|
||||
title="This plot is not currently displaying the latest data. Reset pan/zoom to view latest data."
|
||||
></span>
|
||||
</div>
|
||||
|
||||
<mct-ticks v-show="gridLines"
|
||||
:axis-type="'xAxis'"
|
||||
:position="'right'"
|
||||
@plotTickWidth="onTickWidthChange"
|
||||
/>
|
||||
|
||||
<mct-ticks v-show="gridLines"
|
||||
:axis-type="'yAxis'"
|
||||
:position="'bottom'"
|
||||
@plotTickWidth="onTickWidthChange"
|
||||
/>
|
||||
|
||||
<div ref="chartContainer"
|
||||
class="gl-plot-chart-wrapper"
|
||||
>
|
||||
<mct-chart :series-config="config"
|
||||
:rectangles="rectangles"
|
||||
:highlights="highlights"
|
||||
@plotReinitializeCanvas="initCanvas"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="gl-plot__local-controls h-local-controls h-local-controls--overlay-content c-local-controls--show-on-hover">
|
||||
<div class="c-button-set c-button-set--strip-h">
|
||||
<button class="c-button icon-minus"
|
||||
title="Zoom out"
|
||||
@click="zoom('out', 0.2)"
|
||||
>
|
||||
</button>
|
||||
<button class="c-button icon-plus"
|
||||
title="Zoom in"
|
||||
@click="zoom('in', 0.2)"
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
<div class="c-button-set c-button-set--strip-h"
|
||||
:disabled="!plotHistory.length"
|
||||
>
|
||||
<button class="c-button icon-arrow-left"
|
||||
title="Restore previous pan/zoom"
|
||||
@click="back()"
|
||||
>
|
||||
</button>
|
||||
<button class="c-button icon-reset"
|
||||
title="Reset pan/zoom"
|
||||
@click="clear()"
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--Cursor guides-->
|
||||
<div v-show="cursorGuide"
|
||||
ref="cursorGuideVertical"
|
||||
class="c-cursor-guide--v js-cursor-guide--v"
|
||||
>
|
||||
</div>
|
||||
<div v-show="cursorGuide"
|
||||
ref="cursorGuideHorizontal"
|
||||
class="c-cursor-guide--h js-cursor-guide--h"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<x-axis v-if="config.series.models.length > 0"
|
||||
:series-model="config.series.models[0]"
|
||||
/>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import eventHelpers from './lib/eventHelpers';
|
||||
import LinearScale from "./LinearScale";
|
||||
import PlotConfigurationModel from './configuration/PlotConfigurationModel';
|
||||
import configStore from './configuration/configStore';
|
||||
|
||||
import PlotLegend from "./legend/PlotLegend.vue";
|
||||
import MctTicks from "./MctTicks.vue";
|
||||
import MctChart from "./chart/MctChart.vue";
|
||||
import XAxis from "./axis/XAxis.vue";
|
||||
import YAxis from "./axis/YAxis.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
XAxis,
|
||||
YAxis,
|
||||
PlotLegend,
|
||||
MctTicks,
|
||||
MctChart
|
||||
},
|
||||
inject: ['openmct', 'domainObject'],
|
||||
props: {
|
||||
gridLines: {
|
||||
type: Boolean,
|
||||
default() {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
cursorGuide: {
|
||||
type: Boolean,
|
||||
default() {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
plotTickWidth: {
|
||||
type: Number,
|
||||
default() {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
highlights: [],
|
||||
lockHighlightPoint: false,
|
||||
tickWidth: 0,
|
||||
yKeyOptions: [],
|
||||
yAxisLabel: '',
|
||||
rectangles: [],
|
||||
plotHistory: [],
|
||||
selectedXKeyOption: {},
|
||||
xKeyOptions: [],
|
||||
config: {},
|
||||
pending: 0,
|
||||
loaded: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
plotLegendPositionClass() {
|
||||
return `plot-legend-${this.config.legend.get('position')}`;
|
||||
},
|
||||
plotLegendExpandedStateClass() {
|
||||
if (this.config.legend.get('expanded')) {
|
||||
return 'plot-legend-expanded';
|
||||
} else {
|
||||
return 'plot-legend-collapsed';
|
||||
}
|
||||
}
|
||||
},
|
||||
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.listenTo(this.config.series, 'add', this.addSeries, this);
|
||||
this.listenTo(this.config.series, 'remove', this.removeSeries, this);
|
||||
|
||||
this.config.series.models.forEach(this.addSeries, this);
|
||||
|
||||
this.filterObserver = this.openmct.objects.observe(
|
||||
this.domainObject,
|
||||
'configuration.filters',
|
||||
this.updateFiltersAndResubscribe
|
||||
);
|
||||
|
||||
this.openmct.objectViews.on('clearData', this.clearData);
|
||||
this.followTimeConductor();
|
||||
|
||||
this.loaded = true;
|
||||
|
||||
//We're referencing the canvas elements from the mct-chart in the initialize method.
|
||||
// So we need $nextTick to ensure the component is fully mounted before we can initialize stuff.
|
||||
this.$nextTick(this.initialize);
|
||||
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.destroy();
|
||||
},
|
||||
methods: {
|
||||
followTimeConductor() {
|
||||
this.openmct.time.on('bounds', this.updateDisplayBounds);
|
||||
this.synchronized(true);
|
||||
},
|
||||
getConfig() {
|
||||
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
let config = configStore.get(configId);
|
||||
if (!config) {
|
||||
config = new PlotConfigurationModel({
|
||||
id: configId,
|
||||
domainObject: this.domainObject,
|
||||
openmct: this.openmct
|
||||
});
|
||||
configStore.add(configId, config);
|
||||
}
|
||||
|
||||
return config;
|
||||
},
|
||||
addSeries(series) {
|
||||
this.listenTo(series, 'change:xKey', (xKey) => {
|
||||
this.setDisplayRange(series, xKey);
|
||||
}, this);
|
||||
this.listenTo(series, 'change:yKey', () => {
|
||||
this.loadSeriesData(series);
|
||||
}, this);
|
||||
|
||||
this.listenTo(series, 'change:interpolate', () => {
|
||||
this.loadSeriesData(series);
|
||||
}, this);
|
||||
|
||||
this.loadSeriesData(series);
|
||||
},
|
||||
|
||||
removeSeries(plotSeries) {
|
||||
this.stopListening(plotSeries);
|
||||
},
|
||||
|
||||
loadSeriesData(series) {
|
||||
if (this.$parent.$refs.plotWrapper.offsetWidth === 0) {
|
||||
this.scheduleLoad(series);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.startLoading();
|
||||
const options = {
|
||||
size: this.$parent.$refs.plotWrapper.offsetWidth,
|
||||
domain: this.config.xAxis.get('key')
|
||||
};
|
||||
|
||||
series.load(options)
|
||||
.then(this.stopLoading.bind(this));
|
||||
},
|
||||
|
||||
loadMoreData(range, purge) {
|
||||
this.config.series.forEach(plotSeries => {
|
||||
this.startLoading();
|
||||
plotSeries.load({
|
||||
size: this.$parent.$refs.plotWrapper.offsetWidth,
|
||||
start: range.min,
|
||||
end: range.max,
|
||||
domain: this.config.xAxis.get('key')
|
||||
})
|
||||
.then(this.stopLoading());
|
||||
if (purge) {
|
||||
plotSeries.purgeRecordsOutsideRange(range);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
scheduleLoad(series) {
|
||||
if (!this.scheduledLoads) {
|
||||
this.startLoading();
|
||||
this.scheduledLoads = [];
|
||||
this.checkForSize = setInterval(function () {
|
||||
if (this.$parent.$refs.plotWrapper.offsetWidth === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.stopLoading();
|
||||
this.scheduledLoads.forEach(this.loadSeriesData, this);
|
||||
delete this.scheduledLoads;
|
||||
clearInterval(this.checkForSize);
|
||||
delete this.checkForSize;
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
if (this.scheduledLoads.indexOf(series) === -1) {
|
||||
this.scheduledLoads.push(series);
|
||||
}
|
||||
},
|
||||
|
||||
startLoading() {
|
||||
this.pending += 1;
|
||||
this.updateLoading();
|
||||
},
|
||||
|
||||
stopLoading() {
|
||||
//TODO: Is Vue.$nextTick ok to replace $scope.$evalAsync?
|
||||
this.$nextTick().then(() => {
|
||||
this.pending -= 1;
|
||||
this.updateLoading();
|
||||
});
|
||||
},
|
||||
|
||||
updateLoading() {
|
||||
this.$emit('loadingUpdated', this.pending > 0);
|
||||
},
|
||||
|
||||
updateFiltersAndResubscribe(updatedFilters) {
|
||||
this.config.series.forEach(function (series) {
|
||||
series.updateFiltersAndRefresh(updatedFilters[series.keyString]);
|
||||
});
|
||||
},
|
||||
|
||||
clearData() {
|
||||
this.config.series.forEach(function (series) {
|
||||
series.reset();
|
||||
});
|
||||
},
|
||||
|
||||
setDisplayRange(series, xKey) {
|
||||
if (this.config.series.length !== 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const displayRange = series.getDisplayRange(xKey);
|
||||
this.config.xAxis.set('range', displayRange);
|
||||
},
|
||||
|
||||
/**
|
||||
* Track latest display bounds. Forces update when not receiving ticks.
|
||||
*/
|
||||
updateDisplayBounds(bounds, isTick) {
|
||||
const newRange = {
|
||||
min: bounds.start,
|
||||
max: bounds.end
|
||||
};
|
||||
this.config.xAxis.set('range', newRange);
|
||||
if (!isTick) {
|
||||
this.skipReloadOnInteraction = true;
|
||||
this.clear();
|
||||
this.skipReloadOnInteraction = false;
|
||||
this.loadMoreData(newRange, true);
|
||||
} else {
|
||||
// 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 keepRange = {
|
||||
min: newRange.min - (newRange.max - newRange.min),
|
||||
max: newRange.max
|
||||
};
|
||||
this.config.series.forEach(function (series) {
|
||||
series.purgeRecordsOutsideRange(keepRange);
|
||||
});
|
||||
this.nextPurge = Date.now() + 1000;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle end of user viewport change: load more data for current display
|
||||
* bounds, and mark view as synchronized if bounds match configured bounds.
|
||||
*/
|
||||
userViewportChangeEnd() {
|
||||
const xDisplayRange = this.config.xAxis.get('displayRange');
|
||||
const xRange = this.config.xAxis.get('range');
|
||||
|
||||
if (!this.skipReloadOnInteraction) {
|
||||
this.loadMoreData(xDisplayRange);
|
||||
}
|
||||
|
||||
this.synchronized(xRange.min === xDisplayRange.min
|
||||
&& xRange.max === xDisplayRange.max);
|
||||
},
|
||||
|
||||
/**
|
||||
* Getter/setter for "synchronized" value. If not synchronized and
|
||||
* time conductor is in clock mode, will mark objects as unsynced so that
|
||||
* displays can update accordingly.
|
||||
*/
|
||||
synchronized(value) {
|
||||
if (typeof value !== 'undefined') {
|
||||
this._synchronized = value;
|
||||
const isUnsynced = !value && this.openmct.time.clock();
|
||||
const domainObject = this.openmct.legacyObject(this.domainObject);
|
||||
if (domainObject.getCapability('status')) {
|
||||
domainObject.getCapability('status')
|
||||
.set('timeconductor-unsynced', isUnsynced);
|
||||
}
|
||||
}
|
||||
|
||||
return this._synchronized;
|
||||
},
|
||||
|
||||
initCanvas() {
|
||||
if (this.canvas) {
|
||||
this.stopListening(this.canvas);
|
||||
}
|
||||
|
||||
this.canvas = this.$refs.chartContainer.querySelectorAll('canvas')[1];
|
||||
|
||||
this.listenTo(this.canvas, 'mousemove', this.trackMousePosition, this);
|
||||
this.listenTo(this.canvas, 'mouseleave', this.untrackMousePosition, this);
|
||||
this.listenTo(this.canvas, 'mousedown', this.onMouseDown, this);
|
||||
this.listenTo(this.canvas, 'wheel', this.wheelZoom, this);
|
||||
},
|
||||
|
||||
initialize() {
|
||||
// Setup canvas etc.
|
||||
this.xScale = new LinearScale(this.config.xAxis.get('displayRange'));
|
||||
this.yScale = new LinearScale(this.config.yAxis.get('displayRange'));
|
||||
|
||||
this.pan = undefined;
|
||||
this.marquee = undefined;
|
||||
|
||||
this.chartElementBounds = undefined;
|
||||
this.tickUpdate = false;
|
||||
|
||||
this.canvas = this.$refs.chartContainer.querySelectorAll('canvas')[1];
|
||||
|
||||
this.listenTo(this.canvas, 'mousemove', this.trackMousePosition, this);
|
||||
this.listenTo(this.canvas, 'mouseleave', this.untrackMousePosition, this);
|
||||
this.listenTo(this.canvas, 'mousedown', this.onMouseDown, this);
|
||||
this.listenTo(this.canvas, 'wheel', this.wheelZoom, this);
|
||||
|
||||
this.config.yAxisLabel = this.config.yAxis.get('label');
|
||||
|
||||
this.cursorGuideVertical = this.$refs.cursorGuideVertical;
|
||||
this.cursorGuideHorizontal = this.$refs.cursorGuideHorizontal;
|
||||
|
||||
this.listenTo(this.config.xAxis, 'change:displayRange', this.onXAxisChange, this);
|
||||
this.listenTo(this.config.yAxis, 'change:displayRange', this.onYAxisChange, this);
|
||||
},
|
||||
|
||||
onXAxisChange(displayBounds) {
|
||||
if (displayBounds) {
|
||||
this.xScale.domain(displayBounds);
|
||||
}
|
||||
},
|
||||
|
||||
onYAxisChange(displayBounds) {
|
||||
if (displayBounds) {
|
||||
this.yScale.domain(displayBounds);
|
||||
}
|
||||
},
|
||||
|
||||
onTickWidthChange(width, fromDifferentObject) {
|
||||
if (fromDifferentObject) {
|
||||
// Always accept tick width if it comes from a different object.
|
||||
this.tickWidth = width;
|
||||
} else {
|
||||
// Otherwise, only accept tick with if it's larger.
|
||||
const newWidth = Math.max(width, this.tickWidth);
|
||||
if (newWidth !== this.tickWidth) {
|
||||
this.tickWidth = newWidth;
|
||||
}
|
||||
}
|
||||
|
||||
this.$emit('plotTickWidth', this.tickWidth);
|
||||
},
|
||||
|
||||
trackMousePosition(event) {
|
||||
this.trackChartElementBounds(event);
|
||||
this.xScale.range({
|
||||
min: 0,
|
||||
max: this.chartElementBounds.width
|
||||
});
|
||||
this.yScale.range({
|
||||
min: 0,
|
||||
max: this.chartElementBounds.height
|
||||
});
|
||||
|
||||
this.positionOverElement = {
|
||||
x: event.clientX - this.chartElementBounds.left,
|
||||
y: this.chartElementBounds.height
|
||||
- (event.clientY - this.chartElementBounds.top)
|
||||
};
|
||||
|
||||
this.positionOverPlot = {
|
||||
x: this.xScale.invert(this.positionOverElement.x),
|
||||
y: this.yScale.invert(this.positionOverElement.y)
|
||||
};
|
||||
|
||||
if (this.cursorGuide) {
|
||||
this.updateCrosshairs(event);
|
||||
}
|
||||
|
||||
this.highlightValues(this.positionOverPlot.x);
|
||||
this.updateMarquee();
|
||||
this.updatePan();
|
||||
event.preventDefault();
|
||||
},
|
||||
|
||||
updateCrosshairs(event) {
|
||||
this.cursorGuideVertical.style.left = (event.clientX - this.chartElementBounds.x) + 'px';
|
||||
this.cursorGuideHorizontal.style.top = (event.clientY - this.chartElementBounds.y) + 'px';
|
||||
},
|
||||
|
||||
trackChartElementBounds(event) {
|
||||
if (event.target === this.canvas) {
|
||||
this.chartElementBounds = event.target.getBoundingClientRect();
|
||||
}
|
||||
},
|
||||
|
||||
onPlotHighlightSet($e, point) {
|
||||
if (point === this.highlightPoint) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.highlightValues(point);
|
||||
},
|
||||
|
||||
highlightValues(point) {
|
||||
this.highlightPoint = point;
|
||||
// TODO: used in StackedPlotController
|
||||
this.$emit('plotHighlightUpdate', point);
|
||||
if (this.lockHighlightPoint) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!point) {
|
||||
this.highlights = [];
|
||||
this.config.series.models.forEach(series => delete series.closest);
|
||||
} else {
|
||||
this.highlights = this.config.series.models
|
||||
.filter(series => series.data.length > 0)
|
||||
.map(series => {
|
||||
series.closest = series.nearestPoint(point);
|
||||
|
||||
return {
|
||||
series: series,
|
||||
point: series.closest
|
||||
};
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
untrackMousePosition() {
|
||||
this.positionOverElement = undefined;
|
||||
this.positionOverPlot = undefined;
|
||||
this.highlightValues();
|
||||
},
|
||||
|
||||
onMouseDown(event) {
|
||||
// do not monitor drag events on browser context click
|
||||
if (event.ctrlKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.listenTo(window, 'mouseup', this.onMouseUp, this);
|
||||
this.listenTo(window, 'mousemove', this.trackMousePosition, this);
|
||||
if (event.altKey) {
|
||||
return this.startPan(event);
|
||||
} else {
|
||||
return this.startMarquee(event);
|
||||
}
|
||||
},
|
||||
|
||||
onMouseUp(event) {
|
||||
this.stopListening(window, 'mouseup', this.onMouseUp, this);
|
||||
this.stopListening(window, 'mousemove', this.trackMousePosition, this);
|
||||
|
||||
if (this.isMouseClick()) {
|
||||
this.lockHighlightPoint = !this.lockHighlightPoint;
|
||||
}
|
||||
|
||||
if (this.pan) {
|
||||
return this.endPan(event);
|
||||
}
|
||||
|
||||
if (this.marquee) {
|
||||
return this.endMarquee(event);
|
||||
}
|
||||
},
|
||||
|
||||
isMouseClick() {
|
||||
if (!this.marquee) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { start, end } = this.marquee;
|
||||
|
||||
return start.x === end.x && start.y === end.y;
|
||||
},
|
||||
|
||||
updateMarquee() {
|
||||
if (!this.marquee) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.marquee.end = this.positionOverPlot;
|
||||
this.marquee.endPixels = this.positionOverElement;
|
||||
},
|
||||
|
||||
startMarquee(event) {
|
||||
this.canvas.classList.remove('plot-drag');
|
||||
this.canvas.classList.add('plot-marquee');
|
||||
|
||||
this.trackMousePosition(event);
|
||||
if (this.positionOverPlot) {
|
||||
this.freeze();
|
||||
this.marquee = {
|
||||
startPixels: this.positionOverElement,
|
||||
endPixels: this.positionOverElement,
|
||||
start: this.positionOverPlot,
|
||||
end: this.positionOverPlot,
|
||||
color: [1, 1, 1, 0.5]
|
||||
};
|
||||
this.rectangles.push(this.marquee);
|
||||
this.trackHistory();
|
||||
}
|
||||
},
|
||||
|
||||
endMarquee() {
|
||||
const startPixels = this.marquee.startPixels;
|
||||
const endPixels = this.marquee.endPixels;
|
||||
const marqueeDistance = Math.sqrt(
|
||||
Math.pow(startPixels.x - endPixels.x, 2)
|
||||
+ Math.pow(startPixels.y - endPixels.y, 2)
|
||||
);
|
||||
// Don't zoom if mouse moved less than 7.5 pixels.
|
||||
if (marqueeDistance > 7.5) {
|
||||
this.config.xAxis.set('displayRange', {
|
||||
min: Math.min(this.marquee.start.x, this.marquee.end.x),
|
||||
max: Math.max(this.marquee.start.x, this.marquee.end.x)
|
||||
});
|
||||
this.config.yAxis.set('displayRange', {
|
||||
min: Math.min(this.marquee.start.y, this.marquee.end.y),
|
||||
max: Math.max(this.marquee.start.y, this.marquee.end.y)
|
||||
});
|
||||
this.userViewportChangeEnd();
|
||||
} else {
|
||||
// A history entry is created by startMarquee, need to remove
|
||||
// if marquee zoom doesn't occur.
|
||||
this.plotHistory.pop();
|
||||
}
|
||||
|
||||
this.rectangles = [];
|
||||
this.marquee = undefined;
|
||||
},
|
||||
|
||||
zoom(zoomDirection, zoomFactor) {
|
||||
const currentXaxis = this.config.xAxis.get('displayRange');
|
||||
const currentYaxis = this.config.yAxis.get('displayRange');
|
||||
|
||||
// when there is no plot data, the ranges can be undefined
|
||||
// in which case we should not perform zoom
|
||||
if (!currentXaxis || !currentYaxis) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.freeze();
|
||||
this.trackHistory();
|
||||
|
||||
const xAxisDist = (currentXaxis.max - currentXaxis.min) * zoomFactor;
|
||||
const yAxisDist = (currentYaxis.max - currentYaxis.min) * zoomFactor;
|
||||
|
||||
if (zoomDirection === 'in') {
|
||||
this.config.xAxis.set('displayRange', {
|
||||
min: currentXaxis.min + xAxisDist,
|
||||
max: currentXaxis.max - xAxisDist
|
||||
});
|
||||
|
||||
this.config.yAxis.set('displayRange', {
|
||||
min: currentYaxis.min + yAxisDist,
|
||||
max: currentYaxis.max - yAxisDist
|
||||
});
|
||||
} else if (zoomDirection === 'out') {
|
||||
this.config.xAxis.set('displayRange', {
|
||||
min: currentXaxis.min - xAxisDist,
|
||||
max: currentXaxis.max + xAxisDist
|
||||
});
|
||||
|
||||
this.config.yAxis.set('displayRange', {
|
||||
min: currentYaxis.min - yAxisDist,
|
||||
max: currentYaxis.max + yAxisDist
|
||||
});
|
||||
}
|
||||
|
||||
this.userViewportChangeEnd();
|
||||
},
|
||||
|
||||
wheelZoom(event) {
|
||||
const ZOOM_AMT = 0.1;
|
||||
event.preventDefault();
|
||||
|
||||
if (!this.positionOverPlot) {
|
||||
return;
|
||||
}
|
||||
|
||||
let xDisplayRange = this.config.xAxis.get('displayRange');
|
||||
let yDisplayRange = this.config.yAxis.get('displayRange');
|
||||
|
||||
// when there is no plot data, the ranges can be undefined
|
||||
// in which case we should not perform zoom
|
||||
if (!xDisplayRange || !yDisplayRange) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.freeze();
|
||||
window.clearTimeout(this.stillZooming);
|
||||
|
||||
let xAxisDist = (xDisplayRange.max - xDisplayRange.min);
|
||||
let yAxisDist = (yDisplayRange.max - yDisplayRange.min);
|
||||
let xDistMouseToMax = xDisplayRange.max - this.positionOverPlot.x;
|
||||
let xDistMouseToMin = this.positionOverPlot.x - xDisplayRange.min;
|
||||
let yDistMouseToMax = yDisplayRange.max - this.positionOverPlot.y;
|
||||
let yDistMouseToMin = this.positionOverPlot.y - yDisplayRange.min;
|
||||
let xAxisMaxDist = xDistMouseToMax / xAxisDist;
|
||||
let xAxisMinDist = xDistMouseToMin / xAxisDist;
|
||||
let yAxisMaxDist = yDistMouseToMax / yAxisDist;
|
||||
let yAxisMinDist = yDistMouseToMin / yAxisDist;
|
||||
|
||||
let plotHistoryStep;
|
||||
|
||||
if (!plotHistoryStep) {
|
||||
plotHistoryStep = {
|
||||
x: xDisplayRange,
|
||||
y: yDisplayRange
|
||||
};
|
||||
}
|
||||
|
||||
if (event.wheelDelta < 0) {
|
||||
|
||||
this.config.xAxis.set('displayRange', {
|
||||
min: xDisplayRange.min + ((xAxisDist * ZOOM_AMT) * xAxisMinDist),
|
||||
max: xDisplayRange.max - ((xAxisDist * ZOOM_AMT) * xAxisMaxDist)
|
||||
});
|
||||
|
||||
this.config.yAxis.set('displayRange', {
|
||||
min: yDisplayRange.min + ((yAxisDist * ZOOM_AMT) * yAxisMinDist),
|
||||
max: yDisplayRange.max - ((yAxisDist * ZOOM_AMT) * yAxisMaxDist)
|
||||
});
|
||||
} else if (event.wheelDelta >= 0) {
|
||||
|
||||
this.config.xAxis.set('displayRange', {
|
||||
min: xDisplayRange.min - ((xAxisDist * ZOOM_AMT) * xAxisMinDist),
|
||||
max: xDisplayRange.max + ((xAxisDist * ZOOM_AMT) * xAxisMaxDist)
|
||||
});
|
||||
|
||||
this.config.yAxis.set('displayRange', {
|
||||
min: yDisplayRange.min - ((yAxisDist * ZOOM_AMT) * yAxisMinDist),
|
||||
max: yDisplayRange.max + ((yAxisDist * ZOOM_AMT) * yAxisMaxDist)
|
||||
});
|
||||
}
|
||||
|
||||
this.stillZooming = window.setTimeout(function () {
|
||||
this.plotHistory.push(plotHistoryStep);
|
||||
plotHistoryStep = undefined;
|
||||
this.userViewportChangeEnd();
|
||||
}.bind(this), 250);
|
||||
},
|
||||
|
||||
startPan(event) {
|
||||
this.canvas.classList.add('plot-drag');
|
||||
this.canvas.classList.remove('plot-marquee');
|
||||
|
||||
this.trackMousePosition(event);
|
||||
this.freeze();
|
||||
this.pan = {
|
||||
start: this.positionOverPlot
|
||||
};
|
||||
event.preventDefault();
|
||||
this.trackHistory();
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
updatePan() {
|
||||
// calculate offset between points. Apply that offset to viewport.
|
||||
if (!this.pan) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dX = this.pan.start.x - this.positionOverPlot.x;
|
||||
const dY = this.pan.start.y - this.positionOverPlot.y;
|
||||
const xRange = this.config.xAxis.get('displayRange');
|
||||
const yRange = this.config.yAxis.get('displayRange');
|
||||
|
||||
this.config.xAxis.set('displayRange', {
|
||||
min: xRange.min + dX,
|
||||
max: xRange.max + dX
|
||||
});
|
||||
this.config.yAxis.set('displayRange', {
|
||||
min: yRange.min + dY,
|
||||
max: yRange.max + dY
|
||||
});
|
||||
},
|
||||
|
||||
trackHistory() {
|
||||
this.plotHistory.push({
|
||||
x: this.config.xAxis.get('displayRange'),
|
||||
y: this.config.yAxis.get('displayRange')
|
||||
});
|
||||
},
|
||||
|
||||
endPan() {
|
||||
this.pan = undefined;
|
||||
this.userViewportChangeEnd();
|
||||
},
|
||||
|
||||
freeze() {
|
||||
this.config.yAxis.set('frozen', true);
|
||||
this.config.xAxis.set('frozen', true);
|
||||
},
|
||||
|
||||
clear() {
|
||||
this.config.yAxis.set('frozen', false);
|
||||
this.config.xAxis.set('frozen', false);
|
||||
this.plotHistory = [];
|
||||
this.userViewportChangeEnd();
|
||||
},
|
||||
|
||||
back() {
|
||||
const previousAxisRanges = this.plotHistory.pop();
|
||||
if (this.plotHistory.length === 0) {
|
||||
this.clear();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.config.xAxis.set('displayRange', previousAxisRanges.x);
|
||||
this.config.yAxis.set('displayRange', previousAxisRanges.y);
|
||||
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);
|
||||
},
|
||||
|
||||
destroy() {
|
||||
configStore.deleteStore(this.config.id);
|
||||
|
||||
this.stopListening();
|
||||
if (this.checkForSize) {
|
||||
clearInterval(this.checkForSize);
|
||||
delete this.checkForSize;
|
||||
}
|
||||
|
||||
if (this.filterObserver) {
|
||||
this.filterObserver();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
269
src/plugins/plot/vue/single/MctTicks.vue
Normal file
269
src/plugins/plot/vue/single/MctTicks.vue
Normal file
@@ -0,0 +1,269 @@
|
||||
<!--
|
||||
Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
as represented by the Administrator of the National Aeronautics and Space
|
||||
Administration. All rights reserved.
|
||||
|
||||
Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0.
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
License for the specific language governing permissions and limitations
|
||||
under the License.
|
||||
|
||||
Open MCT includes source code licensed under additional open source
|
||||
licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
this source code distribution or the Licensing information page available
|
||||
at runtime from the About dialog for additional information.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div ref="tickContainer"
|
||||
class="u-contents js-ticks"
|
||||
>
|
||||
<div v-if="position === 'left'"
|
||||
class="gl-plot-tick-wrapper"
|
||||
>
|
||||
<div v-for="tick in ticks"
|
||||
:key="tick.value"
|
||||
class="gl-plot-tick gl-plot-x-tick-label"
|
||||
:style="{
|
||||
left: (100 * (tick.value - min) / interval) + '%'
|
||||
}"
|
||||
:title="tick.fullText || tick.text"
|
||||
>
|
||||
{{ tick.text }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="position === 'top'"
|
||||
class="gl-plot-tick-wrapper"
|
||||
>
|
||||
<div v-for="tick in ticks"
|
||||
:key="tick.value"
|
||||
class="gl-plot-tick gl-plot-y-tick-label"
|
||||
:style="{ top: (100 * (max - tick.value) / interval) + '%' }"
|
||||
:title="tick.fullText || tick.text"
|
||||
style="margin-top: -0.50em; direction: ltr;"
|
||||
>
|
||||
<span>{{ tick.text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- grid lines follow -->
|
||||
<template v-if="position === 'right'">
|
||||
<div v-for="tick in ticks"
|
||||
:key="tick.value"
|
||||
class="gl-plot-hash hash-v"
|
||||
:style="{
|
||||
right: (100 * (max - tick.value) / interval) + '%',
|
||||
height: '100%'
|
||||
}"
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="position === 'bottom'">
|
||||
<div v-for="tick in ticks"
|
||||
:key="tick.value"
|
||||
class="gl-plot-hash hash-h"
|
||||
:style="{ bottom: (100 * (tick.value - min) / interval) + '%', width: '100%' }"
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import eventHelpers from "./lib/eventHelpers";
|
||||
import { ticks, commonPrefix, commonSuffix } from "./tickUtils";
|
||||
import configStore from "./configuration/configStore";
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'domainObject'],
|
||||
props: {
|
||||
axisType: {
|
||||
type: String,
|
||||
default() {
|
||||
return '';
|
||||
},
|
||||
required: true
|
||||
},
|
||||
position: {
|
||||
required: true,
|
||||
type: String,
|
||||
default() {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
ticks: []
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
eventHelpers.extend(this);
|
||||
|
||||
this.axis = this.getAxisFromConfig();
|
||||
|
||||
this.tickCount = 4;
|
||||
this.tickUpdate = false;
|
||||
this.listenTo(this.axis, 'change:displayRange', this.updateTicks, this);
|
||||
this.listenTo(this.axis, 'change:format', this.updateTicks, this);
|
||||
this.listenTo(this.axis, 'change:key', this.updateTicksForceRegeneration, this);
|
||||
this.updateTicks();
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.stopListening();
|
||||
},
|
||||
methods: {
|
||||
getAxisFromConfig() {
|
||||
if (!this.axisType) {
|
||||
return;
|
||||
}
|
||||
|
||||
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
let config = configStore.get(configId);
|
||||
if (config) {
|
||||
return config[this.axisType];
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Determine whether ticks should be regenerated for a given range.
|
||||
* Ticks are updated
|
||||
* a) if they don't exist,
|
||||
* b) if existing ticks are outside of given range,
|
||||
* c) if range exceeds size of tick range by more than one tick step,
|
||||
* d) if forced to regenerate (ex. changing x-axis metadata).
|
||||
*
|
||||
*/
|
||||
shouldRegenerateTicks(range, forceRegeneration) {
|
||||
if (forceRegeneration) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!this.tickRange || !this.ticks || !this.ticks.length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.tickRange.max > range.max || this.tickRange.min < range.min) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Math.abs(range.max - this.tickRange.max) > this.tickRange.step) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Math.abs(this.tickRange.min - range.min) > this.tickRange.step) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
getTicks() {
|
||||
const number = this.tickCount;
|
||||
const clampRange = this.axis.get('values');
|
||||
const range = this.axis.get('displayRange');
|
||||
if (clampRange) {
|
||||
return clampRange.filter(function (value) {
|
||||
return value <= range.max && value >= range.min;
|
||||
}, this);
|
||||
}
|
||||
|
||||
return ticks(range.min, range.max, number);
|
||||
},
|
||||
|
||||
updateTicksForceRegeneration() {
|
||||
this.updateTicks(true);
|
||||
},
|
||||
|
||||
updateTicks(forceRegeneration = false) {
|
||||
const range = this.axis.get('displayRange');
|
||||
if (!range) {
|
||||
delete this.min;
|
||||
delete this.max;
|
||||
delete this.interval;
|
||||
delete this.tickRange;
|
||||
this.ticks = [];
|
||||
delete this.shouldCheckWidth;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const format = this.axis.get('format');
|
||||
if (!format) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.min = range.min;
|
||||
this.max = range.max;
|
||||
this.interval = Math.abs(range.min - range.max);
|
||||
if (this.shouldRegenerateTicks(range, forceRegeneration)) {
|
||||
let newTicks = this.getTicks();
|
||||
this.tickRange = {
|
||||
min: Math.min.apply(Math, newTicks),
|
||||
max: Math.max.apply(Math, newTicks),
|
||||
step: newTicks[1] - newTicks[0]
|
||||
};
|
||||
|
||||
newTicks = newTicks
|
||||
.map(function (tickValue) {
|
||||
return {
|
||||
value: tickValue,
|
||||
text: format(tickValue)
|
||||
};
|
||||
}, this);
|
||||
|
||||
if (newTicks.length && typeof newTicks[0].text === 'string') {
|
||||
const tickText = newTicks.map(function (t) {
|
||||
return t.text;
|
||||
});
|
||||
const prefix = tickText.reduce(commonPrefix);
|
||||
const suffix = tickText.reduce(commonSuffix);
|
||||
newTicks.forEach(function (t) {
|
||||
t.fullText = t.text;
|
||||
if (suffix.length) {
|
||||
t.text = t.text.slice(prefix.length, -suffix.length);
|
||||
} else {
|
||||
t.text = t.text.slice(prefix.length);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.ticks = newTicks;
|
||||
this.shouldCheckWidth = true;
|
||||
}
|
||||
|
||||
this.scheduleTickUpdate();
|
||||
},
|
||||
|
||||
scheduleTickUpdate() {
|
||||
if (this.tickUpdate) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.tickUpdate = true;
|
||||
setTimeout(this.doTickUpdate.bind(this), 0);
|
||||
},
|
||||
|
||||
doTickUpdate() {
|
||||
if (this.shouldCheckWidth) {
|
||||
const tickElements = this.$refs.tickContainer.querySelectorAll('.gl-plot-tick > span');
|
||||
|
||||
const tickWidth = Number([].reduce.call(tickElements, function (memo, first) {
|
||||
return Math.max(memo, first.offsetWidth);
|
||||
}, 0));
|
||||
|
||||
this.tickWidth = tickWidth;
|
||||
this.$emit('plotTickWidth', tickWidth);
|
||||
this.shouldCheckWidth = false;
|
||||
}
|
||||
|
||||
this.tickUpdate = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
125
src/plugins/plot/vue/single/Plot.vue
Normal file
125
src/plugins/plot/vue/single/Plot.vue
Normal file
@@ -0,0 +1,125 @@
|
||||
<!--
|
||||
Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
as represented by the Administrator of the National Aeronautics and Space
|
||||
Administration. All rights reserved.
|
||||
|
||||
Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0.
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
License for the specific language governing permissions and limitations
|
||||
under the License.
|
||||
|
||||
Open MCT includes source code licensed under additional open source
|
||||
licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
this source code distribution or the Licensing information page available
|
||||
at runtime from the About dialog for additional information.
|
||||
-->
|
||||
<template>
|
||||
<div ref="plotWrapper"
|
||||
class="c-plot holder holder-plot has-control-bar"
|
||||
>
|
||||
<div class="c-control-bar">
|
||||
<span class="c-button-set c-button-set--strip-h">
|
||||
<button class="c-button icon-download"
|
||||
title="Export This View's Data as PNG"
|
||||
@click="exportPNG()"
|
||||
>
|
||||
<span class="c-button__label">PNG</span>
|
||||
</button>
|
||||
<button class="c-button"
|
||||
title="Export This View's Data as JPG"
|
||||
@click="exportJPG()"
|
||||
>
|
||||
<span class="c-button__label">JPG</span>
|
||||
</button>
|
||||
</span>
|
||||
<button class="c-button icon-crosshair"
|
||||
:class="{ 'is-active': cursorGuide }"
|
||||
title="Toggle cursor guides"
|
||||
@click="toggleCursorGuide"
|
||||
>
|
||||
</button>
|
||||
<button class="c-button"
|
||||
:class="{ 'icon-grid-on': gridLines, 'icon-grid-off': !gridLines }"
|
||||
title="Toggle grid lines"
|
||||
@click="toggleGridLines"
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div ref="plotContainer"
|
||||
class="l-view-section u-style-receiver js-style-receiver"
|
||||
>
|
||||
<div v-show="!!loading"
|
||||
class="c-loading--overlay loading"
|
||||
></div>
|
||||
<mct-plot :grid-lines="gridLines"
|
||||
:cursor-guide="cursorGuide"
|
||||
@loadingUpdated="loadingUpdated"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import eventHelpers from "./lib/eventHelpers";
|
||||
import MctPlot from './MctPlot.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MctPlot
|
||||
},
|
||||
inject: ['openmct', 'domainObject'],
|
||||
data() {
|
||||
return {
|
||||
//Don't think we need this as it appears to be stacked plot specific
|
||||
// hideExportButtons: false
|
||||
cursorGuide: false,
|
||||
gridLines: true,
|
||||
loading: false
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
eventHelpers.extend(this);
|
||||
|
||||
this.exportImageService = this.openmct.$injector.get('exportImageService');
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.destroy();
|
||||
},
|
||||
methods: {
|
||||
loadingUpdated(loading) {
|
||||
this.loading = loading;
|
||||
},
|
||||
destroy() {
|
||||
this.stopListening();
|
||||
},
|
||||
|
||||
exportJPG() {
|
||||
const plotElement = this.$refs.plotContainer;
|
||||
|
||||
this.exportImageService.exportJPG(plotElement, 'plot.jpg', 'export-plot');
|
||||
},
|
||||
|
||||
exportPNG() {
|
||||
const plotElement = this.$refs.plotContainer;
|
||||
|
||||
this.exportImageService.exportPNG(plotElement, 'plot.png', 'export-plot');
|
||||
},
|
||||
|
||||
toggleCursorGuide() {
|
||||
this.cursorGuide = !this.cursorGuide;
|
||||
},
|
||||
|
||||
toggleGridLines() {
|
||||
this.gridLines = !this.gridLines;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
74
src/plugins/plot/vue/single/PlotViewProvider.js
Normal file
74
src/plugins/plot/vue/single/PlotViewProvider.js
Normal file
@@ -0,0 +1,74 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import Plot from './Plot.vue';
|
||||
import Vue from 'vue';
|
||||
|
||||
export default function PlotViewProvider(openmct) {
|
||||
function hasTelemetry(domainObject) {
|
||||
if (!Object.prototype.hasOwnProperty.call(domainObject, 'telemetry')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let metadata = openmct.telemetry.getMetadata(domainObject);
|
||||
|
||||
return metadata.values().length > 0 && hasDomainAndRange(metadata);
|
||||
}
|
||||
|
||||
function hasDomainAndRange(metadata) {
|
||||
return (metadata.valuesForHints(['range']).length > 0
|
||||
&& metadata.valuesForHints(['domain']).length > 0);
|
||||
}
|
||||
|
||||
return {
|
||||
key: 'plot-single',
|
||||
name: 'Plot',
|
||||
cssClass: 'icon-telemetry',
|
||||
canView(domainObject) {
|
||||
return domainObject.type === 'plot-single' || hasTelemetry(domainObject);
|
||||
},
|
||||
|
||||
view: function (domainObject) {
|
||||
let component;
|
||||
|
||||
return {
|
||||
show: function (element) {
|
||||
component = new Vue({
|
||||
el: element,
|
||||
components: {
|
||||
Plot
|
||||
},
|
||||
provide: {
|
||||
openmct,
|
||||
domainObject
|
||||
},
|
||||
template: '<plot></plot>'
|
||||
});
|
||||
},
|
||||
destroy: function () {
|
||||
component.$destroy();
|
||||
component = undefined;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
151
src/plugins/plot/vue/single/axis/XAxis.vue
Normal file
151
src/plugins/plot/vue/single/axis/XAxis.vue
Normal file
@@ -0,0 +1,151 @@
|
||||
<!--
|
||||
Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
as represented by the Administrator of the National Aeronautics and Space
|
||||
Administration. All rights reserved.
|
||||
|
||||
Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0.
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
License for the specific language governing permissions and limitations
|
||||
under the License.
|
||||
|
||||
Open MCT includes source code licensed under additional open source
|
||||
licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
this source code distribution or the Licensing information page available
|
||||
at runtime from the About dialog for additional information.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div v-if="loaded"
|
||||
class="gl-plot-axis-area gl-plot-x has-local-controls"
|
||||
>
|
||||
<mct-ticks :axis-type="'xAxis'"
|
||||
:position="'left'"
|
||||
@plotTickWidth="onTickWidthChange"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="gl-plot-label gl-plot-x-label"
|
||||
:class="{'icon-gear': isEnabledXKeyToggle()}"
|
||||
>
|
||||
{{ xAxisLabel }}
|
||||
</div>
|
||||
|
||||
<select
|
||||
v-show="isEnabledXKeyToggle()"
|
||||
v-model="selectedXKeyOptionKey"
|
||||
class="gl-plot-x-label__select local-controls--hidden"
|
||||
@change="toggleXKeyOption()"
|
||||
>
|
||||
<option v-for="option in xKeyOptions"
|
||||
:key="option.key"
|
||||
:value="option.key"
|
||||
>{{ option.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MctTicks from "../MctTicks.vue";
|
||||
import eventHelpers from '../lib/eventHelpers';
|
||||
import configStore from "../configuration/configStore";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MctTicks
|
||||
},
|
||||
inject: ['openmct', 'domainObject'],
|
||||
props: {
|
||||
seriesModel: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedXKeyOptionKey: '',
|
||||
xKeyOptions: [],
|
||||
xAxis: {},
|
||||
loaded: false,
|
||||
xAxisLabel: ''
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
eventHelpers.extend(this);
|
||||
this.xAxis = this.getXAxisFromConfig();
|
||||
this.loaded = true;
|
||||
this.setUpXAxisOptions();
|
||||
this.openmct.time.on('timeSystem', this.syncXAxisToTimeSystem);
|
||||
this.listenTo(this.xAxis, 'change', this.setUpXAxisOptions);
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.openmct.time.off('timeSystem', this.syncXAxisToTimeSystem);
|
||||
},
|
||||
methods: {
|
||||
isEnabledXKeyToggle() {
|
||||
const isSinglePlot = this.xKeyOptions && this.xKeyOptions.length > 1 && this.seriesModel;
|
||||
const isFrozen = this.xAxis.get('frozen');
|
||||
const inRealTimeMode = this.openmct.time.clock();
|
||||
|
||||
return isSinglePlot && !isFrozen && !inRealTimeMode;
|
||||
},
|
||||
getXAxisFromConfig() {
|
||||
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
let config = configStore.get(configId);
|
||||
if (config) {
|
||||
return config.xAxis;
|
||||
}
|
||||
},
|
||||
toggleXKeyOption() {
|
||||
const selectedXKey = this.selectedXKeyOptionKey;
|
||||
const dataForSelectedXKey = this.seriesModel.data
|
||||
? this.seriesModel.data[0][selectedXKey]
|
||||
: undefined;
|
||||
|
||||
if (dataForSelectedXKey !== undefined) {
|
||||
this.xAxis.set('key', selectedXKey);
|
||||
} else {
|
||||
this.openmct.notifications.error('Cannot change x-axis view as no data exists for this view type.');
|
||||
const xAxisKey = this.xAxis.get('key');
|
||||
this.selectedXKeyOptionKey = this.getXKeyOption(xAxisKey).key;
|
||||
}
|
||||
},
|
||||
getXKeyOption(key) {
|
||||
return this.xKeyOptions.find(option => option.key === key);
|
||||
},
|
||||
syncXAxisToTimeSystem(timeSystem) {
|
||||
const xAxisKey = this.xAxis.get('key');
|
||||
if (xAxisKey !== timeSystem.key) {
|
||||
this.xAxis.set('key', timeSystem.key);
|
||||
this.xAxis.resetSeries();
|
||||
this.setUpXAxisOptions();
|
||||
}
|
||||
},
|
||||
setUpXAxisOptions() {
|
||||
const xAxisKey = this.xAxis.get('key');
|
||||
|
||||
this.xKeyOptions = this.seriesModel.metadata
|
||||
.valuesForHints(['domain'])
|
||||
.map(function (o) {
|
||||
return {
|
||||
name: o.name,
|
||||
key: o.key
|
||||
};
|
||||
});
|
||||
this.xAxisLabel = this.xAxis.get('label');
|
||||
this.selectedXKeyOptionKey = this.getXKeyOption(xAxisKey).key;
|
||||
},
|
||||
onTickWidthChange(width) {
|
||||
this.$emit('tickWidthChanged', width);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
137
src/plugins/plot/vue/single/axis/YAxis.vue
Normal file
137
src/plugins/plot/vue/single/axis/YAxis.vue
Normal file
@@ -0,0 +1,137 @@
|
||||
<!--
|
||||
Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
as represented by the Administrator of the National Aeronautics and Space
|
||||
Administration. All rights reserved.
|
||||
|
||||
Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0.
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
License for the specific language governing permissions and limitations
|
||||
under the License.
|
||||
|
||||
Open MCT includes source code licensed under additional open source
|
||||
licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
this source code distribution or the Licensing information page available
|
||||
at runtime from the About dialog for additional information.
|
||||
-->
|
||||
<template>
|
||||
<div v-if="loaded"
|
||||
class="gl-plot-axis-area gl-plot-y has-local-controls"
|
||||
:style="{
|
||||
width: (tickWidth + 20) + 'px'
|
||||
}"
|
||||
>
|
||||
|
||||
<div v-if="singleSeries"
|
||||
class="gl-plot-label gl-plot-y-label"
|
||||
:class="{'icon-gear': (yKeyOptions.length > 1)}"
|
||||
>{{ yAxisLabel }}
|
||||
</div>
|
||||
|
||||
<select v-if="yKeyOptions.length > 1 && singleSeries"
|
||||
v-model="yAxisLabel"
|
||||
class="gl-plot-y-label__select local-controls--hidden"
|
||||
@change="toggleYAxisLabel"
|
||||
>
|
||||
<option v-for="(option, index) in yKeyOptions"
|
||||
:key="index"
|
||||
:value="option.name"
|
||||
:selected="option.name === yAxisLabel"
|
||||
>
|
||||
{{ option.name }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<mct-ticks :axis-type="'yAxis'"
|
||||
class="gl-plot-ticks"
|
||||
:position="'top'"
|
||||
@plotTickWidth="onTickWidthChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MctTicks from "../MctTicks.vue";
|
||||
import configStore from "../configuration/configStore";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MctTicks
|
||||
},
|
||||
inject: ['openmct', 'domainObject'],
|
||||
props: {
|
||||
singleSeries: {
|
||||
type: Boolean,
|
||||
default() {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
seriesModel: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
tickWidth: {
|
||||
type: Number,
|
||||
default() {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
yAxisLabel: 'none',
|
||||
loaded: false
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.yAxis = this.getYAxisFromConfig();
|
||||
this.loaded = true;
|
||||
this.setUpYAxisOptions();
|
||||
},
|
||||
methods: {
|
||||
getYAxisFromConfig() {
|
||||
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
let config = configStore.get(configId);
|
||||
if (config) {
|
||||
return config.yAxis;
|
||||
}
|
||||
},
|
||||
setUpYAxisOptions() {
|
||||
this.yKeyOptions = this.seriesModel.metadata
|
||||
.valuesForHints(['range'])
|
||||
.map(function (o) {
|
||||
return {
|
||||
name: o.name,
|
||||
key: o.key
|
||||
};
|
||||
});
|
||||
|
||||
// set yAxisLabel if none is set yet
|
||||
if (this.yAxisLabel === 'none') {
|
||||
let yKey = this.seriesModel.model.yKey;
|
||||
let yKeyModel = this.yKeyOptions.filter(o => o.key === yKey)[0];
|
||||
|
||||
this.yAxisLabel = yKeyModel.name;
|
||||
}
|
||||
},
|
||||
toggleYAxisLabel() {
|
||||
let yAxisObject = this.yKeyOptions.filter(o => o.name === this.yAxisLabel)[0];
|
||||
|
||||
if (yAxisObject) {
|
||||
this.$emit('yKeyChanged', yAxisObject.key);
|
||||
this.yAxis.set('label', this.yAxisLabel);
|
||||
}
|
||||
},
|
||||
onTickWidthChange(width) {
|
||||
this.$emit('tickWidthChanged', width);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
67
src/plugins/plot/vue/single/chart/MCTChartAlarmPointSet.js
Normal file
67
src/plugins/plot/vue/single/chart/MCTChartAlarmPointSet.js
Normal file
@@ -0,0 +1,67 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import eventHelpers from '../lib/eventHelpers';
|
||||
|
||||
export default class MCTChartAlarmPointSet {
|
||||
constructor(series, chart, offset) {
|
||||
this.series = series;
|
||||
this.chart = chart;
|
||||
this.offset = offset;
|
||||
this.points = [];
|
||||
|
||||
eventHelpers.extend(this);
|
||||
|
||||
this.listenTo(series, 'add', this.append, this);
|
||||
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.append(point, index, series);
|
||||
}, this);
|
||||
}
|
||||
|
||||
append(datum) {
|
||||
if (datum.mctLimitState) {
|
||||
this.points.push({
|
||||
x: this.offset.xVal(datum, this.series),
|
||||
y: this.offset.yVal(datum, this.series),
|
||||
datum: datum
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
remove(datum) {
|
||||
this.points = this.points.filter(function (p) {
|
||||
return p.datum !== datum;
|
||||
});
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.points = [];
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.stopListening();
|
||||
}
|
||||
}
|
||||
31
src/plugins/plot/vue/single/chart/MCTChartLineLinear.js
Normal file
31
src/plugins/plot/vue/single/chart/MCTChartLineLinear.js
Normal file
@@ -0,0 +1,31 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import MCTChartSeriesElement from './MCTChartSeriesElement';
|
||||
|
||||
export default class MCTChartLineLinear extends MCTChartSeriesElement {
|
||||
addPoint(point, start, count) {
|
||||
this.buffer[start] = point.x;
|
||||
this.buffer[start + 1] = point.y;
|
||||
}
|
||||
}
|
||||
|
||||
74
src/plugins/plot/vue/single/chart/MCTChartLineStepAfter.js
Normal file
74
src/plugins/plot/vue/single/chart/MCTChartLineStepAfter.js
Normal file
@@ -0,0 +1,74 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import MCTChartSeriesElement from './MCTChartSeriesElement';
|
||||
|
||||
export default class MCTChartLineStepAfter extends MCTChartSeriesElement {
|
||||
removePoint(point, index, count) {
|
||||
if (index > 0 && index / 2 < this.count) {
|
||||
this.buffer[index + 1] = this.buffer[index - 1];
|
||||
}
|
||||
}
|
||||
|
||||
vertexCountForPointAtIndex(index) {
|
||||
if (index === 0 && this.count === 0) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
return 4;
|
||||
}
|
||||
|
||||
startIndexForPointAtIndex(index) {
|
||||
if (index === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 2 + ((index - 1) * 4);
|
||||
}
|
||||
|
||||
addPoint(point, start, count) {
|
||||
if (start === 0 && this.count === 0) {
|
||||
// First point is easy.
|
||||
this.buffer[start] = point.x;
|
||||
this.buffer[start + 1] = point.y; // one point
|
||||
} else if (start === 0 && this.count > 0) {
|
||||
// Unshifting requires adding an extra point.
|
||||
this.buffer[start] = point.x;
|
||||
this.buffer[start + 1] = point.y;
|
||||
this.buffer[start + 2] = this.buffer[start + 4];
|
||||
this.buffer[start + 3] = point.y;
|
||||
} else {
|
||||
// Appending anywhere in line, insert standard two points.
|
||||
this.buffer[start] = point.x;
|
||||
this.buffer[start + 1] = this.buffer[start - 1];
|
||||
this.buffer[start + 2] = point.x;
|
||||
this.buffer[start + 3] = point.y;
|
||||
|
||||
if (start < this.count * 2) {
|
||||
// Insert into the middle, need to update the following
|
||||
// point.
|
||||
this.buffer[start + 5] = point.y;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
32
src/plugins/plot/vue/single/chart/MCTChartPointSet.js
Normal file
32
src/plugins/plot/vue/single/chart/MCTChartPointSet.js
Normal file
@@ -0,0 +1,32 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import MCTChartSeriesElement from './MCTChartSeriesElement';
|
||||
|
||||
// TODO: Is this needed? This is identical to MCTChartLineLinear. Why is it a different class?
|
||||
export default class MCTChartPointSet extends MCTChartSeriesElement {
|
||||
addPoint(point, start, count) {
|
||||
this.buffer[start] = point.x;
|
||||
this.buffer[start + 1] = point.y;
|
||||
}
|
||||
}
|
||||
|
||||
157
src/plugins/plot/vue/single/chart/MCTChartSeriesElement.js
Normal file
157
src/plugins/plot/vue/single/chart/MCTChartSeriesElement.js
Normal file
@@ -0,0 +1,157 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import eventHelpers from '../lib/eventHelpers';
|
||||
|
||||
export default class MCTChartSeriesElement {
|
||||
constructor(series, chart, offset) {
|
||||
this.series = series;
|
||||
this.chart = chart;
|
||||
this.offset = offset;
|
||||
this.buffer = new Float32Array(20000);
|
||||
this.count = 0;
|
||||
|
||||
eventHelpers.extend(this);
|
||||
|
||||
this.listenTo(series, 'add', this.append, this);
|
||||
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.append(point, index, series);
|
||||
}, this);
|
||||
}
|
||||
|
||||
getBuffer() {
|
||||
if (this.isTempBuffer) {
|
||||
this.buffer = new Float32Array(this.buffer);
|
||||
this.isTempBuffer = false;
|
||||
}
|
||||
|
||||
return this.buffer;
|
||||
}
|
||||
|
||||
color() {
|
||||
return this.series.get('color');
|
||||
}
|
||||
|
||||
vertexCountForPointAtIndex(index) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
startIndexForPointAtIndex(index) {
|
||||
return 2 * index;
|
||||
}
|
||||
|
||||
removeSegments(index, count) {
|
||||
const target = index;
|
||||
const start = index + count;
|
||||
const end = this.count * 2;
|
||||
this.buffer.copyWithin(target, start, end);
|
||||
for (let zero = end - count; zero < end; zero++) {
|
||||
this.buffer[zero] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
removePoint(point, index, count) {
|
||||
// by default, do nothing.
|
||||
}
|
||||
|
||||
remove(point, index, series) {
|
||||
const vertexCount = this.vertexCountForPointAtIndex(index);
|
||||
const removalPoint = this.startIndexForPointAtIndex(index);
|
||||
|
||||
this.removeSegments(removalPoint, vertexCount);
|
||||
|
||||
this.removePoint(
|
||||
this.makePoint(point, series),
|
||||
removalPoint,
|
||||
vertexCount
|
||||
);
|
||||
this.count -= (vertexCount / 2);
|
||||
}
|
||||
|
||||
makePoint(point, series) {
|
||||
if (!this.offset.xVal) {
|
||||
this.chart.setOffset(point, undefined, series);
|
||||
}
|
||||
|
||||
return {
|
||||
x: this.offset.xVal(point, series),
|
||||
y: this.offset.yVal(point, series)
|
||||
};
|
||||
}
|
||||
|
||||
append(point, index, series) {
|
||||
const pointsRequired = this.vertexCountForPointAtIndex(index);
|
||||
const insertionPoint = this.startIndexForPointAtIndex(index);
|
||||
this.growIfNeeded(pointsRequired);
|
||||
this.makeInsertionPoint(insertionPoint, pointsRequired);
|
||||
this.addPoint(
|
||||
this.makePoint(point, series),
|
||||
insertionPoint,
|
||||
pointsRequired
|
||||
);
|
||||
this.count += (pointsRequired / 2);
|
||||
}
|
||||
|
||||
makeInsertionPoint(insertionPoint, pointsRequired) {
|
||||
if (this.count * 2 > insertionPoint) {
|
||||
if (!this.isTempBuffer) {
|
||||
this.buffer = Array.prototype.slice.apply(this.buffer);
|
||||
this.isTempBuffer = true;
|
||||
}
|
||||
|
||||
const target = insertionPoint + pointsRequired;
|
||||
let start = insertionPoint;
|
||||
for (; start < target; start++) {
|
||||
this.buffer.splice(start, 0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.buffer = new Float32Array(20000);
|
||||
this.count = 0;
|
||||
if (this.offset.x) {
|
||||
this.series.data.forEach(function (point, index) {
|
||||
this.append(point, index, this.series);
|
||||
}, this);
|
||||
}
|
||||
}
|
||||
|
||||
growIfNeeded(pointsRequired) {
|
||||
const remainingPoints = this.buffer.length - this.count * 2;
|
||||
let temp;
|
||||
|
||||
if (remainingPoints <= pointsRequired) {
|
||||
temp = new Float32Array(this.buffer.length + 20000);
|
||||
temp.set(this.buffer);
|
||||
this.buffer = temp;
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.stopListening();
|
||||
}
|
||||
|
||||
}
|
||||
449
src/plugins/plot/vue/single/chart/MctChart.vue
Normal file
449
src/plugins/plot/vue/single/chart/MctChart.vue
Normal file
@@ -0,0 +1,449 @@
|
||||
<!--
|
||||
Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
as represented by the Administrator of the National Aeronautics and Space
|
||||
Administration. All rights reserved.
|
||||
|
||||
Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0.
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
License for the specific language governing permissions and limitations
|
||||
under the License.
|
||||
|
||||
Open MCT includes source code licensed under additional open source
|
||||
licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
this source code distribution or the Licensing information page available
|
||||
at runtime from the About dialog for additional information.
|
||||
-->
|
||||
<template>
|
||||
<div class="gl-plot-chart-area">
|
||||
<span v-html="canvasTemplate"></span>
|
||||
<span v-html="canvasTemplate"></span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import eventHelpers from "../lib/eventHelpers";
|
||||
import { DrawLoader } from '../draw/DrawLoader';
|
||||
import MCTChartLineLinear from './MCTChartLineLinear';
|
||||
import MCTChartLineStepAfter from './MCTChartLineStepAfter';
|
||||
import MCTChartPointSet from './MCTChartPointSet';
|
||||
import MCTChartAlarmPointSet from './MCTChartAlarmPointSet';
|
||||
import configStore from "../configuration/configStore";
|
||||
import PlotConfigurationModel from "../configuration/PlotConfigurationModel";
|
||||
|
||||
const MARKER_SIZE = 6.0;
|
||||
const HIGHLIGHT_SIZE = MARKER_SIZE * 2.0;
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'domainObject'],
|
||||
props: {
|
||||
rectangles: {
|
||||
type: Array,
|
||||
default() {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
highlights: {
|
||||
type: Array,
|
||||
default() {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
canvasTemplate: '<canvas style="position: absolute; background: none; width: 100%; height: 100%;"></canvas>'
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
highlights() {
|
||||
this.scheduleDraw();
|
||||
},
|
||||
rectangles() {
|
||||
this.scheduleDraw();
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
eventHelpers.extend(this);
|
||||
|
||||
this.config = this.getConfig();
|
||||
this.isDestroyed = false;
|
||||
this.lines = [];
|
||||
this.pointSets = [];
|
||||
this.alarmSets = [];
|
||||
this.offset = {};
|
||||
this.seriesElements = new WeakMap();
|
||||
|
||||
let canvasEls = this.$parent.$refs.chartContainer.querySelectorAll("canvas");
|
||||
const mainCanvas = canvasEls[1];
|
||||
const overlayCanvas = canvasEls[0];
|
||||
if (this.initializeCanvas(mainCanvas, overlayCanvas)) {
|
||||
this.draw();
|
||||
}
|
||||
|
||||
this.listenTo(this.config.series, 'add', this.onSeriesAdd, this);
|
||||
this.listenTo(this.config.series, 'remove', this.onSeriesRemove, this);
|
||||
this.listenTo(this.config.yAxis, 'change:key', this.clearOffset, this);
|
||||
this.listenTo(this.config.yAxis, 'change', this.scheduleDraw);
|
||||
this.listenTo(this.config.xAxis, 'change', this.scheduleDraw);
|
||||
this.config.series.forEach(this.onSeriesAdd, this);
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.destroy();
|
||||
},
|
||||
methods: {
|
||||
getConfig() {
|
||||
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
let config = configStore.get(configId);
|
||||
if (!config) {
|
||||
config = new PlotConfigurationModel({
|
||||
id: configId,
|
||||
domainObject: this.domainObject,
|
||||
openmct: this.openmct
|
||||
});
|
||||
configStore.add(configId, config);
|
||||
}
|
||||
|
||||
return config;
|
||||
},
|
||||
reDraw(mode, o, series) {
|
||||
this.changeInterpolate(mode, o, series);
|
||||
this.changeMarkers(mode, o, series);
|
||||
this.changeAlarmMarkers(mode, o, series);
|
||||
},
|
||||
onSeriesAdd(series) {
|
||||
this.listenTo(series, 'change:xKey', this.reDraw, this);
|
||||
this.listenTo(series, 'change:interpolate', this.changeInterpolate, this);
|
||||
this.listenTo(series, 'change:markers', this.changeMarkers, this);
|
||||
this.listenTo(series, 'change:alarmMarkers', this.changeAlarmMarkers, this);
|
||||
this.listenTo(series, 'change', this.scheduleDraw);
|
||||
this.listenTo(series, 'add', this.scheduleDraw);
|
||||
this.makeChartElement(series);
|
||||
},
|
||||
changeInterpolate(mode, o, series) {
|
||||
if (mode === o) {
|
||||
return;
|
||||
}
|
||||
|
||||
const elements = this.seriesElements.get(series);
|
||||
elements.lines.forEach(function (line) {
|
||||
this.lines.splice(this.lines.indexOf(line), 1);
|
||||
line.destroy();
|
||||
}, this);
|
||||
elements.lines = [];
|
||||
|
||||
const newLine = this.lineForSeries(series);
|
||||
if (newLine) {
|
||||
elements.lines.push(newLine);
|
||||
this.lines.push(newLine);
|
||||
}
|
||||
},
|
||||
changeAlarmMarkers(mode, o, series) {
|
||||
if (mode === o) {
|
||||
return;
|
||||
}
|
||||
|
||||
const elements = this.seriesElements.get(series);
|
||||
if (elements.alarmSet) {
|
||||
elements.alarmSet.destroy();
|
||||
this.alarmSets.splice(this.alarmSets.indexOf(elements.alarmSet), 1);
|
||||
}
|
||||
|
||||
elements.alarmSet = this.alarmPointSetForSeries(series);
|
||||
if (elements.alarmSet) {
|
||||
this.alarmSets.push(elements.alarmSet);
|
||||
}
|
||||
},
|
||||
changeMarkers(mode, o, series) {
|
||||
if (mode === o) {
|
||||
return;
|
||||
}
|
||||
|
||||
const elements = this.seriesElements.get(series);
|
||||
elements.pointSets.forEach(function (pointSet) {
|
||||
this.pointSets.splice(this.pointSets.indexOf(pointSet), 1);
|
||||
pointSet.destroy();
|
||||
}, this);
|
||||
elements.pointSets = [];
|
||||
|
||||
const pointSet = this.pointSetForSeries(series);
|
||||
if (pointSet) {
|
||||
elements.pointSets.push(pointSet);
|
||||
this.pointSets.push(pointSet);
|
||||
}
|
||||
},
|
||||
onSeriesRemove(series) {
|
||||
this.stopListening(series);
|
||||
this.removeChartElement(series);
|
||||
this.scheduleDraw();
|
||||
},
|
||||
destroy() {
|
||||
this.isDestroyed = true;
|
||||
this.stopListening();
|
||||
this.lines.forEach(line => line.destroy());
|
||||
DrawLoader.releaseDrawAPI(this.drawAPI);
|
||||
},
|
||||
clearOffset() {
|
||||
delete this.offset.x;
|
||||
delete this.offset.y;
|
||||
delete this.offset.xVal;
|
||||
delete this.offset.yVal;
|
||||
delete this.offset.xKey;
|
||||
delete this.offset.yKey;
|
||||
this.lines.forEach(function (line) {
|
||||
line.reset();
|
||||
});
|
||||
this.pointSets.forEach(function (pointSet) {
|
||||
pointSet.reset();
|
||||
});
|
||||
},
|
||||
setOffset(offsetPoint, index, series) {
|
||||
if (this.offset.x && this.offset.y) {
|
||||
return;
|
||||
}
|
||||
|
||||
const offsets = {
|
||||
x: series.getXVal(offsetPoint),
|
||||
y: series.getYVal(offsetPoint)
|
||||
};
|
||||
|
||||
this.offset.x = function (x) {
|
||||
return x - offsets.x;
|
||||
}.bind(this);
|
||||
this.offset.y = function (y) {
|
||||
return y - offsets.y;
|
||||
}.bind(this);
|
||||
this.offset.xVal = function (point, pSeries) {
|
||||
return this.offset.x(pSeries.getXVal(point));
|
||||
}.bind(this);
|
||||
this.offset.yVal = function (point, pSeries) {
|
||||
return this.offset.y(pSeries.getYVal(point));
|
||||
}.bind(this);
|
||||
},
|
||||
initializeCanvas(canvas, overlay) {
|
||||
this.canvas = canvas;
|
||||
this.overlay = overlay;
|
||||
this.drawAPI = DrawLoader.getDrawAPI(canvas, overlay);
|
||||
if (this.drawAPI) {
|
||||
this.listenTo(this.drawAPI, 'error', this.fallbackToCanvas, this);
|
||||
}
|
||||
|
||||
return Boolean(this.drawAPI);
|
||||
},
|
||||
fallbackToCanvas() {
|
||||
this.stopListening(this.drawAPI);
|
||||
DrawLoader.releaseDrawAPI(this.drawAPI);
|
||||
// Have to throw away the old canvas elements and replace with new
|
||||
// canvas elements in order to get new drawing contexts.
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = this.TEMPLATE;
|
||||
const mainCanvas = div.querySelectorAll("canvas")[1];
|
||||
const overlayCanvas = div.querySelectorAll("canvas")[0];
|
||||
this.canvas.parentNode.replaceChild(mainCanvas, this.canvas);
|
||||
this.canvas = mainCanvas;
|
||||
this.overlay.parentNode.replaceChild(overlayCanvas, this.overlay);
|
||||
this.overlay = overlayCanvas;
|
||||
this.drawAPI = DrawLoader.getFallbackDrawAPI(this.canvas, this.overlay);
|
||||
this.$emit('plotReinitializeCanvas');
|
||||
},
|
||||
removeChartElement(series) {
|
||||
const elements = this.seriesElements.get(series);
|
||||
|
||||
elements.lines.forEach(function (line) {
|
||||
this.lines.splice(this.lines.indexOf(line), 1);
|
||||
line.destroy();
|
||||
}, this);
|
||||
elements.pointSets.forEach(function (pointSet) {
|
||||
this.pointSets.splice(this.pointSets.indexOf(pointSet), 1);
|
||||
pointSet.destroy();
|
||||
}, this);
|
||||
if (elements.alarmSet) {
|
||||
elements.alarmSet.destroy();
|
||||
this.alarmSets.splice(this.alarmSets.indexOf(elements.alarmSet), 1);
|
||||
}
|
||||
|
||||
this.seriesElements.delete(series);
|
||||
},
|
||||
lineForSeries(series) {
|
||||
if (series.get('interpolate') === 'linear') {
|
||||
return new MCTChartLineLinear(
|
||||
series,
|
||||
this,
|
||||
this.offset
|
||||
);
|
||||
}
|
||||
|
||||
if (series.get('interpolate') === 'stepAfter') {
|
||||
return new MCTChartLineStepAfter(
|
||||
series,
|
||||
this,
|
||||
this.offset
|
||||
);
|
||||
}
|
||||
},
|
||||
pointSetForSeries(series) {
|
||||
if (series.get('markers')) {
|
||||
return new MCTChartPointSet(
|
||||
series,
|
||||
this,
|
||||
this.offset
|
||||
);
|
||||
}
|
||||
},
|
||||
alarmPointSetForSeries(series) {
|
||||
if (series.get('alarmMarkers')) {
|
||||
return new MCTChartAlarmPointSet(
|
||||
series,
|
||||
this,
|
||||
this.offset
|
||||
);
|
||||
}
|
||||
},
|
||||
makeChartElement(series) {
|
||||
const elements = {
|
||||
lines: [],
|
||||
pointSets: []
|
||||
};
|
||||
|
||||
const line = this.lineForSeries(series);
|
||||
if (line) {
|
||||
elements.lines.push(line);
|
||||
this.lines.push(line);
|
||||
}
|
||||
|
||||
const pointSet = this.pointSetForSeries(series);
|
||||
if (pointSet) {
|
||||
elements.pointSets.push(pointSet);
|
||||
this.pointSets.push(pointSet);
|
||||
}
|
||||
|
||||
elements.alarmSet = this.alarmPointSetForSeries(series);
|
||||
if (elements.alarmSet) {
|
||||
this.alarmSets.push(elements.alarmSet);
|
||||
}
|
||||
|
||||
this.seriesElements.set(series, elements);
|
||||
},
|
||||
canDraw() {
|
||||
if (!this.offset.x || !this.offset.y) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
scheduleDraw() {
|
||||
if (!this.drawScheduled) {
|
||||
requestAnimationFrame(this.draw);
|
||||
this.drawScheduled = true;
|
||||
}
|
||||
},
|
||||
draw() {
|
||||
this.drawScheduled = false;
|
||||
if (this.isDestroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.drawAPI.clear();
|
||||
if (this.canDraw()) {
|
||||
this.updateViewport();
|
||||
this.drawSeries();
|
||||
this.drawRectangles();
|
||||
this.drawHighlights();
|
||||
}
|
||||
},
|
||||
updateViewport() {
|
||||
const xRange = this.config.xAxis.get('displayRange');
|
||||
const yRange = this.config.yAxis.get('displayRange');
|
||||
|
||||
if (!xRange || !yRange) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dimensions = [
|
||||
xRange.max - xRange.min,
|
||||
yRange.max - yRange.min
|
||||
];
|
||||
|
||||
const origin = [
|
||||
this.offset.x(xRange.min),
|
||||
this.offset.y(yRange.min)
|
||||
];
|
||||
|
||||
this.drawAPI.setDimensions(
|
||||
dimensions,
|
||||
origin
|
||||
);
|
||||
},
|
||||
drawSeries() {
|
||||
this.lines.forEach(this.drawLine, this);
|
||||
this.pointSets.forEach(this.drawPoints, this);
|
||||
this.alarmSets.forEach(this.drawAlarmPoints, this);
|
||||
},
|
||||
drawAlarmPoints(alarmSet) {
|
||||
this.drawAPI.drawLimitPoints(
|
||||
alarmSet.points,
|
||||
alarmSet.series.get('color').asRGBAArray(),
|
||||
alarmSet.series.get('markerSize')
|
||||
);
|
||||
},
|
||||
drawPoints(chartElement) {
|
||||
this.drawAPI.drawPoints(
|
||||
chartElement.getBuffer(),
|
||||
chartElement.color().asRGBAArray(),
|
||||
chartElement.count,
|
||||
chartElement.series.get('markerSize'),
|
||||
chartElement.series.get('markerShape')
|
||||
);
|
||||
},
|
||||
drawLine(chartElement) {
|
||||
this.drawAPI.drawLine(
|
||||
chartElement.getBuffer(),
|
||||
chartElement.color().asRGBAArray(),
|
||||
chartElement.count
|
||||
);
|
||||
},
|
||||
drawHighlights() {
|
||||
if (this.highlights && this.highlights.length) {
|
||||
this.highlights.forEach(this.drawHighlight, this);
|
||||
}
|
||||
},
|
||||
drawHighlight(highlight) {
|
||||
const points = new Float32Array([
|
||||
this.offset.xVal(highlight.point, highlight.series),
|
||||
this.offset.yVal(highlight.point, highlight.series)
|
||||
]);
|
||||
|
||||
const color = highlight.series.get('color').asRGBAArray();
|
||||
const pointCount = 1;
|
||||
const shape = highlight.series.get('markerShape');
|
||||
|
||||
this.drawAPI.drawPoints(points, color, pointCount, HIGHLIGHT_SIZE, shape);
|
||||
},
|
||||
drawRectangles() {
|
||||
if (this.rectangles) {
|
||||
this.rectangles.forEach(this.drawRectangle, this);
|
||||
}
|
||||
},
|
||||
drawRectangle(rect) {
|
||||
this.drawAPI.drawSquare(
|
||||
[
|
||||
this.offset.x(rect.start.x),
|
||||
this.offset.y(rect.start.y)
|
||||
],
|
||||
[
|
||||
this.offset.x(rect.end.x),
|
||||
this.offset.y(rect.end.y)
|
||||
],
|
||||
rect.color
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
109
src/plugins/plot/vue/single/configuration/Collection.js
Normal file
109
src/plugins/plot/vue/single/configuration/Collection.js
Normal file
@@ -0,0 +1,109 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import Model from './Model';
|
||||
|
||||
export default class Collection extends Model {
|
||||
|
||||
initialize(options) {
|
||||
super.initialize(options);
|
||||
this.modelClass = Model;
|
||||
if (options.models) {
|
||||
this.models = options.models.map(this.modelFn, this);
|
||||
} else {
|
||||
this.models = [];
|
||||
}
|
||||
}
|
||||
|
||||
modelFn(model) {
|
||||
//TODO: Come back to this - why are we doing this?
|
||||
if (model instanceof this.modelClass) {
|
||||
model.collection = this;
|
||||
|
||||
return model;
|
||||
|
||||
}
|
||||
|
||||
return new this.modelClass({
|
||||
collection: this,
|
||||
model: model
|
||||
});
|
||||
}
|
||||
|
||||
first() {
|
||||
return this.at(0);
|
||||
}
|
||||
|
||||
forEach(iteree, context) {
|
||||
this.models.forEach(iteree, context);
|
||||
}
|
||||
|
||||
map(iteree, context) {
|
||||
return this.models.map(iteree, context);
|
||||
}
|
||||
|
||||
filter(iteree, context) {
|
||||
return this.models.filter(iteree, context);
|
||||
}
|
||||
|
||||
size() {
|
||||
return this.models.length;
|
||||
}
|
||||
|
||||
at(index) {
|
||||
return this.models[index];
|
||||
}
|
||||
|
||||
add(model) {
|
||||
model = this.modelFn(model);
|
||||
const index = this.models.length;
|
||||
this.models.push(model);
|
||||
this.emit('add', model, index);
|
||||
}
|
||||
|
||||
insert(model, index) {
|
||||
model = this.modelFn(model);
|
||||
this.models.splice(index, 0, model);
|
||||
this.emit('add', model, index + 1);
|
||||
}
|
||||
|
||||
indexOf(model) {
|
||||
return this.models.findIndex(m => m === model);
|
||||
}
|
||||
|
||||
remove(model) {
|
||||
const index = this.indexOf(model);
|
||||
|
||||
if (index === -1) {
|
||||
throw new Error('model not found in collection.');
|
||||
}
|
||||
|
||||
this.emit('remove', model, index);
|
||||
this.models.splice(index, 1);
|
||||
}
|
||||
|
||||
destroy(model) {
|
||||
this.forEach(function (m) {
|
||||
m.destroy();
|
||||
});
|
||||
this.stopListening();
|
||||
}
|
||||
}
|
||||
58
src/plugins/plot/vue/single/configuration/LegendModel.js
Normal file
58
src/plugins/plot/vue/single/configuration/LegendModel.js
Normal file
@@ -0,0 +1,58 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import Model from "./Model";
|
||||
/**
|
||||
* TODO: doc strings.
|
||||
*/
|
||||
export default class LegendModel extends Model {
|
||||
listenToSeriesCollection(seriesCollection) {
|
||||
this.seriesCollection = seriesCollection;
|
||||
this.listenTo(this.seriesCollection, 'add', this.setHeight, this);
|
||||
this.listenTo(this.seriesCollection, 'remove', this.setHeight, this);
|
||||
this.listenTo(this, 'change:expanded', this.setHeight, this);
|
||||
this.set('expanded', this.get('expandByDefault'));
|
||||
}
|
||||
|
||||
setHeight() {
|
||||
const expanded = this.get('expanded');
|
||||
if (this.get('position') !== 'top') {
|
||||
this.set('height', '0px');
|
||||
} else {
|
||||
this.set('height', expanded ? (20 * (this.seriesCollection.size() + 1) + 40) + 'px' : '21px');
|
||||
}
|
||||
}
|
||||
|
||||
defaults(options) {
|
||||
return {
|
||||
position: 'top',
|
||||
expandByDefault: false,
|
||||
hideLegendWhenSmall: false,
|
||||
valueToShowWhenCollapsed: 'nearestValue',
|
||||
showTimestampWhenExpanded: true,
|
||||
showValueWhenExpanded: true,
|
||||
showMaximumWhenExpanded: true,
|
||||
showMinimumWhenExpanded: true,
|
||||
showUnitsWhenExpanded: true
|
||||
};
|
||||
}
|
||||
}
|
||||
93
src/plugins/plot/vue/single/configuration/Model.js
Normal file
93
src/plugins/plot/vue/single/configuration/Model.js
Normal file
@@ -0,0 +1,93 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import EventEmitter from 'EventEmitter';
|
||||
import eventHelpers from "../lib/eventHelpers";
|
||||
import _ from 'lodash';
|
||||
|
||||
export default class Model extends EventEmitter {
|
||||
constructor(options) {
|
||||
super();
|
||||
|
||||
//need to do this as we're already extending EventEmitter
|
||||
eventHelpers.extend(this);
|
||||
|
||||
if (!options) {
|
||||
options = {};
|
||||
}
|
||||
|
||||
this.id = options.id;
|
||||
this.model = options.model;
|
||||
this.collection = options.collection;
|
||||
const defaults = this.defaults(options);
|
||||
if (!this.model) {
|
||||
this.model = options.model = defaults;
|
||||
} else {
|
||||
_.defaultsDeep(this.model, defaults);
|
||||
}
|
||||
|
||||
this.initialize(options);
|
||||
this.idAttr = 'id';
|
||||
}
|
||||
|
||||
defaults(options) {
|
||||
return {};
|
||||
}
|
||||
|
||||
initialize(model) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the model, removing all listeners and subscriptions.
|
||||
*/
|
||||
destroy() {
|
||||
this.emit('destroy');
|
||||
this.removeAllListeners();
|
||||
}
|
||||
|
||||
id() {
|
||||
return this.get(this.idAttr);
|
||||
}
|
||||
|
||||
get(attribute) {
|
||||
return this.model[attribute];
|
||||
}
|
||||
|
||||
has(attribute) {
|
||||
return _.has(this.model, attribute);
|
||||
}
|
||||
|
||||
set(attribute, value) {
|
||||
const oldValue = this.model[attribute];
|
||||
this.model[attribute] = value;
|
||||
this.emit('change', attribute, value, oldValue, this);
|
||||
this.emit('change:' + attribute, value, oldValue, this);
|
||||
}
|
||||
|
||||
unset(attribute) {
|
||||
const oldValue = this.model[attribute];
|
||||
delete this.model[attribute];
|
||||
this.emit('change', attribute, undefined, oldValue, this);
|
||||
this.emit('change:' + attribute, undefined, oldValue, this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import _ from 'lodash';
|
||||
|
||||
import Model from "./Model";
|
||||
import SeriesCollection from "./SeriesCollection";
|
||||
import XAxisModel from "./XAxisModel";
|
||||
import YAxisModel from "./YAxisModel";
|
||||
import LegendModel from "./LegendModel";
|
||||
/**
|
||||
* PlotConfiguration model stores the configuration of a plot and some
|
||||
* limited state. The indiidual parts of the plot configuration model
|
||||
* handle setting defaults and updating in response to various changes.
|
||||
*
|
||||
*/
|
||||
export default class PlotConfigurationModel extends Model {
|
||||
/**
|
||||
* Initializes all sub models and then passes references to submodels
|
||||
* to those that need it.
|
||||
*/
|
||||
initialize(options) {
|
||||
this.openmct = options.openmct;
|
||||
|
||||
this.xAxis = new XAxisModel({
|
||||
model: options.model.xAxis,
|
||||
plot: this,
|
||||
openmct: options.openmct
|
||||
});
|
||||
this.yAxis = new YAxisModel({
|
||||
model: options.model.yAxis,
|
||||
plot: this,
|
||||
openmct: options.openmct
|
||||
});
|
||||
this.legend = new LegendModel({
|
||||
model: options.model.legend,
|
||||
plot: this,
|
||||
openmct: options.openmct
|
||||
});
|
||||
this.series = new SeriesCollection({
|
||||
models: options.model.series,
|
||||
plot: this,
|
||||
openmct: options.openmct
|
||||
});
|
||||
|
||||
if (this.get('domainObject').type === 'telemetry.plot.overlay') {
|
||||
this.removeMutationListener = this.openmct.objects.observe(
|
||||
this.get('domainObject'),
|
||||
'*',
|
||||
this.updateDomainObject.bind(this)
|
||||
);
|
||||
}
|
||||
|
||||
this.yAxis.listenToSeriesCollection(this.series);
|
||||
this.legend.listenToSeriesCollection(this.series);
|
||||
|
||||
this.listenTo(this, 'destroy', this.onDestroy, this);
|
||||
}
|
||||
/**
|
||||
* Retrieve the persisted series config for a given identifier.
|
||||
*/
|
||||
getPersistedSeriesConfig(identifier) {
|
||||
const domainObject = this.get('domainObject');
|
||||
if (!domainObject.configuration || !domainObject.configuration.series) {
|
||||
return;
|
||||
}
|
||||
|
||||
return domainObject.configuration.series.filter(function (seriesConfig) {
|
||||
return seriesConfig.identifier.key === identifier.key
|
||||
&& seriesConfig.identifier.namespace === identifier.namespace;
|
||||
})[0];
|
||||
}
|
||||
/**
|
||||
* Retrieve the persisted filters for a given identifier.
|
||||
*/
|
||||
getPersistedFilters(identifier) {
|
||||
const domainObject = this.get('domainObject');
|
||||
const keystring = this.openmct.objects.makeKeyString(identifier);
|
||||
|
||||
if (!domainObject.configuration || !domainObject.configuration.filters) {
|
||||
return;
|
||||
}
|
||||
|
||||
return domainObject.configuration.filters[keystring];
|
||||
}
|
||||
/**
|
||||
* Update the domain object with the given value.
|
||||
*/
|
||||
updateDomainObject(domainObject) {
|
||||
this.set('domainObject', domainObject);
|
||||
}
|
||||
/**
|
||||
* Clean up all objects and remove all listeners.
|
||||
*/
|
||||
onDestroy() {
|
||||
this.xAxis.destroy();
|
||||
this.yAxis.destroy();
|
||||
this.series.destroy();
|
||||
this.legend.destroy();
|
||||
if (this.removeMutationListener) {
|
||||
this.removeMutationListener();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Return defaults, which are extracted from the passed in domain
|
||||
* object.
|
||||
*/
|
||||
defaults(options) {
|
||||
return {
|
||||
series: [],
|
||||
domainObject: options.domainObject,
|
||||
xAxis: {
|
||||
},
|
||||
yAxis: _.cloneDeep(_.get(options.domainObject, 'configuration.yAxis', {})),
|
||||
legend: _.cloneDeep(_.get(options.domainObject, 'configuration.legend', {}))
|
||||
};
|
||||
}
|
||||
}
|
||||
451
src/plugins/plot/vue/single/configuration/PlotSeries.js
Normal file
451
src/plugins/plot/vue/single/configuration/PlotSeries.js
Normal file
@@ -0,0 +1,451 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import _ from 'lodash';
|
||||
import Model from "./Model";
|
||||
import { MARKER_SHAPES } from '../draw/MarkerShapes';
|
||||
|
||||
/**
|
||||
* Plot series handle interpreting telemetry metadata for a single telemetry
|
||||
* object, querying for that data, and formatting it for display purposes.
|
||||
*
|
||||
* Plot series emit both collection events and model events:
|
||||
* `change` when any property changes
|
||||
* `change:<prop_name>` when a specific property changes.
|
||||
* `destroy`: when series is destroyed
|
||||
* `add`: whenever a data point is added to a series
|
||||
* `remove`: whenever a data point is removed from a series.
|
||||
* `reset`: whenever the collection is emptied.
|
||||
*
|
||||
* Plot series have the following Model properties:
|
||||
*
|
||||
* `name`: name of series.
|
||||
* `identifier`: the Open MCT identifier for the telemetry source for this
|
||||
* series.
|
||||
* `xKey`: the telemetry value key for x values fetched from this series.
|
||||
* `yKey`: the telemetry value key for y values fetched from this series.
|
||||
* `interpolate`: interpolate method, either `undefined` (no interpolation),
|
||||
* `linear` (points are connected via straight lines), or
|
||||
* `stepAfter` (points are connected by steps).
|
||||
* `markers`: boolean, whether or not this series should render with markers.
|
||||
* `markerShape`: string, shape of markers.
|
||||
* `markerSize`: number, size in pixels of markers for this series.
|
||||
* `alarmMarkers`: whether or not to display alarm markers for this series.
|
||||
* `stats`: An object that tracks the min and max y values observed in this
|
||||
* series. This property is checked and updated whenever data is
|
||||
* added.
|
||||
*
|
||||
* Plot series have the following instance properties:
|
||||
*
|
||||
* `metadata`: the Open MCT Telemetry Metadata Manager for the associated
|
||||
* telemetry point.
|
||||
* `formats`: the Open MCT format map for this telemetry point.
|
||||
*/
|
||||
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);
|
||||
this.persistedConfig = options.persistedConfig;
|
||||
this.filters = options.filters;
|
||||
|
||||
// Model.apply(this, arguments);
|
||||
this.onXKeyChange(this.get('xKey'));
|
||||
this.onYKeyChange(this.get('yKey'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set defaults for telemetry series.
|
||||
*/
|
||||
defaults(options) {
|
||||
this.metadata = options
|
||||
.openmct
|
||||
.telemetry
|
||||
.getMetadata(options.domainObject);
|
||||
this.formats = options
|
||||
.openmct
|
||||
.telemetry
|
||||
.getFormatMap(this.metadata);
|
||||
|
||||
const range = this.metadata.valuesForHints(['range'])[0];
|
||||
|
||||
return {
|
||||
name: options.domainObject.name,
|
||||
unit: range.unit,
|
||||
xKey: options.collection.plot.xAxis.get('key'),
|
||||
yKey: range.key,
|
||||
markers: true,
|
||||
markerShape: 'point',
|
||||
markerSize: 2.0,
|
||||
alarmMarkers: true
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove real-time subscription when destroyed.
|
||||
*/
|
||||
onDestroy(model) {
|
||||
if (this.unsubscribe) {
|
||||
this.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
initialize(options) {
|
||||
this.openmct = options.openmct;
|
||||
this.domainObject = options.domainObject;
|
||||
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
this.limitEvaluator = this.openmct.telemetry.limitEvaluator(options.domainObject);
|
||||
this.on('destroy', this.onDestroy, this);
|
||||
}
|
||||
|
||||
locateOldObject(oldStyleParent) {
|
||||
return oldStyleParent.useCapability('composition')
|
||||
.then(function (children) {
|
||||
this.oldObject = children
|
||||
.filter(function (child) {
|
||||
return child.getId() === this.keyString;
|
||||
}, this)[0];
|
||||
}.bind(this));
|
||||
}
|
||||
/**
|
||||
* Fetch historical data and establish a realtime subscription. Returns
|
||||
* a promise that is resolved when all connections have been successfully
|
||||
* established.
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
fetch(options) {
|
||||
let strategy;
|
||||
|
||||
if (this.model.interpolate !== 'none') {
|
||||
strategy = 'minmax';
|
||||
}
|
||||
|
||||
options = Object.assign({}, {
|
||||
size: 1000,
|
||||
strategy,
|
||||
filters: this.filters
|
||||
}, options || {});
|
||||
|
||||
if (!this.unsubscribe) {
|
||||
this.unsubscribe = this.openmct
|
||||
.telemetry
|
||||
.subscribe(
|
||||
this.domainObject,
|
||||
this.add.bind(this),
|
||||
{
|
||||
filters: this.filters
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/* eslint-disable you-dont-need-lodash-underscore/concat */
|
||||
return this.openmct
|
||||
.telemetry
|
||||
.request(this.domainObject, options)
|
||||
.then(function (points) {
|
||||
const newPoints = _(this.data)
|
||||
.concat(points)
|
||||
.sortBy(this.getXVal)
|
||||
.uniq(true, point => [this.getXVal(point), this.getYVal(point)].join())
|
||||
.value();
|
||||
this.reset(newPoints);
|
||||
}.bind(this));
|
||||
/* eslint-enable you-dont-need-lodash-underscore/concat */
|
||||
}
|
||||
/**
|
||||
* Update x formatter on x change.
|
||||
*/
|
||||
onXKeyChange(xKey) {
|
||||
const format = this.formats[xKey];
|
||||
this.getXVal = format.parse.bind(format);
|
||||
}
|
||||
/**
|
||||
* Update y formatter on change, default to stepAfter interpolation if
|
||||
* y range is an enumeration.
|
||||
*/
|
||||
onYKeyChange(newKey, oldKey) {
|
||||
if (newKey === oldKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const valueMetadata = this.metadata.value(newKey);
|
||||
if (!this.persistedConfig || !this.persistedConfig.interpolate) {
|
||||
if (valueMetadata.format === 'enum') {
|
||||
this.set('interpolate', 'stepAfter');
|
||||
} else {
|
||||
this.set('interpolate', 'linear');
|
||||
}
|
||||
}
|
||||
|
||||
this.evaluate = function (datum) {
|
||||
return this.limitEvaluator.evaluate(datum, valueMetadata);
|
||||
}.bind(this);
|
||||
const format = this.formats[newKey];
|
||||
this.getYVal = format.parse.bind(format);
|
||||
}
|
||||
|
||||
formatX(point) {
|
||||
return this.formats[this.get('xKey')].format(point);
|
||||
}
|
||||
|
||||
formatY(point) {
|
||||
return this.formats[this.get('yKey')].format(point);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear stats and recalculate from existing data.
|
||||
*/
|
||||
resetStats() {
|
||||
this.unset('stats');
|
||||
this.data.forEach(this.updateStats, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset plot series. If new data is provided, will add that
|
||||
* data to series after reset.
|
||||
*/
|
||||
reset(newData) {
|
||||
this.data = [];
|
||||
this.resetStats();
|
||||
this.emit('reset');
|
||||
if (newData) {
|
||||
newData.forEach(function (point) {
|
||||
this.add(point, true);
|
||||
}, this);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Return the point closest to a given x value.
|
||||
*/
|
||||
nearestPoint(xValue) {
|
||||
const insertIndex = this.sortedIndex(xValue);
|
||||
const lowPoint = this.data[insertIndex - 1];
|
||||
const highPoint = this.data[insertIndex];
|
||||
const indexVal = this.getXVal(xValue);
|
||||
const lowDistance = lowPoint
|
||||
? indexVal - this.getXVal(lowPoint)
|
||||
: Number.POSITIVE_INFINITY;
|
||||
const highDistance = highPoint
|
||||
? this.getXVal(highPoint) - indexVal
|
||||
: Number.POSITIVE_INFINITY;
|
||||
const nearestPoint = highDistance < lowDistance ? highPoint : lowPoint;
|
||||
|
||||
return nearestPoint;
|
||||
}
|
||||
/**
|
||||
* Override this to implement plot series loading functionality. Must return
|
||||
* a promise that is resolved when loading is completed.
|
||||
*
|
||||
* @private
|
||||
* @returns {Promise}
|
||||
*/
|
||||
load(options) {
|
||||
return this.fetch(options)
|
||||
.then(function (res) {
|
||||
this.emit('load');
|
||||
|
||||
return res;
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the insert index for a given point to maintain sort order.
|
||||
* @private
|
||||
*/
|
||||
sortedIndex(point) {
|
||||
return _.sortedIndexBy(this.data, point, this.getXVal);
|
||||
}
|
||||
/**
|
||||
* Update min/max stats for the series.
|
||||
* @private
|
||||
*/
|
||||
updateStats(point) {
|
||||
const value = this.getYVal(point);
|
||||
let stats = this.get('stats');
|
||||
let changed = false;
|
||||
if (!stats) {
|
||||
stats = {
|
||||
minValue: value,
|
||||
minPoint: point,
|
||||
maxValue: value,
|
||||
maxPoint: point
|
||||
};
|
||||
changed = true;
|
||||
} else {
|
||||
if (stats.maxValue < value) {
|
||||
stats.maxValue = value;
|
||||
stats.maxPoint = point;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (stats.minValue > value) {
|
||||
stats.minValue = value;
|
||||
stats.minPoint = point;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
this.set('stats', {
|
||||
minValue: stats.minValue,
|
||||
minPoint: stats.minPoint,
|
||||
maxValue: stats.maxValue,
|
||||
maxPoint: stats.maxPoint
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Add a point to the data array while maintaining the sort order of
|
||||
* the array and preventing insertion of points with a duplicate x
|
||||
* value. Can provide an optional argument to append a point without
|
||||
* maintaining sort order and dupe checks, which improves performance
|
||||
* when adding an array of points that are already properly sorted.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} point a telemetry datum.
|
||||
* @param {Boolean} [appendOnly] default false, if true will append
|
||||
* a point to the end without dupe checking.
|
||||
*/
|
||||
add(point, appendOnly) {
|
||||
let insertIndex = this.data.length;
|
||||
const currentYVal = this.getYVal(point);
|
||||
const lastYVal = this.getYVal(this.data[insertIndex - 1]);
|
||||
|
||||
if (this.isValueInvalid(currentYVal) && this.isValueInvalid(lastYVal)) {
|
||||
console.warn('[Plot] Invalid Y Values detected');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!appendOnly) {
|
||||
insertIndex = this.sortedIndex(point);
|
||||
if (this.getXVal(this.data[insertIndex]) === this.getXVal(point)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.getXVal(this.data[insertIndex - 1]) === this.getXVal(point)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.updateStats(point);
|
||||
point.mctLimitState = this.evaluate(point);
|
||||
this.data.splice(insertIndex, 0, point);
|
||||
this.emit('add', point, insertIndex, this);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
isValueInvalid(val) {
|
||||
return Number.isNaN(val) || val === undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a point from the data array and notify listeners.
|
||||
* @private
|
||||
*/
|
||||
remove(point) {
|
||||
const index = this.data.indexOf(point);
|
||||
this.data.splice(index, 1);
|
||||
this.emit('remove', point, index, this);
|
||||
}
|
||||
/**
|
||||
* Purges records outside a given x range. Changes removal method based
|
||||
* on number of records to remove: for large purge, reset data and
|
||||
* rebuild array. for small purge, removes points and emits updates.
|
||||
*
|
||||
* @public
|
||||
* @param {Object} range
|
||||
* @param {number} range.min minimum x value to keep
|
||||
* @param {number} range.max maximum x value to keep.
|
||||
*/
|
||||
purgeRecordsOutsideRange(range) {
|
||||
const startIndex = this.sortedIndex(range.min);
|
||||
const endIndex = this.sortedIndex(range.max) + 1;
|
||||
const pointsToRemove = startIndex + (this.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);
|
||||
this.resetStats();
|
||||
} else {
|
||||
const newData = this.data.slice(startIndex, endIndex);
|
||||
this.reset(newData);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
/**
|
||||
* Updates filters, clears the plot series, unsubscribes and resubscribes
|
||||
* @public
|
||||
*/
|
||||
updateFiltersAndRefresh(updatedFilters) {
|
||||
let deepCopiedFilters = JSON.parse(JSON.stringify(updatedFilters));
|
||||
|
||||
if (this.filters && !_.isEqual(this.filters, deepCopiedFilters)) {
|
||||
this.filters = deepCopiedFilters;
|
||||
this.reset();
|
||||
if (this.unsubscribe) {
|
||||
this.unsubscribe();
|
||||
delete this.unsubscribe;
|
||||
}
|
||||
|
||||
this.fetch();
|
||||
} else {
|
||||
this.filters = deepCopiedFilters;
|
||||
}
|
||||
}
|
||||
getDisplayRange(xKey) {
|
||||
const unsortedData = this.data;
|
||||
this.data = [];
|
||||
unsortedData.forEach(point => this.add(point, false));
|
||||
|
||||
const minValue = this.getXVal(this.data[0]);
|
||||
const maxValue = this.getXVal(this.data[this.data.length - 1]);
|
||||
|
||||
return {
|
||||
min: minValue,
|
||||
max: maxValue
|
||||
};
|
||||
}
|
||||
markerOptionsDisplayText() {
|
||||
const showMarkers = this.get('markers');
|
||||
if (!showMarkers) {
|
||||
return "Disabled";
|
||||
}
|
||||
|
||||
const markerShapeKey = this.get('markerShape');
|
||||
const markerShape = MARKER_SHAPES[markerShapeKey].label;
|
||||
const markerSize = this.get('markerSize');
|
||||
|
||||
return `${markerShape}: ${markerSize}px`;
|
||||
}
|
||||
nameWithUnit() {
|
||||
let unit = this.get('unit');
|
||||
|
||||
return this.get('name') + (unit ? ' ' + unit : '');
|
||||
}
|
||||
}
|
||||
165
src/plugins/plot/vue/single/configuration/SeriesCollection.js
Normal file
165
src/plugins/plot/vue/single/configuration/SeriesCollection.js
Normal file
@@ -0,0 +1,165 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import _ from 'lodash';
|
||||
|
||||
import PlotSeries from "./PlotSeries";
|
||||
import Collection from "./Collection";
|
||||
import Color from "../lib/Color";
|
||||
import ColorPalette from "../lib/ColorPalette";
|
||||
|
||||
export default class SeriesCollection extends Collection {
|
||||
|
||||
initialize(options) {
|
||||
super.initialize(options);
|
||||
this.modelClass = PlotSeries;
|
||||
this.plot = options.plot;
|
||||
this.openmct = options.openmct;
|
||||
this.palette = new ColorPalette();
|
||||
this.listenTo(this, 'add', this.onSeriesAdd, this);
|
||||
this.listenTo(this, 'remove', this.onSeriesRemove, this);
|
||||
this.listenTo(this.plot, 'change:domainObject', this.trackPersistedConfig, this);
|
||||
|
||||
const domainObject = this.plot.get('domainObject');
|
||||
if (domainObject.telemetry) {
|
||||
this.addTelemetryObject(domainObject);
|
||||
} else {
|
||||
this.watchTelemetryContainer(domainObject);
|
||||
}
|
||||
}
|
||||
trackPersistedConfig(domainObject) {
|
||||
domainObject.configuration.series.forEach(function (seriesConfig) {
|
||||
const series = this.byIdentifier(seriesConfig.identifier);
|
||||
if (series) {
|
||||
series.persistedConfig = seriesConfig;
|
||||
}
|
||||
}, this);
|
||||
}
|
||||
watchTelemetryContainer(domainObject) {
|
||||
const composition = this.openmct.composition.get(domainObject);
|
||||
this.listenTo(composition, 'add', this.addTelemetryObject, this);
|
||||
this.listenTo(composition, 'remove', this.removeTelemetryObject, this);
|
||||
composition.load();
|
||||
}
|
||||
addTelemetryObject(domainObject, index) {
|
||||
let seriesConfig = this.plot.getPersistedSeriesConfig(domainObject.identifier);
|
||||
const filters = this.plot.getPersistedFilters(domainObject.identifier);
|
||||
const plotObject = this.plot.get('domainObject');
|
||||
|
||||
if (!seriesConfig) {
|
||||
seriesConfig = {
|
||||
identifier: domainObject.identifier
|
||||
};
|
||||
|
||||
if (plotObject.type === 'telemetry.plot.overlay') {
|
||||
this.openmct.objects.mutate(
|
||||
plotObject,
|
||||
'configuration.series[' + this.size() + ']',
|
||||
seriesConfig
|
||||
);
|
||||
seriesConfig = this.plot
|
||||
.getPersistedSeriesConfig(domainObject.identifier);
|
||||
}
|
||||
}
|
||||
|
||||
// Clone to prevent accidental mutation by ref.
|
||||
seriesConfig = JSON.parse(JSON.stringify(seriesConfig));
|
||||
|
||||
this.add(new PlotSeries({
|
||||
model: seriesConfig,
|
||||
domainObject: domainObject,
|
||||
collection: this,
|
||||
openmct: this.openmct,
|
||||
persistedConfig: this.plot
|
||||
.getPersistedSeriesConfig(domainObject.identifier),
|
||||
filters: filters
|
||||
}));
|
||||
}
|
||||
removeTelemetryObject(identifier) {
|
||||
const plotObject = this.plot.get('domainObject');
|
||||
if (plotObject.type === 'telemetry.plot.overlay') {
|
||||
|
||||
const persistedIndex = plotObject.configuration.series.findIndex(s => {
|
||||
return _.isEqual(identifier, s.identifier);
|
||||
});
|
||||
|
||||
const configIndex = this.models.findIndex(m => {
|
||||
return _.isEqual(m.domainObject.identifier, identifier);
|
||||
});
|
||||
|
||||
/*
|
||||
when cancelling out of edit mode, the config store and domain object are out of sync
|
||||
thus it is necesarry to check both and remove the models that are no longer in composition
|
||||
*/
|
||||
if (persistedIndex === -1) {
|
||||
this.remove(this.at(configIndex));
|
||||
} else {
|
||||
this.remove(this.at(persistedIndex));
|
||||
// Because this is triggered by a composition change, we have
|
||||
// to defer mutation of our plot object, otherwise we might
|
||||
// mutate an outdated version of the plotObject.
|
||||
setTimeout(function () {
|
||||
const newPlotObject = this.plot.get('domainObject');
|
||||
const cSeries = newPlotObject.configuration.series.slice();
|
||||
cSeries.splice(persistedIndex, 1);
|
||||
this.openmct.objects.mutate(newPlotObject, 'configuration.series', cSeries);
|
||||
}.bind(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
onSeriesAdd(series) {
|
||||
let seriesColor = series.get('color');
|
||||
if (seriesColor) {
|
||||
if (!(seriesColor instanceof Color)) {
|
||||
seriesColor = Color.fromHexString(seriesColor);
|
||||
series.set('color', seriesColor);
|
||||
}
|
||||
|
||||
this.palette.remove(seriesColor);
|
||||
} else {
|
||||
series.set('color', this.palette.getNextColor());
|
||||
}
|
||||
|
||||
this.listenTo(series, 'change:color', this.updateColorPalette, this);
|
||||
}
|
||||
onSeriesRemove(series) {
|
||||
this.palette.return(series.get('color'));
|
||||
this.stopListening(series);
|
||||
series.destroy();
|
||||
}
|
||||
updateColorPalette(newColor, oldColor) {
|
||||
this.palette.remove(newColor);
|
||||
const seriesWithColor = this.filter(function (series) {
|
||||
return series.get('color') === newColor;
|
||||
})[0];
|
||||
if (!seriesWithColor) {
|
||||
this.palette.return(oldColor);
|
||||
}
|
||||
}
|
||||
byIdentifier(identifier) {
|
||||
return this.filter(function (series) {
|
||||
const seriesIdentifier = series.get('identifier');
|
||||
|
||||
return seriesIdentifier.namespace === identifier.namespace
|
||||
&& seriesIdentifier.key === identifier.key;
|
||||
})[0];
|
||||
}
|
||||
}
|
||||
88
src/plugins/plot/vue/single/configuration/XAxisModel.js
Normal file
88
src/plugins/plot/vue/single/configuration/XAxisModel.js
Normal file
@@ -0,0 +1,88 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import Model from "./Model";
|
||||
/**
|
||||
* TODO: doc strings.
|
||||
*/
|
||||
export default class XAxisModel extends Model {
|
||||
initialize(options) {
|
||||
this.plot = options.plot;
|
||||
this.set('label', options.model.name || '');
|
||||
this.on('change:range', function (newValue, oldValue, model) {
|
||||
if (!model.get('frozen')) {
|
||||
model.set('displayRange', newValue);
|
||||
}
|
||||
});
|
||||
|
||||
this.on('change:frozen', ((frozen, oldValue, model) => {
|
||||
if (!frozen) {
|
||||
model.set('range', this.get('range'));
|
||||
}
|
||||
}));
|
||||
|
||||
if (this.get('range')) {
|
||||
this.set('range', this.get('range'));
|
||||
}
|
||||
|
||||
this.listenTo(this, 'change:key', this.changeKey, this);
|
||||
}
|
||||
changeKey(newKey) {
|
||||
const series = this.plot.series.first();
|
||||
if (series) {
|
||||
const xMetadata = series.metadata.value(newKey);
|
||||
const xFormat = series.formats[newKey];
|
||||
this.set('label', xMetadata.name);
|
||||
this.set('format', xFormat.format.bind(xFormat));
|
||||
} else {
|
||||
this.set('format', function (x) {
|
||||
return x;
|
||||
});
|
||||
this.set('label', newKey);
|
||||
}
|
||||
|
||||
this.plot.series.forEach(function (plotSeries) {
|
||||
plotSeries.set('xKey', newKey);
|
||||
});
|
||||
}
|
||||
resetSeries() {
|
||||
this.plot.series.forEach(function (plotSeries) {
|
||||
plotSeries.reset();
|
||||
});
|
||||
}
|
||||
defaults(options) {
|
||||
const bounds = options.openmct.time.bounds();
|
||||
const timeSystem = options.openmct.time.timeSystem();
|
||||
const format = options.openmct.$injector.get('formatService')
|
||||
.getFormat(timeSystem.timeFormat);
|
||||
|
||||
return {
|
||||
name: timeSystem.name,
|
||||
key: timeSystem.key,
|
||||
format: format.format.bind(format),
|
||||
range: {
|
||||
min: bounds.start,
|
||||
max: bounds.end
|
||||
},
|
||||
frozen: false
|
||||
};
|
||||
}
|
||||
}
|
||||
235
src/plugins/plot/vue/single/configuration/YAxisModel.js
Normal file
235
src/plugins/plot/vue/single/configuration/YAxisModel.js
Normal file
@@ -0,0 +1,235 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import _ from 'lodash';
|
||||
import Model from './Model';
|
||||
|
||||
/**
|
||||
* YAxis model
|
||||
*
|
||||
* TODO: docstrings.
|
||||
*
|
||||
* has the following Model properties:
|
||||
*
|
||||
* `autoscale`: boolean, whether or not to autoscale.
|
||||
* `autoscalePadding`: float, percent of padding to display in plots.
|
||||
* `displayRange`: the current display range for the x Axis.
|
||||
* `format`: the formatter for the axis.
|
||||
* `frozen`: boolean, if true, displayRange will not be updated automatically.
|
||||
* Used to temporarily disable automatic updates during user interaction.
|
||||
* `label`: label to display on axis.
|
||||
* `stats`: Min and Max Values of data, automatically updated by observing
|
||||
* plot series.
|
||||
* `values`: for enumerated types, an array of possible display values.
|
||||
* `range`: the user-configured range to use for display, when autoscale is
|
||||
* disabled.
|
||||
*
|
||||
*/
|
||||
export default class YAxisModel extends Model {
|
||||
initialize(options) {
|
||||
this.plot = options.plot;
|
||||
this.listenTo(this, 'change:stats', this.calculateAutoscaleExtents, this);
|
||||
this.listenTo(this, 'change:autoscale', this.toggleAutoscale, this);
|
||||
this.listenTo(this, 'change:autoscalePadding', this.updatePadding, this);
|
||||
this.listenTo(this, 'change:frozen', this.toggleFreeze, this);
|
||||
this.listenTo(this, 'change:range', this.updateDisplayRange, this);
|
||||
this.updateDisplayRange(this.get('range'));
|
||||
}
|
||||
listenToSeriesCollection(seriesCollection) {
|
||||
this.seriesCollection = seriesCollection;
|
||||
this.listenTo(this.seriesCollection, 'add', (series => {
|
||||
this.trackSeries(series);
|
||||
this.updateFromSeries(this.seriesCollection);
|
||||
}), this);
|
||||
this.listenTo(this.seriesCollection, 'remove', (series => {
|
||||
this.untrackSeries(series);
|
||||
this.updateFromSeries(this.seriesCollection);
|
||||
}), this);
|
||||
this.seriesCollection.forEach(this.trackSeries, this);
|
||||
this.updateFromSeries(this.seriesCollection);
|
||||
}
|
||||
updateDisplayRange(range) {
|
||||
if (!this.get('autoscale')) {
|
||||
this.set('displayRange', range);
|
||||
}
|
||||
}
|
||||
toggleFreeze(frozen) {
|
||||
if (!frozen) {
|
||||
this.toggleAutoscale(this.get('autoscale'));
|
||||
}
|
||||
}
|
||||
applyPadding(range) {
|
||||
let padding = Math.abs(range.max - range.min) * this.get('autoscalePadding');
|
||||
if (padding === 0) {
|
||||
padding = 1;
|
||||
}
|
||||
|
||||
return {
|
||||
min: range.min - padding,
|
||||
max: range.max + padding
|
||||
};
|
||||
}
|
||||
updatePadding(newPadding) {
|
||||
if (this.get('autoscale') && !this.get('frozen') && this.has('stats')) {
|
||||
this.set('displayRange', this.applyPadding(this.get('stats')));
|
||||
}
|
||||
}
|
||||
calculateAutoscaleExtents(newStats) {
|
||||
if (this.get('autoscale') && !this.get('frozen')) {
|
||||
if (!newStats) {
|
||||
this.unset('displayRange');
|
||||
} else {
|
||||
this.set('displayRange', this.applyPadding(newStats));
|
||||
}
|
||||
}
|
||||
}
|
||||
updateStats(seriesStats) {
|
||||
if (!this.has('stats')) {
|
||||
this.set('stats', {
|
||||
min: seriesStats.minValue,
|
||||
max: seriesStats.maxValue
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const stats = this.get('stats');
|
||||
let changed = false;
|
||||
if (stats.min > seriesStats.minValue) {
|
||||
changed = true;
|
||||
stats.min = seriesStats.minValue;
|
||||
}
|
||||
|
||||
if (stats.max < seriesStats.maxValue) {
|
||||
changed = true;
|
||||
stats.max = seriesStats.maxValue;
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
this.set('stats', {
|
||||
min: stats.min,
|
||||
max: stats.max
|
||||
});
|
||||
}
|
||||
}
|
||||
resetStats() {
|
||||
this.unset('stats');
|
||||
this.seriesCollection.forEach(function (series) {
|
||||
if (series.has('stats')) {
|
||||
this.updateStats(series.get('stats'));
|
||||
}
|
||||
}, this);
|
||||
}
|
||||
trackSeries(series) {
|
||||
this.listenTo(series, 'change:stats', seriesStats => {
|
||||
if (!seriesStats) {
|
||||
this.resetStats();
|
||||
} else {
|
||||
this.updateStats(seriesStats);
|
||||
}
|
||||
});
|
||||
this.listenTo(series, 'change:yKey', () => {
|
||||
this.updateFromSeries(this.seriesCollection);
|
||||
});
|
||||
}
|
||||
untrackSeries(series) {
|
||||
this.stopListening(series);
|
||||
this.resetStats();
|
||||
this.updateFromSeries(this.seriesCollection);
|
||||
}
|
||||
toggleAutoscale(autoscale) {
|
||||
if (autoscale && this.has('stats')) {
|
||||
this.set('displayRange', this.applyPadding(this.get('stats')));
|
||||
} else {
|
||||
this.set('displayRange', this.get('range'));
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Update yAxis format, values, and label from known series.
|
||||
*/
|
||||
updateFromSeries(series) {
|
||||
this.unset('displayRange');
|
||||
const plotModel = this.plot.get('domainObject');
|
||||
const label = _.get(plotModel, 'configuration.yAxis.label');
|
||||
const sampleSeries = series.first();
|
||||
if (!sampleSeries) {
|
||||
if (!label) {
|
||||
this.unset('label');
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const yKey = sampleSeries.get('yKey');
|
||||
const yMetadata = sampleSeries.metadata.value(yKey);
|
||||
const yFormat = sampleSeries.formats[yKey];
|
||||
this.set('format', yFormat.format.bind(yFormat));
|
||||
this.set('values', yMetadata.values);
|
||||
if (!label) {
|
||||
const labelName = series.map(function (s) {
|
||||
return s.metadata.value(s.get('yKey')).name;
|
||||
}).reduce(function (a, b) {
|
||||
if (a === undefined) {
|
||||
return b;
|
||||
}
|
||||
|
||||
if (a === b) {
|
||||
return a;
|
||||
}
|
||||
|
||||
return '';
|
||||
}, undefined);
|
||||
|
||||
if (labelName) {
|
||||
this.set('label', labelName);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const labelUnits = series.map(function (s) {
|
||||
return s.metadata.value(s.get('yKey')).units;
|
||||
}).reduce(function (a, b) {
|
||||
if (a === undefined) {
|
||||
return b;
|
||||
}
|
||||
|
||||
if (a === b) {
|
||||
return a;
|
||||
}
|
||||
|
||||
return '';
|
||||
}, undefined);
|
||||
|
||||
if (labelUnits) {
|
||||
this.set('label', labelUnits);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
defaults(options) {
|
||||
return {
|
||||
frozen: false,
|
||||
autoscale: true,
|
||||
autoscalePadding: 0.1
|
||||
};
|
||||
}
|
||||
}
|
||||
47
src/plugins/plot/vue/single/configuration/configStore.js
Normal file
47
src/plugins/plot/vue/single/configuration/configStore.js
Normal file
@@ -0,0 +1,47 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
function ConfigStore() {
|
||||
this.store = {};
|
||||
}
|
||||
|
||||
ConfigStore.prototype.deleteStore = function (id) {
|
||||
if (this.store[id]) {
|
||||
this.store[id].destroy();
|
||||
delete this.store[id];
|
||||
}
|
||||
};
|
||||
|
||||
ConfigStore.prototype.deleteAll = function () {
|
||||
Object.keys(this.store).forEach(id => this.deleteStore(id));
|
||||
};
|
||||
|
||||
ConfigStore.prototype.add = function (id, config) {
|
||||
this.store[id] = config;
|
||||
};
|
||||
|
||||
ConfigStore.prototype.get = function (id) {
|
||||
return this.store[id];
|
||||
};
|
||||
|
||||
const STORE = new ConfigStore();
|
||||
|
||||
export default STORE;
|
||||
163
src/plugins/plot/vue/single/draw/Draw2D.js
Normal file
163
src/plugins/plot/vue/single/draw/Draw2D.js
Normal file
@@ -0,0 +1,163 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import EventEmitter from 'EventEmitter';
|
||||
import eventHelpers from '../lib/eventHelpers';
|
||||
import { MARKER_SHAPES } from './MarkerShapes';
|
||||
/**
|
||||
* Create a new draw API utilizing the Canvas's 2D API for rendering.
|
||||
*
|
||||
* @constructor
|
||||
* @param {CanvasElement} canvas the canvas object to render upon
|
||||
* @throws {Error} an error is thrown if Canvas's 2D API is unavailab
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create a new draw API utilizing the Canvas's 2D API for rendering.
|
||||
*
|
||||
* @constructor
|
||||
* @param {CanvasElement} canvas the canvas object to render upon
|
||||
* @throws {Error} an error is thrown if Canvas's 2D API is unavailab
|
||||
*/
|
||||
function Draw2D(canvas) {
|
||||
this.canvas = canvas;
|
||||
this.c2d = canvas.getContext('2d');
|
||||
this.width = canvas.width;
|
||||
this.height = canvas.height;
|
||||
this.dimensions = [this.width, this.height];
|
||||
this.origin = [0, 0];
|
||||
|
||||
if (!this.c2d) {
|
||||
throw new Error("Canvas 2d API unavailable.");
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(Draw2D.prototype, EventEmitter.prototype);
|
||||
eventHelpers.extend(Draw2D.prototype);
|
||||
|
||||
// Convert from logical to physical x coordinates
|
||||
Draw2D.prototype.x = function (v) {
|
||||
return ((v - this.origin[0]) / this.dimensions[0]) * this.width;
|
||||
};
|
||||
|
||||
// Convert from logical to physical y coordinates
|
||||
Draw2D.prototype.y = function (v) {
|
||||
return this.height
|
||||
- ((v - this.origin[1]) / this.dimensions[1]) * this.height;
|
||||
};
|
||||
|
||||
// Set the color to be used for drawing operations
|
||||
Draw2D.prototype.setColor = function (color) {
|
||||
const mappedColor = color.map(function (c, i) {
|
||||
return i < 3 ? Math.floor(c * 255) : (c);
|
||||
}).join(',');
|
||||
this.c2d.strokeStyle = "rgba(" + mappedColor + ")";
|
||||
this.c2d.fillStyle = "rgba(" + mappedColor + ")";
|
||||
};
|
||||
|
||||
Draw2D.prototype.clear = function () {
|
||||
this.width = this.canvas.width = this.canvas.offsetWidth;
|
||||
this.height = this.canvas.height = this.canvas.offsetHeight;
|
||||
this.c2d.clearRect(0, 0, this.width, this.height);
|
||||
};
|
||||
|
||||
Draw2D.prototype.setDimensions = function (newDimensions, newOrigin) {
|
||||
this.dimensions = newDimensions;
|
||||
this.origin = newOrigin;
|
||||
};
|
||||
|
||||
Draw2D.prototype.drawLine = function (buf, color, points) {
|
||||
let i;
|
||||
|
||||
this.setColor(color);
|
||||
|
||||
// Configure context to draw two-pixel-thick lines
|
||||
this.c2d.lineWidth = 1;
|
||||
|
||||
// Start a new path...
|
||||
if (buf.length > 1) {
|
||||
this.c2d.beginPath();
|
||||
this.c2d.moveTo(this.x(buf[0]), this.y(buf[1]));
|
||||
}
|
||||
|
||||
// ...and add points to it...
|
||||
for (i = 2; i < points * 2; i = i + 2) {
|
||||
this.c2d.lineTo(this.x(buf[i]), this.y(buf[i + 1]));
|
||||
}
|
||||
|
||||
// ...before finally drawing it.
|
||||
this.c2d.stroke();
|
||||
};
|
||||
|
||||
Draw2D.prototype.drawSquare = function (min, max, color) {
|
||||
const x1 = this.x(min[0]);
|
||||
const y1 = this.y(min[1]);
|
||||
const w = this.x(max[0]) - x1;
|
||||
const h = this.y(max[1]) - y1;
|
||||
|
||||
this.setColor(color);
|
||||
this.c2d.fillRect(x1, y1, w, h);
|
||||
};
|
||||
|
||||
Draw2D.prototype.drawPoints = function (
|
||||
buf,
|
||||
color,
|
||||
points,
|
||||
pointSize,
|
||||
shape
|
||||
) {
|
||||
const drawC2DShape = MARKER_SHAPES[shape].drawC2D.bind(this);
|
||||
|
||||
this.setColor(color);
|
||||
|
||||
for (let i = 0; i < points; i++) {
|
||||
drawC2DShape(
|
||||
this.x(buf[i * 2]),
|
||||
this.y(buf[i * 2 + 1]),
|
||||
pointSize
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
Draw2D.prototype.drawLimitPoint = function (x, y, size) {
|
||||
this.c2d.fillRect(x + size, y, size, size);
|
||||
this.c2d.fillRect(x, y + size, size, size);
|
||||
this.c2d.fillRect(x - size, y, size, size);
|
||||
this.c2d.fillRect(x, y - size, size, size);
|
||||
};
|
||||
|
||||
Draw2D.prototype.drawLimitPoints = function (points, color, pointSize) {
|
||||
const limitSize = pointSize * 2;
|
||||
const offset = limitSize / 2;
|
||||
|
||||
this.setColor(color);
|
||||
|
||||
for (let i = 0; i < points.length; i++) {
|
||||
this.drawLimitPoint(
|
||||
this.x(points[i].x) - offset,
|
||||
this.y(points[i].y) - offset,
|
||||
limitSize
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default Draw2D;
|
||||
102
src/plugins/plot/vue/single/draw/DrawLoader.js
Normal file
102
src/plugins/plot/vue/single/draw/DrawLoader.js
Normal file
@@ -0,0 +1,102 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import DrawWebGL from './DrawWebGL';
|
||||
import Draw2D from './Draw2D';
|
||||
|
||||
const CHARTS = [
|
||||
{
|
||||
MAX_INSTANCES: 16,
|
||||
API: DrawWebGL,
|
||||
ALLOCATIONS: []
|
||||
},
|
||||
{
|
||||
MAX_INSTANCES: Number.POSITIVE_INFINITY,
|
||||
API: Draw2D,
|
||||
ALLOCATIONS: []
|
||||
}
|
||||
];
|
||||
/**
|
||||
* Draw loader attaches a draw API to a canvas element and returns the
|
||||
* draw API.
|
||||
*/
|
||||
|
||||
export const DrawLoader = {
|
||||
/**
|
||||
* Return the first draw API available. Returns
|
||||
* `undefined` if a draw API could not be constructed.
|
||||
*.
|
||||
* @param {CanvasElement} canvas - The canvas eelement to attach
|
||||
the draw API to.
|
||||
*/
|
||||
getDrawAPI: function (canvas, overlay) {
|
||||
let api;
|
||||
|
||||
CHARTS.forEach(function (CHART_TYPE) {
|
||||
if (api) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (CHART_TYPE.ALLOCATIONS.length
|
||||
>= CHART_TYPE.MAX_INSTANCES) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
api = new CHART_TYPE.API(canvas, overlay);
|
||||
CHART_TYPE.ALLOCATIONS.push(api);
|
||||
} catch (e) {
|
||||
console.warn([
|
||||
"Could not instantiate chart",
|
||||
CHART_TYPE.API.name,
|
||||
";",
|
||||
e.message
|
||||
].join(" "));
|
||||
}
|
||||
});
|
||||
|
||||
if (!api) {
|
||||
console.warn("Cannot initialize mct-chart.");
|
||||
}
|
||||
|
||||
return api;
|
||||
},
|
||||
/**
|
||||
* Returns a fallback draw api.
|
||||
*/
|
||||
getFallbackDrawAPI: function (canvas, overlay) {
|
||||
const api = new CHARTS[1].API(canvas, overlay);
|
||||
CHARTS[1].ALLOCATIONS.push(api);
|
||||
|
||||
return api;
|
||||
},
|
||||
releaseDrawAPI: function (api) {
|
||||
CHARTS.forEach(function (CHART_TYPE) {
|
||||
if (api instanceof CHART_TYPE.API) {
|
||||
CHART_TYPE.ALLOCATIONS.splice(CHART_TYPE.ALLOCATIONS.indexOf(api), 1);
|
||||
}
|
||||
});
|
||||
if (api.destroy) {
|
||||
api.destroy();
|
||||
}
|
||||
}
|
||||
};
|
||||
300
src/plugins/plot/vue/single/draw/DrawWebGL.js
Normal file
300
src/plugins/plot/vue/single/draw/DrawWebGL.js
Normal file
@@ -0,0 +1,300 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import EventEmitter from 'EventEmitter';
|
||||
import eventHelpers from '../lib/eventHelpers';
|
||||
import { MARKER_SHAPES } from './MarkerShapes';
|
||||
|
||||
// WebGL shader sources (for drawing plain colors)
|
||||
const FRAGMENT_SHADER = `
|
||||
precision mediump float;
|
||||
uniform vec4 uColor;
|
||||
uniform int uMarkerShape;
|
||||
|
||||
void main(void) {
|
||||
gl_FragColor = uColor;
|
||||
|
||||
if (uMarkerShape > 1) {
|
||||
vec2 clipSpacePointCoord = 2.0 * gl_PointCoord - 1.0;
|
||||
|
||||
if (uMarkerShape == 2) { // circle
|
||||
float distance = length(clipSpacePointCoord);
|
||||
|
||||
if (distance > 1.0) {
|
||||
discard;
|
||||
}
|
||||
} else if (uMarkerShape == 3) { // diamond
|
||||
float distance = abs(clipSpacePointCoord.x) + abs(clipSpacePointCoord.y);
|
||||
|
||||
if (distance > 1.0) {
|
||||
discard;
|
||||
}
|
||||
} else if (uMarkerShape == 4) { // triangle
|
||||
float x = clipSpacePointCoord.x;
|
||||
float y = clipSpacePointCoord.y;
|
||||
float distance = 2.0 * x - 1.0;
|
||||
float distance2 = -2.0 * x - 1.0;
|
||||
|
||||
if (distance > y || distance2 > y) {
|
||||
discard;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const VERTEX_SHADER = `
|
||||
attribute vec2 aVertexPosition;
|
||||
uniform vec2 uDimensions;
|
||||
uniform vec2 uOrigin;
|
||||
uniform float uPointSize;
|
||||
|
||||
void main(void) {
|
||||
gl_Position = vec4(2.0 * ((aVertexPosition - uOrigin) / uDimensions) - vec2(1,1), 0, 1);
|
||||
gl_PointSize = uPointSize;
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* Create a draw api utilizing WebGL.
|
||||
*
|
||||
* @constructor
|
||||
* @param {CanvasElement} canvas the canvas object to render upon
|
||||
* @throws {Error} an error is thrown if WebGL is unavailable.
|
||||
*/
|
||||
function DrawWebGL(canvas, overlay) {
|
||||
this.canvas = canvas;
|
||||
this.gl = this.canvas.getContext("webgl", { preserveDrawingBuffer: true })
|
||||
|| this.canvas.getContext("experimental-webgl", { preserveDrawingBuffer: true });
|
||||
|
||||
this.overlay = overlay;
|
||||
this.c2d = overlay.getContext('2d');
|
||||
if (!this.c2d) {
|
||||
throw new Error("No canvas 2d!");
|
||||
}
|
||||
|
||||
// Ensure a context was actually available before proceeding
|
||||
if (!this.gl) {
|
||||
throw new Error("WebGL unavailable.");
|
||||
}
|
||||
|
||||
this.initContext();
|
||||
|
||||
this.listenTo(this.canvas, "webglcontextlost", this.onContextLost, this);
|
||||
}
|
||||
|
||||
Object.assign(DrawWebGL.prototype, EventEmitter.prototype);
|
||||
eventHelpers.extend(DrawWebGL.prototype);
|
||||
|
||||
DrawWebGL.prototype.onContextLost = function (event) {
|
||||
this.emit('error');
|
||||
this.isContextLost = true;
|
||||
this.destroy();
|
||||
};
|
||||
|
||||
DrawWebGL.prototype.initContext = function () {
|
||||
// Initialize shaders
|
||||
this.vertexShader = this.gl.createShader(this.gl.VERTEX_SHADER);
|
||||
this.gl.shaderSource(this.vertexShader, VERTEX_SHADER);
|
||||
this.gl.compileShader(this.vertexShader);
|
||||
|
||||
this.fragmentShader = this.gl.createShader(this.gl.FRAGMENT_SHADER);
|
||||
this.gl.shaderSource(this.fragmentShader, FRAGMENT_SHADER);
|
||||
this.gl.compileShader(this.fragmentShader);
|
||||
|
||||
// Assemble vertex/fragment shaders into programs
|
||||
this.program = this.gl.createProgram();
|
||||
this.gl.attachShader(this.program, this.vertexShader);
|
||||
this.gl.attachShader(this.program, this.fragmentShader);
|
||||
this.gl.linkProgram(this.program);
|
||||
this.gl.useProgram(this.program);
|
||||
|
||||
// Get locations for attribs/uniforms from the
|
||||
// shader programs (to pass values into shaders at draw-time)
|
||||
this.aVertexPosition = this.gl.getAttribLocation(this.program, "aVertexPosition");
|
||||
this.uColor = this.gl.getUniformLocation(this.program, "uColor");
|
||||
this.uMarkerShape = this.gl.getUniformLocation(this.program, "uMarkerShape");
|
||||
this.uDimensions = this.gl.getUniformLocation(this.program, "uDimensions");
|
||||
this.uOrigin = this.gl.getUniformLocation(this.program, "uOrigin");
|
||||
this.uPointSize = this.gl.getUniformLocation(this.program, "uPointSize");
|
||||
|
||||
this.gl.enableVertexAttribArray(this.aVertexPosition);
|
||||
|
||||
// Create a buffer to holds points which will be drawn
|
||||
this.buffer = this.gl.createBuffer();
|
||||
|
||||
// Enable blending, for smoothness
|
||||
this.gl.enable(this.gl.BLEND);
|
||||
this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
};
|
||||
|
||||
DrawWebGL.prototype.destroy = function () {
|
||||
this.stopListening();
|
||||
};
|
||||
|
||||
// Convert from logical to physical x coordinates
|
||||
DrawWebGL.prototype.x = function (v) {
|
||||
return ((v - this.origin[0]) / this.dimensions[0]) * this.width;
|
||||
};
|
||||
|
||||
// Convert from logical to physical y coordinates
|
||||
DrawWebGL.prototype.y = function (v) {
|
||||
return this.height
|
||||
- ((v - this.origin[1]) / this.dimensions[1]) * this.height;
|
||||
};
|
||||
|
||||
DrawWebGL.prototype.doDraw = function (drawType, buf, color, points, shape) {
|
||||
if (this.isContextLost) {
|
||||
return;
|
||||
}
|
||||
|
||||
const shapeCode = MARKER_SHAPES[shape] ? MARKER_SHAPES[shape].drawWebGL : 0;
|
||||
|
||||
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.buffer);
|
||||
this.gl.bufferData(this.gl.ARRAY_BUFFER, buf, this.gl.DYNAMIC_DRAW);
|
||||
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);
|
||||
};
|
||||
|
||||
DrawWebGL.prototype.clear = function () {
|
||||
if (this.isContextLost) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.height = this.canvas.height = this.canvas.offsetHeight;
|
||||
this.width = this.canvas.width = this.canvas.offsetWidth;
|
||||
this.overlay.height = this.overlay.offsetHeight;
|
||||
this.overlay.width = this.overlay.offsetWidth;
|
||||
// Set the viewport size; note that we use the width/height
|
||||
// that our WebGL context reports, which may be lower
|
||||
// resolution than the canvas we requested.
|
||||
this.gl.viewport(
|
||||
0,
|
||||
0,
|
||||
this.gl.drawingBufferWidth,
|
||||
this.gl.drawingBufferHeight
|
||||
);
|
||||
this.gl.clear(this.gl.COLOR_BUFFER_BIT + this.gl.DEPTH_BUFFER_BIT);
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the logical boundaries of the chart.
|
||||
* @param {number[]} dimensions the horizontal and
|
||||
* vertical dimensions of the chart
|
||||
* @param {number[]} origin the horizontal/vertical
|
||||
* origin of the chart
|
||||
*/
|
||||
DrawWebGL.prototype.setDimensions = function (dimensions, origin) {
|
||||
this.dimensions = dimensions;
|
||||
this.origin = origin;
|
||||
if (this.isContextLost) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (dimensions && dimensions.length > 0
|
||||
&& origin && origin.length > 0) {
|
||||
this.gl.uniform2fv(this.uDimensions, dimensions);
|
||||
this.gl.uniform2fv(this.uOrigin, origin);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Draw the supplied buffer as a line strip (a sequence
|
||||
* of line segments), in the chosen color.
|
||||
* @param {Float32Array} buf the line strip to draw,
|
||||
* in alternating x/y positions
|
||||
* @param {number[]} color the color to use when drawing
|
||||
* the line, as an RGBA color where each element
|
||||
* is in the range of 0.0-1.0
|
||||
* @param {number} points the number of points to draw
|
||||
*/
|
||||
DrawWebGL.prototype.drawLine = function (buf, color, points) {
|
||||
if (this.isContextLost) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.doDraw(this.gl.LINE_STRIP, buf, color, points);
|
||||
};
|
||||
|
||||
/**
|
||||
* Draw the buffer as points.
|
||||
*
|
||||
*/
|
||||
DrawWebGL.prototype.drawPoints = function (buf, color, points, pointSize, shape) {
|
||||
if (this.isContextLost) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.gl.uniform1f(this.uPointSize, pointSize);
|
||||
this.doDraw(this.gl.POINTS, buf, color, points, shape);
|
||||
};
|
||||
|
||||
/**
|
||||
* Draw a rectangle extending from one corner to another,
|
||||
* in the chosen color.
|
||||
* @param {number[]} min the first corner of the rectangle
|
||||
* @param {number[]} max the opposite corner
|
||||
* @param {number[]} color the color to use when drawing
|
||||
* the rectangle, as an RGBA color where each element
|
||||
* is in the range of 0.0-1.0
|
||||
*/
|
||||
DrawWebGL.prototype.drawSquare = function (min, max, color) {
|
||||
if (this.isContextLost) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.doDraw(this.gl.TRIANGLE_FAN, new Float32Array(
|
||||
min.concat([min[0], max[1]]).concat(max).concat([max[0], min[1]])
|
||||
), color, 4);
|
||||
};
|
||||
|
||||
DrawWebGL.prototype.drawLimitPoint = function (x, y, size) {
|
||||
this.c2d.fillRect(x + size, y, size, size);
|
||||
this.c2d.fillRect(x, y + size, size, size);
|
||||
this.c2d.fillRect(x - size, y, size, size);
|
||||
this.c2d.fillRect(x, y - size, size, size);
|
||||
};
|
||||
|
||||
DrawWebGL.prototype.drawLimitPoints = function (points, color, pointSize) {
|
||||
const limitSize = pointSize * 2;
|
||||
const offset = limitSize / 2;
|
||||
|
||||
const mappedColor = color.map(function (c, i) {
|
||||
return i < 3 ? Math.floor(c * 255) : (c);
|
||||
}).join(',');
|
||||
this.c2d.strokeStyle = "rgba(" + mappedColor + ")";
|
||||
this.c2d.fillStyle = "rgba(" + mappedColor + ")";
|
||||
|
||||
for (let i = 0; i < points.length; i++) {
|
||||
this.drawLimitPoint(
|
||||
this.x(points[i].x) - offset,
|
||||
this.y(points[i].y) - offset,
|
||||
limitSize
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default DrawWebGL;
|
||||
86
src/plugins/plot/vue/single/draw/MarkerShapes.js
Normal file
86
src/plugins/plot/vue/single/draw/MarkerShapes.js
Normal file
@@ -0,0 +1,86 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* @label string (required) display name of shape
|
||||
* @drawWebGL integer (unique, required) index provided to WebGL Fragment Shader
|
||||
* @drawC2D function (required) canvas2d draw function
|
||||
*/
|
||||
export const MARKER_SHAPES = {
|
||||
point: {
|
||||
label: 'Point',
|
||||
drawWebGL: 1,
|
||||
drawC2D: function (x, y, size) {
|
||||
const offset = size / 2;
|
||||
|
||||
this.c2d.fillRect(x - offset, y - offset, size, size);
|
||||
}
|
||||
},
|
||||
circle: {
|
||||
label: 'Circle',
|
||||
drawWebGL: 2,
|
||||
drawC2D: function (x, y, size) {
|
||||
const radius = size / 2;
|
||||
|
||||
this.c2d.beginPath();
|
||||
this.c2d.arc(x, y, radius, 0, 2 * Math.PI, false);
|
||||
this.c2d.closePath();
|
||||
this.c2d.fill();
|
||||
}
|
||||
},
|
||||
diamond: {
|
||||
label: 'Diamond',
|
||||
drawWebGL: 3,
|
||||
drawC2D: function (x, y, size) {
|
||||
const offset = size / 2;
|
||||
const top = [x, y + offset];
|
||||
const right = [x + offset, y];
|
||||
const bottom = [x, y - offset];
|
||||
const left = [x - offset, y];
|
||||
|
||||
this.c2d.beginPath();
|
||||
this.c2d.moveTo(...top);
|
||||
this.c2d.lineTo(...right);
|
||||
this.c2d.lineTo(...bottom);
|
||||
this.c2d.lineTo(...left);
|
||||
this.c2d.closePath();
|
||||
this.c2d.fill();
|
||||
}
|
||||
},
|
||||
triangle: {
|
||||
label: 'Triangle',
|
||||
drawWebGL: 4,
|
||||
drawC2D: function (x, y, size) {
|
||||
const offset = size / 2;
|
||||
const v1 = [x, y - offset];
|
||||
const v2 = [x - offset, y + offset];
|
||||
const v3 = [x + offset, y + offset];
|
||||
|
||||
this.c2d.beginPath();
|
||||
this.c2d.moveTo(...v1);
|
||||
this.c2d.lineTo(...v2);
|
||||
this.c2d.lineTo(...v3);
|
||||
this.c2d.closePath();
|
||||
this.c2d.fill();
|
||||
}
|
||||
}
|
||||
};
|
||||
153
src/plugins/plot/vue/single/legend/PlotLegend.vue
Normal file
153
src/plugins/plot/vue/single/legend/PlotLegend.vue
Normal file
@@ -0,0 +1,153 @@
|
||||
<!--
|
||||
Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
as represented by the Administrator of the National Aeronautics and Space
|
||||
Administration. All rights reserved.
|
||||
|
||||
Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0.
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
License for the specific language governing permissions and limitations
|
||||
under the License.
|
||||
|
||||
Open MCT includes source code licensed under additional open source
|
||||
licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
this source code distribution or the Licensing information page available
|
||||
at runtime from the About dialog for additional information.
|
||||
-->
|
||||
<template>
|
||||
<div class="c-plot-legend gl-plot-legend"
|
||||
:class="{
|
||||
'hover-on-plot': !!highlights.length,
|
||||
'is-legend-hidden': isLegendHidden
|
||||
}"
|
||||
>
|
||||
<div class="c-plot-legend__view-control gl-plot-legend__view-control c-disclosure-triangle is-enabled"
|
||||
:class="{ 'c-disclosure-triangle--expanded': isLegendExpanded }"
|
||||
@click="expandLegend"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="c-plot-legend__wrapper"
|
||||
:class="{ 'is-cursor-locked': cursorLocked }"
|
||||
>
|
||||
|
||||
<!-- COLLAPSED PLOT LEGEND -->
|
||||
<div class="plot-wrapper-collapsed-legend"
|
||||
:class="{'is-cursor-locked': cursorLocked }"
|
||||
>
|
||||
<div class="c-state-indicator__alert-cursor-lock icon-cursor-lock"
|
||||
title="Cursor is point locked. Click anywhere in the plot to unlock."
|
||||
></div>
|
||||
<plot-legend-item-collapsed v-for="seriesObject in series"
|
||||
:key="seriesObject.keyString"
|
||||
:highlights="highlights"
|
||||
:value-to-show-when-collapsed="legend.get('valueToShowWhenCollapsed')"
|
||||
:series-object="seriesObject"
|
||||
:closest="seriesObject.closest"
|
||||
/>
|
||||
</div>
|
||||
<!-- EXPANDED PLOT LEGEND -->
|
||||
<div class="plot-wrapper-expanded-legend"
|
||||
:class="{'is-cursor-locked': cursorLocked }"
|
||||
>
|
||||
<div class="c-state-indicator__alert-cursor-lock--verbose icon-cursor-lock"
|
||||
title="Click anywhere in the plot to unlock."
|
||||
> Cursor locked to point</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th v-if="showTimestampWhenExpanded">
|
||||
Timestamp
|
||||
</th>
|
||||
<th v-if="showValueWhenExpanded">
|
||||
Value
|
||||
</th>
|
||||
<th v-if="showUnitsWhenExpanded">
|
||||
Unit
|
||||
</th>
|
||||
<th v-if="showMinimumWhenExpanded"
|
||||
class="mobile-hide"
|
||||
>
|
||||
Min
|
||||
</th>
|
||||
<th v-if="showMaximumWhenExpanded"
|
||||
class="mobile-hide"
|
||||
>
|
||||
Max
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<plot-legend-item-expanded v-for="seriesObject in series"
|
||||
:key="seriesObject.keyString"
|
||||
:series-object="seriesObject"
|
||||
:highlights="highlights"
|
||||
:legend="legend"
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import PlotLegendItemCollapsed from "./PlotLegendItemCollapsed.vue";
|
||||
import PlotLegendItemExpanded from "./PlotLegendItemExpanded.vue";
|
||||
export default {
|
||||
components: {
|
||||
PlotLegendItemExpanded,
|
||||
PlotLegendItemCollapsed
|
||||
},
|
||||
inject: ['openmct', 'domainObject'],
|
||||
props: {
|
||||
cursorLocked: {
|
||||
type: Boolean,
|
||||
default() {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
series: {
|
||||
type: Array,
|
||||
default() {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
highlights: {
|
||||
type: Array,
|
||||
default() {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isLegendHidden: this.legend.get('hideLegendWhenSmall') !== true,
|
||||
isLegendExpanded: this.legend.get('expanded') === true,
|
||||
showTimestampWhenExpanded: this.legend.get('showTimestampWhenExpanded') === true,
|
||||
showValueWhenExpanded: this.legend.get('showValueWhenExpanded') === true,
|
||||
showUnitsWhenExpanded: this.legend.get('showUnitsWhenExpanded') === true,
|
||||
showMinimumWhenExpanded: this.legend.get('showMinimumWhenExpanded') === true,
|
||||
showMaximumWhenExpanded: this.legend.get('showMaximumWhenExpanded') === true
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
expandLegend() {
|
||||
this.isLegendExpanded = !this.isLegendExpanded;
|
||||
this.legend.set('expanded', this.isLegendExpanded);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
128
src/plugins/plot/vue/single/legend/PlotLegendItemCollapsed.vue
Normal file
128
src/plugins/plot/vue/single/legend/PlotLegendItemCollapsed.vue
Normal file
@@ -0,0 +1,128 @@
|
||||
<!--
|
||||
Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
as represented by the Administrator of the National Aeronautics and Space
|
||||
Administration. All rights reserved.
|
||||
|
||||
Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0.
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
License for the specific language governing permissions and limitations
|
||||
under the License.
|
||||
|
||||
Open MCT includes source code licensed under additional open source
|
||||
licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
this source code distribution or the Licensing information page available
|
||||
at runtime from the About dialog for additional information.
|
||||
-->
|
||||
<template>
|
||||
<div class="plot-legend-item"
|
||||
:class="{
|
||||
'is-status--missing': isMissing
|
||||
}"
|
||||
>
|
||||
<div class="plot-series-swatch-and-name">
|
||||
<span class="plot-series-color-swatch"
|
||||
:style="{ 'background-color': colorAsHexString }"
|
||||
>
|
||||
</span>
|
||||
<span class="is-status__indicator"
|
||||
title="This item is missing or suspect"
|
||||
></span>
|
||||
<span class="plot-series-name">{{ nameWithUnit }}</span>
|
||||
</div>
|
||||
<div v-show="!!highlights.length && (valueToShowWhenCollapsed !== 'none')"
|
||||
class="plot-series-value hover-value-enabled"
|
||||
:class="[{ 'cursor-hover': notNearest }, valueToDisplayWhenCollapsedClass, mctLimitStateClass]"
|
||||
>
|
||||
<span v-if="valueToShowWhenCollapsed === 'nearestValue'">{{ formattedYValue }}</span>
|
||||
<span v-else-if="valueToShowWhenCollapsed === 'nearestTimestamp'">{{ formattedXValue }}</span>
|
||||
<span v-else>{{ formattedYValueFromStats }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: {
|
||||
valueToShowWhenCollapsed: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
seriesObject: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default() {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
highlights: {
|
||||
type: Array,
|
||||
default() {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isMissing: false,
|
||||
colorAsHexString: '',
|
||||
nameWithUnit: '',
|
||||
formattedYValue: '',
|
||||
formattedXValue: '',
|
||||
mctLimitStateClass: '',
|
||||
formattedYValueFromStats: ''
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
valueToDisplayWhenCollapsedClass() {
|
||||
return `value-to-display-${ this.valueToShowWhenCollapsed }`;
|
||||
},
|
||||
notNearest() {
|
||||
return (this.valueToShowWhenCollapsed.indexOf('nearest') > -1);
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
highlights(newHighlights) {
|
||||
const highlightedObject = newHighlights.find(highlight => highlight.series.keyString === this.seriesObject.keyString);
|
||||
if (newHighlights.length === 0 || highlightedObject) {
|
||||
this.initialize(highlightedObject);
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.initialize();
|
||||
},
|
||||
methods: {
|
||||
initialize(highlightedObject) {
|
||||
const seriesObject = highlightedObject ? highlightedObject.series : this.seriesObject;
|
||||
this.isMissing = seriesObject.domainObject.status === 'missing';
|
||||
this.colorAsHexString = seriesObject.get('color').asHexString();
|
||||
this.nameWithUnit = seriesObject.nameWithUnit();
|
||||
|
||||
const closest = seriesObject.closest;
|
||||
if (closest) {
|
||||
this.formattedYValue = seriesObject.formatY(closest);
|
||||
this.formattedXValue = seriesObject.formatX(closest);
|
||||
this.mctLimitStateClass = closest.mctLimitState ? `${closest.mctLimitState.cssClass}` : '';
|
||||
} else {
|
||||
this.formattedYValue = '';
|
||||
this.formattedXValue = '';
|
||||
this.mctLimitStateClass = '';
|
||||
}
|
||||
|
||||
const stats = seriesObject.get('stats');
|
||||
if (stats) {
|
||||
this.formattedYValueFromStats = seriesObject.formatY(stats[this.valueToShowWhenCollapsed + 'Point']);
|
||||
} else {
|
||||
this.formattedYValueFromStats = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
154
src/plugins/plot/vue/single/legend/PlotLegendItemExpanded.vue
Normal file
154
src/plugins/plot/vue/single/legend/PlotLegendItemExpanded.vue
Normal file
@@ -0,0 +1,154 @@
|
||||
<!--
|
||||
Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
as represented by the Administrator of the National Aeronautics and Space
|
||||
Administration. All rights reserved.
|
||||
|
||||
Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0.
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
License for the specific language governing permissions and limitations
|
||||
under the License.
|
||||
|
||||
Open MCT includes source code licensed under additional open source
|
||||
licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
this source code distribution or the Licensing information page available
|
||||
at runtime from the About dialog for additional information.
|
||||
-->
|
||||
<template>
|
||||
<tr
|
||||
class="plot-legend-item"
|
||||
:class="{
|
||||
'is-status--missing': isMissing
|
||||
}"
|
||||
>
|
||||
<td class="plot-series-swatch-and-name">
|
||||
<span class="plot-series-color-swatch"
|
||||
:style="{ 'background-color': colorAsHexString }"
|
||||
>
|
||||
</span>
|
||||
<span class="is-status__indicator"
|
||||
title="This item is missing or suspect"
|
||||
></span>
|
||||
<span class="plot-series-name">{{ name }}</span>
|
||||
</td>
|
||||
|
||||
<td v-if="showTimestampWhenExpanded">
|
||||
<span class="plot-series-value cursor-hover hover-value-enabled">
|
||||
{{ formattedXValue }}
|
||||
</span>
|
||||
</td>
|
||||
<td v-if="showValueWhenExpanded">
|
||||
<span class="plot-series-value cursor-hover hover-value-enabled"
|
||||
:class="[mctLimitStateClass]"
|
||||
>
|
||||
{{ formattedYValue }}
|
||||
</span>
|
||||
</td>
|
||||
<td v-if="showUnitsWhenExpanded">
|
||||
<span class="plot-series-value cursor-hover hover-value-enabled">
|
||||
{{ unit }}
|
||||
</span>
|
||||
</td>
|
||||
<td v-if="showMinimumWhenExpanded"
|
||||
class="mobile-hide"
|
||||
>
|
||||
<span class="plot-series-value">
|
||||
{{ formattedMinY }}
|
||||
</span>
|
||||
</td>
|
||||
<td v-if="showMaximumWhenExpanded"
|
||||
class="mobile-hide"
|
||||
>
|
||||
<span class="plot-series-value">
|
||||
{{ formattedMaxY }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
seriesObject: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default() {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
highlights: {
|
||||
type: Array,
|
||||
default() {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showTimestampWhenExpanded: this.legend.get('showTimestampWhenExpanded'),
|
||||
showValueWhenExpanded: this.legend.get('showValueWhenExpanded'),
|
||||
showUnitsWhenExpanded: this.legend.get('showUnitsWhenExpanded'),
|
||||
showMinimumWhenExpanded: this.legend.get('showMinimumWhenExpanded'),
|
||||
showMaximumWhenExpanded: this.legend.get('showMaximumWhenExpanded'),
|
||||
isMissing: false,
|
||||
colorAsHexString: '',
|
||||
name: '',
|
||||
unit: '',
|
||||
formattedYValue: '',
|
||||
formattedXValue: '',
|
||||
formattedMinY: '',
|
||||
formattedMaxY: '',
|
||||
mctLimitStateClass: ''
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
highlights(newHighlights) {
|
||||
const highlightedObject = newHighlights.find(highlight => highlight.series.keyString === this.seriesObject.keyString);
|
||||
if (newHighlights.length === 0 || highlightedObject) {
|
||||
this.initialize(highlightedObject);
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.initialize();
|
||||
},
|
||||
methods: {
|
||||
initialize(highlightedObject) {
|
||||
const seriesObject = highlightedObject ? highlightedObject.series : this.seriesObject;
|
||||
|
||||
this.isMissing = seriesObject.domainObject.status === 'missing';
|
||||
this.colorAsHexString = seriesObject.get('color').asHexString();
|
||||
this.name = seriesObject.get('name');
|
||||
this.unit = seriesObject.get('unit');
|
||||
const closest = seriesObject.closest;
|
||||
if (closest) {
|
||||
this.formattedYValue = seriesObject.formatY(closest);
|
||||
this.formattedXValue = seriesObject.formatX(closest);
|
||||
this.mctLimitStateClass = seriesObject.closest.mctLimitState ? seriesObject.closest.mctLimitState.cssClass : '';
|
||||
} else {
|
||||
this.formattedYValue = '';
|
||||
this.formattedXValue = '';
|
||||
this.mctLimitStateClass = '';
|
||||
}
|
||||
|
||||
const stats = seriesObject.get('stats');
|
||||
if (stats) {
|
||||
this.formattedMinY = seriesObject.formatY(stats.minPoint);
|
||||
this.formattedMaxY = seriesObject.formatY(stats.maxPoint);
|
||||
} else {
|
||||
this.formattedMinY = '';
|
||||
this.formattedMaxY = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
98
src/plugins/plot/vue/single/lib/Color.js
Normal file
98
src/plugins/plot/vue/single/lib/Color.js
Normal file
@@ -0,0 +1,98 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* A representation of a color that allows conversions between different
|
||||
* formats.
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
/**
|
||||
* A representation of a color that allows conversions between different
|
||||
* formats.
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
function Color(integerArray) {
|
||||
this.integerArray = integerArray;
|
||||
}
|
||||
|
||||
Color.fromHexString = function (hexString) {
|
||||
if (!/#([0-9a-fA-F]{2}){2}/.test(hexString)) {
|
||||
throw new Error(
|
||||
'Invalid input "'
|
||||
+ hexString
|
||||
+ '". Hex string must be in CSS format e.g. #00FF00'
|
||||
);
|
||||
}
|
||||
|
||||
return new Color([
|
||||
parseInt(hexString.slice(1, 3), 16),
|
||||
parseInt(hexString.slice(3, 5), 16),
|
||||
parseInt(hexString.slice(5, 7), 16)
|
||||
]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return color as a three element array of RGB values, where each value
|
||||
* is a integer in the range of 0-255.
|
||||
*
|
||||
* @return {number[]} the color, as integer RGB values
|
||||
*/
|
||||
Color.prototype.asIntegerArray = function () {
|
||||
return this.integerArray.map(function (c) {
|
||||
return c;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return color as a string using #-prefixed six-digit RGB hex notation
|
||||
* (e.g. #FF0000). See http://www.w3.org/TR/css3-color/#rgb-color.
|
||||
*
|
||||
* @return {string} the color, as a style-friendly string
|
||||
*/
|
||||
|
||||
Color.prototype.asHexString = function () {
|
||||
return '#' + this.integerArray.map(function (c) {
|
||||
return (c < 16 ? '0' : '') + c.toString(16);
|
||||
}).join('');
|
||||
};
|
||||
|
||||
/**
|
||||
* Return color as a RGBA float array.
|
||||
*
|
||||
* This format is present specifically to support use with
|
||||
* WebGL, which expects colors of that form.
|
||||
*
|
||||
* @return {number[]} the color, as floating-point RGBA values
|
||||
*/
|
||||
Color.prototype.asRGBAArray = function () {
|
||||
return this.integerArray.map(function (c) {
|
||||
return c / 255.0;
|
||||
}).concat([1]);
|
||||
};
|
||||
|
||||
Color.prototype.equalTo = function (otherColor) {
|
||||
return this.asHexString() === otherColor.asHexString();
|
||||
};
|
||||
|
||||
export default Color;
|
||||
62
src/plugins/plot/vue/single/lib/ColorHelper.js
Normal file
62
src/plugins/plot/vue/single/lib/ColorHelper.js
Normal file
@@ -0,0 +1,62 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
export const COLOR_PALETTE = [
|
||||
[0x20, 0xB2, 0xAA],
|
||||
[0x9A, 0xCD, 0x32],
|
||||
[0xFF, 0x8C, 0x00],
|
||||
[0xD2, 0xB4, 0x8C],
|
||||
[0x40, 0xE0, 0xD0],
|
||||
[0x41, 0x69, 0xFF],
|
||||
[0xFF, 0xD7, 0x00],
|
||||
[0x6A, 0x5A, 0xCD],
|
||||
[0xEE, 0x82, 0xEE],
|
||||
[0xCC, 0x99, 0x66],
|
||||
[0x99, 0xCC, 0xCC],
|
||||
[0x66, 0xCC, 0x33],
|
||||
[0xFF, 0xCC, 0x00],
|
||||
[0xFF, 0x66, 0x33],
|
||||
[0xCC, 0x66, 0xFF],
|
||||
[0xFF, 0x00, 0x66],
|
||||
[0xFF, 0xFF, 0x00],
|
||||
[0x80, 0x00, 0x80],
|
||||
[0x00, 0x86, 0x8B],
|
||||
[0x00, 0x8A, 0x00],
|
||||
[0xFF, 0x00, 0x00],
|
||||
[0x00, 0x00, 0xFF],
|
||||
[0xF5, 0xDE, 0xB3],
|
||||
[0xBC, 0x8F, 0x8F],
|
||||
[0x46, 0x82, 0xB4],
|
||||
[0xFF, 0xAF, 0xAF],
|
||||
[0x43, 0xCD, 0x80],
|
||||
[0xCD, 0xC1, 0xC5],
|
||||
[0xA0, 0x52, 0x2D],
|
||||
[0x64, 0x95, 0xED]
|
||||
];
|
||||
|
||||
export function isDefaultColor(color) {
|
||||
const a = color.asIntegerArray();
|
||||
|
||||
return COLOR_PALETTE.some(function (b) {
|
||||
return a[0] === b[0] && a[1] === b[1] && a[2] === b[2];
|
||||
});
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user