Compare commits
57 Commits
fix-missin
...
compass-ro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
98a048062f | ||
|
|
9b114c49df | ||
|
|
2c0c998e29 | ||
|
|
a001e07600 | ||
|
|
4b40233bf3 | ||
|
|
f3f833a337 | ||
|
|
e6e8b8e048 | ||
|
|
6a2c079336 | ||
|
|
334aeb42ae | ||
|
|
5900bb0d98 | ||
|
|
a2c350b105 | ||
|
|
174f212328 | ||
|
|
a28ec45f71 | ||
|
|
fcc6bb9873 | ||
|
|
45578b113f | ||
|
|
5ef14b0975 | ||
|
|
2be429a04f | ||
|
|
3804fe1a1e | ||
|
|
7576673e77 | ||
|
|
63e04caab6 | ||
|
|
956cfbd01f | ||
|
|
6c77be32c7 | ||
|
|
c0742d521c | ||
|
|
f732167e02 | ||
|
|
1d56fd98dc | ||
|
|
b43fef6e21 | ||
|
|
d04c29345b | ||
|
|
24b96cdb47 | ||
|
|
14ce4a1aa0 | ||
|
|
28d97be60e | ||
|
|
1f6e91c6b5 | ||
|
|
0b078497f1 | ||
|
|
060a1b17db | ||
|
|
417f81b7fd | ||
|
|
f219394abd | ||
|
|
4e5c74ecef | ||
|
|
218530e436 | ||
|
|
0890499a2b | ||
|
|
d9dad09dfd | ||
|
|
9af5df0f20 | ||
|
|
1580a61092 | ||
|
|
c236444a05 | ||
|
|
39c1eb1d5b | ||
|
|
92737b43af | ||
|
|
95caab944d | ||
|
|
ac89e51d1b | ||
|
|
85d9ed8287 | ||
|
|
aedc24a2da | ||
|
|
8b0f6885ee | ||
|
|
823eda4465 | ||
|
|
7eaa1d3e2b | ||
|
|
9af2d15cef | ||
|
|
4633436cbd | ||
|
|
e60d8d08a4 | ||
|
|
b68a7e27c9 | ||
|
|
3e9b567fce | ||
|
|
6f51de85db |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openmct",
|
||||
"version": "1.5.0-SNAPSHOT",
|
||||
"version": "1.6.1-SNAPSHOT",
|
||||
"description": "The Open MCT core platform",
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -44,9 +44,9 @@ define(
|
||||
// is also invoked during the create process which should be allowed,
|
||||
// because it may be saved elsewhere
|
||||
if ((key === 'edit' && category === 'view-control') || key === 'properties') {
|
||||
let newStyleObject = objectUtils.toNewFormat(domainObject, domainObject.getId());
|
||||
let identifier = this.openmct.objects.parseKeyString(domainObject.getId());
|
||||
|
||||
return this.openmct.objects.isPersistable(newStyleObject);
|
||||
return this.openmct.objects.isPersistable(identifier);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -43,7 +43,8 @@ define(
|
||||
);
|
||||
|
||||
mockObjectAPI = jasmine.createSpyObj('objectAPI', [
|
||||
'isPersistable'
|
||||
'isPersistable',
|
||||
'parseKeyString'
|
||||
]);
|
||||
|
||||
mockAPI = {
|
||||
|
||||
@@ -48,9 +48,9 @@ define(
|
||||
// prevents editing of objects that cannot be persisted, so we can assume that this
|
||||
// is a new object.
|
||||
if (!(parent.hasCapability('editor') && parent.getCapability('editor').isEditContextRoot())) {
|
||||
let newStyleObject = objectUtils.toNewFormat(parent, parent.getId());
|
||||
let identifier = this.openmct.objects.parseKeyString(parent.getId());
|
||||
|
||||
return this.openmct.objects.isPersistable(newStyleObject);
|
||||
return this.openmct.objects.isPersistable(identifier);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -33,7 +33,8 @@ define(
|
||||
|
||||
beforeEach(function () {
|
||||
objectAPI = jasmine.createSpyObj('objectsAPI', [
|
||||
'isPersistable'
|
||||
'isPersistable',
|
||||
'parseKeyString'
|
||||
]);
|
||||
|
||||
mockOpenMCT = {
|
||||
|
||||
@@ -219,7 +219,7 @@ define([
|
||||
* @memberof module:openmct.MCT#
|
||||
* @name objects
|
||||
*/
|
||||
this.objects = new api.ObjectAPI();
|
||||
this.objects = new api.ObjectAPI.default(this.types);
|
||||
|
||||
/**
|
||||
* An interface for retrieving and interpreting telemetry data associated
|
||||
@@ -371,7 +371,7 @@ define([
|
||||
* MCT; if undefined, MCT will be run in the body of the document
|
||||
*/
|
||||
MCT.prototype.start = function (domElement = document.body, isHeadlessMode = false) {
|
||||
if (!this.plugins.DisplayLayout._installed) {
|
||||
if (this.types.get('layout') === undefined) {
|
||||
this.install(this.plugins.DisplayLayout({
|
||||
showAsView: ['summary-widget']
|
||||
}));
|
||||
|
||||
@@ -61,6 +61,7 @@ define([
|
||||
const newStyleObject = utils.toNewFormat(legacyObject.getModel(), legacyObject.getId());
|
||||
const keystring = utils.makeKeyString(newStyleObject.identifier);
|
||||
|
||||
this.eventEmitter.emit(keystring + ':$_synchronize_model', newStyleObject);
|
||||
this.eventEmitter.emit(keystring + ":*", newStyleObject);
|
||||
this.eventEmitter.emit('mutation', newStyleObject);
|
||||
}.bind(this);
|
||||
|
||||
@@ -50,6 +50,10 @@ describe('The ActionCollection', () => {
|
||||
}
|
||||
}
|
||||
];
|
||||
openmct.objects.addProvider('', jasmine.createSpyObj('mockMutableObjectProvider', [
|
||||
'create',
|
||||
'update'
|
||||
]));
|
||||
mockView = {
|
||||
getViewContext: () => {
|
||||
return {
|
||||
|
||||
@@ -60,6 +60,17 @@ define([
|
||||
};
|
||||
this.onProviderAdd = this.onProviderAdd.bind(this);
|
||||
this.onProviderRemove = this.onProviderRemove.bind(this);
|
||||
this.mutables = {};
|
||||
|
||||
if (this.domainObject.isMutable) {
|
||||
this.returnMutables = true;
|
||||
let unobserve = this.domainObject.$on('$_destroy', () => {
|
||||
Object.values(this.mutables).forEach(mutable => {
|
||||
this.publicAPI.objects.destroyMutable(mutable);
|
||||
});
|
||||
unobserve();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,10 +86,6 @@ define([
|
||||
throw new Error('Event not supported by composition: ' + event);
|
||||
}
|
||||
|
||||
if (!this.mutationListener) {
|
||||
this._synchronize();
|
||||
}
|
||||
|
||||
if (this.provider.on && this.provider.off) {
|
||||
if (event === 'add') {
|
||||
this.provider.on(
|
||||
@@ -189,6 +196,13 @@ define([
|
||||
|
||||
this.provider.add(this.domainObject, child.identifier);
|
||||
} else {
|
||||
if (this.returnMutables && this.publicAPI.objects.supportsMutation(child)) {
|
||||
let keyString = this.publicAPI.objects.makeKeyString(child.identifier);
|
||||
|
||||
child = this.publicAPI.objects._toMutable(child);
|
||||
this.mutables[keyString] = child;
|
||||
}
|
||||
|
||||
this.emit('add', child);
|
||||
}
|
||||
};
|
||||
@@ -202,6 +216,8 @@ define([
|
||||
* @name load
|
||||
*/
|
||||
CompositionCollection.prototype.load = function () {
|
||||
this.cleanUpMutables();
|
||||
|
||||
return this.provider.load(this.domainObject)
|
||||
.then(function (children) {
|
||||
return Promise.all(children.map((c) => this.publicAPI.objects.get(c)));
|
||||
@@ -234,6 +250,14 @@ define([
|
||||
if (!skipMutate) {
|
||||
this.provider.remove(this.domainObject, child.identifier);
|
||||
} else {
|
||||
if (this.returnMutables) {
|
||||
let keyString = this.publicAPI.objects.makeKeyString(child);
|
||||
if (this.mutables[keyString] !== undefined && this.mutables[keyString].isMutable) {
|
||||
this.publicAPI.objects.destroyMutable(this.mutables[keyString]);
|
||||
delete this.mutables[keyString];
|
||||
}
|
||||
}
|
||||
|
||||
this.emit('remove', child);
|
||||
}
|
||||
};
|
||||
@@ -281,12 +305,6 @@ define([
|
||||
this.remove(child, true);
|
||||
};
|
||||
|
||||
CompositionCollection.prototype._synchronize = function () {
|
||||
this.mutationListener = this.publicAPI.objects.observe(this.domainObject, '*', (newDomainObject) => {
|
||||
this.domainObject = JSON.parse(JSON.stringify(newDomainObject));
|
||||
});
|
||||
};
|
||||
|
||||
CompositionCollection.prototype._destroy = function () {
|
||||
if (this.mutationListener) {
|
||||
this.mutationListener();
|
||||
@@ -308,5 +326,11 @@ define([
|
||||
});
|
||||
};
|
||||
|
||||
CompositionCollection.prototype.cleanUpMutables = function () {
|
||||
Object.values(this.mutables).forEach(mutable => {
|
||||
this.publicAPI.objects.destroyMutable(mutable);
|
||||
});
|
||||
};
|
||||
|
||||
return CompositionCollection;
|
||||
});
|
||||
|
||||
154
src/api/notifications/NotificationAPISpec.js
Normal file
154
src/api/notifications/NotificationAPISpec.js
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.
|
||||
*****************************************************************************/
|
||||
|
||||
import NotificationAPI from './NotificationAPI';
|
||||
|
||||
describe('The Notifiation API', () => {
|
||||
let notificationAPIInstance;
|
||||
let defaultTimeout = 4000;
|
||||
|
||||
beforeAll(() => {
|
||||
notificationAPIInstance = new NotificationAPI();
|
||||
});
|
||||
|
||||
describe('the info method', () => {
|
||||
let message = 'Example Notification Message';
|
||||
let severity = 'info';
|
||||
let notificationModel;
|
||||
|
||||
beforeAll(() => {
|
||||
notificationModel = notificationAPIInstance.info(message).model;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
notificationAPIInstance.dismissAllNotifications();
|
||||
});
|
||||
|
||||
it('shows a string message with info severity', () => {
|
||||
expect(notificationModel.message).toEqual(message);
|
||||
expect(notificationModel.severity).toEqual(severity);
|
||||
});
|
||||
|
||||
it('auto dismisses the notification after a brief timeout', (done) => {
|
||||
window.setTimeout(() => {
|
||||
expect(notificationAPIInstance.notifications.length).toEqual(0);
|
||||
done();
|
||||
}, defaultTimeout);
|
||||
});
|
||||
});
|
||||
|
||||
describe('the alert method', () => {
|
||||
let message = 'Example alert message';
|
||||
let severity = 'alert';
|
||||
let notificationModel;
|
||||
|
||||
beforeAll(() => {
|
||||
notificationModel = notificationAPIInstance.alert(message).model;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
notificationAPIInstance.dismissAllNotifications();
|
||||
});
|
||||
|
||||
it('shows a string message, with alert severity', () => {
|
||||
expect(notificationModel.message).toEqual(message);
|
||||
expect(notificationModel.severity).toEqual(severity);
|
||||
});
|
||||
|
||||
it('does not auto dismiss the notification', (done) => {
|
||||
window.setTimeout(() => {
|
||||
expect(notificationAPIInstance.notifications.length).toEqual(1);
|
||||
done();
|
||||
}, defaultTimeout);
|
||||
});
|
||||
});
|
||||
|
||||
describe('the error method', () => {
|
||||
let message = 'Example error message';
|
||||
let severity = 'error';
|
||||
let notificationModel;
|
||||
|
||||
beforeAll(() => {
|
||||
notificationModel = notificationAPIInstance.error(message).model;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
notificationAPIInstance.dismissAllNotifications();
|
||||
});
|
||||
|
||||
it('shows a string message, with severity error', () => {
|
||||
expect(notificationModel.message).toEqual(message);
|
||||
expect(notificationModel.severity).toEqual(severity);
|
||||
});
|
||||
|
||||
it('does not auto dismiss the notification', (done) => {
|
||||
window.setTimeout(() => {
|
||||
expect(notificationAPIInstance.notifications.length).toEqual(1);
|
||||
done();
|
||||
}, defaultTimeout);
|
||||
});
|
||||
});
|
||||
|
||||
describe('the progress method', () => {
|
||||
let title = 'This is a progress notification';
|
||||
let message1 = 'Example progress message 1';
|
||||
let message2 = 'Example progress message 2';
|
||||
let percentage1 = 50;
|
||||
let percentage2 = 99.9;
|
||||
let severity = 'info';
|
||||
let notification;
|
||||
let updatedPercentage;
|
||||
let updatedMessage;
|
||||
|
||||
beforeAll(() => {
|
||||
notification = notificationAPIInstance.progress(title, percentage1, message1);
|
||||
notification.on('progress', (percentage, text) => {
|
||||
updatedPercentage = percentage;
|
||||
updatedMessage = text;
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
notificationAPIInstance.dismissAllNotifications();
|
||||
});
|
||||
|
||||
it ('shows a notification with a message, progress message, percentage and info severity', () => {
|
||||
expect(notification.model.message).toEqual(title);
|
||||
expect(notification.model.severity).toEqual(severity);
|
||||
expect(notification.model.progressText).toEqual(message1);
|
||||
expect(notification.model.progressPerc).toEqual(percentage1);
|
||||
});
|
||||
|
||||
it ('allows dynamically updating the progress attributes', () => {
|
||||
notification.progress(percentage2, message2);
|
||||
|
||||
expect(updatedPercentage).toEqual(percentage2);
|
||||
expect(updatedMessage).toEqual(message2);
|
||||
});
|
||||
|
||||
it ('allows dynamically dismissing of progress notification', () => {
|
||||
notification.dismiss();
|
||||
|
||||
expect(notificationAPIInstance.notifications.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
137
src/api/objects/MutableDomainObject.js
Normal file
137
src/api/objects/MutableDomainObject.js
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.
|
||||
*****************************************************************************/
|
||||
import _ from 'lodash';
|
||||
import utils from './object-utils.js';
|
||||
import EventEmitter from 'EventEmitter';
|
||||
|
||||
const ANY_OBJECT_EVENT = 'mutation';
|
||||
|
||||
/**
|
||||
* Wraps a domain object to keep its model synchronized with other instances of the same object.
|
||||
*
|
||||
* Creating a MutableDomainObject will automatically register listeners to keep its model in sync. As such, developers
|
||||
* should be careful to destroy MutableDomainObject in order to avoid memory leaks.
|
||||
*
|
||||
* All Open MCT API functions that provide objects will provide MutableDomainObjects where possible, except
|
||||
* `openmct.objects.get()`, and will manage that object's lifecycle for you. Calling `openmct.objects.getMutable()`
|
||||
* will result in the creation of a new MutableDomainObject and you will be responsible for destroying it
|
||||
* (via openmct.objects.destroy) when you're done with it.
|
||||
*
|
||||
* @typedef MutableDomainObject
|
||||
* @memberof module:openmct
|
||||
*/
|
||||
class MutableDomainObject {
|
||||
constructor(eventEmitter) {
|
||||
Object.defineProperties(this, {
|
||||
_globalEventEmitter: {
|
||||
value: eventEmitter,
|
||||
// Property should not be serialized
|
||||
enumerable: false
|
||||
},
|
||||
_instanceEventEmitter: {
|
||||
value: new EventEmitter(),
|
||||
// Property should not be serialized
|
||||
enumerable: false
|
||||
},
|
||||
_observers: {
|
||||
value: [],
|
||||
// Property should not be serialized
|
||||
enumerable: false
|
||||
},
|
||||
isMutable: {
|
||||
value: true,
|
||||
// Property should not be serialized
|
||||
enumerable: false
|
||||
}
|
||||
});
|
||||
}
|
||||
$observe(path, callback) {
|
||||
let fullPath = qualifiedEventName(this, path);
|
||||
let eventOff =
|
||||
this._globalEventEmitter.off.bind(this._globalEventEmitter, fullPath, callback);
|
||||
|
||||
this._globalEventEmitter.on(fullPath, callback);
|
||||
this._observers.push(eventOff);
|
||||
|
||||
return eventOff;
|
||||
}
|
||||
$set(path, value) {
|
||||
_.set(this, path, value);
|
||||
_.set(this, 'modified', Date.now());
|
||||
|
||||
//Emit secret synchronization event first, so that all objects are in sync before subsequent events fired.
|
||||
this._globalEventEmitter.emit(qualifiedEventName(this, '$_synchronize_model'), this);
|
||||
|
||||
//Emit a general "any object" event
|
||||
this._globalEventEmitter.emit(ANY_OBJECT_EVENT, this);
|
||||
//Emit wildcard event, with path so that callback knows what changed
|
||||
this._globalEventEmitter.emit(qualifiedEventName(this, '*'), this, path, value);
|
||||
|
||||
//Emit events specific to properties affected
|
||||
let parentPropertiesList = path.split('.');
|
||||
for (let index = parentPropertiesList.length; index > 0; index--) {
|
||||
let parentPropertyPath = parentPropertiesList.slice(0, index).join('.');
|
||||
this._globalEventEmitter.emit(qualifiedEventName(this, parentPropertyPath), _.get(this, parentPropertyPath));
|
||||
}
|
||||
|
||||
//TODO: Emit events for listeners of child properties when parent changes.
|
||||
// Do it at observer time - also register observers for parent attribute path.
|
||||
}
|
||||
$on(event, callback) {
|
||||
this._instanceEventEmitter.on(event, callback);
|
||||
|
||||
return () => this._instanceEventEmitter.off(event, callback);
|
||||
}
|
||||
$destroy() {
|
||||
this._observers.forEach(observer => observer());
|
||||
delete this._globalEventEmitter;
|
||||
delete this._observers;
|
||||
this._instanceEventEmitter.emit('$_destroy');
|
||||
}
|
||||
|
||||
static createMutable(object, mutationTopic) {
|
||||
let mutable = Object.create(new MutableDomainObject(mutationTopic));
|
||||
Object.assign(mutable, object);
|
||||
|
||||
mutable.$observe('$_synchronize_model', (updatedObject) => {
|
||||
let clone = JSON.parse(JSON.stringify(updatedObject));
|
||||
let deleted = _.difference(Object.keys(mutable), Object.keys(updatedObject));
|
||||
deleted.forEach((propertyName) => delete mutable[propertyName]);
|
||||
Object.assign(mutable, clone);
|
||||
});
|
||||
|
||||
return mutable;
|
||||
}
|
||||
|
||||
static mutateObject(object, path, value) {
|
||||
_.set(object, path, value);
|
||||
_.set(object, 'modified', Date.now());
|
||||
}
|
||||
}
|
||||
|
||||
function qualifiedEventName(object, eventName) {
|
||||
let keystring = utils.makeKeyString(object.identifier);
|
||||
|
||||
return [keystring, eventName].join(':');
|
||||
}
|
||||
|
||||
export default MutableDomainObject;
|
||||
@@ -1,102 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
|
||||
define([
|
||||
'objectUtils',
|
||||
'lodash'
|
||||
], function (
|
||||
utils,
|
||||
_
|
||||
) {
|
||||
const ANY_OBJECT_EVENT = "mutation";
|
||||
|
||||
/**
|
||||
* The MutableObject wraps a DomainObject and provides getters and
|
||||
* setters for
|
||||
* @param eventEmitter
|
||||
* @param object
|
||||
* @interface MutableObject
|
||||
*/
|
||||
function MutableObject(eventEmitter, object) {
|
||||
this.eventEmitter = eventEmitter;
|
||||
this.object = object;
|
||||
this.unlisteners = [];
|
||||
}
|
||||
|
||||
function qualifiedEventName(object, eventName) {
|
||||
const keystring = utils.makeKeyString(object.identifier);
|
||||
|
||||
return [keystring, eventName].join(':');
|
||||
}
|
||||
|
||||
MutableObject.prototype.stopListening = function () {
|
||||
this.unlisteners.forEach(function (unlisten) {
|
||||
unlisten();
|
||||
});
|
||||
this.unlisteners = [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Observe changes to this domain object.
|
||||
* @param {string} path the property to observe
|
||||
* @param {Function} callback a callback to invoke when new values for
|
||||
* this property are observed
|
||||
* @method on
|
||||
* @memberof module:openmct.MutableObject#
|
||||
*/
|
||||
MutableObject.prototype.on = function (path, callback) {
|
||||
const fullPath = qualifiedEventName(this.object, path);
|
||||
const eventOff =
|
||||
this.eventEmitter.off.bind(this.eventEmitter, fullPath, callback);
|
||||
|
||||
this.eventEmitter.on(fullPath, callback);
|
||||
this.unlisteners.push(eventOff);
|
||||
};
|
||||
|
||||
/**
|
||||
* Modify this domain object.
|
||||
* @param {string} path the property to modify
|
||||
* @param {*} value the new value for this property
|
||||
* @method set
|
||||
* @memberof module:openmct.MutableObject#
|
||||
*/
|
||||
MutableObject.prototype.set = function (path, value) {
|
||||
_.set(this.object, path, value);
|
||||
_.set(this.object, 'modified', Date.now());
|
||||
|
||||
const handleRecursiveMutation = function (newObject) {
|
||||
this.object = newObject;
|
||||
}.bind(this);
|
||||
|
||||
//Emit wildcard event
|
||||
this.eventEmitter.emit(qualifiedEventName(this.object, '*'), this.object);
|
||||
//Emit a general "any object" event
|
||||
this.eventEmitter.emit(ANY_OBJECT_EVENT, this.object);
|
||||
|
||||
this.eventEmitter.on(qualifiedEventName(this.object, '*'), handleRecursiveMutation);
|
||||
//Emit event specific to property
|
||||
this.eventEmitter.emit(qualifiedEventName(this.object, path), value);
|
||||
this.eventEmitter.off(qualifiedEventName(this.object, '*'), handleRecursiveMutation);
|
||||
};
|
||||
|
||||
return MutableObject;
|
||||
});
|
||||
@@ -20,365 +20,425 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define([
|
||||
'lodash',
|
||||
'objectUtils',
|
||||
'./MutableObject',
|
||||
'./RootRegistry',
|
||||
'./RootObjectProvider',
|
||||
'./InterceptorRegistry',
|
||||
'EventEmitter'
|
||||
], function (
|
||||
_,
|
||||
utils,
|
||||
MutableObject,
|
||||
RootRegistry,
|
||||
RootObjectProvider,
|
||||
InterceptorRegistry,
|
||||
EventEmitter
|
||||
) {
|
||||
import utils from 'objectUtils';
|
||||
import MutableDomainObject from './MutableDomainObject';
|
||||
import RootRegistry from './RootRegistry';
|
||||
import RootObjectProvider from './RootObjectProvider';
|
||||
import EventEmitter from 'EventEmitter';
|
||||
import InterceptorRegistry from './InterceptorRegistry';
|
||||
|
||||
/**
|
||||
* Utilities for loading, saving, and manipulating domain objects.
|
||||
* @interface ObjectAPI
|
||||
* @memberof module:openmct
|
||||
*/
|
||||
/**
|
||||
* Utilities for loading, saving, and manipulating domain objects.
|
||||
* @interface ObjectAPI
|
||||
* @memberof module:openmct
|
||||
*/
|
||||
|
||||
function ObjectAPI() {
|
||||
this.eventEmitter = new EventEmitter();
|
||||
this.providers = {};
|
||||
this.rootRegistry = new RootRegistry();
|
||||
this.rootProvider = new RootObjectProvider.default(this.rootRegistry);
|
||||
this.cache = {};
|
||||
this.interceptorRegistry = new InterceptorRegistry.default();
|
||||
function ObjectAPI(typeRegistry) {
|
||||
this.typeRegistry = typeRegistry;
|
||||
this.eventEmitter = new EventEmitter();
|
||||
this.providers = {};
|
||||
this.rootRegistry = new RootRegistry();
|
||||
this.rootProvider = new RootObjectProvider(this.rootRegistry);
|
||||
this.cache = {};
|
||||
this.interceptorRegistry = new InterceptorRegistry();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set fallback provider, this is an internal API for legacy reasons.
|
||||
* @private
|
||||
*/
|
||||
ObjectAPI.prototype.supersecretSetFallbackProvider = function (p) {
|
||||
this.fallbackProvider = p;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the provider for a given identifier.
|
||||
* @private
|
||||
*/
|
||||
ObjectAPI.prototype.getProvider = function (identifier) {
|
||||
if (identifier.key === 'ROOT') {
|
||||
return this.rootProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set fallback provider, this is an internal API for legacy reasons.
|
||||
* @private
|
||||
*/
|
||||
ObjectAPI.prototype.supersecretSetFallbackProvider = function (p) {
|
||||
this.fallbackProvider = p;
|
||||
};
|
||||
return this.providers[identifier.namespace] || this.fallbackProvider;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the provider for a given identifier.
|
||||
* @private
|
||||
*/
|
||||
ObjectAPI.prototype.getProvider = function (identifier) {
|
||||
if (identifier.key === 'ROOT') {
|
||||
return this.rootProvider;
|
||||
}
|
||||
/**
|
||||
* Get the root-level object.
|
||||
* @returns {Promise.<DomainObject>} a promise for the root object
|
||||
*/
|
||||
ObjectAPI.prototype.getRoot = function () {
|
||||
return this.rootProvider.get();
|
||||
};
|
||||
|
||||
return this.providers[identifier.namespace] || this.fallbackProvider;
|
||||
};
|
||||
/**
|
||||
* Register a new object provider for a particular namespace.
|
||||
*
|
||||
* @param {string} namespace the namespace for which to provide objects
|
||||
* @param {module:openmct.ObjectProvider} provider the provider which
|
||||
* will handle loading domain objects from this namespace
|
||||
* @memberof {module:openmct.ObjectAPI#}
|
||||
* @name addProvider
|
||||
*/
|
||||
ObjectAPI.prototype.addProvider = function (namespace, provider) {
|
||||
this.providers[namespace] = provider;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the root-level object.
|
||||
* @returns {Promise.<DomainObject>} a promise for the root object
|
||||
*/
|
||||
ObjectAPI.prototype.getRoot = function () {
|
||||
return this.rootProvider.get();
|
||||
};
|
||||
/**
|
||||
* Provides the ability to read, write, and delete domain objects.
|
||||
*
|
||||
* When registering a new object provider, all methods on this interface
|
||||
* are optional.
|
||||
*
|
||||
* @interface ObjectProvider
|
||||
* @memberof module:openmct
|
||||
*/
|
||||
|
||||
/**
|
||||
* Register a new object provider for a particular namespace.
|
||||
*
|
||||
* @param {string} namespace the namespace for which to provide objects
|
||||
* @param {module:openmct.ObjectProvider} provider the provider which
|
||||
* will handle loading domain objects from this namespace
|
||||
* @memberof {module:openmct.ObjectAPI#}
|
||||
* @name addProvider
|
||||
*/
|
||||
ObjectAPI.prototype.addProvider = function (namespace, provider) {
|
||||
this.providers[namespace] = provider;
|
||||
};
|
||||
/**
|
||||
* Create the given domain object in the corresponding persistence store
|
||||
*
|
||||
* @method create
|
||||
* @memberof module:openmct.ObjectProvider#
|
||||
* @param {module:openmct.DomainObject} domainObject the domain object to
|
||||
* create
|
||||
* @returns {Promise} a promise which will resolve when the domain object
|
||||
* has been created, or be rejected if it cannot be saved
|
||||
*/
|
||||
|
||||
/**
|
||||
* Provides the ability to read, write, and delete domain objects.
|
||||
*
|
||||
* When registering a new object provider, all methods on this interface
|
||||
* are optional.
|
||||
*
|
||||
* @interface ObjectProvider
|
||||
* @memberof module:openmct
|
||||
*/
|
||||
/**
|
||||
* Update this domain object in its persistence store
|
||||
*
|
||||
* @method update
|
||||
* @memberof module:openmct.ObjectProvider#
|
||||
* @param {module:openmct.DomainObject} domainObject the domain object to
|
||||
* update
|
||||
* @returns {Promise} a promise which will resolve when the domain object
|
||||
* has been updated, or be rejected if it cannot be saved
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create the given domain object in the corresponding persistence store
|
||||
*
|
||||
* @method create
|
||||
* @memberof module:openmct.ObjectProvider#
|
||||
* @param {module:openmct.DomainObject} domainObject the domain object to
|
||||
* create
|
||||
* @returns {Promise} a promise which will resolve when the domain object
|
||||
* has been created, or be rejected if it cannot be saved
|
||||
*/
|
||||
/**
|
||||
* Delete this domain object.
|
||||
*
|
||||
* @method delete
|
||||
* @memberof module:openmct.ObjectProvider#
|
||||
* @param {module:openmct.DomainObject} domainObject the domain object to
|
||||
* delete
|
||||
* @returns {Promise} a promise which will resolve when the domain object
|
||||
* has been deleted, or be rejected if it cannot be deleted
|
||||
*/
|
||||
|
||||
/**
|
||||
* Update this domain object in its persistence store
|
||||
*
|
||||
* @method update
|
||||
* @memberof module:openmct.ObjectProvider#
|
||||
* @param {module:openmct.DomainObject} domainObject the domain object to
|
||||
* update
|
||||
* @returns {Promise} a promise which will resolve when the domain object
|
||||
* has been updated, or be rejected if it cannot be saved
|
||||
*/
|
||||
/**
|
||||
* Get a domain object.
|
||||
*
|
||||
* @method get
|
||||
* @memberof module:openmct.ObjectProvider#
|
||||
* @param {string} key the key for the domain object to load
|
||||
* @returns {Promise} a promise which will resolve when the domain object
|
||||
* has been saved, or be rejected if it cannot be saved
|
||||
*/
|
||||
|
||||
/**
|
||||
* Delete this domain object.
|
||||
*
|
||||
* @method delete
|
||||
* @memberof module:openmct.ObjectProvider#
|
||||
* @param {module:openmct.DomainObject} domainObject the domain object to
|
||||
* delete
|
||||
* @returns {Promise} a promise which will resolve when the domain object
|
||||
* has been deleted, or be rejected if it cannot be deleted
|
||||
*/
|
||||
ObjectAPI.prototype.get = function (identifier) {
|
||||
let keystring = this.makeKeyString(identifier);
|
||||
if (this.cache[keystring] !== undefined) {
|
||||
return this.cache[keystring];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a domain object.
|
||||
*
|
||||
* @method get
|
||||
* @memberof module:openmct.ObjectProvider#
|
||||
* @param {string} key the key for the domain object to load
|
||||
* @returns {Promise} a promise which will resolve when the domain object
|
||||
* has been saved, or be rejected if it cannot be saved
|
||||
*/
|
||||
identifier = utils.parseKeyString(identifier);
|
||||
const provider = this.getProvider(identifier);
|
||||
|
||||
/**
|
||||
* Get a domain object.
|
||||
*
|
||||
* @method get
|
||||
* @memberof module:openmct.ObjectAPI#
|
||||
* @param {module:openmct.ObjectAPI~Identifier} identifier
|
||||
* the identifier for the domain object to load
|
||||
* @returns {Promise} a promise which will resolve when the domain object
|
||||
* has been saved, or be rejected if it cannot be saved
|
||||
*/
|
||||
ObjectAPI.prototype.get = function (identifier) {
|
||||
let keystring = this.makeKeyString(identifier);
|
||||
if (this.cache[keystring] !== undefined) {
|
||||
return this.cache[keystring];
|
||||
}
|
||||
if (!provider) {
|
||||
throw new Error('No Provider Matched');
|
||||
}
|
||||
|
||||
identifier = utils.parseKeyString(identifier);
|
||||
const provider = this.getProvider(identifier);
|
||||
if (!provider.get) {
|
||||
throw new Error('Provider does not support get!');
|
||||
}
|
||||
|
||||
if (!provider) {
|
||||
throw new Error('No Provider Matched');
|
||||
}
|
||||
let objectPromise = provider.get(identifier);
|
||||
this.cache[keystring] = objectPromise;
|
||||
|
||||
if (!provider.get) {
|
||||
throw new Error('Provider does not support get!');
|
||||
}
|
||||
|
||||
let objectPromise = provider.get(identifier);
|
||||
|
||||
this.cache[keystring] = objectPromise;
|
||||
|
||||
return objectPromise.then(result => {
|
||||
delete this.cache[keystring];
|
||||
const interceptors = this.listGetInterceptors(identifier, result);
|
||||
interceptors.forEach(interceptor => {
|
||||
result = interceptor.invoke(identifier, result);
|
||||
});
|
||||
|
||||
return result;
|
||||
return objectPromise.then(result => {
|
||||
delete this.cache[keystring];
|
||||
const interceptors = this.listGetInterceptors(identifier, result);
|
||||
interceptors.forEach(interceptor => {
|
||||
result = interceptor.invoke(identifier, result);
|
||||
});
|
||||
};
|
||||
|
||||
ObjectAPI.prototype.delete = function () {
|
||||
throw new Error('Delete not implemented');
|
||||
};
|
||||
|
||||
ObjectAPI.prototype.isPersistable = function (domainObject) {
|
||||
let provider = this.getProvider(domainObject.identifier);
|
||||
|
||||
return provider !== undefined
|
||||
&& provider.create !== undefined
|
||||
&& provider.update !== undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Save this domain object in its current state. EXPERIMENTAL
|
||||
*
|
||||
* @private
|
||||
* @memberof module:openmct.ObjectAPI#
|
||||
* @param {module:openmct.DomainObject} domainObject the domain object to
|
||||
* save
|
||||
* @returns {Promise} a promise which will resolve when the domain object
|
||||
* has been saved, or be rejected if it cannot be saved
|
||||
*/
|
||||
ObjectAPI.prototype.save = function (domainObject) {
|
||||
let provider = this.getProvider(domainObject.identifier);
|
||||
let savedResolve;
|
||||
let result;
|
||||
|
||||
if (!this.isPersistable(domainObject)) {
|
||||
result = Promise.reject('Object provider does not support saving');
|
||||
} else if (hasAlreadyBeenPersisted(domainObject)) {
|
||||
result = Promise.resolve(true);
|
||||
} else {
|
||||
const persistedTime = Date.now();
|
||||
if (domainObject.persisted === undefined) {
|
||||
result = new Promise((resolve) => {
|
||||
savedResolve = resolve;
|
||||
});
|
||||
domainObject.persisted = persistedTime;
|
||||
provider.create(domainObject).then((response) => {
|
||||
this.mutate(domainObject, 'persisted', persistedTime);
|
||||
savedResolve(response);
|
||||
});
|
||||
} else {
|
||||
domainObject.persisted = persistedTime;
|
||||
this.mutate(domainObject, 'persisted', persistedTime);
|
||||
result = provider.update(domainObject);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Add a root-level object.
|
||||
* @param {module:openmct.ObjectAPI~Identifier|function} an array of
|
||||
* identifiers for root level objects, or a function that returns a
|
||||
* promise for an identifier or an array of root level objects.
|
||||
* @method addRoot
|
||||
* @memberof module:openmct.ObjectAPI#
|
||||
*/
|
||||
ObjectAPI.prototype.addRoot = function (key) {
|
||||
this.rootRegistry.addRoot(key);
|
||||
};
|
||||
|
||||
/**
|
||||
* Modify a domain object.
|
||||
* @param {module:openmct.DomainObject} object the object to mutate
|
||||
* @param {string} path the property to modify
|
||||
* @param {*} value the new value for this property
|
||||
* @method mutate
|
||||
* @memberof module:openmct.ObjectAPI#
|
||||
*/
|
||||
ObjectAPI.prototype.mutate = function (domainObject, path, value) {
|
||||
const mutableObject =
|
||||
new MutableObject(this.eventEmitter, domainObject);
|
||||
|
||||
return mutableObject.set(path, value);
|
||||
};
|
||||
|
||||
/**
|
||||
* Observe changes to a domain object.
|
||||
* @param {module:openmct.DomainObject} object the object to observe
|
||||
* @param {string} path the property to observe
|
||||
* @param {Function} callback a callback to invoke when new values for
|
||||
* this property are observed
|
||||
* @method observe
|
||||
* @memberof module:openmct.ObjectAPI#
|
||||
*/
|
||||
ObjectAPI.prototype.observe = function (domainObject, path, callback) {
|
||||
const mutableObject =
|
||||
new MutableObject(this.eventEmitter, domainObject);
|
||||
mutableObject.on(path, callback);
|
||||
|
||||
return mutableObject.stopListening.bind(mutableObject);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {module:openmct.ObjectAPI~Identifier} identifier
|
||||
* @returns {string} A string representation of the given identifier, including namespace and key
|
||||
*/
|
||||
ObjectAPI.prototype.makeKeyString = function (identifier) {
|
||||
return utils.makeKeyString(identifier);
|
||||
};
|
||||
|
||||
/**
|
||||
* Given any number of identifiers, will return true if they are all equal, otherwise false.
|
||||
* @param {module:openmct.ObjectAPI~Identifier[]} identifiers
|
||||
*/
|
||||
ObjectAPI.prototype.areIdsEqual = function (...identifiers) {
|
||||
return identifiers.map(utils.parseKeyString)
|
||||
.every(identifier => {
|
||||
return identifier === identifiers[0]
|
||||
|| (identifier.namespace === identifiers[0].namespace
|
||||
&& identifier.key === identifiers[0].key);
|
||||
});
|
||||
};
|
||||
|
||||
ObjectAPI.prototype.getOriginalPath = function (identifier, path = []) {
|
||||
return this.get(identifier).then((domainObject) => {
|
||||
path.push(domainObject);
|
||||
let location = domainObject.location;
|
||||
|
||||
if (location) {
|
||||
return this.getOriginalPath(utils.parseKeyString(location), path);
|
||||
} else {
|
||||
return path;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Register an object interceptor that transforms a domain object requested via module:openmct.ObjectAPI.get
|
||||
* The domain object will be transformed after it is retrieved from the persistence store
|
||||
* The domain object will be transformed only if the interceptor is applicable to that domain object as defined by the InterceptorDef
|
||||
*
|
||||
* @param {module:openmct.InterceptorDef} interceptorDef the interceptor definition to add
|
||||
* @method addGetInterceptor
|
||||
* @memberof module:openmct.InterceptorRegistry#
|
||||
*/
|
||||
ObjectAPI.prototype.addGetInterceptor = function (interceptorDef) {
|
||||
this.interceptorRegistry.addInterceptor(interceptorDef);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the interceptors for a given domain object.
|
||||
* @private
|
||||
*/
|
||||
ObjectAPI.prototype.listGetInterceptors = function (identifier, object) {
|
||||
return this.interceptorRegistry.getInterceptors(identifier, object);
|
||||
};
|
||||
|
||||
/**
|
||||
* Uniquely identifies a domain object.
|
||||
*
|
||||
* @typedef Identifier
|
||||
* @memberof module:openmct.ObjectAPI~
|
||||
* @property {string} namespace the namespace to/from which this domain
|
||||
* object should be loaded/stored.
|
||||
* @property {string} key a unique identifier for the domain object
|
||||
* within that namespace
|
||||
*/
|
||||
|
||||
/**
|
||||
* A domain object is an entity of relevance to a user's workflow, that
|
||||
* should appear as a distinct and meaningful object within the user
|
||||
* interface. Examples of domain objects are folders, telemetry sensors,
|
||||
* and so forth.
|
||||
*
|
||||
* A few common properties are defined for domain objects. Beyond these,
|
||||
* individual types of domain objects may add more as they see fit.
|
||||
*
|
||||
* @property {module:openmct.ObjectAPI~Identifier} identifier a key/namespace pair which
|
||||
* uniquely identifies this domain object
|
||||
* @property {string} type the type of domain object
|
||||
* @property {string} name the human-readable name for this domain object
|
||||
* @property {string} [creator] the user name of the creator of this domain
|
||||
* object
|
||||
* @property {number} [modified] the time, in milliseconds since the UNIX
|
||||
* epoch, at which this domain object was last modified
|
||||
* @property {module:openmct.ObjectAPI~Identifier[]} [composition] if
|
||||
* present, this will be used by the default composition provider
|
||||
* to load domain objects
|
||||
* @typedef DomainObject
|
||||
* @memberof module:openmct
|
||||
*/
|
||||
|
||||
function hasAlreadyBeenPersisted(domainObject) {
|
||||
return domainObject.persisted !== undefined
|
||||
&& domainObject.persisted === domainObject.modified;
|
||||
/**
|
||||
* 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.
|
||||
* The platform will provide mutable objects to views automatically if the underlying object can be mutated. The
|
||||
* platform will manage the lifecycle of any mutable objects that it provides. If you use `getMutable` you are
|
||||
* committing to managing that lifecycle yourself. `.destroy` should be called when the object is no longer needed.
|
||||
*
|
||||
* @memberof {module:openmct.ObjectAPI#}
|
||||
* @returns {Promise.<MutableDomainObject>} a promise that will resolve with a MutableDomainObject if
|
||||
* the object can be mutated.
|
||||
*/
|
||||
ObjectAPI.prototype.getMutable = function (identifier) {
|
||||
if (!this.supportsMutation(identifier)) {
|
||||
throw new Error(`Object "${this.makeKeyString(identifier)}" does not support mutation.`);
|
||||
}
|
||||
|
||||
return ObjectAPI;
|
||||
});
|
||||
return this.get(identifier).then((object) => {
|
||||
return this._toMutable(object);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* This function is for cleaning up a mutable domain object when you're done with it.
|
||||
* You only need to use this if you retrieved the object using `getMutable()`. If the object was provided by the
|
||||
* platform (eg. passed into a `view()` function) then the platform is responsible for its lifecycle.
|
||||
* @param {MutableDomainObject} domainObject
|
||||
*/
|
||||
ObjectAPI.prototype.destroyMutable = function (domainObject) {
|
||||
if (domainObject.isMutable) {
|
||||
return domainObject.$destroy();
|
||||
} else {
|
||||
throw new Error("Attempted to destroy non-mutable domain object");
|
||||
}
|
||||
};
|
||||
|
||||
ObjectAPI.prototype.delete = function () {
|
||||
throw new Error('Delete not implemented');
|
||||
};
|
||||
|
||||
ObjectAPI.prototype.isPersistable = function (idOrKeyString) {
|
||||
let identifier = utils.parseKeyString(idOrKeyString);
|
||||
let provider = this.getProvider(identifier);
|
||||
|
||||
return provider !== undefined
|
||||
&& provider.create !== undefined
|
||||
&& provider.update !== undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Save this domain object in its current state. EXPERIMENTAL
|
||||
*
|
||||
* @private
|
||||
* @memberof module:openmct.ObjectAPI#
|
||||
* @param {module:openmct.DomainObject} domainObject the domain object to
|
||||
* save
|
||||
* @returns {Promise} a promise which will resolve when the domain object
|
||||
* has been saved, or be rejected if it cannot be saved
|
||||
*/
|
||||
ObjectAPI.prototype.save = function (domainObject) {
|
||||
let provider = this.getProvider(domainObject.identifier);
|
||||
let savedResolve;
|
||||
let result;
|
||||
|
||||
if (!this.isPersistable(domainObject.identifier)) {
|
||||
result = Promise.reject('Object provider does not support saving');
|
||||
} else if (hasAlreadyBeenPersisted(domainObject)) {
|
||||
result = Promise.resolve(true);
|
||||
} else {
|
||||
const persistedTime = Date.now();
|
||||
if (domainObject.persisted === undefined) {
|
||||
result = new Promise((resolve) => {
|
||||
savedResolve = resolve;
|
||||
});
|
||||
domainObject.persisted = persistedTime;
|
||||
provider.create(domainObject).then((response) => {
|
||||
this.mutate(domainObject, 'persisted', persistedTime);
|
||||
savedResolve(response);
|
||||
});
|
||||
} else {
|
||||
domainObject.persisted = persistedTime;
|
||||
this.mutate(domainObject, 'persisted', persistedTime);
|
||||
result = provider.update(domainObject);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Add a root-level object.
|
||||
* @param {module:openmct.ObjectAPI~Identifier|function} an array of
|
||||
* identifiers for root level objects, or a function that returns a
|
||||
* promise for an identifier or an array of root level objects.
|
||||
* @method addRoot
|
||||
* @memberof module:openmct.ObjectAPI#
|
||||
*/
|
||||
ObjectAPI.prototype.addRoot = function (key) {
|
||||
this.rootRegistry.addRoot(key);
|
||||
};
|
||||
|
||||
/**
|
||||
* Register an object interceptor that transforms a domain object requested via module:openmct.ObjectAPI.get
|
||||
* The domain object will be transformed after it is retrieved from the persistence store
|
||||
* The domain object will be transformed only if the interceptor is applicable to that domain object as defined by the InterceptorDef
|
||||
*
|
||||
* @param {module:openmct.InterceptorDef} interceptorDef the interceptor definition to add
|
||||
* @method addGetInterceptor
|
||||
* @memberof module:openmct.InterceptorRegistry#
|
||||
*/
|
||||
ObjectAPI.prototype.addGetInterceptor = function (interceptorDef) {
|
||||
this.interceptorRegistry.addInterceptor(interceptorDef);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the interceptors for a given domain object.
|
||||
* @private
|
||||
*/
|
||||
ObjectAPI.prototype.listGetInterceptors = function (identifier, object) {
|
||||
return this.interceptorRegistry.getInterceptors(identifier, object);
|
||||
};
|
||||
|
||||
/**
|
||||
* Modify a domain object.
|
||||
* @param {module:openmct.DomainObject} object the object to mutate
|
||||
* @param {string} path the property to modify
|
||||
* @param {*} value the new value for this property
|
||||
* @method mutate
|
||||
* @memberof module:openmct.ObjectAPI#
|
||||
*/
|
||||
ObjectAPI.prototype.mutate = function (domainObject, path, value) {
|
||||
if (!this.supportsMutation(domainObject.identifier)) {
|
||||
throw `Error: Attempted to mutate immutable object ${domainObject.name}`;
|
||||
}
|
||||
|
||||
if (domainObject.isMutable) {
|
||||
domainObject.$set(path, value);
|
||||
} else {
|
||||
//Creating a temporary mutable domain object allows other mutable instances of the
|
||||
//object to be kept in sync.
|
||||
let mutableDomainObject = this._toMutable(domainObject);
|
||||
|
||||
//Mutate original object
|
||||
MutableDomainObject.mutateObject(domainObject, path, value);
|
||||
|
||||
//Mutate temporary mutable object, in the process informing any other mutable instances
|
||||
mutableDomainObject.$set(path, value);
|
||||
|
||||
//Destroy temporary mutable object
|
||||
this.destroyMutable(mutableDomainObject);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
ObjectAPI.prototype._toMutable = function (object) {
|
||||
if (object.isMutable) {
|
||||
return object;
|
||||
} else {
|
||||
return MutableDomainObject.createMutable(object, this.eventEmitter);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param module:openmct.ObjectAPI~Identifier identifier An object identifier
|
||||
* @returns {boolean} true if the object can be mutated, otherwise returns false
|
||||
*/
|
||||
ObjectAPI.prototype.supportsMutation = function (identifier) {
|
||||
return this.isPersistable(identifier);
|
||||
};
|
||||
|
||||
/**
|
||||
* Observe changes to a domain object.
|
||||
* @param {module:openmct.DomainObject} object the object to observe
|
||||
* @param {string} path the property to observe
|
||||
* @param {Function} callback a callback to invoke when new values for
|
||||
* this property are observed
|
||||
* @method observe
|
||||
* @memberof module:openmct.ObjectAPI#
|
||||
*/
|
||||
ObjectAPI.prototype.observe = function (domainObject, path, callback) {
|
||||
if (domainObject.isMutable) {
|
||||
return domainObject.$observe(path, callback);
|
||||
} else {
|
||||
let mutable = this._toMutable(domainObject);
|
||||
mutable.$observe(path, callback);
|
||||
|
||||
return () => mutable.$destroy();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {module:openmct.ObjectAPI~Identifier} identifier
|
||||
* @returns {string} A string representation of the given identifier, including namespace and key
|
||||
*/
|
||||
ObjectAPI.prototype.makeKeyString = function (identifier) {
|
||||
return utils.makeKeyString(identifier);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} keyString A string representation of the given identifier, that is, a namespace and key separated by a colon.
|
||||
* @returns {module:openmct.ObjectAPI~Identifier} An identifier object
|
||||
*/
|
||||
ObjectAPI.prototype.parseKeyString = function (keyString) {
|
||||
return utils.parseKeyString(keyString);
|
||||
};
|
||||
|
||||
/**
|
||||
* Given any number of identifiers, will return true if they are all equal, otherwise false.
|
||||
* @param {module:openmct.ObjectAPI~Identifier[]} identifiers
|
||||
*/
|
||||
ObjectAPI.prototype.areIdsEqual = function (...identifiers) {
|
||||
return identifiers.map(utils.parseKeyString)
|
||||
.every(identifier => {
|
||||
return identifier === identifiers[0]
|
||||
|| (identifier.namespace === identifiers[0].namespace
|
||||
&& identifier.key === identifiers[0].key);
|
||||
});
|
||||
};
|
||||
|
||||
ObjectAPI.prototype.getOriginalPath = function (identifier, path = []) {
|
||||
return this.get(identifier).then((domainObject) => {
|
||||
path.push(domainObject);
|
||||
let location = domainObject.location;
|
||||
|
||||
if (location) {
|
||||
return this.getOriginalPath(utils.parseKeyString(location), path);
|
||||
} else {
|
||||
return path;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Uniquely identifies a domain object.
|
||||
*
|
||||
* @typedef Identifier
|
||||
* @memberof module:openmct.ObjectAPI~
|
||||
* @property {string} namespace the namespace to/from which this domain
|
||||
* object should be loaded/stored.
|
||||
* @property {string} key a unique identifier for the domain object
|
||||
* within that namespace
|
||||
*/
|
||||
|
||||
/**
|
||||
* A domain object is an entity of relevance to a user's workflow, that
|
||||
* should appear as a distinct and meaningful object within the user
|
||||
* interface. Examples of domain objects are folders, telemetry sensors,
|
||||
* and so forth.
|
||||
*
|
||||
* A few common properties are defined for domain objects. Beyond these,
|
||||
* individual types of domain objects may add more as they see fit.
|
||||
*
|
||||
* @property {module:openmct.ObjectAPI~Identifier} identifier a key/namespace pair which
|
||||
* uniquely identifies this domain object
|
||||
* @property {string} type the type of domain object
|
||||
* @property {string} name the human-readable name for this domain object
|
||||
* @property {string} [creator] the user name of the creator of this domain
|
||||
* object
|
||||
* @property {number} [modified] the time, in milliseconds since the UNIX
|
||||
* epoch, at which this domain object was last modified
|
||||
* @property {module:openmct.ObjectAPI~Identifier[]} [composition] if
|
||||
* present, this will be used by the default composition provider
|
||||
* to load domain objects
|
||||
* @typedef DomainObject
|
||||
* @memberof module:openmct
|
||||
*/
|
||||
|
||||
function hasAlreadyBeenPersisted(domainObject) {
|
||||
return domainObject.persisted !== undefined
|
||||
&& domainObject.persisted === domainObject.modified;
|
||||
}
|
||||
|
||||
export default ObjectAPI;
|
||||
|
||||
@@ -2,12 +2,16 @@ import ObjectAPI from './ObjectAPI.js';
|
||||
|
||||
describe("The Object API", () => {
|
||||
let objectAPI;
|
||||
let typeRegistry;
|
||||
let mockDomainObject;
|
||||
const TEST_NAMESPACE = "test-namespace";
|
||||
const FIFTEEN_MINUTES = 15 * 60 * 1000;
|
||||
|
||||
beforeEach(() => {
|
||||
objectAPI = new ObjectAPI();
|
||||
typeRegistry = jasmine.createSpyObj('typeRegistry', [
|
||||
'get'
|
||||
]);
|
||||
objectAPI = new ObjectAPI(typeRegistry);
|
||||
mockDomainObject = {
|
||||
identifier: {
|
||||
namespace: TEST_NAMESPACE,
|
||||
@@ -33,6 +37,7 @@ describe("The Object API", () => {
|
||||
"update"
|
||||
]);
|
||||
mockProvider.create.and.returnValue(Promise.resolve(true));
|
||||
mockProvider.update.and.returnValue(Promise.resolve(true));
|
||||
objectAPI.addProvider(TEST_NAMESPACE, mockProvider);
|
||||
});
|
||||
it("Calls 'create' on provider if object is new", () => {
|
||||
@@ -128,4 +133,131 @@ describe("The Object API", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("the mutation API", () => {
|
||||
let testObject;
|
||||
let mutable;
|
||||
let mockProvider;
|
||||
|
||||
beforeEach(function () {
|
||||
objectAPI = new ObjectAPI(typeRegistry);
|
||||
testObject = {
|
||||
identifier: {
|
||||
namespace: TEST_NAMESPACE,
|
||||
key: 'test-key'
|
||||
},
|
||||
name: 'test object',
|
||||
otherAttribute: 'other-attribute-value',
|
||||
objectAttribute: {
|
||||
embeddedObject: {
|
||||
embeddedKey: 'embedded-value'
|
||||
}
|
||||
}
|
||||
};
|
||||
mockProvider = jasmine.createSpyObj("mock provider", [
|
||||
"get",
|
||||
"create",
|
||||
"update"
|
||||
]);
|
||||
mockProvider.get.and.returnValue(Promise.resolve(testObject));
|
||||
objectAPI.addProvider(TEST_NAMESPACE, mockProvider);
|
||||
|
||||
return objectAPI.getMutable(testObject.identifier)
|
||||
.then(object => {
|
||||
mutable = object;
|
||||
|
||||
return mutable;
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mutable.$destroy();
|
||||
});
|
||||
|
||||
it('mutates the original object', () => {
|
||||
const MUTATED_NAME = 'mutated name';
|
||||
objectAPI.mutate(testObject, 'name', MUTATED_NAME);
|
||||
expect(testObject.name).toBe(MUTATED_NAME);
|
||||
});
|
||||
|
||||
describe ('uses a MutableDomainObject', () => {
|
||||
it('and retains properties of original object ', function () {
|
||||
expect(hasOwnProperty(mutable, 'identifier')).toBe(true);
|
||||
expect(hasOwnProperty(mutable, 'otherAttribute')).toBe(true);
|
||||
expect(mutable.identifier).toEqual(testObject.identifier);
|
||||
expect(mutable.otherAttribute).toEqual(testObject.otherAttribute);
|
||||
});
|
||||
|
||||
it('that is identical to original object when serialized', function () {
|
||||
expect(JSON.stringify(mutable)).toEqual(JSON.stringify(testObject));
|
||||
});
|
||||
});
|
||||
|
||||
describe('uses events', function () {
|
||||
let testObjectDuplicate;
|
||||
let mutableSecondInstance;
|
||||
|
||||
beforeEach(function () {
|
||||
// Duplicate object to guarantee we are not sharing object instance, which would invalidate test
|
||||
testObjectDuplicate = JSON.parse(JSON.stringify(testObject));
|
||||
mutableSecondInstance = objectAPI._toMutable(testObjectDuplicate);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mutableSecondInstance.$destroy();
|
||||
});
|
||||
|
||||
it('to stay synchronized when mutated', function () {
|
||||
objectAPI.mutate(mutable, 'otherAttribute', 'new-attribute-value');
|
||||
expect(mutableSecondInstance.otherAttribute).toBe('new-attribute-value');
|
||||
});
|
||||
|
||||
it('to indicate when a property changes', function () {
|
||||
let mutationCallback = jasmine.createSpy('mutation-callback');
|
||||
let unlisten;
|
||||
|
||||
return new Promise(function (resolve) {
|
||||
mutationCallback.and.callFake(resolve);
|
||||
unlisten = objectAPI.observe(mutableSecondInstance, 'otherAttribute', mutationCallback);
|
||||
objectAPI.mutate(mutable, 'otherAttribute', 'some-new-value');
|
||||
}).then(function () {
|
||||
expect(mutationCallback).toHaveBeenCalledWith('some-new-value');
|
||||
unlisten();
|
||||
});
|
||||
});
|
||||
|
||||
it('to indicate when a child property has changed', function () {
|
||||
let embeddedKeyCallback = jasmine.createSpy('embeddedKeyCallback');
|
||||
let embeddedObjectCallback = jasmine.createSpy('embeddedObjectCallback');
|
||||
let objectAttributeCallback = jasmine.createSpy('objectAttribute');
|
||||
let listeners = [];
|
||||
|
||||
return new Promise(function (resolve) {
|
||||
objectAttributeCallback.and.callFake(resolve);
|
||||
|
||||
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject.embeddedKey', embeddedKeyCallback));
|
||||
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject', embeddedObjectCallback));
|
||||
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute', objectAttributeCallback));
|
||||
|
||||
objectAPI.mutate(mutable, 'objectAttribute.embeddedObject.embeddedKey', 'updated-embedded-value');
|
||||
}).then(function () {
|
||||
expect(embeddedKeyCallback).toHaveBeenCalledWith('updated-embedded-value');
|
||||
expect(embeddedObjectCallback).toHaveBeenCalledWith({
|
||||
embeddedKey: 'updated-embedded-value'
|
||||
});
|
||||
expect(objectAttributeCallback).toHaveBeenCalledWith({
|
||||
embeddedObject: {
|
||||
embeddedKey: 'updated-embedded-value'
|
||||
}
|
||||
});
|
||||
|
||||
listeners.forEach(listener => listener());
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function hasOwnProperty(object, property) {
|
||||
return Object.prototype.hasOwnProperty.call(object, property);
|
||||
}
|
||||
|
||||
@@ -43,12 +43,16 @@ export default function LADTableViewProvider(openmct) {
|
||||
components: {
|
||||
LadTableComponent: LadTable
|
||||
},
|
||||
provide: {
|
||||
openmct,
|
||||
domainObject,
|
||||
objectPath
|
||||
data: () => {
|
||||
return {
|
||||
domainObject,
|
||||
objectPath
|
||||
};
|
||||
},
|
||||
template: '<lad-table-component></lad-table-component>'
|
||||
provide: {
|
||||
openmct
|
||||
},
|
||||
template: '<lad-table-component :domain-object="domainObject" :object-path="objectPath"></lad-table-component>'
|
||||
});
|
||||
},
|
||||
destroy: function (element) {
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
class="js-lad-table__body__row"
|
||||
@contextmenu.prevent="showContextMenu"
|
||||
>
|
||||
<td class="js-first-data">{{ name }}</td>
|
||||
<td class="js-first-data">{{ domainObject.name }}</td>
|
||||
<td class="js-second-data">{{ formattedTimestamp }}</td>
|
||||
<td
|
||||
class="js-third-data"
|
||||
@@ -50,12 +50,16 @@ const CONTEXT_MENU_ACTIONS = [
|
||||
];
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'objectPath'],
|
||||
inject: ['openmct'],
|
||||
props: {
|
||||
domainObject: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
objectPath: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
hasUnits: {
|
||||
type: Boolean,
|
||||
requred: true
|
||||
@@ -66,7 +70,6 @@ export default {
|
||||
currentObjectPath.unshift(this.domainObject);
|
||||
|
||||
return {
|
||||
name: this.domainObject.name,
|
||||
timestamp: undefined,
|
||||
value: '---',
|
||||
valueClass: '',
|
||||
@@ -89,14 +92,6 @@ export default {
|
||||
.telemetry
|
||||
.limitEvaluator(this.domainObject);
|
||||
|
||||
this.stopWatchingMutation = this.openmct
|
||||
.objects
|
||||
.observe(
|
||||
this.domainObject,
|
||||
'*',
|
||||
this.updateName
|
||||
);
|
||||
|
||||
this.openmct.time.on('timeSystem', this.updateTimeSystem);
|
||||
this.openmct.time.on('bounds', this.updateBounds);
|
||||
|
||||
@@ -119,7 +114,6 @@ export default {
|
||||
}
|
||||
},
|
||||
destroyed() {
|
||||
this.stopWatchingMutation();
|
||||
this.unsubscribe();
|
||||
this.openmct.time.off('timeSystem', this.updateTimeSystem);
|
||||
this.openmct.time.off('bounds', this.updateBounds);
|
||||
@@ -160,9 +154,6 @@ export default {
|
||||
})
|
||||
.then((array) => this.updateValues(array[array.length - 1]));
|
||||
},
|
||||
updateName(name) {
|
||||
this.name = name;
|
||||
},
|
||||
updateBounds(bounds, isTick) {
|
||||
this.bounds = bounds;
|
||||
if (!isTick) {
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
v-for="item in items"
|
||||
:key="item.key"
|
||||
:domain-object="item.domainObject"
|
||||
:object-path="objectPath"
|
||||
:has-units="hasUnits"
|
||||
/>
|
||||
</tbody>
|
||||
@@ -47,10 +48,20 @@
|
||||
import LadRow from './LADRow.vue';
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'domainObject', 'objectPath'],
|
||||
inject: ['openmct'],
|
||||
components: {
|
||||
LadRow
|
||||
},
|
||||
props: {
|
||||
domainObject: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
objectPath: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
items: []
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
v-model="expanded"
|
||||
class="c-tree__item__view-control"
|
||||
:enabled="hasChildren"
|
||||
:propagate="false"
|
||||
/>
|
||||
<div class="c-tree__item__label c-object-label">
|
||||
<div
|
||||
|
||||
@@ -152,10 +152,7 @@ export default {
|
||||
}
|
||||
},
|
||||
data() {
|
||||
let domainObject = JSON.parse(JSON.stringify(this.domainObject));
|
||||
|
||||
return {
|
||||
internalDomainObject: domainObject,
|
||||
initSelectIndex: undefined,
|
||||
selection: [],
|
||||
showGrid: true
|
||||
@@ -163,10 +160,10 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
gridSize() {
|
||||
return this.internalDomainObject.configuration.layoutGrid;
|
||||
return this.domainObject.configuration.layoutGrid;
|
||||
},
|
||||
layoutItems() {
|
||||
return this.internalDomainObject.configuration.items;
|
||||
return this.domainObject.configuration.items;
|
||||
},
|
||||
selectedLayoutItems() {
|
||||
return this.layoutItems.filter(item => {
|
||||
@@ -174,7 +171,7 @@ export default {
|
||||
});
|
||||
},
|
||||
layoutDimensions() {
|
||||
return this.internalDomainObject.configuration.layoutDimensions;
|
||||
return this.domainObject.configuration.layoutDimensions;
|
||||
},
|
||||
shouldDisplayLayoutDimensions() {
|
||||
return this.layoutDimensions
|
||||
@@ -206,12 +203,9 @@ export default {
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.unlisten = this.openmct.objects.observe(this.internalDomainObject, '*', function (obj) {
|
||||
this.internalDomainObject = JSON.parse(JSON.stringify(obj));
|
||||
}.bind(this));
|
||||
this.openmct.selection.on('change', this.setSelection);
|
||||
this.initializeItems();
|
||||
this.composition = this.openmct.composition.get(this.internalDomainObject);
|
||||
this.composition = this.openmct.composition.get(this.domainObject);
|
||||
this.composition.on('add', this.addChild);
|
||||
this.composition.on('remove', this.removeChild);
|
||||
this.composition.load();
|
||||
@@ -220,7 +214,6 @@ export default {
|
||||
this.openmct.selection.off('change', this.setSelection);
|
||||
this.composition.off('add', this.addChild);
|
||||
this.composition.off('remove', this.removeChild);
|
||||
this.unlisten();
|
||||
},
|
||||
methods: {
|
||||
addElement(itemType, element) {
|
||||
@@ -347,7 +340,7 @@ export default {
|
||||
this.startingMinY2 = undefined;
|
||||
},
|
||||
mutate(path, value) {
|
||||
this.openmct.objects.mutate(this.internalDomainObject, path, value);
|
||||
this.openmct.objects.mutate(this.domainObject, path, value);
|
||||
},
|
||||
handleDrop($event) {
|
||||
if (!$event.dataTransfer.types.includes('openmct/domain-object-path')) {
|
||||
@@ -387,11 +380,11 @@ export default {
|
||||
}
|
||||
},
|
||||
containsObject(identifier) {
|
||||
return _.get(this.internalDomainObject, 'composition')
|
||||
return _.get(this.domainObject, 'composition')
|
||||
.some(childId => this.openmct.objects.areIdsEqual(childId, identifier));
|
||||
},
|
||||
handleDragOver($event) {
|
||||
if (this.internalDomainObject.locked) {
|
||||
if (this.domainObject.locked) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -420,7 +413,7 @@ export default {
|
||||
item.id = uuid();
|
||||
this.trackItem(item);
|
||||
this.layoutItems.push(item);
|
||||
this.openmct.objects.mutate(this.internalDomainObject, "configuration.items", this.layoutItems);
|
||||
this.openmct.objects.mutate(this.domainObject, "configuration.items", this.layoutItems);
|
||||
this.initSelectIndex = this.layoutItems.length - 1;
|
||||
},
|
||||
trackItem(item) {
|
||||
@@ -477,7 +470,7 @@ export default {
|
||||
}
|
||||
},
|
||||
removeFromComposition(keyString) {
|
||||
let composition = _.get(this.internalDomainObject, 'composition');
|
||||
let composition = _.get(this.domainObject, 'composition');
|
||||
composition = composition.filter(identifier => {
|
||||
return this.openmct.objects.makeKeyString(identifier) !== keyString;
|
||||
});
|
||||
@@ -629,10 +622,10 @@ export default {
|
||||
createNewDomainObject(domainObject, composition, viewType, nameExtension, model) {
|
||||
let identifier = {
|
||||
key: uuid(),
|
||||
namespace: this.internalDomainObject.identifier.namespace
|
||||
namespace: this.domainObject.identifier.namespace
|
||||
};
|
||||
let type = this.openmct.types.get(viewType);
|
||||
let parentKeyString = this.openmct.objects.makeKeyString(this.internalDomainObject.identifier);
|
||||
let parentKeyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
let objectName = nameExtension ? `${domainObject.name}-${nameExtension}` : domainObject.name;
|
||||
let object = {};
|
||||
|
||||
@@ -689,7 +682,7 @@ export default {
|
||||
});
|
||||
},
|
||||
duplicateItem(selectedItems) {
|
||||
let objectStyles = this.internalDomainObject.configuration.objectStyles || {};
|
||||
let objectStyles = this.domainObject.configuration.objectStyles || {};
|
||||
let selectItemsArray = [];
|
||||
let newDomainObjectsArray = [];
|
||||
|
||||
@@ -728,8 +721,8 @@ export default {
|
||||
});
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.openmct.objects.mutate(this.internalDomainObject, "configuration.items", this.layoutItems);
|
||||
this.openmct.objects.mutate(this.internalDomainObject, "configuration.objectStyles", objectStyles);
|
||||
this.openmct.objects.mutate(this.domainObject, "configuration.items", this.layoutItems);
|
||||
this.openmct.objects.mutate(this.domainObject, "configuration.objectStyles", objectStyles);
|
||||
this.$el.click(); //clear selection;
|
||||
|
||||
newDomainObjectsArray.forEach(domainObject => {
|
||||
@@ -768,13 +761,13 @@ export default {
|
||||
};
|
||||
this.createNewDomainObject(mockDomainObject, overlayPlotIdentifiers, viewType).then((newDomainObject) => {
|
||||
let newDomainObjectKeyString = this.openmct.objects.makeKeyString(newDomainObject.identifier);
|
||||
let internalDomainObjectKeyString = this.openmct.objects.makeKeyString(this.internalDomainObject.identifier);
|
||||
let domainObjectKeyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
|
||||
this.composition.add(newDomainObject);
|
||||
this.addItem('subobject-view', newDomainObject, position);
|
||||
|
||||
overlayPlots.forEach(overlayPlot => {
|
||||
if (overlayPlot.location === internalDomainObjectKeyString) {
|
||||
if (overlayPlot.location === domainObjectKeyString) {
|
||||
this.openmct.objects.mutate(overlayPlot, 'location', newDomainObjectKeyString);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -129,13 +129,22 @@ export default {
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.openmct.objects.get(this.item.identifier)
|
||||
.then(this.setObject);
|
||||
if (this.openmct.objects.supportsMutation(this.item.identifier)) {
|
||||
this.openmct.objects.getMutable(this.item.identifier)
|
||||
.then(this.setObject);
|
||||
} else {
|
||||
this.openmct.objects.get(this.item.identifier)
|
||||
.then(this.setObject);
|
||||
}
|
||||
},
|
||||
destroyed() {
|
||||
beforeDestroy() {
|
||||
if (this.removeSelectable) {
|
||||
this.removeSelectable();
|
||||
}
|
||||
|
||||
if (this.domainObject.isMutable) {
|
||||
this.openmct.objects.destroyMutable(this.domainObject);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setObject(domainObject) {
|
||||
|
||||
@@ -212,14 +212,20 @@ export default {
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.openmct.objects.get(this.item.identifier)
|
||||
.then(this.setObject);
|
||||
if (this.openmct.objects.supportsMutation(this.item.identifier)) {
|
||||
this.openmct.objects.getMutable(this.item.identifier)
|
||||
.then(this.setObject);
|
||||
} else {
|
||||
this.openmct.objects.get(this.item.identifier)
|
||||
.then(this.setObject);
|
||||
}
|
||||
|
||||
this.openmct.time.on("bounds", this.refreshData);
|
||||
|
||||
this.status = this.openmct.status.get(this.item.identifier);
|
||||
this.removeStatusListener = this.openmct.status.observe(this.item.identifier, this.setStatus);
|
||||
},
|
||||
destroyed() {
|
||||
beforeDestroy() {
|
||||
this.removeSubscription();
|
||||
this.removeStatusListener();
|
||||
|
||||
@@ -228,6 +234,10 @@ export default {
|
||||
}
|
||||
|
||||
this.openmct.time.off("bounds", this.refreshData);
|
||||
|
||||
if (this.domainObject.isMutable) {
|
||||
this.openmct.objects.destroyMutable(this.domainObject);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
formattedValueForCopy() {
|
||||
|
||||
@@ -57,6 +57,7 @@ describe("The Duplicate Action plugin", () => {
|
||||
overwrite: {
|
||||
folder: {
|
||||
name: "Parent Folder",
|
||||
type: "folder",
|
||||
composition: [childObject.identifier]
|
||||
}
|
||||
}
|
||||
@@ -104,6 +105,7 @@ describe("The Duplicate Action plugin", () => {
|
||||
|
||||
// already installed by default, but never hurts, just adds to context menu
|
||||
openmct.install(DuplicateActionPlugin());
|
||||
openmct.types.addType('folder', {creatable: true});
|
||||
|
||||
openmct.on('start', done);
|
||||
openmct.startHeadless();
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
130
src/plugins/imagery/components/Compass/Compass.vue
Normal file
130
src/plugins/imagery/components/Compass/Compass.vue
Normal file
@@ -0,0 +1,130 @@
|
||||
/*****************************************************************************
|
||||
* 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"
|
||||
:rover-heading="roverHeading"
|
||||
:rover-roll="roverRoll"
|
||||
:sun-heading="sunHeading"
|
||||
:camera-field-of-view="cameraFieldOfView"
|
||||
:camera-pan="cameraPan"
|
||||
/>
|
||||
<CompassRose
|
||||
v-if="shouldDisplayCompassRose"
|
||||
:rover-heading="roverHeading"
|
||||
: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.roverHeading !== undefined;
|
||||
},
|
||||
shouldDisplayCompassHUD() {
|
||||
return this.roverHeading !== undefined;
|
||||
},
|
||||
// degrees from north heading
|
||||
roverHeading() {
|
||||
return this.image['Rover Heading'];
|
||||
},
|
||||
roverRoll() {
|
||||
return this.image['Rover Roll'];
|
||||
},
|
||||
roverYaw() {
|
||||
return this.image['Rover Yaw'];
|
||||
},
|
||||
roverPitch() {
|
||||
return this.image['Rover Pitch'];
|
||||
},
|
||||
// degrees from north heading
|
||||
sunHeading() {
|
||||
return this.image['Sun Orientation'];
|
||||
},
|
||||
// degrees from rover heading
|
||||
cameraPan() {
|
||||
return this.image['Camera Pan'];
|
||||
},
|
||||
cameraTilt() {
|
||||
return this.image['Camera Tilt'];
|
||||
},
|
||||
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: {
|
||||
roverHeading: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
roverRoll: {
|
||||
type: Number,
|
||||
default: undefined
|
||||
},
|
||||
sunHeading: {
|
||||
type: Number,
|
||||
default: undefined
|
||||
},
|
||||
cameraFieldOfView: {
|
||||
type: Number,
|
||||
default: undefined
|
||||
},
|
||||
cameraPan: {
|
||||
type: Number,
|
||||
default: undefined
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
skewCompassHUDStyle() {
|
||||
if (this.roverRoll === undefined || this.roverRoll === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const origin = this.roverRoll > 0 ? 'left bottom' : 'right top';
|
||||
|
||||
return {
|
||||
'transform-origin': origin,
|
||||
transform: `skew(0, ${ this.roverRoll }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);
|
||||
},
|
||||
normalizedRoverHeading() {
|
||||
return normalizeDegrees(this.roverHeading);
|
||||
},
|
||||
visibleRange() {
|
||||
const min = normalizeDegrees(this.normalizedRoverHeading + this.cameraPan - this.cameraFieldOfView / 2);
|
||||
const max = normalizeDegrees(this.normalizedRoverHeading + 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-rover-body"
|
||||
:style="roverHeadingStyle"
|
||||
>
|
||||
</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: {
|
||||
roverHeading: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
sunHeading: {
|
||||
type: Number,
|
||||
default: undefined
|
||||
},
|
||||
cameraFieldOfView: {
|
||||
type: Number,
|
||||
default: undefined
|
||||
},
|
||||
cameraPan: {
|
||||
type: Number,
|
||||
default: undefined
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
lockBezel: true
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
compassRoverHeading() {
|
||||
return this.lockBezel ? normalizeDegrees(this.roverHeading) : 0;
|
||||
},
|
||||
north() {
|
||||
return normalizeDegrees(this.compassRoverHeading - this.roverHeading);
|
||||
},
|
||||
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 }`
|
||||
};
|
||||
},
|
||||
roverHeadingStyle() {
|
||||
return {
|
||||
transform: `translateX(-50%) rotate(${ this.compassRoverHeading }deg)`
|
||||
};
|
||||
},
|
||||
cameraFOVHeading() {
|
||||
return this.compassRoverHeading + 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/***************************** ROVER BODY */
|
||||
.c-rover-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"
|
||||
@@ -97,7 +128,9 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
import Compass from './Compass/Compass.vue';
|
||||
|
||||
const DEFAULT_DURATION_FORMATTER = 'duration';
|
||||
const REFRESH_CSS_MS = 500;
|
||||
@@ -117,6 +150,9 @@ const ARROW_LEFT = 37;
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'domainObject'],
|
||||
components: {
|
||||
Compass
|
||||
},
|
||||
data() {
|
||||
let timeSystem = this.openmct.time.timeSystem();
|
||||
|
||||
@@ -137,7 +173,13 @@ export default {
|
||||
refreshCSS: false,
|
||||
keyString: undefined,
|
||||
focusedImageIndex: undefined,
|
||||
numericDuration: undefined
|
||||
focusedImageRelatedData: {},
|
||||
numericDuration: undefined,
|
||||
metadataEndpoints: {},
|
||||
relatedTelemetry: {},
|
||||
focusedImageNaturalAspectRatio: undefined,
|
||||
imageContainerWidth: undefined,
|
||||
imageContainerHeight: undefined
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -195,15 +237,23 @@ export default {
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
shouldDisplayCompass() {
|
||||
return this.focusedImage !== undefined
|
||||
&& this.focusedImageNaturalAspectRatio !== undefined
|
||||
&& this.imageContainerWidth !== undefined
|
||||
&& this.imageContainerHeight !== undefined;
|
||||
}
|
||||
},
|
||||
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 +262,17 @@ 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);
|
||||
this.roverKeys = ['Rover Heading', 'Rover Roll', 'Rover Yaw', 'Rover Pitch'];
|
||||
this.cameraKeys = ['Camera Pan', 'Camera Tilt'];
|
||||
this.sunKeys = ['Sun Orientation'];
|
||||
|
||||
// DELETE WHEN DONE
|
||||
if (!this.imageHints.relatedTelemetry) {
|
||||
this.temporaryForImageEnhancements();
|
||||
}
|
||||
|
||||
// initialize
|
||||
this.timeKey = this.timeSystem.key;
|
||||
@@ -222,6 +281,12 @@ export default {
|
||||
// kickoff
|
||||
this.subscribe();
|
||||
this.requestHistory();
|
||||
await this.initializeRelatedTelemetry();
|
||||
|
||||
// for when people are scrolling through images quickly
|
||||
_.debounce(this.updateRelatedTelemetryForFocusedImage, 400);
|
||||
|
||||
this.pollResizeImageContainerID = setInterval(this.pollResizeImageContainer, 300);
|
||||
},
|
||||
updated() {
|
||||
this.scrollToRight();
|
||||
@@ -236,8 +301,222 @@ export default {
|
||||
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.hasRelatedTelemetry) {
|
||||
for (let key of this.relatedTelemetry.keys) {
|
||||
if (this.relatedTelemetry[key].unsubscribe) {
|
||||
this.relatedTelemetry[key].unsubscribe();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clearInterval(this.pollResizeImageContainerID);
|
||||
},
|
||||
methods: {
|
||||
// for local dev, to be DELETED
|
||||
temporaryForImageEnhancements() {
|
||||
|
||||
this.searchService = this.openmct.$injector.get('searchService');
|
||||
this.temporaryDev = true;
|
||||
|
||||
// mock related telemetry metadata
|
||||
this.imageHints.relatedTelemetry = {};
|
||||
|
||||
// populate temp keys in imageHints for local testing
|
||||
[...this.roverKeys, ...this.cameraKeys, ...this.sunKeys].forEach(key => {
|
||||
|
||||
this.imageHints.relatedTelemetry[key] = {
|
||||
dev: true,
|
||||
areEqual: function (valueOne, valueTwo) {
|
||||
const DECIMAL_COMPARISON_TOLERANCE = 1;
|
||||
const WHOLE = Math.pow(10, DECIMAL_COMPARISON_TOLERANCE);
|
||||
|
||||
return Math.floor(valueOne.toFixed(DECIMAL_COMPARISON_TOLERANCE) * WHOLE) === Math.floor(valueTwo.toFixed(DECIMAL_COMPARISON_TOLERANCE) * WHOLE);
|
||||
},
|
||||
realtime: {
|
||||
telemetryObjectId: key,
|
||||
valueKey: 'sin'
|
||||
},
|
||||
historical: {
|
||||
telemetryObjectId: key,
|
||||
valueKey: 'sin'
|
||||
},
|
||||
devInit: async () => {
|
||||
const searchResults = await this.searchService.query(key);
|
||||
const endpoint = searchResults.hits[0].id;
|
||||
const domainObject = await this.openmct.objects.get(endpoint);
|
||||
|
||||
return domainObject;
|
||||
}
|
||||
};
|
||||
});
|
||||
},
|
||||
async initializeRelatedTelemetry() {
|
||||
if (this.imageHints.relatedTelemetry === undefined) {
|
||||
this.hasRelatedTelemetry = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// DELETE
|
||||
if (this.temporaryDev) {
|
||||
let searchIndexBuildDelay = new Promise((resolve, reject) => {
|
||||
setTimeout(resolve, 3000);
|
||||
});
|
||||
|
||||
await searchIndexBuildDelay;
|
||||
}
|
||||
|
||||
let keys = Object.keys(this.imageHints.relatedTelemetry);
|
||||
|
||||
this.hasRelatedTelemetry = true;
|
||||
this.relatedTelemetry = {
|
||||
keys,
|
||||
...this.imageHints.relatedTelemetry
|
||||
};
|
||||
|
||||
// grab historical and subscribe to realtime
|
||||
for (let key of keys) {
|
||||
let historicalId;
|
||||
let realtimeId;
|
||||
|
||||
if (this.relatedTelemetry[key].historical) {
|
||||
if (this.relatedTelemetry[key].historical.telemetryObjectId) {
|
||||
historicalId = this.relatedTelemetry[key].historical.telemetryObjectId;
|
||||
} else {
|
||||
this.relatedTelemetry[key].historicalValuesOnTelemetry = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.relatedTelemetry[key].realtime && this.relatedTelemetry[key].realtime.telemetryObjectId) {
|
||||
realtimeId = this.relatedTelemetry[key].realtime.telemetryObjectId;
|
||||
}
|
||||
|
||||
// if we have a historical object id, then values will NOT be on the imagery datum
|
||||
if (historicalId) {
|
||||
|
||||
// DELETE temp
|
||||
if (this.relatedTelemetry[key].dev) {
|
||||
this.relatedTelemetry[key].historicalDomainObject = await this.relatedTelemetry[key].devInit();
|
||||
} else {
|
||||
this.relatedTelemetry[key].historicalDomainObject = await this.openmct.objects.get(historicalId);
|
||||
}
|
||||
|
||||
this.relatedTelemetry[key].requestLatestFor = async (datum) => {
|
||||
const options = {
|
||||
start: this.openmct.time.bounds().start,
|
||||
end: datum[this.timeKey],
|
||||
strategy: 'latest'
|
||||
};
|
||||
let results = await this.openmct.telemetry
|
||||
.request(this.relatedTelemetry[key].historicalDomainObject, options);
|
||||
|
||||
return results[results.length - 1];
|
||||
};
|
||||
}
|
||||
|
||||
if (realtimeId) {
|
||||
|
||||
if (this.relatedTelemetry[key].dev) {
|
||||
this.relatedTelemetry[key].realtimeDomainObject = await this.relatedTelemetry[key].devInit();
|
||||
} else {
|
||||
this.relatedTelemetry[key].realtimeDomainObject = await this.openmct.objects.get(realtimeId);
|
||||
}
|
||||
|
||||
// set up listeners
|
||||
this.relatedTelemetry[key].listeners = [];
|
||||
this.relatedTelemetry[key].subscribe = (callback) => {
|
||||
|
||||
if (!this.relatedTelemetry[key].isSubscribed) {
|
||||
this.subscribeToDataForKey(key);
|
||||
}
|
||||
|
||||
if (!this.relatedTelemetry[key].listeners.includes(callback)) {
|
||||
this.relatedTelemetry[key].listeners.push(callback);
|
||||
|
||||
return () => {
|
||||
this.relatedTelemetry[key].listeners.remove(callback);
|
||||
};
|
||||
} else {
|
||||
return () => {};
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
async getMostRecentRelatedTelemetry(key, targetDatum) {
|
||||
if (!this.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].historicalValuesOnTelemetry;
|
||||
|
||||
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.hasRelatedTelemetry) {
|
||||
return;
|
||||
}
|
||||
|
||||
const image = this.imageHistory[this.focusedImageIndex];
|
||||
|
||||
// set data ON image telemetry as well as in focusedImageRelatedData
|
||||
for (let key of this.relatedTelemetry.keys) {
|
||||
if (this.relatedTelemetry[key] && this.relatedTelemetry[key].historical) {
|
||||
let valuesOnTelemetry = this.relatedTelemetry[key].historicalValuesOnTelemetry;
|
||||
let value = await this.getMostRecentRelatedTelemetry(key, this.focusedImage);
|
||||
|
||||
if (!valuesOnTelemetry) {
|
||||
image[key] = value; // manually add to telemetry
|
||||
}
|
||||
|
||||
this.$set(this.focusedImageRelatedData, key, value);
|
||||
}
|
||||
}
|
||||
},
|
||||
focusElement() {
|
||||
this.$el.focus();
|
||||
},
|
||||
@@ -509,6 +788,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 });
|
||||
},
|
||||
pollResizeImageContainer() {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -113,6 +113,13 @@ let openmct;
|
||||
describe('Notebook Entries:', () => {
|
||||
beforeEach(done => {
|
||||
openmct = createOpenMct();
|
||||
openmct.types.addType('notebook', {
|
||||
creatable: true
|
||||
});
|
||||
openmct.objects.addProvider('', jasmine.createSpyObj('mockNotebookProvider', [
|
||||
'create',
|
||||
'update'
|
||||
]));
|
||||
window.localStorage.setItem('notebook-storage', null);
|
||||
|
||||
done();
|
||||
|
||||
@@ -62,7 +62,10 @@ describe('Notebook Storage:', () => {
|
||||
beforeEach((done) => {
|
||||
openmct = createOpenMct();
|
||||
window.localStorage.setItem('notebook-storage', null);
|
||||
|
||||
openmct.objects.addProvider('', jasmine.createSpyObj('mockNotebookProvider', [
|
||||
'create',
|
||||
'update'
|
||||
]));
|
||||
done();
|
||||
});
|
||||
|
||||
|
||||
@@ -39,10 +39,12 @@ describe('the plugin', () => {
|
||||
identifier: {
|
||||
namespace: 'mct',
|
||||
key: 'some-value'
|
||||
}
|
||||
},
|
||||
type: 'mock-type'
|
||||
};
|
||||
openmct = createOpenMct(false);
|
||||
openmct.install(new CouchPlugin(testPath));
|
||||
openmct.types.addType('mock-type', {creatable: true});
|
||||
|
||||
element = document.createElement('div');
|
||||
child = document.createElement('div');
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
<object-view
|
||||
v-if="internalDomainObject.keep_alive ? currentTab : isCurrent(tab)"
|
||||
class="c-tabs-view__object"
|
||||
:object="tab.domainObject"
|
||||
:default-object="tab.domainObject"
|
||||
:object-path="tab.objectPath"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -68,6 +68,10 @@ describe("the plugin", () => {
|
||||
end: 4
|
||||
});
|
||||
|
||||
openmct.types.addType('test-object', {
|
||||
creatable: true
|
||||
});
|
||||
|
||||
spyOnBuiltins(['requestAnimationFrame']);
|
||||
window.requestAnimationFrame.and.callFake((callBack) => {
|
||||
callBack();
|
||||
|
||||
@@ -157,6 +157,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash';
|
||||
import ConductorMode from './ConductorMode.vue';
|
||||
import ConductorTimeSystem from './ConductorTimeSystem.vue';
|
||||
import DatePicker from './DatePicker.vue';
|
||||
@@ -220,7 +221,7 @@ export default {
|
||||
document.addEventListener('keydown', this.handleKeyDown);
|
||||
document.addEventListener('keyup', this.handleKeyUp);
|
||||
this.setTimeSystem(JSON.parse(JSON.stringify(this.openmct.time.timeSystem())));
|
||||
this.openmct.time.on('bounds', this.handleNewBounds);
|
||||
this.openmct.time.on('bounds', _.throttle(this.handleNewBounds, 300));
|
||||
this.openmct.time.on('timeSystem', this.setTimeSystem);
|
||||
this.openmct.time.on('clock', this.setViewFromClock);
|
||||
this.openmct.time.on('clockOffsets', this.setViewFromOffsets);
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
$handW: 2px;
|
||||
$handH: $d * 0.4;
|
||||
animation-iteration-count: infinite;
|
||||
animation-timing-function: linear;
|
||||
animation-timing-function: steps(12);
|
||||
transform-origin: bottom;
|
||||
position: absolute;
|
||||
height: $handW;
|
||||
|
||||
@@ -241,11 +241,8 @@ define(
|
||||
element.addEventListener('click', capture, true);
|
||||
element.addEventListener('click', selectCapture);
|
||||
|
||||
let unlisten = undefined;
|
||||
if (context.item) {
|
||||
unlisten = this.openmct.objects.observe(context.item, "*", function (newItem) {
|
||||
context.item = newItem;
|
||||
});
|
||||
context.item = this.openmct.objects._toMutable(context.item);
|
||||
}
|
||||
|
||||
if (select) {
|
||||
@@ -256,14 +253,14 @@ define(
|
||||
}
|
||||
}
|
||||
|
||||
return function () {
|
||||
return (function () {
|
||||
element.removeEventListener('click', capture, true);
|
||||
element.removeEventListener('click', selectCapture);
|
||||
|
||||
if (unlisten !== undefined) {
|
||||
unlisten();
|
||||
if (context.item !== undefined && context.item.isMutable) {
|
||||
this.openmct.objects.destroyMutable(context.item);
|
||||
}
|
||||
};
|
||||
}).bind(this);
|
||||
};
|
||||
|
||||
return Selection;
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
@import "../plugins/folderView/components/list-item.scss";
|
||||
@import "../plugins/folderView/components/list-view.scss";
|
||||
@import "../plugins/imagery/components/imagery-view-layout.scss";
|
||||
@import "../plugins/imagery/components/Compass/compass.scss";
|
||||
@import "../plugins/telemetryTable/components/table-row.scss";
|
||||
@import "../plugins/telemetryTable/components/table-footer-indicator.scss";
|
||||
@import "../plugins/tabs/components/tabs.scss";
|
||||
|
||||
@@ -88,7 +88,6 @@
|
||||
<object-view
|
||||
ref="objectView"
|
||||
class="c-so-view__object-view"
|
||||
:object="domainObject"
|
||||
:show-edit-view="showEditView"
|
||||
:object-path="objectPath"
|
||||
:layout-font-size="layoutFontSize"
|
||||
@@ -161,6 +160,7 @@ export default {
|
||||
mounted() {
|
||||
this.status = this.openmct.status.get(this.domainObject.identifier);
|
||||
this.removeStatusListener = this.openmct.status.observe(this.domainObject.identifier, this.setStatus);
|
||||
this.$refs.objectView.show(this.domainObject, undefined, false, this.objectPath);
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.removeStatusListener();
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
class="c-tree__item__label c-object-label"
|
||||
:class="[statusClass]"
|
||||
draggable="true"
|
||||
:href="objectLink"
|
||||
@dragstart="dragStart"
|
||||
@click="navigateOrPreview"
|
||||
>
|
||||
@@ -16,7 +15,7 @@
|
||||
></span>
|
||||
</div>
|
||||
<div class="c-tree__item__name c-object-label__name">
|
||||
{{ observedObject.name }}
|
||||
{{ domainObject.name }}
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
@@ -46,13 +45,12 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
observedObject: this.domainObject,
|
||||
status: ''
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
typeClass() {
|
||||
let type = this.openmct.types.get(this.observedObject.type);
|
||||
let type = this.openmct.types.get(this.domainObject.type);
|
||||
if (!type) {
|
||||
return 'icon-object-unknown';
|
||||
}
|
||||
@@ -64,15 +62,8 @@ export default {
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.observedObject) {
|
||||
let removeListener = this.openmct.objects.observe(this.observedObject, '*', (newObject) => {
|
||||
this.observedObject = newObject;
|
||||
});
|
||||
this.$once('hook:destroyed', removeListener);
|
||||
}
|
||||
|
||||
this.removeStatusListener = this.openmct.status.observe(this.observedObject.identifier, this.setStatus);
|
||||
this.status = this.openmct.status.get(this.observedObject.identifier);
|
||||
this.removeStatusListener = this.openmct.status.observe(this.domainObject.identifier, this.setStatus);
|
||||
this.status = this.openmct.status.get(this.domainObject.identifier);
|
||||
this.previewAction = new PreviewAction(this.openmct);
|
||||
},
|
||||
destroyed() {
|
||||
@@ -84,6 +75,8 @@ export default {
|
||||
event.preventDefault();
|
||||
this.preview();
|
||||
}
|
||||
|
||||
window.location.assign(this.objectLink);
|
||||
},
|
||||
preview() {
|
||||
if (this.previewAction.appliesTo(this.objectPath)) {
|
||||
@@ -100,7 +93,7 @@ export default {
|
||||
* that point. If dragged object can be composed by navigated object, then indicate with presence of
|
||||
* 'composable-domain-object' in data transfer
|
||||
*/
|
||||
if (this.openmct.composition.checkPolicy(navigatedObject, this.observedObject)) {
|
||||
if (this.openmct.composition.checkPolicy(navigatedObject, this.domainObject)) {
|
||||
event.dataTransfer.setData("openmct/composable-domain-object", JSON.stringify(this.domainObject));
|
||||
}
|
||||
|
||||
|
||||
@@ -10,11 +10,11 @@ import {STYLE_CONSTANTS} from "@/plugins/condition/utils/constants";
|
||||
export default {
|
||||
inject: ["openmct"],
|
||||
props: {
|
||||
object: {
|
||||
showEditView: Boolean,
|
||||
defaultObject: {
|
||||
type: Object,
|
||||
default: undefined
|
||||
},
|
||||
showEditView: Boolean,
|
||||
objectPath: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
@@ -32,12 +32,12 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentObject: this.object
|
||||
domainObject: this.defaultObject
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
objectFontStyle() {
|
||||
return this.currentObject && this.currentObject.configuration && this.currentObject.configuration.fontStyle;
|
||||
return this.domainObject && this.domainObject.configuration && this.domainObject.configuration.fontStyle;
|
||||
},
|
||||
fontSize() {
|
||||
return this.objectFontStyle ? this.objectFontStyle.fontSize : this.layoutFontSize;
|
||||
@@ -46,22 +46,12 @@ export default {
|
||||
return this.objectFontStyle ? this.objectFontStyle.font : this.layoutFont;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
object(newObject, oldObject) {
|
||||
this.currentObject = newObject;
|
||||
this.debounceUpdateView();
|
||||
}
|
||||
},
|
||||
destroyed() {
|
||||
this.clear();
|
||||
if (this.releaseEditModeHandler) {
|
||||
this.releaseEditModeHandler();
|
||||
}
|
||||
|
||||
if (this.unlisten) {
|
||||
this.unlisten();
|
||||
}
|
||||
|
||||
if (this.stopListeningStyles) {
|
||||
this.stopListeningStyles();
|
||||
}
|
||||
@@ -92,7 +82,7 @@ export default {
|
||||
capture: true
|
||||
});
|
||||
this.$el.addEventListener('drop', this.addObjectToParent);
|
||||
if (this.currentObject) {
|
||||
if (this.domainObject) {
|
||||
//This is to apply styles to subobjects in a layout
|
||||
this.initObjectStyles();
|
||||
}
|
||||
@@ -135,7 +125,7 @@ export default {
|
||||
invokeEditModeHandler(editMode) {
|
||||
let edit;
|
||||
|
||||
if (this.currentObject.locked) {
|
||||
if (this.domainObject.locked) {
|
||||
edit = false;
|
||||
} else {
|
||||
edit = editMode;
|
||||
@@ -175,13 +165,13 @@ export default {
|
||||
},
|
||||
updateView(immediatelySelect) {
|
||||
this.clear();
|
||||
if (!this.currentObject) {
|
||||
if (!this.domainObject) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.composition = this.openmct.composition.get(this.currentObject);
|
||||
this.composition = this.openmct.composition.get(this.domainObject);
|
||||
|
||||
if (this.composition) {
|
||||
this.composition._synchronize();
|
||||
this.loadComposition();
|
||||
}
|
||||
|
||||
@@ -197,15 +187,15 @@ export default {
|
||||
|
||||
if (provider.edit && this.showEditView) {
|
||||
if (this.openmct.editor.isEditing()) {
|
||||
this.currentView = provider.edit(this.currentObject, true, objectPath);
|
||||
this.currentView = provider.edit(this.domainObject, true, objectPath);
|
||||
} else {
|
||||
this.currentView = provider.view(this.currentObject, objectPath);
|
||||
this.currentView = provider.view(this.domainObject, objectPath);
|
||||
}
|
||||
|
||||
this.openmct.editor.on('isEditing', this.toggleEditView);
|
||||
this.releaseEditModeHandler = () => this.openmct.editor.off('isEditing', this.toggleEditView);
|
||||
} else {
|
||||
this.currentView = provider.view(this.currentObject, objectPath);
|
||||
this.currentView = provider.view(this.domainObject, objectPath);
|
||||
|
||||
if (this.currentView.onEditModeChange) {
|
||||
this.openmct.editor.on('isEditing', this.invokeEditModeHandler);
|
||||
@@ -247,16 +237,12 @@ export default {
|
||||
this.composition._destroy();
|
||||
}
|
||||
|
||||
this.currentObject = object;
|
||||
this.domainObject = object;
|
||||
|
||||
if (currentObjectPath) {
|
||||
this.currentObjectPath = currentObjectPath;
|
||||
}
|
||||
|
||||
this.unlisten = this.openmct.objects.observe(this.currentObject, '*', (mutatedObject) => {
|
||||
this.currentObject = mutatedObject;
|
||||
});
|
||||
|
||||
this.viewKey = viewKey;
|
||||
|
||||
this.updateView(immediatelySelect);
|
||||
@@ -265,16 +251,16 @@ export default {
|
||||
},
|
||||
initObjectStyles() {
|
||||
if (!this.styleRuleManager) {
|
||||
this.styleRuleManager = new StyleRuleManager((this.currentObject.configuration && this.currentObject.configuration.objectStyles), this.openmct, this.updateStyle.bind(this), true);
|
||||
this.styleRuleManager = new StyleRuleManager((this.domainObject.configuration && this.domainObject.configuration.objectStyles), this.openmct, this.updateStyle.bind(this), true);
|
||||
} else {
|
||||
this.styleRuleManager.updateObjectStyleConfig(this.currentObject.configuration && this.currentObject.configuration.objectStyles);
|
||||
this.styleRuleManager.updateObjectStyleConfig(this.domainObject.configuration && this.domainObject.configuration.objectStyles);
|
||||
}
|
||||
|
||||
if (this.stopListeningStyles) {
|
||||
this.stopListeningStyles();
|
||||
}
|
||||
|
||||
this.stopListeningStyles = this.openmct.objects.observe(this.currentObject, 'configuration.objectStyles', (newObjectStyle) => {
|
||||
this.stopListeningStyles = this.openmct.objects.observe(this.domainObject, 'configuration.objectStyles', (newObjectStyle) => {
|
||||
//Updating styles in the inspector view will trigger this so that the changes are reflected immediately
|
||||
this.styleRuleManager.updateObjectStyleConfig(newObjectStyle);
|
||||
});
|
||||
@@ -282,7 +268,7 @@ export default {
|
||||
this.setFontSize(this.fontSize);
|
||||
this.setFont(this.font);
|
||||
|
||||
this.stopListeningFontStyles = this.openmct.objects.observe(this.currentObject, 'configuration.fontStyle', (newFontStyle) => {
|
||||
this.stopListeningFontStyles = this.openmct.objects.observe(this.domainObject, 'configuration.fontStyle', (newFontStyle) => {
|
||||
this.setFontSize(newFontStyle.fontSize);
|
||||
this.setFont(newFontStyle.font);
|
||||
});
|
||||
@@ -294,7 +280,7 @@ export default {
|
||||
if (this.currentView && this.currentView.getSelectionContext) {
|
||||
return this.currentView.getSelectionContext();
|
||||
} else {
|
||||
return { item: this.currentObject };
|
||||
return { item: this.domainObject };
|
||||
}
|
||||
},
|
||||
onDragOver(event) {
|
||||
@@ -321,7 +307,7 @@ export default {
|
||||
let provider = this.openmct.objectViews.getByProviderKey(this.viewKey);
|
||||
|
||||
if (!provider) {
|
||||
provider = this.openmct.objectViews.get(this.currentObject)[0];
|
||||
provider = this.openmct.objectViews.get(this.domainObject)[0];
|
||||
if (!provider) {
|
||||
return;
|
||||
}
|
||||
@@ -333,7 +319,7 @@ export default {
|
||||
let provider = this.getViewProvider();
|
||||
if (provider
|
||||
&& provider.canEdit
|
||||
&& provider.canEdit(this.currentObject)
|
||||
&& provider.canEdit(this.domainObject)
|
||||
&& this.isEditingAllowed()
|
||||
&& !this.openmct.editor.isEditing()) {
|
||||
this.openmct.editor.edit();
|
||||
@@ -350,7 +336,7 @@ export default {
|
||||
clearData(domainObject) {
|
||||
if (domainObject) {
|
||||
let clearKeyString = this.openmct.objects.makeKeyString(domainObject.identifier);
|
||||
let currentObjectKeyString = this.openmct.objects.makeKeyString(this.currentObject.identifier);
|
||||
let currentObjectKeyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
|
||||
if (clearKeyString === currentObjectKeyString) {
|
||||
if (this.currentView.onClearData) {
|
||||
@@ -364,11 +350,11 @@ export default {
|
||||
}
|
||||
},
|
||||
isEditingAllowed() {
|
||||
let browseObject = this.openmct.layout.$refs.browseObject.currentObject;
|
||||
let browseObject = this.openmct.layout.$refs.browseObject.domainObject;
|
||||
let objectPath = this.currentObjectPath || this.objectPath;
|
||||
let parentObject = objectPath[1];
|
||||
|
||||
return [browseObject, parentObject, this.currentObject].every(object => object && !object.locked);
|
||||
return [browseObject, parentObject, this.domainObject].every(object => object && !object.locked);
|
||||
},
|
||||
setFontSize(newSize) {
|
||||
let elemToStyle = this.getStyleReceiver();
|
||||
|
||||
@@ -22,10 +22,6 @@ export default {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
propagate: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
controlClass: {
|
||||
type: String,
|
||||
default: 'c-disclosure-triangle'
|
||||
@@ -33,10 +29,8 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
handleClick(event) {
|
||||
event.stopPropagation();
|
||||
this.$emit('input', !this.value);
|
||||
if (!this.propagate) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -82,10 +82,6 @@ export default {
|
||||
this.openmct.editor.off('isEditing', this.setEditState);
|
||||
this.openmct.selection.off('change', this.showSelection);
|
||||
|
||||
if (this.mutationUnobserver) {
|
||||
this.mutationUnobserver();
|
||||
}
|
||||
|
||||
if (this.compositionUnlistener) {
|
||||
this.compositionUnlistener();
|
||||
}
|
||||
@@ -106,18 +102,11 @@ export default {
|
||||
this.listeners = [];
|
||||
this.parentObject = selection && selection[0] && selection[0][0].context.item;
|
||||
|
||||
if (this.mutationUnobserver) {
|
||||
this.mutationUnobserver();
|
||||
}
|
||||
|
||||
if (this.compositionUnlistener) {
|
||||
this.compositionUnlistener();
|
||||
}
|
||||
|
||||
if (this.parentObject) {
|
||||
this.mutationUnobserver = this.openmct.objects.observe(this.parentObject, '*', (updatedModel) => {
|
||||
this.parentObject = updatedModel;
|
||||
});
|
||||
this.composition = this.openmct.composition.get(this.parentObject);
|
||||
|
||||
if (this.composition) {
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<template>
|
||||
<div class="c-toolbar">
|
||||
<toolbar-select-menu
|
||||
class="menus-to-left menus-no-icon"
|
||||
:options="fontSizeMenuOptions"
|
||||
@change="setFontSize"
|
||||
class="menus-to-left menus-no-icon"
|
||||
/>
|
||||
<div class="c-toolbar__separator"></div>
|
||||
<toolbar-select-menu
|
||||
class="menus-to-left menus-no-icon"
|
||||
:options="fontMenuOptions"
|
||||
@change="setFont"
|
||||
class="menus-to-left menus-no-icon"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -211,14 +211,6 @@ export default {
|
||||
},
|
||||
watch: {
|
||||
domainObject() {
|
||||
if (this.mutationObserver) {
|
||||
this.mutationObserver();
|
||||
}
|
||||
|
||||
this.mutationObserver = this.openmct.objects.observe(this.domainObject, '*', (domainObject) => {
|
||||
this.domainObject = domainObject;
|
||||
});
|
||||
|
||||
if (this.removeStatusListener) {
|
||||
this.removeStatusListener();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<template>
|
||||
<div
|
||||
ref="me"
|
||||
:style="{
|
||||
'top': virtualScroll ? itemTop : 'auto',
|
||||
'position': virtualScroll ? 'absolute' : 'relative'
|
||||
@@ -14,8 +13,11 @@
|
||||
'is-navigated-object': navigated,
|
||||
'is-context-clicked': contextClickActive
|
||||
}"
|
||||
@click.capture="handleClick"
|
||||
@contextmenu.capture="handleContextMenu"
|
||||
>
|
||||
<view-control
|
||||
ref="navUp"
|
||||
v-model="expanded"
|
||||
class="c-tree__item__view-control"
|
||||
:control-class="'c-nav__up'"
|
||||
@@ -23,6 +25,7 @@
|
||||
@input="resetTreeHere"
|
||||
/>
|
||||
<object-label
|
||||
ref="objectLabel"
|
||||
:domain-object="node.object"
|
||||
:object-path="node.objectPath"
|
||||
:navigate-to-path="navigationPath"
|
||||
@@ -30,6 +33,7 @@
|
||||
@context-click-active="setContextClickActive"
|
||||
/>
|
||||
<view-control
|
||||
ref="navDown"
|
||||
v-model="expanded"
|
||||
class="c-tree__item__view-control"
|
||||
:control-class="'c-nav__down'"
|
||||
@@ -137,6 +141,19 @@ export default {
|
||||
this.openmct.router.off('change:path', this.highlightIfNavigated);
|
||||
},
|
||||
methods: {
|
||||
handleClick(event) {
|
||||
// skip for navigation, let viewControl handle click
|
||||
if ([this.$refs.navUp.$el, this.$refs.navDown.$el].includes(event.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.stopPropagation();
|
||||
this.$refs.objectLabel.navigateOrPreview(event);
|
||||
},
|
||||
handleContextMenu(event) {
|
||||
event.stopPropagation();
|
||||
this.$refs.objectLabel.showContextMenu(event);
|
||||
},
|
||||
isNavigated() {
|
||||
return this.navigationPath === this.openmct.router.currentLocation.path;
|
||||
},
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
class="l-browse-bar__object-name--w c-object-label"
|
||||
>
|
||||
<div class="c-object-label__type-icon"
|
||||
:class="type.cssClass"
|
||||
:class="type.definition.cssClass"
|
||||
></div>
|
||||
<span class="l-browse-bar__object-name c-object-label__name">
|
||||
{{ domainObject.name }}
|
||||
|
||||
@@ -10,12 +10,14 @@ define([
|
||||
let unobserve = undefined;
|
||||
let currentObjectPath;
|
||||
let isRoutingInProgress = false;
|
||||
let mutable;
|
||||
|
||||
openmct.router.route(/^\/browse\/?$/, navigateToFirstChildOfRoot);
|
||||
|
||||
openmct.router.route(/^\/browse\/(.*)$/, (path, results, params) => {
|
||||
isRoutingInProgress = true;
|
||||
let navigatePath = results[1];
|
||||
clearMutationListeners();
|
||||
navigateToPath(navigatePath, params.view);
|
||||
onParamsChanged(null, null, params);
|
||||
});
|
||||
@@ -36,10 +38,25 @@ define([
|
||||
}
|
||||
|
||||
function viewObject(object, viewProvider) {
|
||||
if (mutable) {
|
||||
openmct.objects.destroyMutable(mutable);
|
||||
mutable = undefined;
|
||||
}
|
||||
|
||||
if (openmct.objects.supportsMutation(object)) {
|
||||
mutable = openmct.objects._toMutable(object);
|
||||
}
|
||||
|
||||
currentObjectPath = openmct.router.path;
|
||||
|
||||
openmct.layout.$refs.browseObject.show(object, viewProvider.key, true, currentObjectPath);
|
||||
openmct.layout.$refs.browseBar.domainObject = object;
|
||||
if (mutable !== undefined) {
|
||||
openmct.layout.$refs.browseObject.show(mutable, viewProvider.key, true, currentObjectPath);
|
||||
openmct.layout.$refs.browseBar.domainObject = mutable;
|
||||
} else {
|
||||
openmct.layout.$refs.browseObject.show(object, viewProvider.key, true, currentObjectPath);
|
||||
openmct.layout.$refs.browseBar.domainObject = object;
|
||||
}
|
||||
|
||||
openmct.layout.$refs.browseBar.viewKey = viewProvider.key;
|
||||
}
|
||||
|
||||
@@ -64,23 +81,14 @@ define([
|
||||
return; // Prevent race.
|
||||
}
|
||||
|
||||
let navigatedObject = objects[objects.length - 1];
|
||||
objects = objects.reverse();
|
||||
|
||||
// FIXME: this is a hack to support create action, intended to
|
||||
// expose the current routed path. We need to rewrite the
|
||||
// navigation service and router to expose a clear and minimal
|
||||
// API for this.
|
||||
openmct.router.path = objects.reverse();
|
||||
openmct.router.path = objects;
|
||||
browseObject = objects[0];
|
||||
|
||||
unobserve = openmct.objects.observe(openmct.router.path[0], '*', (newObject) => {
|
||||
openmct.router.path[0] = newObject;
|
||||
browseObject = newObject;
|
||||
});
|
||||
openmct.layout.$refs.browseBar.domainObject = browseObject;
|
||||
|
||||
openmct.layout.$refs.browseBar.domainObject = navigatedObject;
|
||||
browseObject = navigatedObject;
|
||||
|
||||
if (!navigatedObject) {
|
||||
if (!browseObject) {
|
||||
openmct.layout.$refs.browseObject.clear();
|
||||
|
||||
return;
|
||||
@@ -92,13 +100,13 @@ define([
|
||||
|
||||
document.title = browseObject.name; //change document title to current object in main view
|
||||
|
||||
if (currentProvider && currentProvider.canView(navigatedObject)) {
|
||||
viewObject(navigatedObject, currentProvider);
|
||||
if (currentProvider && currentProvider.canView(browseObject)) {
|
||||
viewObject(browseObject, currentProvider);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let defaultProvider = openmct.objectViews.get(navigatedObject)[0];
|
||||
let defaultProvider = openmct.objectViews.get(browseObject)[0];
|
||||
if (defaultProvider) {
|
||||
openmct.router.updateParams({
|
||||
view: defaultProvider.key
|
||||
@@ -114,7 +122,13 @@ define([
|
||||
|
||||
function pathToObjects(path) {
|
||||
return Promise.all(path.map((keyString) => {
|
||||
return openmct.objects.get(keyString);
|
||||
let identifier = openmct.objects.parseKeyString(keyString);
|
||||
|
||||
if (openmct.objects.supportsMutation(identifier)) {
|
||||
return openmct.objects.getMutable(identifier);
|
||||
} else {
|
||||
return openmct.objects.get(identifier);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -132,5 +146,15 @@ define([
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function clearMutationListeners() {
|
||||
if (openmct.router.path !== undefined) {
|
||||
openmct.router.path.forEach((pathObject) => {
|
||||
if (pathObject.isMutable) {
|
||||
openmct.objects.destroyMutable(pathObject);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user