Compare commits

..

23 Commits

Author SHA1 Message Date
4f3317969e Fix broken test 2021-02-22 15:25:52 -08:00
56780879b0 null check for mutated object 2021-02-22 15:06:29 -08:00
2093a9d687 Merge branch 'master' into couchdb-object-synchronization 2021-02-22 14:46:02 -08:00
ea3b7d3da4 Address review comments 2021-02-22 14:33:49 -08:00
3932ccdbad Remove comments 2021-02-22 14:17:21 -08:00
dd45fa3a7c Fix tests for CouchObjectProvider 2021-02-22 14:13:35 -08:00
6f3004898b Resolves issue with finding the couchdb provider when the namespace is '' 2021-02-19 13:34:31 -08:00
44f05d16c5 Fixes bug with supportsMutation API call parameters
Fixes bug with 'mct' vs '' namespace when retrieving a provider
2021-02-19 13:08:36 -08:00
813e4a5656 Merge branch 'master' of https://github.com/nasa/openmct into couchdb-object-synchronization 2021-02-19 10:39:07 -08:00
ea66292141 Adds observers for mutable objects 2021-02-18 22:35:42 -08:00
8fdb983a3d Remove unneeded code 2021-02-18 15:27:12 -08:00
8e04d6409f Populating a virtual folder of plans from CouchDB 2021-02-18 14:31:53 -08:00
91ce09217a Merge branch 'master' of https://github.com/nasa/openmct into couchdb-object-synchronization 2021-02-18 10:23:48 -08:00
ca6e9387c3 Adds tests to object api for synchronize function 2021-02-17 09:53:36 -08:00
5734a1a69f Merge branch 'master' into couchdb-object-synchronization 2021-02-17 09:33:44 -08:00
ab3319128d Merge branch 'master' into couchdb-object-synchronization 2021-02-12 13:50:12 -08:00
46d00e6d61 Merge branch 'master' into couchdb-object-synchronization 2021-02-12 09:14:10 -08:00
56cd0cb5e1 Adds tests 2021-02-11 06:16:05 -08:00
de303d6497 Don't create a folder if the provider doesn't support synchronization 2021-02-09 09:43:09 -08:00
64c9d29059 Implements ObjectAPI changes to refresh objects when an update is received from the database. 2021-02-08 14:22:55 -08:00
4794cd5711 Merge branch 'master' of https://github.com/nasa/openmct into couchdb-subscription-draft 2021-02-05 06:41:51 -08:00
5b3762e90f Use selectors to filter the changes 2021-01-21 17:07:18 -08:00
6eadddd8d2 Draft of getting continuous data from couchDB 2021-01-21 17:01:08 -08:00
77 changed files with 932 additions and 3379 deletions

View File

@ -86,9 +86,7 @@
openmct.install(openmct.plugins.MyItems()); openmct.install(openmct.plugins.MyItems());
openmct.install(openmct.plugins.Generator()); openmct.install(openmct.plugins.Generator());
openmct.install(openmct.plugins.ExampleImagery()); openmct.install(openmct.plugins.ExampleImagery());
openmct.install(openmct.plugins.PlanLayout());
openmct.install(openmct.plugins.Timeline()); openmct.install(openmct.plugins.Timeline());
openmct.install(openmct.plugins.PlotVue());
openmct.install(openmct.plugins.UTCTimeSystem()); openmct.install(openmct.plugins.UTCTimeSystem());
openmct.install(openmct.plugins.AutoflowView({ openmct.install(openmct.plugins.AutoflowView({
type: "telemetry.panel" type: "telemetry.panel"

View File

@ -1,6 +1,6 @@
{ {
"name": "openmct", "name": "openmct",
"version": "1.7.0-SNAPSHOT", "version": "1.6.2-SNAPSHOT",
"description": "The Open MCT core platform", "description": "The Open MCT core platform",
"dependencies": {}, "dependencies": {},
"devDependencies": { "devDependencies": {

View File

@ -71,10 +71,10 @@ define(
openmct.editor.cancel(); openmct.editor.cancel();
} }
function isFirstViewEditable(domainObject, objectPath) { function isFirstViewEditable(domainObject) {
let firstView = openmct.objectViews.get(domainObject, objectPath)[0]; let firstView = openmct.objectViews.get(domainObject)[0];
return firstView && firstView.canEdit && firstView.canEdit(domainObject, objectPath); return firstView && firstView.canEdit && firstView.canEdit(domainObject);
} }
function navigateAndEdit(object) { function navigateAndEdit(object) {
@ -88,7 +88,7 @@ define(
window.location.href = url; window.location.href = url;
if (isFirstViewEditable(object.useCapability('adapter'), objectPath)) { if (isFirstViewEditable(object.useCapability('adapter'))) {
openmct.editor.edit(); openmct.editor.edit();
} }
} }

View File

@ -37,7 +37,7 @@ define(
this.$q = $q; this.$q = $q;
} }
LocatingObjectDecorator.prototype.getObjects = function (ids, abortSignal) { LocatingObjectDecorator.prototype.getObjects = function (ids) {
var $q = this.$q, var $q = this.$q,
$log = this.$log, $log = this.$log,
objectService = this.objectService, objectService = this.objectService,
@ -79,7 +79,7 @@ define(
}); });
} }
return objectService.getObjects([id], abortSignal).then(attachContext); return objectService.getObjects([id]).then(attachContext);
} }
ids.forEach(function (id) { ids.forEach(function (id) {

View File

@ -80,15 +80,12 @@ define([
* @param {Function} [filter] if provided, will be called for every * @param {Function} [filter] if provided, will be called for every
* potential modelResult. If it returns false, the model result will be * potential modelResult. If it returns false, the model result will be
* excluded from the search results. * excluded from the search results.
* @param {AbortController.signal} abortSignal (optional) can pass in an abortSignal to cancel any
* downstream fetch requests.
* @returns {Promise} A Promise for a search result object. * @returns {Promise} A Promise for a search result object.
*/ */
SearchAggregator.prototype.query = function ( SearchAggregator.prototype.query = function (
inputText, inputText,
maxResults, maxResults,
filter, filter
abortSignal
) { ) {
var aggregator = this, var aggregator = this,
@ -123,7 +120,7 @@ define([
modelResults = aggregator.applyFilter(modelResults, filter); modelResults = aggregator.applyFilter(modelResults, filter);
modelResults = aggregator.removeDuplicates(modelResults); modelResults = aggregator.removeDuplicates(modelResults);
return aggregator.asObjectResults(modelResults, abortSignal); return aggregator.asObjectResults(modelResults);
}); });
}; };
@ -196,19 +193,16 @@ define([
* Convert modelResults to objectResults by fetching them from the object * Convert modelResults to objectResults by fetching them from the object
* service. * service.
* *
* @param {Object} modelResults an object containing the results from the search
* @param {AbortController.signal} abortSignal (optional) abort signal to cancel any
* downstream fetch requests
* @returns {Promise} for an objectResults object. * @returns {Promise} for an objectResults object.
*/ */
SearchAggregator.prototype.asObjectResults = function (modelResults, abortSignal) { SearchAggregator.prototype.asObjectResults = function (modelResults) {
var objectIds = modelResults.hits.map(function (modelResult) { var objectIds = modelResults.hits.map(function (modelResult) {
return modelResult.id; return modelResult.id;
}); });
return this return this
.objectService .objectService
.getObjects(objectIds, abortSignal) .getObjects(objectIds)
.then(function (objects) { .then(function (objects) {
var objectResults = { var objectResults = {

View File

@ -37,7 +37,7 @@ define([
context.domainObject.getModel(), context.domainObject.getModel(),
objectUtils.parseKeyString(context.domainObject.getId()) objectUtils.parseKeyString(context.domainObject.getId())
); );
const providers = mct.propertyEditors.get(domainObject, mct.router.path); const providers = mct.propertyEditors.get(domainObject);
if (providers.length > 0) { if (providers.length > 0) {
action.dialogService = Object.create(action.dialogService); action.dialogService = Object.create(action.dialogService);

View File

@ -32,7 +32,7 @@ define([], function () {
if (Object.prototype.hasOwnProperty.call(view, 'provider')) { if (Object.prototype.hasOwnProperty.call(view, 'provider')) {
const domainObject = legacyObject.useCapability('adapter'); const domainObject = legacyObject.useCapability('adapter');
return view.provider.canView(domainObject, this.openmct.router.path); return view.provider.canView(domainObject);
} }
return true; return true;

View File

@ -139,12 +139,10 @@ define([
}); });
}; };
ObjectServiceProvider.prototype.superSecretFallbackSearch = function (query, abortSignal) { ObjectServiceProvider.prototype.superSecretFallbackSearch = function (query, options) {
const searchService = this.$injector.get('searchService'); const searchService = this.$injector.get('searchService');
// need to pass the abortSignal down, so need to return searchService.query(query);
// pass in undefined for maxResults and filter on query
return searchService.query(query, undefined, undefined, abortSignal);
}; };
// Injects new object API as a decorator so that it hijacks all requests. // Injects new object API as a decorator so that it hijacks all requests.
@ -152,13 +150,13 @@ define([
function LegacyObjectAPIInterceptor(openmct, ROOTS, instantiate, topic, objectService) { function LegacyObjectAPIInterceptor(openmct, ROOTS, instantiate, topic, objectService) {
const eventEmitter = openmct.objects.eventEmitter; const eventEmitter = openmct.objects.eventEmitter;
this.getObjects = function (keys, abortSignal) { this.getObjects = function (keys) {
const results = {}; const results = {};
const promises = keys.map(function (keyString) { const promises = keys.map(function (keyString) {
const key = utils.parseKeyString(keyString); const key = utils.parseKeyString(keyString);
return openmct.objects.get(key, abortSignal) return openmct.objects.get(key)
.then(function (object) { .then(function (object) {
object = utils.toOldFormat(object); object = utils.toOldFormat(object);
results[keyString] = instantiate(object, keyString); results[keyString] = instantiate(object, keyString);

View File

@ -29,22 +29,9 @@ describe('The ActionCollection', () => {
let mockApplicableActions; let mockApplicableActions;
let mockObjectPath; let mockObjectPath;
let mockView; let mockView;
let mockIdentifierService;
beforeEach(() => { beforeEach(() => {
openmct = createOpenMct(); openmct = createOpenMct();
openmct.$injector = jasmine.createSpyObj('$injector', ['get']);
mockIdentifierService = jasmine.createSpyObj(
'identifierService',
['parse']
);
mockIdentifierService.parse.and.returnValue({
getSpace: () => {
return '';
}
});
openmct.$injector.get.and.returnValue(mockIdentifierService);
mockObjectPath = [ mockObjectPath = [
{ {
name: 'mock folder', name: 'mock folder',

View File

@ -154,12 +154,11 @@ ObjectAPI.prototype.addProvider = function (namespace, provider) {
* @method get * @method get
* @memberof module:openmct.ObjectProvider# * @memberof module:openmct.ObjectProvider#
* @param {string} key the key for the domain object to load * @param {string} key the key for the domain object to load
* @param {AbortController.signal} abortSignal (optional) signal to abort fetch requests
* @returns {Promise} a promise which will resolve when the domain object * @returns {Promise} a promise which will resolve when the domain object
* has been saved, or be rejected if it cannot be saved * has been saved, or be rejected if it cannot be saved
*/ */
ObjectAPI.prototype.get = function (identifier, abortSignal) { ObjectAPI.prototype.get = function (identifier) {
let keystring = this.makeKeyString(identifier); let keystring = this.makeKeyString(identifier);
if (this.cache[keystring] !== undefined) { if (this.cache[keystring] !== undefined) {
return this.cache[keystring]; return this.cache[keystring];
@ -176,12 +175,15 @@ ObjectAPI.prototype.get = function (identifier, abortSignal) {
throw new Error('Provider does not support get!'); throw new Error('Provider does not support get!');
} }
let objectPromise = provider.get(identifier, abortSignal); let objectPromise = provider.get(identifier);
this.cache[keystring] = objectPromise; this.cache[keystring] = objectPromise;
return objectPromise.then(result => { return objectPromise.then(result => {
delete this.cache[keystring]; delete this.cache[keystring];
result = this.applyGetInterceptors(identifier, result); const interceptors = this.listGetInterceptors(identifier, result);
interceptors.forEach(interceptor => {
result = interceptor.invoke(identifier, result);
});
return result; return result;
}); });
@ -198,24 +200,19 @@ ObjectAPI.prototype.get = function (identifier, abortSignal) {
* @method search * @method search
* @memberof module:openmct.ObjectAPI# * @memberof module:openmct.ObjectAPI#
* @param {string} query the term to search for * @param {string} query the term to search for
* @param {AbortController.signal} abortSignal (optional) signal to cancel downstream fetch requests * @param {Object} options search options
* @returns {Array.<Promise.<module:openmct.DomainObject>>} * @returns {Array.<Promise.<module:openmct.DomainObject>>}
* an array of promises returned from each object provider's search function * an array of promises returned from each object provider's search function
* each resolving to domain objects matching provided search query and options. * each resolving to domain objects matching provided search query and options.
*/ */
ObjectAPI.prototype.search = function (query, abortSignal) { ObjectAPI.prototype.search = function (query, options) {
const searchPromises = Object.values(this.providers) const searchPromises = Object.values(this.providers)
.filter(provider => provider.search !== undefined) .filter(provider => provider.search !== undefined)
.map(provider => provider.search(query, abortSignal)); .map(provider => provider.search(query, options));
searchPromises.push(this.fallbackProvider.superSecretFallbackSearch(query, abortSignal) searchPromises.push(this.fallbackProvider.superSecretFallbackSearch(query, options)
.then(results => results.hits .then(results => results.hits
.map(hit => { .map(hit => utils.toNewFormat(hit.object.getModel(), hit.object.getId()))));
let domainObject = utils.toNewFormat(hit.object.getModel(), hit.object.getId());
domainObject = this.applyGetInterceptors(domainObject.identifier, domainObject);
return domainObject;
})));
return searchPromises; return searchPromises;
}; };
@ -231,13 +228,29 @@ ObjectAPI.prototype.search = function (query, abortSignal) {
* @returns {Promise.<MutableDomainObject>} a promise that will resolve with a MutableDomainObject if * @returns {Promise.<MutableDomainObject>} a promise that will resolve with a MutableDomainObject if
* the object can be mutated. * the object can be mutated.
*/ */
ObjectAPI.prototype.getMutable = function (identifier) { ObjectAPI.prototype.getMutable = function (idOrKeyString) {
if (!this.supportsMutation(identifier)) { if (!this.supportsMutation(idOrKeyString)) {
throw new Error(`Object "${this.makeKeyString(identifier)}" does not support mutation.`); throw new Error(`Object "${this.makeKeyString(idOrKeyString)}" does not support mutation.`);
} }
return this.get(identifier).then((object) => { return this.get(idOrKeyString).then((object) => {
return this._toMutable(object); const mutableDomainObject = this._toMutable(object);
// Check if provider supports realtime updates
let identifier = utils.parseKeyString(idOrKeyString);
let provider = this.getProvider(identifier);
if (provider !== undefined
&& provider.observe !== undefined) {
let unobserve = provider.observe(identifier, (updatedModel) => {
mutableDomainObject.$refresh(updatedModel);
});
mutableDomainObject.$on('$destroy', () => {
unobserve();
});
}
return mutableDomainObject;
}); });
}; };
@ -341,19 +354,6 @@ ObjectAPI.prototype.listGetInterceptors = function (identifier, object) {
return this.interceptorRegistry.getInterceptors(identifier, object); return this.interceptorRegistry.getInterceptors(identifier, object);
}; };
/**
* Inovke interceptors if applicable for a given domain object.
* @private
*/
ObjectAPI.prototype.applyGetInterceptors = function (identifier, domainObject) {
const interceptors = this.listGetInterceptors(identifier, domainObject);
interceptors.forEach(interceptor => {
domainObject = interceptor.invoke(identifier, domainObject);
});
return domainObject;
};
/** /**
* Modify a domain object. * Modify a domain object.
* @param {module:openmct.DomainObject} object the object to mutate * @param {module:openmct.DomainObject} object the object to mutate
@ -389,29 +389,11 @@ ObjectAPI.prototype.mutate = function (domainObject, path, value) {
* @private * @private
*/ */
ObjectAPI.prototype._toMutable = function (object) { ObjectAPI.prototype._toMutable = function (object) {
let mutableObject;
if (object.isMutable) { if (object.isMutable) {
mutableObject = object; return object;
} else { } else {
mutableObject = MutableDomainObject.createMutable(object, this.eventEmitter); return MutableDomainObject.createMutable(object, this.eventEmitter);
} }
// Check if provider supports realtime updates
let identifier = utils.parseKeyString(mutableObject.identifier);
let provider = this.getProvider(identifier);
if (provider !== undefined
&& provider.observe !== undefined) {
let unobserve = provider.observe(identifier, (updatedModel) => {
mutableObject.$refresh(updatedModel);
});
mutableObject.$on('$destroy', () => {
unobserve();
});
}
return mutableObject;
}; };
/** /**

View File

@ -1,4 +1,4 @@
export default function (folderName, couchPlugin, searchFilter) { export default function (couchPlugin, searchFilter) {
return function install(openmct) { return function install(openmct) {
const couchProvider = couchPlugin.couchProvider; const couchProvider = couchPlugin.couchProvider;
@ -15,7 +15,7 @@ export default function (folderName, couchPlugin, searchFilter) {
return Promise.resolve({ return Promise.resolve({
identifier, identifier,
type: 'folder', type: 'folder',
name: folderName || "CouchDB Documents" name: "CouchDB Documents"
}); });
} }
} }

View File

@ -39,7 +39,7 @@ describe('the plugin', function () {
let couchPlugin = openmct.plugins.CouchDB(testPath); let couchPlugin = openmct.plugins.CouchDB(testPath);
openmct.install(couchPlugin); openmct.install(couchPlugin);
openmct.install(new CouchDBSearchFolderPlugin('CouchDB Documents', couchPlugin, { openmct.install(new CouchDBSearchFolderPlugin(couchPlugin, {
"selector": { "selector": {
"model": { "model": {
"type": "plan" "type": "plan"

View File

@ -98,7 +98,7 @@ describe("The LAD Table", () => {
}); });
it("should provide a table view only for lad table objects", () => { it("should provide a table view only for lad table objects", () => {
let applicableViews = openmct.objectViews.get(mockObj.ladTable, []); let applicableViews = openmct.objectViews.get(mockObj.ladTable);
let ladTableView = applicableViews.find( let ladTableView = applicableViews.find(
(viewProvider) => viewProvider.key === ladTableKey (viewProvider) => viewProvider.key === ladTableKey
@ -185,7 +185,7 @@ describe("The LAD Table", () => {
end: bounds.end end: bounds.end
}); });
applicableViews = openmct.objectViews.get(mockObj.ladTable, []); applicableViews = openmct.objectViews.get(mockObj.ladTable);
ladTableViewProvider = applicableViews.find((viewProvider) => viewProvider.key === ladTableKey); ladTableViewProvider = applicableViews.find((viewProvider) => viewProvider.key === ladTableKey);
ladTableView = ladTableViewProvider.view(mockObj.ladTable, [mockObj.ladTable]); ladTableView = ladTableViewProvider.view(mockObj.ladTable, [mockObj.ladTable]);
ladTableView.show(child, true); ladTableView.show(child, true);
@ -296,7 +296,7 @@ describe("The LAD Table Set", () => {
}); });
it("should provide a lad table set view only for lad table set objects", () => { it("should provide a lad table set view only for lad table set objects", () => {
let applicableViews = openmct.objectViews.get(mockObj.ladTableSet, []); let applicableViews = openmct.objectViews.get(mockObj.ladTableSet);
let ladTableSetView = applicableViews.find( let ladTableSetView = applicableViews.find(
(viewProvider) => viewProvider.key === ladTableSetKey (viewProvider) => viewProvider.key === ladTableSetKey
@ -391,7 +391,7 @@ describe("The LAD Table Set", () => {
end: bounds.end end: bounds.end
}); });
applicableViews = openmct.objectViews.get(mockObj.ladTableSet, []); applicableViews = openmct.objectViews.get(mockObj.ladTableSet);
ladTableSetViewProvider = applicableViews.find((viewProvider) => viewProvider.key === ladTableSetKey); ladTableSetViewProvider = applicableViews.find((viewProvider) => viewProvider.key === ladTableSetKey);
ladTableSetView = ladTableSetViewProvider.view(mockObj.ladTableSet, [mockObj.ladTableSet]); ladTableSetView = ladTableSetViewProvider.view(mockObj.ladTableSet, [mockObj.ladTableSet]);
ladTableSetView.show(child, true); ladTableSetView.show(child, true);

View File

@ -67,11 +67,11 @@ describe("AutoflowTabularPlugin", () => {
}); });
it("applies its view to the type from options", () => { it("applies its view to the type from options", () => {
expect(provider.canView(testObject, [])).toBe(true); expect(provider.canView(testObject)).toBe(true);
}); });
it("does not apply to other types", () => { it("does not apply to other types", () => {
expect(provider.canView({ type: 'foo' }, [])).toBe(false); expect(provider.canView({ type: 'foo' })).toBe(false);
}); });
describe("provides a view which", () => { describe("provides a view which", () => {

View File

@ -136,7 +136,7 @@ describe('the plugin', function () {
} }
}; };
const applicableViews = openmct.objectViews.get(testViewObject, []); const applicableViews = openmct.objectViews.get(testViewObject);
let conditionSetView = applicableViews.find((viewProvider) => viewProvider.key === 'conditionSet.view'); let conditionSetView = applicableViews.find((viewProvider) => viewProvider.key === 'conditionSet.view');
expect(conditionSetView).toBeDefined(); expect(conditionSetView).toBeDefined();
}); });

View File

@ -109,8 +109,7 @@ export default {
data() { data() {
return { return {
domainObject: undefined, domainObject: undefined,
currentObjectPath: [], currentObjectPath: []
mutablePromise: undefined
}; };
}, },
watch: { watch: {
@ -131,7 +130,7 @@ export default {
}, },
mounted() { mounted() {
if (this.openmct.objects.supportsMutation(this.item.identifier)) { if (this.openmct.objects.supportsMutation(this.item.identifier)) {
this.mutablePromise = this.openmct.objects.getMutable(this.item.identifier) this.openmct.objects.getMutable(this.item.identifier)
.then(this.setObject); .then(this.setObject);
} else { } else {
this.openmct.objects.get(this.item.identifier) this.openmct.objects.get(this.item.identifier)
@ -143,18 +142,13 @@ export default {
this.removeSelectable(); this.removeSelectable();
} }
if (this.mutablePromise) { if (this.domainObject.isMutable) {
this.mutablePromise.then(() => {
this.openmct.objects.destroyMutable(this.domainObject);
});
} else {
this.openmct.objects.destroyMutable(this.domainObject); this.openmct.objects.destroyMutable(this.domainObject);
} }
}, },
methods: { methods: {
setObject(domainObject) { setObject(domainObject) {
this.domainObject = domainObject; this.domainObject = domainObject;
this.mutablePromise = undefined;
this.currentObjectPath = [this.domainObject].concat(this.objectPath.slice()); this.currentObjectPath = [this.domainObject].concat(this.objectPath.slice());
this.$nextTick(() => { this.$nextTick(() => {
let reference = this.$refs.objectFrame; let reference = this.$refs.objectFrame;

View File

@ -131,8 +131,7 @@ export default {
domainObject: undefined, domainObject: undefined,
formats: undefined, formats: undefined,
viewKey: `alphanumeric-format-${Math.random()}`, viewKey: `alphanumeric-format-${Math.random()}`,
status: '', status: ''
mutablePromise: undefined
}; };
}, },
computed: { computed: {
@ -214,7 +213,7 @@ export default {
}, },
mounted() { mounted() {
if (this.openmct.objects.supportsMutation(this.item.identifier)) { if (this.openmct.objects.supportsMutation(this.item.identifier)) {
this.mutablePromise = this.openmct.objects.getMutable(this.item.identifier) this.openmct.objects.getMutable(this.item.identifier)
.then(this.setObject); .then(this.setObject);
} else { } else {
this.openmct.objects.get(this.item.identifier) this.openmct.objects.get(this.item.identifier)
@ -236,11 +235,7 @@ export default {
this.openmct.time.off("bounds", this.refreshData); this.openmct.time.off("bounds", this.refreshData);
if (this.mutablePromise) { if (this.domainObject.isMutable) {
this.mutablePromise.then(() => {
this.openmct.objects.destroyMutable(this.domainObject);
});
} else {
this.openmct.objects.destroyMutable(this.domainObject); this.openmct.objects.destroyMutable(this.domainObject);
} }
}, },
@ -301,7 +296,6 @@ export default {
}, },
setObject(domainObject) { setObject(domainObject) {
this.domainObject = domainObject; this.domainObject = domainObject;
this.mutablePromise = undefined;
this.keyString = this.openmct.objects.makeKeyString(domainObject.identifier); this.keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject); this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
this.limitEvaluator = this.openmct.telemetry.limitEvaluator(this.domainObject); this.limitEvaluator = this.openmct.telemetry.limitEvaluator(this.domainObject);

View File

@ -83,7 +83,7 @@ describe('the plugin', function () {
} }
}; };
const applicableViews = openmct.objectViews.get(testViewObject, []); const applicableViews = openmct.objectViews.get(testViewObject);
let displayLayoutViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'layout.view'); let displayLayoutViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'layout.view');
expect(displayLayoutViewProvider).toBeDefined(); expect(displayLayoutViewProvider).toBeDefined();
}); });

View File

@ -271,6 +271,11 @@ export default {
}); });
}, },
removeFromComposition(identifier) { removeFromComposition(identifier) {
let keystring = this.openmct.objects.makeKeyString(identifier);
this.identifierMap[keystring] = undefined;
delete this.identifierMap[keystring];
this.composition.remove({identifier}); this.composition.remove({identifier});
}, },
setSelectionToParent() { setSelectionToParent() {
@ -350,9 +355,6 @@ export default {
removeChildObject(identifier) { removeChildObject(identifier) {
let removeIdentifier = this.openmct.objects.makeKeyString(identifier); let removeIdentifier = this.openmct.objects.makeKeyString(identifier);
this.identifierMap[removeIdentifier] = undefined;
delete this.identifierMap[removeIdentifier];
this.containers.forEach(container => { this.containers.forEach(container => {
container.frames = container.frames.filter(frame => { container.frames = container.frames.filter(frame => {
let frameIdentifier = this.openmct.objects.makeKeyString(frame.domainObjectIdentifier); let frameIdentifier = this.openmct.objects.makeKeyString(frame.domainObjectIdentifier);

View File

@ -1,25 +1,3 @@
/*****************************************************************************
* 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 ImageryViewLayout from './components/ImageryViewLayout.vue';
import Vue from 'vue'; import Vue from 'vue';

View File

@ -1,124 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
<template>
<div
class="c-compass"
:style="compassDimensionsStyle"
>
<CompassHUD
v-if="hasCameraFieldOfView"
:sun-heading="sunHeading"
:camera-angle-of-view="cameraAngleOfView"
:camera-pan="cameraPan"
/>
<CompassRose
v-if="hasCameraFieldOfView"
:heading="heading"
:sun-heading="sunHeading"
:camera-angle-of-view="cameraAngleOfView"
:camera-pan="cameraPan"
:lock-compass="lockCompass"
@toggle-lock-compass="toggleLockCompass"
/>
</div>
</template>
<script>
import CompassHUD from './CompassHUD.vue';
import CompassRose from './CompassRose.vue';
const CAMERA_ANGLE_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
},
lockCompass: {
type: Boolean,
required: true
}
},
computed: {
hasCameraFieldOfView() {
return this.cameraPan !== undefined && this.cameraAngleOfView > 0;
},
// horizontal rotation from north in degrees
heading() {
return this.image.heading;
},
// horizontal rotation from north in degrees
sunHeading() {
return this.image.sunOrientation;
},
// horizontal rotation from north in degrees
cameraPan() {
return this.image.cameraPan;
},
cameraAngleOfView() {
return CAMERA_ANGLE_OF_VIEW;
},
compassDimensionsStyle() {
const containerAspectRatio = this.containerWidth / this.containerHeight;
let width;
let height;
if (containerAspectRatio < this.naturalAspectRatio) {
width = '100%';
height = `${ this.containerWidth / this.naturalAspectRatio }px`;
} else {
width = `${ this.containerHeight * this.naturalAspectRatio }px`;
height = '100%';
}
return {
width: width,
height: height
};
}
},
methods: {
toggleLockCompass() {
this.$emit('toggle-lock-compass');
}
}
};
</script>

View File

@ -1,141 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
<template>
<div
class="c-compass__hud c-hud"
>
<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 {
rotate,
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: {
sunHeading: {
type: Number,
default: undefined
},
cameraAngleOfView: {
type: Number,
default: undefined
},
cameraPan: {
type: Number,
required: true
}
},
computed: {
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.sunHeading, this.visibleRange);
},
sunPositionStyle() {
const percentage = percentOfRange(this.sunHeading, this.visibleRange);
return {
left: `${ percentage * 100 }%`
};
},
visibleRange() {
return [
rotate(this.cameraPan, -this.cameraAngleOfView / 2),
rotate(this.cameraPan, this.cameraAngleOfView / 2)
];
}
}
};
</script>

View File

@ -1,261 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
<template>
<div
class="c-direction-rose"
@click="toggleLockCompass"
>
<div
class="c-nsew"
:style="compassRoseStyle"
>
<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
v-if="hasHeading"
class="c-spacecraft-body"
:style="headingStyle"
>
</div>
<div
v-if="hasSunHeading"
class="c-sun"
:style="sunHeadingStyle"
></div>
<div
class="c-cam-field"
:style="cameraPanStyle"
>
<div class="cam-field-half cam-field-half-l">
<div
class="cam-field-area"
:style="cameraFOVStyleLeftHalf"
></div>
</div>
<div class="cam-field-half cam-field-half-r">
<div
class="cam-field-area"
:style="cameraFOVStyleRightHalf"
></div>
</div>
</div>
</div>
</template>
<script>
import { rotate } from './utils';
export default {
props: {
heading: {
type: Number,
required: true
},
sunHeading: {
type: Number,
default: undefined
},
cameraAngleOfView: {
type: Number,
default: undefined
},
cameraPan: {
type: Number,
required: true
},
lockCompass: {
type: Boolean,
required: true
}
},
computed: {
north() {
return this.lockCompass ? rotate(-this.cameraPan) : 0;
},
compassRoseStyle() {
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 }`
};
},
hasHeading() {
return this.heading !== undefined;
},
headingStyle() {
const rotation = rotate(this.north, this.heading);
return {
transform: `translateX(-50%) rotate(${ rotation }deg)`
};
},
hasSunHeading() {
return this.sunHeading !== undefined;
},
sunHeadingStyle() {
const rotation = rotate(this.north, this.sunHeading);
return {
transform: `rotate(${ rotation }deg)`
};
},
cameraPanStyle() {
const rotation = rotate(this.north, this.cameraPan);
return {
transform: `rotate(${ rotation }deg)`
};
},
// left half of camera field of view
// rotated counter-clockwise from camera pan angle
cameraFOVStyleLeftHalf() {
return {
transform: `translateX(50%) rotate(${ -this.cameraAngleOfView / 2 }deg)`
};
},
// right half of camera field of view
// rotated clockwise from camera pan angle
cameraFOVStyleRightHalf() {
return {
transform: `translateX(-50%) rotate(${ this.cameraAngleOfView / 2 }deg)`
};
}
},
methods: {
toggleLockCompass() {
this.$emit('toggle-lock-compass');
}
}
};
</script>

View File

@ -1,214 +0,0 @@
/***************************** 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;
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: 0 1px 2px black;
top: 50%;
transform: translate(-50%,-50%);
z-index: 2;
}
[class*="__dir--sub"] {
font-weight: normal;
opacity: 0.5;
}
&__sun {
$s: 10px;
@include sun('circle farthest-side at bottom');
bottom: $padTB + 2px;
height: $s; width: $s*2;
opacity: 0.8;
transform: translateX(-50%);
z-index: 1;
}
}
/***************************** COMPASS DIRECTIONS */
.c-nsew {
$color: $interfaceKeyColor;
$inset: 7%;
$tickHeightPerc: 15%;
text-shadow: black 0 0 10px;
top: $inset; right: $inset; bottom: $inset; left: $inset;
z-index: 3;
&__tick,
&__label {
fill: $color;
}
&__minor-ticks {
opacity: 0.5;
transform-origin: center;
transform: rotate(45deg);
}
&__label {
dominant-baseline: central;
font-size: 0.8em;
font-weight: bold;
}
.c-label-n {
font-size: 1.1em;
}
}
/***************************** CAMERA FIELD ANGLE */
.c-cam-field {
$color: white;
opacity: 0.2;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 2;
.cam-field-half {
top: 0;
right: 0;
bottom: 0;
left: 0;
.cam-field-area {
background: $color;
top: -30%;
right: 0;
bottom: -30%;
left: 0;
}
// clip-paths overlap a bit to avoid a gap between halves
&-l {
clip-path: polygon(0 0, 50.5% 0, 50.5% 100%, 0 100%);
.cam-field-area {
transform-origin: left center;
}
}
&-r {
clip-path: polygon(49.5% 0, 100% 0, 100% 100%, 49.5% 100%);
.cam-field-area {
transform-origin: right center;
}
}
}
}
/***************************** SPACECRAFT BODY */
.c-spacecraft-body {
$color: $interfaceKeyColor;
$s: 30%;
background: $color;
border-radius: 3px;
height: $s; width: $s;
left: 50%; top: 50%;
opacity: 0.4;
transform-origin: center top;
&:before {
// Direction arrow
$color: rgba(black, 0.5);
$arwPointerY: 60%;
$arwBodyOffset: 25%;
background: $color;
content: '';
display: block;
position: absolute;
top: 10%; right: 20%; bottom: 50%; left: 20%;
clip-path: polygon(50% 0, 100% $arwPointerY, 100%-$arwBodyOffset $arwPointerY, 100%-$arwBodyOffset 100%, $arwBodyOffset 100%, $arwBodyOffset $arwPointerY, 0 $arwPointerY);
}
}
/***************************** DIRECTION ROSE */
.c-direction-rose {
$d: 100px;
$c2: rgba(white, 0.1);
background: $elemBg;
background-image: radial-gradient(circle closest-side, transparent, transparent 80%, $c2);
width: $d;
height: $d;
transform-origin: 0 0;
position: absolute;
bottom: 10px; left: 10px;
clip-path: circle(50% at 50% 50%);
border-radius: 100%;
svg, div {
position: absolute;
}
// Sun
.c-sun {
top: 0;
right: 0;
bottom: 0;
left: 0;
&:before {
$s: 35%;
@include sun();
content: '';
display: block;
position: absolute;
opacity: 0.7;
top: 0; left: 50%;
height:$s; width: $s;
transform: translate(-50%, -60%);
}
}
}

View File

@ -1,84 +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.
*****************************************************************************/
import Compass from './Compass.vue';
import Vue from 'vue';
const COMPASS_ROSE_CLASS = '.c-direction-rose';
const COMPASS_HUD_CLASS = '.c-compass__hud';
describe("The Compass component", () => {
let app;
let instance;
beforeEach(() => {
let imageDatum = {
heading: 100,
roll: 90,
pitch: 90,
cameraTilt: 100,
cameraPan: 90,
sunAngle: 30
};
let propsData = {
containerWidth: 600,
containerHeight: 600,
naturalAspectRatio: 0.9,
image: imageDatum
};
app = new Vue({
components: { Compass },
data() {
return propsData;
},
template: `<Compass
:container-width="containerWidth"
:container-height="containerHeight"
:natural-aspect-ratio="naturalAspectRatio"
:image="image" />`
});
instance = app.$mount();
});
afterAll(() => {
app.$destroy();
});
describe("when a heading value exists on the image", () => {
it("should display a compass rose", () => {
let compassRoseElement = instance.$el.querySelector(COMPASS_ROSE_CLASS
);
expect(compassRoseElement).toBeDefined();
});
it("should display a compass HUD", () => {
let compassHUDElement = instance.$el.querySelector(COMPASS_HUD_CLASS);
expect(compassHUDElement).toBeDefined();
});
});
});

View File

@ -1,44 +0,0 @@
/**
*
* sums an arbitrary number of absolute rotations
* (meaning rotations relative to one common direction 0)
* normalizes the rotation to the range [0, 360)
*
* @param {...number} rotations in degrees
* @returns {number} normalized sum of all rotations - [0, 360) degrees
*/
export function rotate(...rotations) {
const rotation = rotations.reduce((a, b) => a + b, 0);
return normalizeCompassDirection(rotation);
}
export function inRange(degrees, [min, max]) {
const point = rotate(degrees);
return min > max
? (point >= min && point < 360) || (point <= max && point >= 0)
: point >= min && point <= max;
}
export function percentOfRange(degrees, [min, max]) {
let distance = rotate(degrees);
let minRange = min;
let maxRange = max;
if (min > max) {
if (distance < max) {
distance += 360;
}
maxRange += 360;
}
return (distance - minRange) / (maxRange - minRange);
}
function normalizeCompassDirection(degrees) {
const base = degrees % 360;
return base >= 0 ? base : 360 + base;
}

View File

@ -1,25 +1,3 @@
/*****************************************************************************
* 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> <template>
<div <div
tabindex="0" tabindex="0"
@ -58,25 +36,14 @@
<div class="c-imagery__main-image__bg" <div class="c-imagery__main-image__bg"
:class="{'paused unnsynced': isPaused,'stale':false }" :class="{'paused unnsynced': isPaused,'stale':false }"
> >
<img <div class="c-imagery__main-image__image js-imageryView-image"
ref="focusedImage" :style="{
class="c-imagery__main-image__image js-imageryView-image" 'background-image': imageUrl ? `url(${imageUrl})` : 'none',
:src="imageUrl" 'filter': `brightness(${filters.brightness}%) contrast(${filters.contrast}%)`
:style="{ }"
'filter': `brightness(${filters.brightness}%) contrast(${filters.contrast}%)` :data-openmct-image-timestamp="time"
}" :data-openmct-object-keystring="keyString"
:data-openmct-image-timestamp="time" ></div>
:data-openmct-object-keystring="keyString"
>
<Compass
v-if="shouldDisplayCompass"
:container-width="imageContainerWidth"
:container-height="imageContainerHeight"
:natural-aspect-ratio="focusedImageNaturalAspectRatio"
:image="focusedImage"
:lock-compass="lockCompass"
@toggle-lock-compass="toggleLockCompass"
/>
</div> </div>
<div class="c-local-controls c-local-controls--show-on-hover c-imagery__prev-next-buttons"> <div class="c-local-controls c-local-controls--show-on-hover c-imagery__prev-next-buttons">
<button class="c-nav c-nav--prev" <button class="c-nav c-nav--prev"
@ -94,25 +61,11 @@
<div class="c-imagery__control-bar"> <div class="c-imagery__control-bar">
<div class="c-imagery__time"> <div class="c-imagery__time">
<div class="c-imagery__timestamp u-style-receiver js-style-receiver">{{ time }}</div> <div class="c-imagery__timestamp u-style-receiver js-style-receiver">{{ time }}</div>
<!-- image fresh -->
<div <div
v-if="canTrackDuration" v-if="canTrackDuration"
:class="{'c-imagery--new': isImageNew && !refreshCSS}" :class="{'c-imagery--new': isImageNew && !refreshCSS}"
class="c-imagery__age icon-timer" class="c-imagery__age icon-timer"
>{{ formattedDuration }}</div> >{{ formattedDuration }}</div>
<!-- spacecraft position fresh -->
<div
v-if="relatedTelemetry.hasRelatedTelemetry && isSpacecraftPositionFresh"
class="c-imagery__age icon-check c-imagery--new"
>POS</div>
<!-- camera position fresh -->
<div
v-if="relatedTelemetry.hasRelatedTelemetry && isCameraPositionFresh"
class="c-imagery__age icon-check c-imagery--new"
>CAM</div>
</div> </div>
<div class="h-local-controls"> <div class="h-local-controls">
<button <button
@ -123,32 +76,28 @@
</div> </div>
</div> </div>
</div> </div>
<div <div ref="thumbsWrapper"
ref="thumbsWrapper" class="c-imagery__thumbs-wrapper"
class="c-imagery__thumbs-wrapper" :class="{'is-paused': isPaused}"
:class="{'is-paused': isPaused}" @scroll="handleScroll"
@scroll="handleScroll"
> >
<div v-for="(image, index) in imageHistory" <div v-for="(datum, index) in imageHistory"
:key="image.url + image.time" :key="datum.url"
class="c-imagery__thumb c-thumb" class="c-imagery__thumb c-thumb"
:class="{ selected: focusedImageIndex === index && isPaused }" :class="{ selected: focusedImageIndex === index && isPaused }"
@click="setFocusedImage(index, thumbnailClick)" @click="setFocusedImage(index, thumbnailClick)"
> >
<img class="c-thumb__image" <img class="c-thumb__image"
:src="image.url" :src="formatImageUrl(datum)"
> >
<div class="c-thumb__timestamp">{{ image.formattedTime }}</div> <div class="c-thumb__timestamp">{{ formatTime(datum) }}</div>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import _ from 'lodash';
import moment from 'moment'; import moment from 'moment';
import Compass from './Compass/Compass.vue';
import RelatedTelemetry from './RelatedTelemetry/RelatedTelemetry';
const DEFAULT_DURATION_FORMATTER = 'duration'; const DEFAULT_DURATION_FORMATTER = 'duration';
const REFRESH_CSS_MS = 500; const REFRESH_CSS_MS = 500;
@ -167,9 +116,6 @@ const ARROW_RIGHT = 39;
const ARROW_LEFT = 37; const ARROW_LEFT = 37;
export default { export default {
components: {
Compass
},
inject: ['openmct', 'domainObject'], inject: ['openmct', 'domainObject'],
data() { data() {
let timeSystem = this.openmct.time.timeSystem(); let timeSystem = this.openmct.time.timeSystem();
@ -191,15 +137,7 @@ export default {
refreshCSS: false, refreshCSS: false,
keyString: undefined, keyString: undefined,
focusedImageIndex: undefined, focusedImageIndex: undefined,
focusedImageRelatedTelemetry: {}, numericDuration: undefined
numericDuration: undefined,
metadataEndpoints: {},
relatedTelemetry: {},
latestRelatedTelemetry: {},
focusedImageNaturalAspectRatio: undefined,
imageContainerWidth: undefined,
imageContainerHeight: undefined,
lockCompass: true
}; };
}, },
computed: { computed: {
@ -257,83 +195,15 @@ export default {
} }
return result; return result;
},
shouldDisplayCompass() {
return this.focusedImage !== undefined
&& this.focusedImageNaturalAspectRatio !== undefined
&& this.imageContainerWidth !== undefined
&& this.imageContainerHeight !== undefined;
},
isSpacecraftPositionFresh() {
let isFresh = undefined;
let latest = this.latestRelatedTelemetry;
let focused = this.focusedImageRelatedTelemetry;
if (this.relatedTelemetry.hasRelatedTelemetry) {
isFresh = true;
for (let key of this.spacecraftPositionKeys) {
if (this.relatedTelemetry[key] && latest[key] && focused[key]) {
isFresh = isFresh && Boolean(this.relatedTelemetry[key].comparisonFunction(latest[key], focused[key]));
} else {
isFresh = false;
}
}
}
return isFresh;
},
isSpacecraftOrientationFresh() {
let isFresh = undefined;
let latest = this.latestRelatedTelemetry;
let focused = this.focusedImageRelatedTelemetry;
if (this.relatedTelemetry.hasRelatedTelemetry) {
isFresh = true;
for (let key of this.spacecraftOrientationKeys) {
if (this.relatedTelemetry[key] && latest[key] && focused[key]) {
isFresh = isFresh && Boolean(this.relatedTelemetry[key].comparisonFunction(latest[key], focused[key]));
} else {
isFresh = false;
}
}
}
return isFresh;
},
isCameraPositionFresh() {
let isFresh = undefined;
let latest = this.latestRelatedTelemetry;
let focused = this.focusedImageRelatedTelemetry;
if (this.relatedTelemetry.hasRelatedTelemetry) {
isFresh = true;
// camera freshness relies on spacecraft position freshness
if (this.isSpacecraftPositionFresh && this.isSpacecraftOrientationFresh) {
for (let key of this.cameraKeys) {
if (this.relatedTelemetry[key] && latest[key] && focused[key]) {
isFresh = isFresh && Boolean(this.relatedTelemetry[key].comparisonFunction(latest[key], focused[key]));
} else {
isFresh = false;
}
}
} else {
isFresh = false;
}
}
return isFresh;
} }
}, },
watch: { watch: {
focusedImageIndex() { focusedImageIndex() {
this.trackDuration(); this.trackDuration();
this.resetAgeCSS(); this.resetAgeCSS();
this.updateRelatedTelemetryForFocusedImage();
this.getImageNaturalDimensions();
} }
}, },
async mounted() { mounted() {
// listen // listen
this.openmct.time.on('bounds', this.boundsChange); this.openmct.time.on('bounds', this.boundsChange);
this.openmct.time.on('timeSystem', this.timeSystemChange); this.openmct.time.on('timeSystem', this.timeSystemChange);
@ -342,15 +212,8 @@ export default {
// set // set
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier); this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject); 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.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
this.imageFormatter = this.openmct.telemetry.getValueFormatter(this.imageHints); this.imageFormatter = this.openmct.telemetry.getValueFormatter(this.metadata.valuesForHints(['image'])[0]);
// related telemetry keys
this.spacecraftPositionKeys = ['positionX', 'positionY', 'positionZ'];
this.spacecraftOrientationKeys = ['heading'];
this.cameraKeys = ['cameraPan', 'cameraTilt'];
this.sunKeys = ['sunOrientation'];
// initialize // initialize
this.timeKey = this.timeSystem.key; this.timeKey = this.timeSystem.key;
@ -359,18 +222,6 @@ export default {
// kickoff // kickoff
this.subscribe(); this.subscribe();
this.requestHistory(); this.requestHistory();
// related telemetry
await this.initializeRelatedTelemetry();
this.updateRelatedTelemetryForFocusedImage();
this.trackLatestRelatedTelemetry();
// for scrolling through images quickly and resizing the object view
_.debounce(this.updateRelatedTelemetryForFocusedImage, 400);
_.debounce(this.resizeImageContainer, 400);
this.imageContainerResizeObserver = new ResizeObserver(this.resizeImageContainer);
this.imageContainerResizeObserver.observe(this.$refs.focusedImage);
}, },
updated() { updated() {
this.scrollToRight(); this.scrollToRight();
@ -381,120 +232,12 @@ export default {
delete this.unsubscribe; delete this.unsubscribe;
} }
this.imageContainerResizeObserver.disconnect();
if (this.relatedTelemetry.hasRelatedTelemetry) {
this.relatedTelemetry.destroy();
}
this.stopDurationTracking(); this.stopDurationTracking();
this.openmct.time.off('bounds', this.boundsChange); this.openmct.time.off('bounds', this.boundsChange);
this.openmct.time.off('timeSystem', this.timeSystemChange); this.openmct.time.off('timeSystem', this.timeSystemChange);
this.openmct.time.off('clock', this.clockChange); this.openmct.time.off('clock', this.clockChange);
// unsubscribe from related telemetry
if (this.relatedTelemetry.hasRelatedTelemetry) {
for (let key of this.relatedTelemetry.keys) {
if (this.relatedTelemetry[key] && this.relatedTelemetry[key].unsubscribe) {
this.relatedTelemetry[key].unsubscribe();
}
}
}
}, },
methods: { methods: {
async initializeRelatedTelemetry() {
this.relatedTelemetry = new RelatedTelemetry(
this.openmct,
this.domainObject,
[...this.spacecraftPositionKeys, ...this.spacecraftOrientationKeys, ...this.cameraKeys, ...this.sunKeys]
);
if (this.relatedTelemetry.hasRelatedTelemetry) {
await this.relatedTelemetry.load();
}
},
async getMostRecentRelatedTelemetry(key, targetDatum) {
if (!this.relatedTelemetry.hasRelatedTelemetry) {
throw new Error(`${this.domainObject.name} does not have any related telemetry`);
}
if (!this.relatedTelemetry[key]) {
throw new Error(`${key} does not exist on related telemetry`);
}
let mostRecent;
let valueKey = this.relatedTelemetry[key].historical.valueKey;
let valuesOnTelemetry = this.relatedTelemetry[key].hasTelemetryOnDatum;
if (valuesOnTelemetry) {
mostRecent = targetDatum[valueKey];
if (mostRecent) {
return mostRecent;
} else {
console.warn(`Related Telemetry for ${key} does NOT exist on this telemetry datum as configuration implied.`);
return;
}
}
mostRecent = await this.relatedTelemetry[key].requestLatestFor(targetDatum);
return mostRecent[valueKey];
},
// will subscribe to data for this key if not already done
subscribeToDataForKey(key) {
if (this.relatedTelemetry[key].isSubscribed) {
return;
}
if (this.relatedTelemetry[key].realtimeDomainObject) {
this.relatedTelemetry[key].unsubscribe = this.openmct.telemetry.subscribe(
this.relatedTelemetry[key].realtimeDomainObject, datum => {
this.relatedTelemetry[key].listeners.forEach(callback => {
callback(datum);
});
}
);
this.relatedTelemetry[key].isSubscribed = true;
}
},
async updateRelatedTelemetryForFocusedImage() {
if (!this.relatedTelemetry.hasRelatedTelemetry || !this.focusedImage) {
return;
}
// set data ON image telemetry as well as in focusedImageRelatedTelemetry
for (let key of this.relatedTelemetry.keys) {
if (
this.relatedTelemetry[key]
&& this.relatedTelemetry[key].historical
&& this.relatedTelemetry[key].requestLatestFor
) {
let valuesOnTelemetry = this.relatedTelemetry[key].hasTelemetryOnDatum;
let value = await this.getMostRecentRelatedTelemetry(key, this.focusedImage);
if (!valuesOnTelemetry) {
this.$set(this.imageHistory[this.focusedImageIndex], key, value); // manually add to telemetry
}
this.$set(this.focusedImageRelatedTelemetry, key, value);
}
}
},
trackLatestRelatedTelemetry() {
[...this.spacecraftPositionKeys, ...this.spacecraftOrientationKeys, ...this.cameraKeys, ...this.sunKeys].forEach(key => {
if (this.relatedTelemetry[key] && this.relatedTelemetry[key].subscribe) {
this.relatedTelemetry[key].subscribe((datum) => {
let valueKey = this.relatedTelemetry[key].realtime.valueKey;
this.$set(this.latestRelatedTelemetry, key, datum[valueKey]);
});
}
});
},
focusElement() { focusElement() {
this.$el.focus(); this.$el.focus();
}, },
@ -615,7 +358,6 @@ export default {
this.requestCount++; this.requestCount++;
const requestId = this.requestCount; const requestId = this.requestCount;
this.imageHistory = []; this.imageHistory = [];
let data = await this.openmct.telemetry let data = await this.openmct.telemetry
.request(this.domainObject, bounds) || []; .request(this.domainObject, bounds) || [];
@ -651,12 +393,7 @@ export default {
return; return;
} }
let image = { ...datum }; this.imageHistory.push(datum);
image.formattedTime = this.formatTime(datum);
image.url = this.formatImageUrl(datum);
image.time = datum[this.timeKey];
this.imageHistory.push(image);
if (setFocused) { if (setFocused) {
this.setFocusedImage(this.imageHistory.length - 1); this.setFocusedImage(this.imageHistory.length - 1);
@ -772,28 +509,6 @@ export default {
}, },
isLeftOrRightArrowKey(keyCode) { isLeftOrRightArrowKey(keyCode) {
return [ARROW_RIGHT, ARROW_LEFT].includes(keyCode); return [ARROW_RIGHT, ARROW_LEFT].includes(keyCode);
},
getImageNaturalDimensions() {
this.focusedImageNaturalAspectRatio = undefined;
const img = this.$refs.focusedImage;
// TODO - should probably cache this
img.addEventListener('load', () => {
this.focusedImageNaturalAspectRatio = img.naturalWidth / img.naturalHeight;
}, { once: true });
},
resizeImageContainer() {
if (this.$refs.focusedImage.clientWidth !== this.imageContainerWidth) {
this.imageContainerWidth = this.$refs.focusedImage.clientWidth;
}
if (this.$refs.focusedImage.clientHeight !== this.imageContainerHeight) {
this.imageContainerHeight = this.$refs.focusedImage.clientHeight;
}
},
toggleLockCompass() {
this.lockCompass = !this.lockCompass;
} }
} }
}; };

View File

@ -1,164 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
function copyRelatedMetadata(metadata) {
let compare = metadata.comparisonFunction;
let copiedMetadata = JSON.parse(JSON.stringify(metadata));
copiedMetadata.comparisonFunction = compare;
return copiedMetadata;
}
export default class RelatedTelemetry {
constructor(openmct, domainObject, telemetryKeys) {
this._openmct = openmct;
this._domainObject = domainObject;
let metadata = this._openmct.telemetry.getMetadata(this._domainObject);
let imageHints = metadata.valuesForHints(['image'])[0];
this.hasRelatedTelemetry = imageHints.relatedTelemetry !== undefined;
if (this.hasRelatedTelemetry) {
this.keys = telemetryKeys;
this._timeFormatter = undefined;
this._timeSystemChange(this._openmct.time.timeSystem());
// grab related telemetry metadata
for (let key of this.keys) {
if (imageHints.relatedTelemetry[key]) {
this[key] = copyRelatedMetadata(imageHints.relatedTelemetry[key]);
}
}
this.load = this.load.bind(this);
this._parseTime = this._parseTime.bind(this);
this._timeSystemChange = this._timeSystemChange.bind(this);
this.destroy = this.destroy.bind(this);
this._openmct.time.on('timeSystem', this._timeSystemChange);
}
}
async load() {
if (!this.hasRelatedTelemetry) {
throw new Error('This domain object does not have related telemetry, use "hasRelatedTelemetry" to check before loading.');
}
await Promise.all(
this.keys.map(async (key) => {
if (this[key]) {
if (this[key].historical) {
await this._initializeHistorical(key);
}
if (this[key].realtime && this[key].realtime.telemetryObjectId && this[key].realtime.telemetryObjectId !== '') {
await this._intializeRealtime(key);
}
}
})
);
}
async _initializeHistorical(key) {
if (!this[key].historical.telemetryObjectId) {
this[key].historical.hasTelemetryOnDatum = true;
} else if (this[key].historical.telemetryObjectId !== '') {
this[key].historicalDomainObject = await this._openmct.objects.get(this[key].historical.telemetryObjectId);
this[key].requestLatestFor = async (datum) => {
const options = {
start: this._openmct.time.bounds().start,
end: this._parseTime(datum),
strategy: 'latest'
};
let results = await this._openmct.telemetry
.request(this[key].historicalDomainObject, options);
return results[results.length - 1];
};
}
}
async _intializeRealtime(key) {
this[key].realtimeDomainObject = await this._openmct.objects.get(this[key].realtime.telemetryObjectId);
this[key].listeners = [];
this[key].subscribe = (callback) => {
if (!this[key].isSubscribed) {
this._subscribeToDataForKey(key);
}
if (!this[key].listeners.includes(callback)) {
this[key].listeners.push(callback);
return () => {
this[key].listeners.remove(callback);
};
} else {
return () => {};
}
};
}
_subscribeToDataForKey(key) {
if (this[key].isSubscribed) {
return;
}
if (this[key].realtimeDomainObject) {
this[key].unsubscribe = this._openmct.telemetry.subscribe(
this[key].realtimeDomainObject, datum => {
this[key].listeners.forEach(callback => {
callback(datum);
});
}
);
this[key].isSubscribed = true;
}
}
_parseTime(datum) {
return this._timeFormatter.parse(datum);
}
_timeSystemChange(system) {
let key = system.key;
let metadata = this._openmct.telemetry.getMetadata(this._domainObject);
let metadataValue = metadata.value(key) || { format: key };
this._timeFormatter = this._openmct.telemetry.getValueFormatter(metadataValue);
}
destroy() {
this._openmct.time.off('timeSystem', this._timeSystemChange);
for (let key of this.keys) {
if (this[key] && this[key].unsubscribe) {
this[key].unsubscribe();
}
}
}
}

View File

@ -23,7 +23,6 @@
background-color: $colorPlotBg; background-color: $colorPlotBg;
border: 1px solid transparent; border: 1px solid transparent;
flex: 1 1 auto; flex: 1 1 auto;
height: 0;
&.unnsynced{ &.unnsynced{
@include sUnsynced(); @include sUnsynced();
@ -31,9 +30,10 @@
} }
&__image { &__image {
height: 100%; @include abs(); // Safari fix
width: 100%; background-position: center;
object-fit: contain; background-repeat: no-repeat;
background-size: contain;
} }
} }
@ -71,14 +71,13 @@
} }
&__age { &__age {
border-radius: $smallCr; border-radius: $controlCr;
display: flex; display: flex;
flex-shrink: 0; flex-shrink: 0;
align-items: center; align-items: baseline;
padding: 2px $interiorMarginSm; padding: 1px $interiorMarginSm;
&:before { &:before {
font-size: 0.9em;
opacity: 0.5; opacity: 0.5;
margin-right: $interiorMarginSm; margin-right: $interiorMarginSm;
} }
@ -87,9 +86,8 @@
&--new { &--new {
// New imagery // New imagery
$bgColor: $colorOk; $bgColor: $colorOk;
color: $colorOkFg;
background: rgba($bgColor, 0.5); background: rgba($bgColor, 0.5);
@include flash($animName: flashImageAge, $iter: 2, $dur: 250ms, $valStart: rgba($colorOk, 0.7), $valEnd: rgba($colorOk, 0)); @include flash($animName: flashImageAge, $dur: 250ms, $valStart: rgba($colorOk, 0.7), $valEnd: rgba($colorOk, 0));
} }
&__thumbs-wrapper { &__thumbs-wrapper {

View File

@ -1,25 +1,3 @@
/*****************************************************************************
* 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'; import ImageryViewProvider from './ImageryViewProvider';
export default function () { export default function () {

View File

@ -32,25 +32,12 @@ const TEN_MINUTES = ONE_MINUTE * 10;
const MAIN_IMAGE_CLASS = '.js-imageryView-image'; const MAIN_IMAGE_CLASS = '.js-imageryView-image';
const NEW_IMAGE_CLASS = '.c-imagery__age.c-imagery--new'; const NEW_IMAGE_CLASS = '.c-imagery__age.c-imagery--new';
const REFRESH_CSS_MS = 500; const REFRESH_CSS_MS = 500;
const TOLERANCE = 0.50;
function comparisonFunction(valueOne, valueTwo) {
let larger = valueOne;
let smaller = valueTwo;
if (larger < smaller) {
larger = valueTwo;
smaller = valueOne;
}
return (larger - smaller) < TOLERANCE;
}
function getImageInfo(doc) { function getImageInfo(doc) {
let imageElement = doc.querySelectorAll(MAIN_IMAGE_CLASS)[0]; let imageElement = doc.querySelectorAll(MAIN_IMAGE_CLASS)[0];
let timestamp = imageElement.dataset.openmctImageTimestamp; let timestamp = imageElement.dataset.openmctImageTimestamp;
let identifier = imageElement.dataset.openmctObjectKeystring; let identifier = imageElement.dataset.openmctObjectKeystring;
let url = imageElement.src; let url = imageElement.style.backgroundImage;
return { return {
timestamp, timestamp,
@ -76,8 +63,7 @@ function generateTelemetry(start, count) {
"name": stringRep + " Imagery", "name": stringRep + " Imagery",
"utc": start + (i * ONE_MINUTE), "utc": start + (i * ONE_MINUTE),
"url": location.host + '/' + logo + '?time=' + stringRep, "url": location.host + '/' + logo + '?time=' + stringRep,
"timeId": stringRep, "timeId": stringRep
"value": 100
}); });
} }
@ -119,51 +105,7 @@ describe("The Imagery View Layout", () => {
"image": 1, "image": 1,
"priority": 3 "priority": 3
}, },
"source": "url", "source": "url"
"relatedTelemetry": {
"heading": {
"comparisonFunction": comparisonFunction,
"historical": {
"telemetryObjectId": "heading",
"valueKey": "value"
}
},
"roll": {
"comparisonFunction": comparisonFunction,
"historical": {
"telemetryObjectId": "roll",
"valueKey": "value"
}
},
"pitch": {
"comparisonFunction": comparisonFunction,
"historical": {
"telemetryObjectId": "pitch",
"valueKey": "value"
}
},
"cameraPan": {
"comparisonFunction": comparisonFunction,
"historical": {
"telemetryObjectId": "cameraPan",
"valueKey": "value"
}
},
"cameraTilt": {
"comparisonFunction": comparisonFunction,
"historical": {
"telemetryObjectId": "cameraTilt",
"valueKey": "value"
}
},
"sunOrientation": {
"comparisonFunction": comparisonFunction,
"historical": {
"telemetryObjectId": "sunOrientation",
"valueKey": "value"
}
}
}
}, },
{ {
"name": "Name", "name": "Name",
@ -209,11 +151,6 @@ describe("The Imagery View Layout", () => {
child = document.createElement('div'); child = document.createElement('div');
parent.appendChild(child); parent.appendChild(child);
spyOn(window, 'ResizeObserver').and.returnValue({
observe() {},
disconnect() {}
});
spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([])); spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([]));
imageryPlugin = new ImageryPlugin(); imageryPlugin = new ImageryPlugin();
@ -235,7 +172,7 @@ describe("The Imagery View Layout", () => {
}); });
it("should provide an imagery view only for imagery producing objects", () => { it("should provide an imagery view only for imagery producing objects", () => {
let applicableViews = openmct.objectViews.get(imageryObject, []); let applicableViews = openmct.objectViews.get(imageryObject);
let imageryView = applicableViews.find( let imageryView = applicableViews.find(
viewProvider => viewProvider.key === imageryKey viewProvider => viewProvider.key === imageryKey
); );
@ -265,7 +202,7 @@ describe("The Imagery View Layout", () => {
end: bounds.end + 100 end: bounds.end + 100
}); });
applicableViews = openmct.objectViews.get(imageryObject, []); applicableViews = openmct.objectViews.get(imageryObject);
imageryViewProvider = applicableViews.find(viewProvider => viewProvider.key === imageryKey); imageryViewProvider = applicableViews.find(viewProvider => viewProvider.key === imageryKey);
imageryView = imageryViewProvider.view(imageryObject); imageryView = imageryViewProvider.view(imageryObject);
imageryView.show(child); imageryView.show(child);
@ -276,10 +213,6 @@ describe("The Imagery View Layout", () => {
return done(); return done();
}); });
afterEach(() => {
imageryView.destroy();
});
it("on mount should show the the most recent image", () => { it("on mount should show the the most recent image", () => {
const imageInfo = getImageInfo(parent); const imageInfo = getImageInfo(parent);

View File

@ -69,7 +69,7 @@ export default {
methods: { methods: {
deletePage(id) { deletePage(id) {
const selectedSection = this.sections.find(s => s.isSelected); const selectedSection = this.sections.find(s => s.isSelected);
const page = this.pages.find(p => p.id === id); const page = this.pages.find(p => p.id !== id);
deleteNotebookEntries(this.openmct, this.domainObject, selectedSection, page); deleteNotebookEntries(this.openmct, this.domainObject, selectedSection, page);
const selectedPage = this.pages.find(p => p.isSelected); const selectedPage = this.pages.find(p => p.isSelected);

View File

@ -101,7 +101,7 @@ describe("Notebook plugin:", () => {
creatable: true creatable: true
}; };
const applicableViews = openmct.objectViews.get(notebookViewObject, []); const applicableViews = openmct.objectViews.get(notebookViewObject);
notebookViewProvider = applicableViews.find(viewProvider => viewProvider.key === notebookObject.key); notebookViewProvider = applicableViews.find(viewProvider => viewProvider.key === notebookObject.key);
notebookView = notebookViewProvider.view(notebookViewObject); notebookView = notebookViewProvider.view(notebookViewObject);

View File

@ -56,24 +56,11 @@ const notebookStorage = {
} }
}; };
let openmct; let openmct = createOpenMct();
let mockIdentifierService;
describe('Notebook Storage:', () => { describe('Notebook Storage:', () => {
beforeEach((done) => { beforeEach((done) => {
openmct = createOpenMct(); openmct = createOpenMct();
openmct.$injector = jasmine.createSpyObj('$injector', ['get']);
mockIdentifierService = jasmine.createSpyObj(
'identifierService',
['parse']
);
mockIdentifierService.parse.and.returnValue({
getSpace: () => {
return '';
}
});
openmct.$injector.get.and.returnValue(mockIdentifierService);
window.localStorage.setItem('notebook-storage', null); window.localStorage.setItem('notebook-storage', null);
openmct.objects.addProvider('', jasmine.createSpyObj('mockNotebookProvider', [ openmct.objects.addProvider('', jasmine.createSpyObj('mockNotebookProvider', [
'create', 'create',

View File

@ -57,20 +57,11 @@ export default class CouchObjectProvider {
return options; return options;
} }
request(subPath, method, body, signal) { request(subPath, method, value) {
let fetchOptions = { return fetch(this.url + '/' + subPath, {
method, method: method,
body, body: JSON.stringify(value)
signal }).then(response => response.json())
};
// stringify body if needed
if (fetchOptions.body) {
fetchOptions.body = JSON.stringify(fetchOptions.body);
}
return fetch(this.url + '/' + subPath, fetchOptions)
.then(response => response.json())
.then(function (response) { .then(function (response) {
return response; return response;
}, function () { }, function () {
@ -130,8 +121,8 @@ export default class CouchObjectProvider {
} }
} }
get(identifier, abortSignal) { get(identifier) {
return this.request(identifier.key, "GET", undefined, abortSignal).then(this.getModel.bind(this)); return this.request(identifier.key, "GET").then(this.getModel.bind(this));
} }
async getObjectsByFilter(filter) { async getObjectsByFilter(filter) {
@ -154,8 +145,7 @@ export default class CouchObjectProvider {
const reader = response.body.getReader(); const reader = response.body.getReader();
let completed = false; let completed = false;
let decoder = new TextDecoder("utf-8");
let decodedChunk = '';
while (!completed) { while (!completed) {
const {done, value} = await reader.read(); const {done, value} = await reader.read();
//done is true when we lose connection with the provider //done is true when we lose connection with the provider
@ -166,24 +156,23 @@ export default class CouchObjectProvider {
if (value) { if (value) {
let chunk = new Uint8Array(value.length); let chunk = new Uint8Array(value.length);
chunk.set(value, 0); chunk.set(value, 0);
const partial = decoder.decode(chunk, {stream: !completed}); const decodedChunk = new TextDecoder("utf-8").decode(chunk);
decodedChunk = decodedChunk + partial; try {
} const json = JSON.parse(decodedChunk);
} if (json) {
let docs = json.docs;
try { docs.forEach(doc => {
const json = JSON.parse(decodedChunk); let object = this.getModel(doc);
if (json) { if (object) {
let docs = json.docs; objects.push(object);
docs.forEach(doc => { }
let object = this.getModel(doc); });
if (object) {
objects.push(object);
} }
}); } catch (e) {
//do nothing
}
} }
} catch (e) {
//do nothing
} }
return objects; return objects;
@ -322,8 +311,7 @@ export default class CouchObjectProvider {
this.enqueueObject(key, model, intermediateResponse); this.enqueueObject(key, model, intermediateResponse);
this.objectQueue[key].pending = true; this.objectQueue[key].pending = true;
const queued = this.objectQueue[key].dequeue(); const queued = this.objectQueue[key].dequeue();
let document = new CouchDocument(key, queued.model); this.request(key, "PUT", new CouchDocument(key, queued.model)).then((response) => {
this.request(key, "PUT", document).then((response) => {
this.checkResponse(response, queued.intermediateResponse); this.checkResponse(response, queued.intermediateResponse);
}); });
@ -334,8 +322,7 @@ export default class CouchObjectProvider {
if (!this.objectQueue[key].pending) { if (!this.objectQueue[key].pending) {
this.objectQueue[key].pending = true; this.objectQueue[key].pending = true;
const queued = this.objectQueue[key].dequeue(); const queued = this.objectQueue[key].dequeue();
let document = new CouchDocument(key, queued.model, this.objectQueue[key].rev); this.request(key, "PUT", new CouchDocument(key, queued.model, this.objectQueue[key].rev)).then((response) => {
this.request(key, "PUT", document).then((response) => {
this.checkResponse(response, queued.intermediateResponse); this.checkResponse(response, queued.intermediateResponse);
}); });
} }

View File

@ -1,483 +0,0 @@
<template>
<div ref="plan"
class="c-plan c-timeline-holder"
>
<template v-if="viewBounds && !options.compact">
<swim-lane>
<template slot="label">{{ timeSystem.name }}</template>
<timeline-axis
slot="object"
:bounds="viewBounds"
:time-system="timeSystem"
:content-height="height"
:rendering-engine="renderingEngine"
/>
</swim-lane>
</template>
<div ref="planHolder"
class="c-plan__contents u-contents"
>
</div>
</div>
</template>
<script>
import * as d3Scale from 'd3-scale';
import TimelineAxis from "../../ui/components/TimeSystemAxis.vue";
import SwimLane from "@/ui/components/swim-lane/SwimLane.vue";
import { getValidatedPlan } from "./util";
import Vue from "vue";
//TODO: UI direction needed for the following property values
const PADDING = 1;
const OUTER_TEXT_PADDING = 12;
const INNER_TEXT_PADDING = 17;
const TEXT_LEFT_PADDING = 5;
const ROW_PADDING = 12;
const RESIZE_POLL_INTERVAL = 200;
const ROW_HEIGHT = 25;
const LINE_HEIGHT = 12;
const MAX_TEXT_WIDTH = 300;
const EDGE_ROUNDING = 5;
const DEFAULT_COLOR = '#cc9922';
export default {
components: {
TimelineAxis,
SwimLane
},
inject: ['openmct', 'domainObject'],
props: {
options: {
type: Object,
default() {
return {
compact: false
};
}
},
renderingEngine: {
type: String,
default() {
return 'svg';
}
}
},
data() {
return {
viewBounds: undefined,
timeSystem: undefined,
height: 0
};
},
mounted() {
this.getPlanData(this.domainObject);
this.canvas = this.$refs.plan.appendChild(document.createElement('canvas'));
this.canvas.height = 0;
this.canvasContext = this.canvas.getContext('2d');
this.setDimensions();
this.updateViewBounds();
this.openmct.time.on("timeSystem", this.setScaleAndPlotActivities);
this.openmct.time.on("bounds", this.updateViewBounds);
this.resizeTimer = setInterval(this.resize, RESIZE_POLL_INTERVAL);
this.unlisten = this.openmct.objects.observe(this.domainObject, '*', this.observeForChanges);
},
beforeDestroy() {
clearInterval(this.resizeTimer);
this.openmct.time.off("timeSystem", this.setScaleAndPlotActivities);
this.openmct.time.off("bounds", this.updateViewBounds);
if (this.unlisten) {
this.unlisten();
}
},
methods: {
observeForChanges(mutatedObject) {
this.getPlanData(mutatedObject);
this.setScaleAndPlotActivities();
},
resize() {
let clientWidth = this.getClientWidth();
if (clientWidth !== this.width) {
this.setDimensions();
this.updateViewBounds();
}
},
getClientWidth() {
let clientWidth = this.$refs.plan.clientWidth;
if (!clientWidth) {
//this is a hack - need a better way to find the parent of this component
let parent = this.openmct.layout.$refs.browseObject.$el;
if (parent) {
clientWidth = parent.getBoundingClientRect().width;
}
}
return clientWidth - 200;
},
getPlanData(domainObject) {
this.planData = getValidatedPlan(domainObject);
},
updateViewBounds() {
this.viewBounds = this.openmct.time.bounds();
if (this.timeSystem === undefined) {
this.timeSystem = this.openmct.time.timeSystem();
}
this.setScaleAndPlotActivities();
},
setScaleAndPlotActivities(timeSystem) {
if (timeSystem !== undefined) {
this.timeSystem = timeSystem;
}
this.setScale(this.timeSystem);
this.clearPreviousActivities();
if (this.xScale) {
this.calculatePlanLayout();
this.drawPlan();
}
},
clearPreviousActivities() {
let activities = this.$el.querySelectorAll(".c-plan__contents > div");
activities.forEach(activity => activity.remove());
},
setDimensions() {
const planHolder = this.$refs.plan;
this.width = this.getClientWidth();
this.height = Math.round(planHolder.getBoundingClientRect().height);
},
setScale(timeSystem) {
if (!this.width) {
return;
}
if (timeSystem === undefined) {
timeSystem = this.openmct.time.timeSystem();
}
if (timeSystem.isUTCBased) {
this.xScale = d3Scale.scaleUtc();
this.xScale.domain(
[new Date(this.viewBounds.start), new Date(this.viewBounds.end)]
);
} else {
this.xScale = d3Scale.scaleLinear();
this.xScale.domain(
[this.viewBounds.start, this.viewBounds.end]
);
}
this.xScale.range([PADDING, this.width - PADDING * 2]);
},
isActivityInBounds(activity) {
return (activity.start < this.viewBounds.end) && (activity.end > this.viewBounds.start);
},
getTextWidth(name) {
let metrics = this.canvasContext.measureText(name);
return parseInt(metrics.width, 10);
},
sortFn(a, b) {
const numA = parseInt(a, 10);
const numB = parseInt(b, 10);
if (numA > numB) {
return 1;
}
if (numA < numB) {
return -1;
}
return 0;
},
// Get the row where the next activity will land.
getRowForActivity(rectX, width, activitiesByRow) {
let currentRow;
let sortedActivityRows = Object.keys(activitiesByRow).sort(this.sortFn);
function getOverlap(rects) {
return rects.every(rect => {
const { start, end } = rect;
const calculatedEnd = rectX + width;
const hasOverlap = (rectX >= start && rectX <= end) || (calculatedEnd >= start && calculatedEnd <= end) || (rectX <= start && calculatedEnd >= end);
return !hasOverlap;
});
}
for (let i = 0; i < sortedActivityRows.length; i++) {
let row = sortedActivityRows[i];
if (getOverlap(activitiesByRow[row])) {
currentRow = row;
break;
}
}
if (currentRow === undefined && sortedActivityRows.length) {
let row = parseInt(sortedActivityRows[sortedActivityRows.length - 1], 10);
currentRow = row + ROW_HEIGHT + ROW_PADDING;
}
return (currentRow || 0);
},
calculatePlanLayout() {
let groups = Object.keys(this.planData);
this.groupActivities = {};
groups.forEach((key, index) => {
let activitiesByRow = {};
let currentRow = 0;
let activities = this.planData[key];
activities.forEach((activity) => {
if (this.isActivityInBounds(activity)) {
const currentStart = Math.max(this.viewBounds.start, activity.start);
const currentEnd = Math.min(this.viewBounds.end, activity.end);
const rectX = this.xScale(currentStart);
const rectY = this.xScale(currentEnd);
const rectWidth = rectY - rectX;
const activityNameWidth = this.getTextWidth(activity.name) + TEXT_LEFT_PADDING;
//TODO: Fix bug for SVG where the rectWidth is not proportional to the canvas measuredWidth of the text
const activityNameFitsRect = (rectWidth >= activityNameWidth);
const textStart = (activityNameFitsRect ? rectX : rectY) + TEXT_LEFT_PADDING;
const color = activity.color || DEFAULT_COLOR;
let textColor = '';
if (activity.textColor) {
textColor = activity.textColor;
} else if (activityNameFitsRect) {
textColor = this.getContrastingColor(color);
}
let textLines = this.getActivityDisplayText(this.canvasContext, activity.name, activityNameFitsRect);
const textWidth = textStart + this.getTextWidth(textLines[0]) + TEXT_LEFT_PADDING;
if (activityNameFitsRect) {
currentRow = this.getRowForActivity(rectX, rectWidth, activitiesByRow);
} else {
currentRow = this.getRowForActivity(rectX, textWidth, activitiesByRow);
}
let textY = parseInt(currentRow, 10) + (activityNameFitsRect ? INNER_TEXT_PADDING : OUTER_TEXT_PADDING);
if (!activitiesByRow[currentRow]) {
activitiesByRow[currentRow] = [];
}
activitiesByRow[currentRow].push({
activity: {
color: color,
textColor: textColor,
name: activity.name,
exceeds: {
start: this.xScale(this.viewBounds.start) > this.xScale(activity.start),
end: this.xScale(this.viewBounds.end) < this.xScale(activity.end)
}
},
textLines: textLines,
textStart: textStart,
textClass: activityNameFitsRect ? "" : "activity-label--outside-rect",
textY: textY,
start: rectX,
end: activityNameFitsRect ? rectY : textStart + textWidth,
rectWidth: rectWidth
});
}
});
this.groupActivities[key] = {
heading: key,
activitiesByRow
};
});
},
getActivityDisplayText(context, text, activityNameFitsRect) {
//TODO: If the activity start is less than viewBounds.start then the text should be cropped on the left/should be off-screen)
let words = text.split(' ');
let line = '';
let activityText = [];
let rows = 1;
for (let n = 0; (n < words.length) && (rows <= 2); n++) {
let testLine = line + words[n] + ' ';
let metrics = context.measureText(testLine);
let testWidth = metrics.width;
if (!activityNameFitsRect && (testWidth > MAX_TEXT_WIDTH && n > 0)) {
activityText.push(line);
line = words[n] + ' ';
testLine = line + words[n] + ' ';
rows = rows + 1;
}
line = testLine;
}
return activityText.length ? activityText : [line];
},
getGroupContainer(activityRows, heading) {
let svgHeight = 30;
let svgWidth = 200;
const rows = Object.keys(activityRows);
const isNested = this.options.isChildObject;
if (rows.length) {
const lastActivityRow = rows[rows.length - 1];
svgHeight = parseInt(lastActivityRow, 10) + ROW_HEIGHT;
svgWidth = this.width;
}
let component = new Vue({
components: {
SwimLane
},
data() {
return {
heading,
isNested,
height: svgHeight,
width: svgWidth
};
},
template: `<swim-lane :is-nested="isNested"><template slot="label">{{heading}}</template><template slot="object"><svg :height="height" :width="width"></svg></template></swim-lane>`
});
this.$refs.planHolder.appendChild(component.$mount().$el);
let groupLabel = component.$el.querySelector('div:nth-child(1)');
let groupSVG = component.$el.querySelector('svg');
return {
groupLabel,
groupSVG
};
},
drawPlan() {
Object.keys(this.groupActivities).forEach((group, index) => {
const activitiesByRow = this.groupActivities[group].activitiesByRow;
const heading = this.groupActivities[group].heading;
const groupElements = this.getGroupContainer(activitiesByRow, heading);
let groupSVG = groupElements.groupSVG;
let activityRows = Object.keys(activitiesByRow);
if (activityRows.length <= 0) {
this.plotNoItems(groupSVG);
}
activityRows.forEach((row) => {
const items = activitiesByRow[row];
items.forEach(item => {
//TODO: Don't draw the left-border of the rectangle if the activity started before viewBounds.start
this.plotActivity(item, parseInt(row, 10), groupSVG);
});
});
});
},
plotNoItems(svgElement) {
let textElement = document.createElementNS('http://www.w3.org/2000/svg', 'text');
this.setNSAttributesForElement(textElement, {
x: "10",
y: "20",
class: "activity-label--outside-rect"
});
textElement.innerHTML = 'No activities within timeframe';
svgElement.appendChild(textElement);
},
setNSAttributesForElement(element, attributes) {
Object.keys(attributes).forEach((key) => {
element.setAttributeNS(null, key, attributes[key]);
});
},
// Experimental for now - unused
addForeignElement(svgElement, label, x, y) {
let foreign = document.createElementNS('http://www.w3.org/2000/svg', "foreignObject");
this.setNSAttributesForElement(foreign, {
width: String(MAX_TEXT_WIDTH),
height: String(LINE_HEIGHT * 2),
x: x,
y: y
});
let textEl = document.createElement('div');
let textNode = document.createTextNode(label);
textEl.appendChild(textNode);
foreign.appendChild(textEl);
svgElement.appendChild(foreign);
},
plotActivity(item, row, svgElement) {
const activity = item.activity;
let width = item.rectWidth;
let rectElement = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
if (item.activity.exceeds.start) {
width = width + EDGE_ROUNDING;
}
if (item.activity.exceeds.end) {
width = width + EDGE_ROUNDING;
}
width = Math.max(width, 1); // Set width to a minimum of 1
// rx: don't round corners if the width of the rect is smaller than the rounding radius
this.setNSAttributesForElement(rectElement, {
class: 'activity-bounds',
x: item.activity.exceeds.start ? item.start - EDGE_ROUNDING : item.start,
y: row,
rx: (width < EDGE_ROUNDING * 2) ? 0 : EDGE_ROUNDING,
width: width,
height: String(ROW_HEIGHT),
fill: activity.color
});
svgElement.appendChild(rectElement);
item.textLines.forEach((line, index) => {
let textElement = document.createElementNS('http://www.w3.org/2000/svg', 'text');
this.setNSAttributesForElement(textElement, {
class: `activity-label ${item.textClass}`,
x: item.textStart,
y: item.textY + (index * LINE_HEIGHT),
fill: activity.textColor
});
const textNode = document.createTextNode(line);
textElement.appendChild(textNode);
svgElement.appendChild(textElement);
});
// this.addForeignElement(svgElement, activity.name, item.textStart, item.textY - LINE_HEIGHT);
},
cutHex(h, start, end) {
const hStr = (h.charAt(0) === '#') ? h.substring(1, 7) : h;
return parseInt(hStr.substring(start, end), 16);
},
getContrastingColor(hexColor) {
// https://codepen.io/davidhalford/pen/ywEva/
// TODO: move this into a general utility function?
const cThreshold = 130;
if (hexColor.indexOf('#') === -1) {
// We weren't given a hex color
return "#ff0000";
}
const hR = this.cutHex(hexColor, 0, 2);
const hG = this.cutHex(hexColor, 2, 4);
const hB = this.cutHex(hexColor, 4, 6);
const cBrightness = ((hR * 299) + (hG * 587) + (hB * 114)) / 1000;
return cBrightness > cThreshold ? "#000000" : "#ffffff";
}
}
};
</script>

View File

@ -1,77 +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.
*****************************************************************************/
import Plan from './Plan.vue';
import Vue from 'vue';
export default function PlanViewProvider(openmct) {
function isCompactView(objectPath) {
return objectPath.find(object => object.type === 'time-strip') !== undefined;
}
return {
key: 'plan.view',
name: 'Plan',
cssClass: 'icon-calendar',
canView(domainObject) {
return domainObject.type === 'plan';
},
canEdit(domainObject) {
return domainObject.type === 'plan';
},
view: function (domainObject, objectPath) {
let component;
return {
show: function (element) {
let isCompact = isCompactView(objectPath);
component = new Vue({
el: element,
components: {
Plan
},
provide: {
openmct,
domainObject
},
data() {
return {
options: {
compact: isCompact,
isChildObject: isCompact
}
};
},
template: '<plan :options="options"></plan>'
});
},
destroy: function () {
component.$destroy();
component = undefined;
}
};
}
};
}

View File

@ -1,19 +0,0 @@
.c-plan {
svg {
text-rendering: geometricPrecision;
text {
stroke: none;
}
.activity-label {
&--outside-rect {
fill: $colorBodyFg !important;
}
}
}
canvas {
display: none;
}
}

View File

@ -1,49 +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.
*****************************************************************************/
import PlanViewProvider from './PlanViewProvider';
export default function () {
return function install(openmct) {
openmct.types.addType('plan', {
name: 'Plan',
key: 'plan',
description: 'A plan',
creatable: true,
cssClass: 'icon-calendar',
form: [
{
name: 'Upload Plan (JSON File)',
key: 'selectFile',
control: 'file-input',
required: true,
text: 'Select File',
type: 'application/json'
}
],
initialize: function (domainObject) {
}
});
openmct.objectViews.addProvider(new PlanViewProvider(openmct));
};
}

View File

@ -1,166 +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.
*****************************************************************************/
import {createOpenMct, resetApplicationState} from "utils/testing";
import PlanPlugin from "../plan/plugin";
import Vue from 'vue';
describe('the plugin', function () {
let planDefinition;
let element;
let child;
let openmct;
beforeEach((done) => {
const appHolder = document.createElement('div');
appHolder.style.width = '640px';
appHolder.style.height = '480px';
openmct = createOpenMct();
openmct.install(new PlanPlugin());
planDefinition = openmct.types.get('plan').definition;
element = document.createElement('div');
element.style.width = '640px';
element.style.height = '480px';
child = document.createElement('div');
child.style.width = '640px';
child.style.height = '480px';
element.appendChild(child);
openmct.time.timeSystem('utc', {
start: 1597160002854,
end: 1597181232854
});
openmct.on('start', done);
openmct.start(appHolder);
});
afterEach(() => {
return resetApplicationState(openmct);
});
let mockPlanObject = {
name: 'Plan',
key: 'plan',
creatable: true
};
it('defines a plan object type with the correct key', () => {
expect(planDefinition.key).toEqual(mockPlanObject.key);
});
it('is creatable', () => {
expect(planDefinition.creatable).toEqual(mockPlanObject.creatable);
});
describe('the plan view', () => {
it('provides a plan view', () => {
const testViewObject = {
id: "test-object",
type: "plan"
};
const applicableViews = openmct.objectViews.get(testViewObject, []);
let planView = applicableViews.find((viewProvider) => viewProvider.key === 'plan.view');
expect(planView).toBeDefined();
});
});
describe('the plan view displays activities', () => {
let planDomainObject;
let mockObjectPath = [
{
identifier: {
key: 'test',
namespace: ''
},
type: 'time-strip',
name: 'Test Parent Object'
}
];
let planView;
beforeEach((done) => {
planDomainObject = {
identifier: {
key: 'test-object',
namespace: ''
},
type: 'plan',
id: "test-object",
selectFile: {
body: JSON.stringify({
"TEST-GROUP": [
{
"name": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua",
"start": 1597170002854,
"end": 1597171032854,
"type": "TEST-GROUP",
"color": "fuchsia",
"textColor": "black"
},
{
"name": "Sed ut perspiciatis",
"start": 1597171132854,
"end": 1597171232854,
"type": "TEST-GROUP",
"color": "fuchsia",
"textColor": "black"
}
]
})
}
};
const applicableViews = openmct.objectViews.get(planDomainObject, []);
planView = applicableViews.find((viewProvider) => viewProvider.key === 'plan.view');
let view = planView.view(planDomainObject, mockObjectPath);
view.show(child, true);
return Vue.nextTick().then(() => {
done();
});
});
it('loads activities into the view', () => {
const svgEls = element.querySelectorAll('.c-plan__contents svg');
expect(svgEls.length).toEqual(1);
});
it('displays the group label', () => {
const labelEl = element.querySelector('.c-plan__contents .c-object-label .c-object-label__name');
expect(labelEl.innerHTML).toEqual('TEST-GROUP');
});
it('displays the activities and their labels', () => {
const rectEls = element.querySelectorAll('.c-plan__contents rect');
expect(rectEls.length).toEqual(2);
const textEls = element.querySelectorAll('.c-plan__contents text');
expect(textEls.length).toEqual(3);
});
});
});

View File

@ -1,15 +0,0 @@
export function getValidatedPlan(domainObject) {
let body = domainObject.selectFile.body;
let json = {};
if (typeof body === 'string') {
try {
json = JSON.parse(body);
} catch (e) {
return json;
}
} else {
json = body;
}
return json;
}

View File

@ -413,21 +413,6 @@ define([
return; return;
} }
const isPinchToZoom = event.ctrlKey === true;
let isZoomIn = event.wheelDelta < 0;
let isZoomOut = event.wheelDelta >= 0;
//Flip the zoom direction if this is pinch to zoom
if (isPinchToZoom) {
if (isZoomIn === true) {
isZoomOut = true;
isZoomIn = false;
} else if (isZoomOut === true) {
isZoomIn = true;
isZoomOut = false;
}
}
let xDisplayRange = this.$scope.xAxis.get('displayRange'); let xDisplayRange = this.$scope.xAxis.get('displayRange');
let yDisplayRange = this.$scope.yAxis.get('displayRange'); let yDisplayRange = this.$scope.yAxis.get('displayRange');
@ -460,7 +445,7 @@ define([
}; };
} }
if (isZoomIn) { if (event.wheelDelta < 0) {
this.$scope.xAxis.set('displayRange', { this.$scope.xAxis.set('displayRange', {
min: xDisplayRange.min + ((xAxisDist * ZOOM_AMT) * xAxisMinDist), min: xDisplayRange.min + ((xAxisDist * ZOOM_AMT) * xAxisMinDist),
@ -471,7 +456,7 @@ define([
min: yDisplayRange.min + ((yAxisDist * ZOOM_AMT) * yAxisMinDist), min: yDisplayRange.min + ((yAxisDist * ZOOM_AMT) * yAxisMinDist),
max: yDisplayRange.max - ((yAxisDist * ZOOM_AMT) * yAxisMaxDist) max: yDisplayRange.max - ((yAxisDist * ZOOM_AMT) * yAxisMaxDist)
}); });
} else if (isZoomOut) { } else if (event.wheelDelta >= 0) {
this.$scope.xAxis.set('displayRange', { this.$scope.xAxis.set('displayRange', {
min: xDisplayRange.min - ((xAxisDist * ZOOM_AMT) * xAxisMinDist), min: xDisplayRange.min - ((xAxisDist * ZOOM_AMT) * xAxisMinDist),

View File

@ -24,28 +24,23 @@ import Plot from '../single/Plot.vue';
import Vue from 'vue'; import Vue from 'vue';
export default function OverlayPlotViewProvider(openmct) { export default function OverlayPlotViewProvider(openmct) {
function isCompactView(objectPath) {
return objectPath.find(object => object.type === 'time-strip');
}
return { return {
key: 'plot-overlay', key: 'plot-overlay',
name: 'Overlay Plot', name: 'Overlay Plot',
cssClass: 'icon-telemetry', cssClass: 'icon-telemetry',
canView(domainObject, objectPath) { canView(domainObject) {
return isCompactView(objectPath) && domainObject.type === 'telemetry.plot.overlay'; return domainObject.type === 'telemetry.plot.overlay';
}, },
canEdit(domainObject, objectPath) { canEdit(domainObject) {
return isCompactView(objectPath) && domainObject.type === 'telemetry.plot.overlay'; return domainObject.type === 'telemetry.plot.overlay';
}, },
view: function (domainObject, objectPath) { view: function (domainObject) {
let component; let component;
return { return {
show: function (element) { show: function (element) {
let isCompact = isCompactView(objectPath);
component = new Vue({ component = new Vue({
el: element, el: element,
components: { components: {
@ -55,14 +50,7 @@ export default function OverlayPlotViewProvider(openmct) {
openmct, openmct,
domainObject domainObject
}, },
data() { template: '<plot></plot>'
return {
options: {
compact: isCompact
}
};
},
template: '<plot :options="options"></plot>'
}); });
}, },
destroy: function () { destroy: function () {

View File

@ -50,7 +50,7 @@
></span> ></span>
</div> </div>
<mct-ticks v-show="gridLines && !options.compact" <mct-ticks v-show="gridLines"
:axis-type="'xAxis'" :axis-type="'xAxis'"
:position="'right'" :position="'right'"
@plotTickWidth="onTickWidthChange" @plotTickWidth="onTickWidthChange"
@ -113,7 +113,7 @@
> >
</div> </div>
</div> </div>
<x-axis v-if="config.series.models.length > 0 && !options.compact" <x-axis v-if="config.series.models.length > 0"
:series-model="config.series.models[0]" :series-model="config.series.models[0]"
/> />
@ -146,14 +146,6 @@ export default {
}, },
inject: ['openmct', 'domainObject'], inject: ['openmct', 'domainObject'],
props: { props: {
options: {
type: Object,
default() {
return {
compact: false
};
}
},
gridLines: { gridLines: {
type: Boolean, type: Boolean,
default() { default() {
@ -893,9 +885,6 @@ export default {
if (this.filterObserver) { if (this.filterObserver) {
this.filterObserver(); this.filterObserver();
} }
this.openmct.time.off('bounds', this.updateDisplayBounds);
this.openmct.objectViews.off('clearData', this.clearData);
} }
} }
}; };

View File

@ -76,7 +76,7 @@
<script> <script>
import eventHelpers from "./lib/eventHelpers"; import eventHelpers from "./lib/eventHelpers";
import { ticks, getFormattedTicks } from "./tickUtils"; import { ticks, commonPrefix, commonSuffix } from "./tickUtils";
import configStore from "./configuration/configStore"; import configStore from "./configuration/configStore";
export default { export default {
@ -208,7 +208,29 @@ export default {
step: newTicks[1] - newTicks[0] step: newTicks[1] - newTicks[0]
}; };
newTicks = getFormattedTicks(newTicks, format); newTicks = newTicks
.map(function (tickValue) {
return {
value: tickValue,
text: format(tickValue)
};
}, this);
if (newTicks.length && typeof newTicks[0].text === 'string') {
const tickText = newTicks.map(function (t) {
return t.text;
});
const prefix = tickText.reduce(commonPrefix);
const suffix = tickText.reduce(commonSuffix);
newTicks.forEach(function (t) {
t.fullText = t.text;
if (suffix.length) {
t.text = t.text.slice(prefix.length, -suffix.length);
} else {
t.text = t.text.slice(prefix.length);
}
});
}
this.ticks = newTicks; this.ticks = newTicks;
this.shouldCheckWidth = true; this.shouldCheckWidth = true;

View File

@ -23,9 +23,7 @@
<div ref="plotWrapper" <div ref="plotWrapper"
class="c-plot holder holder-plot has-control-bar" class="c-plot holder holder-plot has-control-bar"
> >
<div v-if="!options.compact" <div class="c-control-bar">
class="c-control-bar"
>
<span class="c-button-set c-button-set--strip-h"> <span class="c-button-set c-button-set--strip-h">
<button class="c-button icon-download" <button class="c-button icon-download"
title="Export This View's Data as PNG" title="Export This View's Data as PNG"
@ -62,7 +60,6 @@
></div> ></div>
<mct-plot :grid-lines="gridLines" <mct-plot :grid-lines="gridLines"
:cursor-guide="cursorGuide" :cursor-guide="cursorGuide"
:options="options"
@loadingUpdated="loadingUpdated" @loadingUpdated="loadingUpdated"
/> />
</div> </div>
@ -78,22 +75,12 @@ export default {
MctPlot MctPlot
}, },
inject: ['openmct', 'domainObject'], inject: ['openmct', 'domainObject'],
props: {
options: {
type: Object,
default() {
return {
compact: false
};
}
}
},
data() { data() {
return { return {
//Don't think we need this as it appears to be stacked plot specific //Don't think we need this as it appears to be stacked plot specific
// hideExportButtons: false // hideExportButtons: false
cursorGuide: false, cursorGuide: false,
gridLines: !this.options.compact, gridLines: true,
loading: false loading: false
}; };
}, },

View File

@ -39,24 +39,19 @@ export default function PlotViewProvider(openmct) {
&& metadata.valuesForHints(['domain']).length > 0); && metadata.valuesForHints(['domain']).length > 0);
} }
function isCompactView(objectPath) {
return objectPath.find(object => object.type === 'time-strip');
}
return { return {
key: 'plot-simple', key: 'plot-single',
name: 'Plot', name: 'Plot',
cssClass: 'icon-telemetry', cssClass: 'icon-telemetry',
canView(domainObject, objectPath) { canView(domainObject) {
return isCompactView(objectPath) && hasTelemetry(domainObject, openmct); return domainObject.type === 'plot-single' || hasTelemetry(domainObject);
}, },
view: function (domainObject, objectPath) { view: function (domainObject) {
let component; let component;
return { return {
show: function (element) { show: function (element) {
let isCompact = isCompactView(objectPath);
component = new Vue({ component = new Vue({
el: element, el: element,
components: { components: {
@ -66,14 +61,7 @@ export default function PlotViewProvider(openmct) {
openmct, openmct,
domainObject domainObject
}, },
data() { template: '<plot></plot>'
return {
options: {
compact: isCompact
}
};
},
template: '<plot :options="options"></plot>'
}); });
}, },
destroy: function () { destroy: function () {

View File

@ -33,27 +33,8 @@ describe("the plugin", function () {
let openmct; let openmct;
let telemetryPromise; let telemetryPromise;
let cleanupFirst; let cleanupFirst;
let mockObjectPath;
beforeEach((done) => { beforeEach((done) => {
mockObjectPath = [
{
name: 'mock folder',
type: 'fake-folder',
identifier: {
key: 'mock-folder',
namespace: ''
}
},
{
name: 'mock parent folder',
type: 'time-strip',
identifier: {
key: 'mock-parent-folder',
namespace: ''
}
}
];
const testTelemetry = [ const testTelemetry = [
{ {
'utc': 1, 'utc': 1,
@ -153,8 +134,8 @@ describe("the plugin", function () {
} }
}; };
const applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath); const applicableViews = openmct.objectViews.get(testTelemetryObject);
let plotView = applicableViews.find((viewProvider) => viewProvider.key === "plot-simple"); let plotView = applicableViews.find((viewProvider) => viewProvider.key === "plot-single");
expect(plotView).toBeDefined(); expect(plotView).toBeDefined();
}); });
@ -169,7 +150,7 @@ describe("the plugin", function () {
} }
}; };
const applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath); const applicableViews = openmct.objectViews.get(testTelemetryObject);
let plotView = applicableViews.find((viewProvider) => viewProvider.key === "plot-overlay"); let plotView = applicableViews.find((viewProvider) => viewProvider.key === "plot-overlay");
expect(plotView).toBeDefined(); expect(plotView).toBeDefined();
}); });
@ -185,7 +166,7 @@ describe("the plugin", function () {
} }
}; };
const applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath); const applicableViews = openmct.objectViews.get(testTelemetryObject);
let plotView = applicableViews.find((viewProvider) => viewProvider.key === "plot-stacked"); let plotView = applicableViews.find((viewProvider) => viewProvider.key === "plot-stacked");
expect(plotView).toBeDefined(); expect(plotView).toBeDefined();
}); });
@ -237,8 +218,8 @@ describe("the plugin", function () {
} }
}; };
applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath); applicableViews = openmct.objectViews.get(testTelemetryObject);
plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === "plot-simple"); plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === "plot-single");
plotView = plotViewProvider.view(testTelemetryObject, [testTelemetryObject]); plotView = plotViewProvider.view(testTelemetryObject, [testTelemetryObject]);
plotView.show(child, true); plotView.show(child, true);

View File

@ -87,31 +87,3 @@ export function commonSuffix(a, b) {
return a.slice(a.length - breakpoint); return a.slice(a.length - breakpoint);
} }
export function getFormattedTicks(newTicks, format) {
newTicks = newTicks
.map(function (tickValue) {
return {
value: tickValue,
text: format(tickValue)
};
});
if (newTicks.length && typeof newTicks[0].text === 'string') {
const tickText = newTicks.map(function (t) {
return t.text;
});
const prefix = tickText.reduce(commonPrefix);
const suffix = tickText.reduce(commonSuffix);
newTicks.forEach(function (t) {
t.fullText = t.text;
if (suffix.length) {
t.text = t.text.slice(prefix.length, -suffix.length);
} else {
t.text = t.text.slice(prefix.length);
}
});
}
return newTicks;
}

View File

@ -22,7 +22,7 @@
<template> <template>
<div class="c-plot c-plot--stacked holder holder-plot has-control-bar"> <div class="c-plot c-plot--stacked holder holder-plot has-control-bar">
<div v-show="!hideExportButtons && !options.compact" <div v-show="!hideExportButtons"
class="c-control-bar" class="c-control-bar"
> >
<span class="c-button-set c-button-set--strip-h"> <span class="c-button-set c-button-set--strip-h">
@ -56,7 +56,6 @@
:key="object.id" :key="object.id"
class="c-plot--stacked-container" class="c-plot--stacked-container"
:object="object" :object="object"
:options="options"
:grid-lines="gridLines" :grid-lines="gridLines"
:cursor-guide="cursorGuide" :cursor-guide="cursorGuide"
:plot-tick-width="maxTickWidth" :plot-tick-width="maxTickWidth"
@ -75,14 +74,6 @@ export default {
StackedPlotItem StackedPlotItem
}, },
inject: ['openmct', 'domainObject', 'composition'], inject: ['openmct', 'domainObject', 'composition'],
props: {
options: {
type: Object,
default() {
return {};
}
}
},
data() { data() {
return { return {
hideExportButtons: false, hideExportButtons: false,

View File

@ -36,12 +36,6 @@ export default {
return {}; return {};
} }
}, },
options: {
type: Object,
default() {
return {};
}
},
gridLines: { gridLines: {
type: Boolean, type: Boolean,
default() { default() {
@ -114,7 +108,7 @@ export default {
loadingUpdated loadingUpdated
}; };
}, },
template: '<div ref="plotWrapper" class="l-view-section u-style-receiver js-style-receiver"><div v-show="!!loading" class="c-loading--overlay loading"></div><mct-plot :grid-lines="gridLines" :cursor-guide="cursorGuide" :plot-tick-width="plotTickWidth" :options="options" @plotTickWidth="onTickWidthChange" @loadingUpdated="loadingUpdated"/></div>' template: '<div ref="plotWrapper" class="l-view-section u-style-receiver js-style-receiver"><div v-show="!!loading" class="c-loading--overlay loading"></div><mct-plot :grid-lines="gridLines" :cursor-guide="cursorGuide" :plot-tick-width="plotTickWidth" @plotTickWidth="onTickWidthChange" @loadingUpdated="loadingUpdated"/></div>'
}); });
}, },
onTickWidthChange() { onTickWidthChange() {
@ -128,8 +122,7 @@ export default {
gridLines: this.gridLines, gridLines: this.gridLines,
cursorGuide: this.cursorGuide, cursorGuide: this.cursorGuide,
plotTickWidth: this.plotTickWidth, plotTickWidth: this.plotTickWidth,
loading: this.loading, loading: this.loading
options: this.options
}; };
} }
} }

View File

@ -24,29 +24,23 @@ import StackedPlot from './StackedPlot.vue';
import Vue from 'vue'; import Vue from 'vue';
export default function StackedPlotViewProvider(openmct) { export default function StackedPlotViewProvider(openmct) {
function isCompactView(objectPath) {
return objectPath.find(object => object.type === 'time-strip');
}
return { return {
key: 'plot-stacked', key: 'plot-stacked',
name: 'Stacked Plot', name: 'Stacked Plot',
cssClass: 'icon-telemetry', cssClass: 'icon-telemetry',
canView(domainObject, objectPath) { canView(domainObject) {
return isCompactView(objectPath) && domainObject.type === 'telemetry.plot.stacked'; return domainObject.type === 'telemetry.plot.stacked';
}, },
canEdit(domainObject, objectPath) { canEdit(domainObject) {
return isCompactView(objectPath) && domainObject.type === 'telemetry.plot.stacked'; return domainObject.type === 'telemetry.plot.stacked';
}, },
view: function (domainObject, objectPath) { view: function (domainObject) {
let component; let component;
return { return {
show: function (element) { show: function (element) {
let isCompact = isCompactView(objectPath);
component = new Vue({ component = new Vue({
el: element, el: element,
components: { components: {
@ -57,14 +51,7 @@ export default function StackedPlotViewProvider(openmct) {
domainObject, domainObject,
composition: openmct.composition.get(domainObject) composition: openmct.composition.get(domainObject)
}, },
data() { template: '<stacked-plot></stacked-plot>'
return {
options: {
compact: isCompact
}
};
},
template: '<stacked-plot :options="options"></stacked-plot>'
}); });
}, },
destroy: function () { destroy: function () {

View File

@ -60,12 +60,11 @@ define([
'./nonEditableFolder/plugin', './nonEditableFolder/plugin',
'./persistence/couch/plugin', './persistence/couch/plugin',
'./defaultRootName/plugin', './defaultRootName/plugin',
'./plan/plugin', './timeline/plugin',
'./viewDatumAction/plugin', './viewDatumAction/plugin',
'./interceptors/plugin', './interceptors/plugin',
'./performanceIndicator/plugin', './performanceIndicator/plugin',
'./CouchDBSearchFolder/plugin', './CouchDBSearchFolder/plugin'
'./timeline/plugin'
], function ( ], function (
_, _,
UTCTimeSystem, UTCTimeSystem,
@ -106,12 +105,11 @@ define([
NonEditableFolder, NonEditableFolder,
CouchDBPlugin, CouchDBPlugin,
DefaultRootName, DefaultRootName,
PlanLayout, Timeline,
ViewDatumAction, ViewDatumAction,
ObjectInterceptors, ObjectInterceptors,
PerformanceIndicator, PerformanceIndicator,
CouchDBSearchFolder, CouchDBSearchFolder
Timeline
) { ) {
const bundleMap = { const bundleMap = {
LocalStorage: 'platform/persistence/local', LocalStorage: 'platform/persistence/local',
@ -206,12 +204,11 @@ define([
plugins.NonEditableFolder = NonEditableFolder.default; plugins.NonEditableFolder = NonEditableFolder.default;
plugins.ISOTimeFormat = ISOTimeFormat.default; plugins.ISOTimeFormat = ISOTimeFormat.default;
plugins.DefaultRootName = DefaultRootName.default; plugins.DefaultRootName = DefaultRootName.default;
plugins.PlanLayout = PlanLayout.default; plugins.Timeline = Timeline.default;
plugins.ViewDatumAction = ViewDatumAction.default; plugins.ViewDatumAction = ViewDatumAction.default;
plugins.ObjectInterceptors = ObjectInterceptors.default; plugins.ObjectInterceptors = ObjectInterceptors.default;
plugins.PerformanceIndicator = PerformanceIndicator.default; plugins.PerformanceIndicator = PerformanceIndicator.default;
plugins.CouchDBSearchFolder = CouchDBSearchFolder.default; plugins.CouchDBSearchFolder = CouchDBSearchFolder.default;
plugins.Timeline = Timeline.default;
return plugins; return plugins;
}); });

View File

@ -103,7 +103,7 @@ describe("the plugin", () => {
} }
}; };
const applicableViews = openmct.objectViews.get(testTelemetryObject, []); const applicableViews = openmct.objectViews.get(testTelemetryObject);
let tableView = applicableViews.find((viewProvider) => viewProvider.key === 'table'); let tableView = applicableViews.find((viewProvider) => viewProvider.key === 'table');
expect(tableView).toBeDefined(); expect(tableView).toBeDefined();
}); });
@ -174,7 +174,7 @@ describe("the plugin", () => {
openmct.router.path = [testTelemetryObject]; openmct.router.path = [testTelemetryObject];
applicableViews = openmct.objectViews.get(testTelemetryObject, []); applicableViews = openmct.objectViews.get(testTelemetryObject);
tableViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'table'); tableViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'table');
tableView = tableViewProvider.view(testTelemetryObject, [testTelemetryObject]); tableView = tableViewProvider.view(testTelemetryObject, [testTelemetryObject]);
tableView.show(child, true); tableView.show(child, true);

View File

@ -67,10 +67,6 @@
&.is-in-month { &.is-in-month {
background: $colorMenuElementHilite; background: $colorMenuElementHilite;
} }
&.selected {
background: #1ac6ff; // this should be a variable... CHARLESSSSSS
}
} }
&__day { &__day {

View File

@ -0,0 +1,454 @@
<template>
<div ref="axisHolder"
class="c-timeline-plan"
>
<div class="nowMarker"><span class="icon-arrow-down"></span></div>
</div>
</template>
<script>
import * as d3Selection from 'd3-selection';
import * as d3Axis from 'd3-axis';
import * as d3Scale from 'd3-scale';
import utcMultiTimeFormat from "@/plugins/timeConductor/utcMultiTimeFormat";
//TODO: UI direction needed for the following property values
const PADDING = 1;
const OUTER_TEXT_PADDING = 12;
const INNER_TEXT_PADDING = 17;
const TEXT_LEFT_PADDING = 5;
const ROW_PADDING = 12;
// const DEFAULT_DURATION_FORMATTER = 'duration';
const RESIZE_POLL_INTERVAL = 200;
const PIXELS_PER_TICK = 100;
const PIXELS_PER_TICK_WIDE = 200;
const ROW_HEIGHT = 30;
const LINE_HEIGHT = 12;
const MAX_TEXT_WIDTH = 300;
const TIMELINE_HEIGHT = 30;
//This offset needs to be re-considered
const TIMELINE_OFFSET_HEIGHT = 70;
const GROUP_OFFSET = 100;
export default {
inject: ['openmct', 'domainObject'],
props: {
"renderingEngine": {
type: String,
default() {
return 'canvas';
}
}
},
mounted() {
this.validateJSON(this.domainObject.selectFile.body);
if (this.renderingEngine === 'svg') {
this.useSVG = true;
}
this.container = d3Selection.select(this.$refs.axisHolder);
this.svgElement = this.container.append("svg:svg");
// draw x axis with labels. CSS is used to position them.
this.axisElement = this.svgElement.append("g")
.attr("class", "axis");
this.xAxis = d3Axis.axisTop();
this.canvas = this.container.append('canvas').node();
this.canvasContext = this.canvas.getContext('2d');
this.setDimensions();
this.updateViewBounds();
this.openmct.time.on("timeSystem", this.setScaleAndPlotActivities);
this.openmct.time.on("bounds", this.updateViewBounds);
this.resizeTimer = setInterval(this.resize, RESIZE_POLL_INTERVAL);
if (this.openmct.objects.supportsMutation(this.domainObject.identifier)) {
this.openmct.objects.getMutable(this.domainObject.identifier)
.then(this.observeForChanges);
}
this.unlisten = this.openmct.objects.observe(this.domainObject, '*', this.observeForChanges);
},
destroyed() {
clearInterval(this.resizeTimer);
this.openmct.time.off("timeSystem", this.setScaleAndPlotActivities);
this.openmct.time.off("bounds", this.updateViewBounds);
if (this.unlisten) {
this.unlisten();
}
},
methods: {
observeForChanges(mutatedObject) {
if (mutatedObject.selectFile) {
this.validateJSON(mutatedObject.selectFile.body);
this.setScaleAndPlotActivities();
}
},
resize() {
if (this.$refs.axisHolder.clientWidth !== this.width) {
this.setDimensions();
this.updateViewBounds();
}
},
validateJSON(jsonString) {
try {
this.json = JSON.parse(jsonString);
} catch (e) {
return false;
}
return true;
},
updateViewBounds() {
this.viewBounds = this.openmct.time.bounds();
// this.viewBounds.end = this.viewBounds.end + (30 * 60 * 1000);
this.setScaleAndPlotActivities();
},
updateNowMarker() {
if (this.openmct.time.clock() === undefined) {
let nowMarker = document.querySelector('.nowMarker');
if (nowMarker) {
nowMarker.parentNode.removeChild(nowMarker);
}
} else {
let nowMarker = document.querySelector('.nowMarker');
if (nowMarker) {
const svgEl = d3Selection.select(this.svgElement).node();
const height = this.useSVG ? svgEl.style('height') : this.canvas.height + 'px';
nowMarker.style.height = height;
const now = this.xScale(Date.now());
nowMarker.style.left = now + GROUP_OFFSET + 'px';
}
}
},
setScaleAndPlotActivities() {
this.setScale();
this.clearPreviousActivities();
if (this.xScale) {
this.calculatePlanLayout();
this.drawPlan();
this.updateNowMarker();
}
},
clearPreviousActivities() {
if (this.useSVG) {
d3Selection.selectAll("svg > :not(g)").remove();
} else {
this.canvasContext.clearRect(0, 0, this.canvas.width, this.canvas.height);
}
},
setDimensions() {
const axisHolder = this.$refs.axisHolder;
const rect = axisHolder.getBoundingClientRect();
this.left = Math.round(rect.left);
this.top = Math.round(rect.top);
this.width = axisHolder.clientWidth;
this.offsetWidth = this.width - GROUP_OFFSET;
const axisHolderParent = this.$parent.$refs.planHolder;
this.height = Math.round(axisHolderParent.getBoundingClientRect().height);
if (this.useSVG) {
this.svgElement.attr("width", this.width);
this.svgElement.attr("height", this.height);
} else {
this.svgElement.attr("height", 50);
this.canvas.width = this.width;
this.canvas.height = this.height;
}
this.canvasContext.font = "normal normal 12px sans-serif";
},
setScale(timeSystem) {
if (!this.width) {
return;
}
if (timeSystem === undefined) {
timeSystem = this.openmct.time.timeSystem();
}
if (timeSystem.isUTCBased) {
this.xScale = d3Scale.scaleUtc();
this.xScale.domain(
[new Date(this.viewBounds.start), new Date(this.viewBounds.end)]
);
} else {
this.xScale = d3Scale.scaleLinear();
this.xScale.domain(
[this.viewBounds.start, this.viewBounds.end]
);
}
this.xScale.range([PADDING, this.offsetWidth - PADDING * 2]);
this.xAxis.scale(this.xScale);
this.xAxis.tickFormat(utcMultiTimeFormat);
this.axisElement.call(this.xAxis);
if (this.width > 1800) {
this.xAxis.ticks(this.offsetWidth / PIXELS_PER_TICK_WIDE);
} else {
this.xAxis.ticks(this.offsetWidth / PIXELS_PER_TICK);
}
},
isActivityInBounds(activity) {
return (activity.start < this.viewBounds.end) && (activity.end > this.viewBounds.start);
},
getTextWidth(name) {
// canvasContext.font = font;
let metrics = this.canvasContext.measureText(name);
return parseInt(metrics.width, 10);
},
sortFn(a, b) {
const numA = parseInt(a, 10);
const numB = parseInt(b, 10);
if (numA > numB) {
return 1;
}
if (numA < numB) {
return -1;
}
return 0;
},
// Get the row where the next activity will land.
getRowForActivity(rectX, width, minimumActivityRow = 0) {
let currentRow;
let sortedActivityRows = Object.keys(this.activitiesByRow).sort(this.sortFn);
function getOverlap(rects) {
return rects.every(rect => {
const { start, end } = rect;
const calculatedEnd = rectX + width;
const hasOverlap = (rectX >= start && rectX <= end) || (calculatedEnd >= start && calculatedEnd <= end) || (rectX <= start && calculatedEnd >= end);
return !hasOverlap;
});
}
for (let i = 0; i < sortedActivityRows.length; i++) {
let row = sortedActivityRows[i];
if (row >= minimumActivityRow && getOverlap(this.activitiesByRow[row])) {
currentRow = row;
break;
}
}
if (currentRow === undefined && sortedActivityRows.length) {
let row = Math.max(parseInt(sortedActivityRows[sortedActivityRows.length - 1], 10), minimumActivityRow);
currentRow = row + ROW_HEIGHT + ROW_PADDING;
}
return (currentRow || minimumActivityRow);
},
calculatePlanLayout() {
this.activitiesByRow = {};
let currentRow = 0;
let groups = Object.keys(this.json);
groups.forEach((key, index) => {
let activities = this.json[key];
//set the new group's first row. It should be greater than the largest row of the last group
let sortedActivityRows = Object.keys(this.activitiesByRow).sort(this.sortFn);
const groupRowStart = sortedActivityRows.length ? parseInt(sortedActivityRows[sortedActivityRows.length - 1], 10) + 1 : 0;
let newGroup = true;
activities.forEach((activity) => {
if (this.isActivityInBounds(activity)) {
const currentStart = Math.max(this.viewBounds.start, activity.start);
const currentEnd = Math.min(this.viewBounds.end, activity.end);
const rectX = this.xScale(currentStart);
const rectY = this.xScale(currentEnd);
const rectWidth = rectY - rectX;
const activityNameWidth = this.getTextWidth(activity.name) + TEXT_LEFT_PADDING;
//TODO: Fix bug for SVG where the rectWidth is not proportional to the canvas measuredWidth of the text
const activityNameFitsRect = (rectWidth >= activityNameWidth);
const textStart = (activityNameFitsRect ? rectX : (rectX + rectWidth)) + TEXT_LEFT_PADDING;
let textLines = this.getActivityDisplayText(this.canvasContext, activity.name, activityNameFitsRect);
const textWidth = textStart + this.getTextWidth(textLines[0]) + TEXT_LEFT_PADDING;
if (activityNameFitsRect) {
currentRow = this.getRowForActivity(rectX, rectWidth, groupRowStart);
} else {
currentRow = this.getRowForActivity(rectX, textWidth, groupRowStart);
}
let textY = parseInt(currentRow, 10) + (activityNameFitsRect ? INNER_TEXT_PADDING : OUTER_TEXT_PADDING);
if (!this.activitiesByRow[currentRow]) {
this.activitiesByRow[currentRow] = [];
}
this.activitiesByRow[currentRow].push({
heading: newGroup ? key : '',
activity: {
color: activity.color,
textColor: activity.textColor
},
textLines: textLines,
textStart: textStart,
textY: textY,
start: rectX,
end: activityNameFitsRect ? rectX + rectWidth : textStart + textWidth,
rectWidth: rectWidth
});
newGroup = false;
}
});
});
},
getActivityDisplayText(context, text, activityNameFitsRect) {
//TODO: If the activity start is less than viewBounds.start then the text should be cropped on the left/should be off-screen)
let words = text.split(' ');
let line = '';
let activityText = [];
let rows = 1;
for (let n = 0; (n < words.length) && (rows <= 2); n++) {
let testLine = line + words[n] + ' ';
let metrics = context.measureText(testLine);
let testWidth = metrics.width;
if (!activityNameFitsRect && (testWidth > MAX_TEXT_WIDTH && n > 0)) {
activityText.push(line);
line = words[n] + ' ';
testLine = line + words[n] + ' ';
rows = rows + 1;
}
line = testLine;
}
return activityText.length ? activityText : [line];
},
getGroupHeading(row) {
let groupHeadingRow;
let groupHeadingBorder;
if (row) {
groupHeadingBorder = row + ROW_PADDING + OUTER_TEXT_PADDING;
groupHeadingRow = groupHeadingBorder + OUTER_TEXT_PADDING;
} else {
groupHeadingRow = TIMELINE_HEIGHT + OUTER_TEXT_PADDING;
}
return {
groupHeadingRow,
groupHeadingBorder
};
},
getPlanHeight(activityRows) {
return parseInt(activityRows[activityRows.length - 1], 10) + TIMELINE_OFFSET_HEIGHT;
},
drawPlan() {
const activityRows = Object.keys(this.activitiesByRow);
if (activityRows.length) {
let planHeight = this.getPlanHeight(activityRows);
planHeight = Math.max(this.height, planHeight);
if (this.useSVG) {
this.svgElement.attr("height", planHeight);
} else {
// This needs to happen before we draw on the canvas or the canvas will get wiped out when height is set
this.canvas.height = planHeight;
}
activityRows.forEach((key) => {
const items = this.activitiesByRow[key];
const row = parseInt(key, 10);
items.forEach((item) => {
//TODO: Don't draw the left-border of the rectangle if the activity started before viewBounds.start
if (this.useSVG) {
this.plotSVG(item, row);
} else {
this.plotCanvas(item, row);
}
});
});
}
},
plotSVG(item, row) {
const headingText = item.heading;
const { groupHeadingRow, groupHeadingBorder } = this.getGroupHeading(row);
if (headingText) {
if (groupHeadingBorder) {
this.svgElement.append("line")
.attr("class", "activity")
.attr("x1", 0)
.attr("y1", groupHeadingBorder)
.attr("x2", this.width)
.attr("y2", groupHeadingBorder)
.attr('stroke', "white");
}
this.svgElement.append("text").text(headingText)
.attr("class", "activity")
.attr("x", 0)
.attr("y", groupHeadingRow)
.attr('fill', "white");
}
const activity = item.activity;
const rectY = row + TIMELINE_HEIGHT;
this.svgElement.append("rect")
.attr("class", "activity")
.attr("x", item.start + GROUP_OFFSET)
.attr("y", rectY + TIMELINE_HEIGHT)
.attr("width", item.rectWidth)
.attr("height", ROW_HEIGHT)
.attr('fill', activity.color)
.attr('stroke', "lightgray");
item.textLines.forEach((line, index) => {
this.svgElement.append("text").text(line)
.attr("class", "activity")
.attr("x", item.textStart + GROUP_OFFSET)
.attr("y", item.textY + TIMELINE_HEIGHT + (index * LINE_HEIGHT))
.attr('fill', activity.textColor);
});
//TODO: Ending border
},
plotCanvas(item, row) {
const headingText = item.heading;
const { groupHeadingRow, groupHeadingBorder } = this.getGroupHeading(row);
if (headingText) {
if (groupHeadingBorder) {
this.canvasContext.strokeStyle = "white";
this.canvasContext.beginPath();
this.canvasContext.moveTo(0, groupHeadingBorder);
this.canvasContext.lineTo(this.width, groupHeadingBorder);
this.canvasContext.stroke();
}
this.canvasContext.fillStyle = "white";
this.canvasContext.fillText(headingText, 0, groupHeadingRow);
}
const activity = item.activity;
const rectX = item.start;
const rectY = row + TIMELINE_HEIGHT;
const rectWidth = item.rectWidth;
this.canvasContext.fillStyle = activity.color;
this.canvasContext.strokeStyle = "lightgray";
this.canvasContext.fillRect(rectX + GROUP_OFFSET, rectY, rectWidth, ROW_HEIGHT);
this.canvasContext.strokeRect(rectX + GROUP_OFFSET, rectY, rectWidth, ROW_HEIGHT);
this.canvasContext.fillStyle = activity.textColor;
item.textLines.forEach((line, index) => {
this.canvasContext.fillText(line, item.textStart + GROUP_OFFSET, item.textY + TIMELINE_HEIGHT + (index * LINE_HEIGHT));
});
//TODO: Ending border
}
}
};
</script>

View File

@ -21,175 +21,25 @@
*****************************************************************************/ *****************************************************************************/
<template> <template>
<div ref="timelineHolder" <div ref="planHolder"
class="c-timeline-holder" class="c-timeline"
> >
<div class="c-timeline"> <plan :rendering-engine="'canvas'" />
<div v-for="timeSystemItem in timeSystems"
:key="timeSystemItem.timeSystem.key"
class="u-contents"
>
<swim-lane>
<template slot="label">
{{ timeSystemItem.timeSystem.name }}
</template>
<template slot="object">
<timeline-axis :bounds="timeSystemItem.bounds"
:time-system="timeSystemItem.timeSystem"
:content-height="height"
:rendering-engine="'svg'"
/>
</template>
</swim-lane>
</div>
<div ref="contentHolder"
class="u-contents c-timeline__objects c-timeline__content-holder"
>
<div
v-for="item in items"
:key="item.keyString"
class="u-contents c-timeline__content"
>
<swim-lane :icon-class="item.type.definition.cssClass"
:min-height="item.height"
:show-ucontents="item.domainObject.type === 'plan'"
:span-rows-count="item.rowCount"
>
<template slot="label">
{{ item.domainObject.name }}
</template>
<object-view
slot="object"
class="u-contents"
:default-object="item.domainObject"
:object-view-key="item.viewKey"
:object-path="item.objectPath"
/>
</swim-lane>
</div>
</div>
</div>
</div> </div>
</template> </template>
<script> <script>
import ObjectView from '@/ui/components/ObjectView.vue'; import Plan from './Plan.vue';
import TimelineAxis from '../../ui/components/TimeSystemAxis.vue';
import SwimLane from "@/ui/components/swim-lane/SwimLane.vue";
import { getValidatedPlan } from "../plan/util";
const unknownObjectType = {
definition: {
cssClass: 'icon-object-unknown',
name: 'Unknown Type'
}
};
function getViewKey(domainObject, objectPath, openmct) {
let viewKey = '';
const plotView = openmct.objectViews.get(domainObject, objectPath).find((view) => {
return view.key.startsWith('plot-') && view.key !== 'plot-single';
});
if (plotView) {
viewKey = plotView.key;
}
return viewKey;
}
export default { export default {
components: { components: {
ObjectView, Plan
TimelineAxis,
SwimLane
}, },
inject: ['openmct', 'domainObject', 'composition', 'objectPath'], inject: ['openmct', 'domainObject'],
data() { data() {
return { return {
items: [], plans: []
timeSystems: [],
height: 0
}; };
},
beforeDestroy() {
this.composition.off('add', this.addItem);
this.composition.off('remove', this.removeItem);
this.composition.off('reorder', this.reorder);
this.openmct.time.off("bounds", this.updateViewBounds);
},
mounted() {
if (this.composition) {
this.composition.on('add', this.addItem);
this.composition.on('remove', this.removeItem);
this.composition.on('reorder', this.reorder);
this.composition.load();
}
this.getTimeSystems();
this.openmct.time.on("bounds", this.updateViewBounds);
},
methods: {
addItem(domainObject) {
let type = this.openmct.types.get(domainObject.type) || unknownObjectType;
let keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
let objectPath = [domainObject].concat(this.objectPath.slice());
let viewKey = getViewKey(domainObject, objectPath, this.openmct);
let rowCount = 0;
if (domainObject.type === 'plan') {
rowCount = Object.keys(getValidatedPlan(domainObject)).length;
}
let height = domainObject.type === 'telemetry.plot.stacked' ? `${domainObject.composition.length * 100}px` : '100px';
let item = {
domainObject,
objectPath,
type,
keyString,
viewKey,
rowCount,
height
};
this.items.push(item);
this.updateContentHeight();
},
removeItem(identifier) {
let index = this.items.findIndex(item => this.openmct.objects.areIdsEqual(identifier, item.domainObject.identifier));
this.items.splice(index, 1);
},
reorder(reorderPlan) {
let oldItems = this.items.slice();
reorderPlan.forEach((reorderEvent) => {
this.$set(this.items, reorderEvent.newIndex, oldItems[reorderEvent.oldIndex]);
});
},
updateContentHeight() {
this.height = Math.round(this.$refs.contentHolder.getBoundingClientRect().height);
},
getTimeSystems() {
const timeSystems = this.openmct.time.getAllTimeSystems();
timeSystems.forEach(timeSystem => {
this.timeSystems.push({
timeSystem,
bounds: this.getBoundsForTimeSystem(timeSystem)
});
});
},
getBoundsForTimeSystem(timeSystem) {
const currentBounds = this.openmct.time.bounds();
//TODO: Some kind of translation via an offset? of current bounds to target timeSystem
return currentBounds;
},
updateViewBounds(bounds) {
let currentTimeSystem = this.timeSystems.find(item => item.timeSystem.key === this.openmct.time.timeSystem().key);
if (currentTimeSystem) {
currentTimeSystem.bounds = bounds;
}
}
} }
}; };
</script> </script>

View File

@ -26,18 +26,18 @@ import Vue from 'vue';
export default function TimelineViewProvider(openmct) { export default function TimelineViewProvider(openmct) {
return { return {
key: 'time-strip.view', key: 'timeline.view',
name: 'TimeStrip', name: 'Timeline',
cssClass: 'icon-clock', cssClass: 'icon-clock',
canView(domainObject) { canView(domainObject) {
return domainObject.type === 'time-strip'; return domainObject.type === 'plan';
}, },
canEdit(domainObject) { canEdit(domainObject) {
return domainObject.type === 'time-strip'; return domainObject.type === 'plan';
}, },
view: function (domainObject, objectPath) { view: function (domainObject) {
let component; let component;
return { return {
@ -49,9 +49,7 @@ export default function TimelineViewProvider(openmct) {
}, },
provide: { provide: {
openmct, openmct,
domainObject, domainObject
composition: openmct.composition.get(domainObject),
objectPath
}, },
template: '<timeline-view-layout></timeline-view-layout>' template: '<timeline-view-layout></timeline-view-layout>'
}); });

View File

@ -20,18 +20,27 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
import TimelineViewProvider from '../timeline/TimelineViewProvider'; import TimelineViewProvider from './TimelineViewProvider';
export default function () { export default function () {
return function install(openmct) { return function install(openmct) {
openmct.types.addType('time-strip', { openmct.types.addType('plan', {
name: 'Time Strip', name: 'Plan',
key: 'time-strip', key: 'plan',
description: 'An activity timeline', description: 'An activity timeline',
creatable: true, creatable: true,
cssClass: 'icon-timeline', cssClass: 'icon-timeline',
form: [
{
name: 'Upload Plan (JSON File)',
key: 'selectFile',
control: 'file-input',
required: true,
text: 'Select File',
type: 'application/json'
}
],
initialize: function (domainObject) { initialize: function (domainObject) {
domainObject.composition = [];
} }
}); });
openmct.objectViews.addProvider(new TimelineViewProvider(openmct)); openmct.objectViews.addProvider(new TimelineViewProvider(openmct));

View File

@ -23,33 +23,15 @@
import { createOpenMct, resetApplicationState } from "utils/testing"; import { createOpenMct, resetApplicationState } from "utils/testing";
import TimelinePlugin from "./plugin"; import TimelinePlugin from "./plugin";
import Vue from 'vue'; import Vue from 'vue';
import TimelineViewLayout from "./TimelineViewLayout.vue";
describe('the plugin', function () { describe('the plugin', function () {
let objectDef; let planDefinition;
let element; let element;
let child; let child;
let openmct; let openmct;
let mockObjectPath;
beforeEach((done) => { beforeEach((done) => {
mockObjectPath = [
{
name: 'mock folder',
type: 'fake-folder',
identifier: {
key: 'mock-folder',
namespace: ''
}
},
{
name: 'mock parent folder',
type: 'time-strip',
identifier: {
key: 'mock-parent-folder',
namespace: ''
}
}
];
const appHolder = document.createElement('div'); const appHolder = document.createElement('div');
appHolder.style.width = '640px'; appHolder.style.width = '640px';
appHolder.style.height = '480px'; appHolder.style.height = '480px';
@ -57,7 +39,7 @@ describe('the plugin', function () {
openmct = createOpenMct(); openmct = createOpenMct();
openmct.install(new TimelinePlugin()); openmct.install(new TimelinePlugin());
objectDef = openmct.types.get('time-strip').definition; planDefinition = openmct.types.get('plan').definition;
element = document.createElement('div'); element = document.createElement('div');
element.style.width = '640px'; element.style.width = '640px';
@ -67,7 +49,7 @@ describe('the plugin', function () {
child.style.height = '480px'; child.style.height = '480px';
element.appendChild(child); element.appendChild(child);
openmct.time.timeSystem('utc', { openmct.time.bounds({
start: 1597160002854, start: 1597160002854,
end: 1597181232854 end: 1597181232854
}); });
@ -80,46 +62,147 @@ describe('the plugin', function () {
return resetApplicationState(openmct); return resetApplicationState(openmct);
}); });
let mockObject = { let mockPlanObject = {
name: 'Time Strip', name: 'Plan',
key: 'time-strip', key: 'plan',
creatable: true creatable: true
}; };
it('defines a time-strip object type with the correct key', () => { it('defines a plan object type with the correct key', () => {
expect(objectDef.key).toEqual(mockObject.key); expect(planDefinition.key).toEqual(mockPlanObject.key);
}); });
describe('the time-strip object', () => { describe('the plan object', () => {
it('is creatable', () => { it('is creatable', () => {
expect(objectDef.creatable).toEqual(mockObject.creatable); expect(planDefinition.creatable).toEqual(mockPlanObject.creatable);
}); });
});
describe('the view', () => { it('provides a timeline view', () => {
let timelineView;
beforeEach((done) => {
const testViewObject = { const testViewObject = {
id: "test-object", id: "test-object",
type: "time-strip" type: "plan"
}; };
const applicableViews = openmct.objectViews.get(testViewObject, mockObjectPath); const applicableViews = openmct.objectViews.get(testViewObject);
timelineView = applicableViews.find((viewProvider) => viewProvider.key === 'time-strip.view'); let timelineView = applicableViews.find((viewProvider) => viewProvider.key === 'timeline.view');
let view = timelineView.view(testViewObject, element);
view.show(child, true);
Vue.nextTick(done);
});
it('provides a view', () => {
expect(timelineView).toBeDefined(); expect(timelineView).toBeDefined();
}); });
it('displays a time axis', () => { });
const el = element.querySelector('.c-timesystem-axis');
expect(el).toBeDefined(); describe('the timeline view displays activities', () => {
let planDomainObject;
let component;
let planViewComponent;
beforeEach((done) => {
planDomainObject = {
identifier: {
key: 'test-object',
namespace: ''
},
type: 'plan',
id: "test-object",
selectFile: {
body: JSON.stringify({
"TEST-GROUP": [
{
"name": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua",
"start": 1597170002854,
"end": 1597171032854,
"type": "TEST-GROUP",
"color": "fuchsia",
"textColor": "black"
},
{
"name": "Sed ut perspiciatis",
"start": 1597171132854,
"end": 1597171232854,
"type": "TEST-GROUP",
"color": "fuchsia",
"textColor": "black"
}
]
})
}
};
let viewContainer = document.createElement('div');
child.append(viewContainer);
component = new Vue({
el: viewContainer,
components: {
TimelineViewLayout
},
provide: {
openmct: openmct,
domainObject: planDomainObject
},
template: '<timeline-view-layout/>'
});
return Vue.nextTick().then(() => {
planViewComponent = component.$root.$children[0].$children[0];
setTimeout(() => {
clearInterval(planViewComponent.resizeTimer);
//TODO: this is a hack to ensure the canvas has a width - maybe there's a better way to set the width of the plan div
planViewComponent.width = 1200;
planViewComponent.setScaleAndPlotActivities();
done();
}, 300);
});
});
it('loads activities into the view', () => {
expect(planViewComponent.json).toBeDefined();
expect(planViewComponent.json["TEST-GROUP"].length).toEqual(2);
});
it('loads a time axis into the view', () => {
let ticks = planViewComponent.axisElement.node().querySelectorAll('g.tick');
expect(ticks.length).toEqual(11);
});
it('calculates the activity layout', () => {
const expectedActivitiesByRow = {
"0": [
{
"heading": "TEST-GROUP",
"activity": {
"color": "fuchsia",
"textColor": "black"
},
"textLines": [
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, ",
"sed sed do eiusmod tempor incididunt ut labore et "
],
"textStart": -47.51342439943476,
"textY": 12,
"start": -47.51625058878945,
"end": 204.97315120113046,
"rectWidth": -4.9971738106453145
}
],
"42": [
{
"heading": "",
"activity": {
"color": "fuchsia",
"textColor": "black"
},
"textLines": [
"Sed ut perspiciatis "
],
"textStart": -48.483749411210546,
"textY": 54,
"start": -52.99858690532266,
"end": 9.032501177578908,
"rectWidth": -0.48516250588788523
}
]
};
expect(Object.keys(planViewComponent.activitiesByRow)).toEqual(Object.keys(expectedActivitiesByRow));
}); });
}); });

View File

@ -0,0 +1,57 @@
.c-timeline {
$h: 18px;
$tickYPos: ($h / 2) + 12px + 10px;
$tickXPos: 100px;
height: 100%;
svg {
text-rendering: geometricPrecision;
width: 100%;
height: 100%;
> g.axis {
// Overall Tick holder
transform: translateY($tickYPos) translateX($tickXPos);
g {
//Each tick. These move on drag.
line {
// Line beneath ticks
display: none;
}
}
}
text:not(.activity) {
// Tick labels
fill: $colorBodyFg;
font-size: 1em;
paint-order: stroke;
font-weight: bold;
stroke: $colorBodyBg;
stroke-linecap: butt;
stroke-linejoin: bevel;
stroke-width: 6px;
}
text.activity {
stroke: none;
}
}
.nowMarker {
width: 2px;
position: absolute;
z-index: 10;
background: gray;
& .icon-arrow-down {
font-size: large;
position: absolute;
top: -8px;
left: -8px;
}
}
}

View File

@ -1,4 +0,0 @@
.c-timeline-holder {
@include abs();
overflow-x: hidden;
}

View File

@ -17,7 +17,6 @@
@import "../plugins/folderView/components/list-item.scss"; @import "../plugins/folderView/components/list-item.scss";
@import "../plugins/folderView/components/list-view.scss"; @import "../plugins/folderView/components/list-view.scss";
@import "../plugins/imagery/components/imagery-view-layout.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-row.scss";
@import "../plugins/telemetryTable/components/table-footer-indicator.scss"; @import "../plugins/telemetryTable/components/table-footer-indicator.scss";
@import "../plugins/tabs/components/tabs.scss"; @import "../plugins/tabs/components/tabs.scss";
@ -27,16 +26,13 @@
@import "../plugins/timeConductor/conductor-mode.scss"; @import "../plugins/timeConductor/conductor-mode.scss";
@import "../plugins/timeConductor/conductor-mode-icon.scss"; @import "../plugins/timeConductor/conductor-mode-icon.scss";
@import "../plugins/timeConductor/date-picker.scss"; @import "../plugins/timeConductor/date-picker.scss";
@import "../plugins/timeline/timeline.scss"; @import "../plugins/timeline/timeline-axis.scss";
@import "../plugins/plan/plan";
@import "../plugins/viewDatumAction/components/metadata-list.scss"; @import "../plugins/viewDatumAction/components/metadata-list.scss";
@import "../ui/components/object-frame.scss"; @import "../ui/components/object-frame.scss";
@import "../ui/components/object-label.scss"; @import "../ui/components/object-label.scss";
@import "../ui/components/progress-bar.scss"; @import "../ui/components/progress-bar.scss";
@import "../ui/components/search.scss"; @import "../ui/components/search.scss";
@import "../ui/components/swim-lane/swimlane.scss";
@import "../ui/components/toggle-switch.scss"; @import "../ui/components/toggle-switch.scss";
@import "../ui/components/timesystem-axis.scss";
@import "../ui/inspector/elements.scss"; @import "../ui/inspector/elements.scss";
@import "../ui/inspector/inspector.scss"; @import "../ui/inspector/inspector.scss";
@import "../ui/inspector/location.scss"; @import "../ui/inspector/location.scss";

View File

@ -28,10 +28,6 @@ export default {
layoutFont: { layoutFont: {
type: String, type: String,
default: '' default: ''
},
objectViewKey: {
type: String,
default: ''
} }
}, },
data() { data() {
@ -307,21 +303,11 @@ export default {
event.stopPropagation(); event.stopPropagation();
} }
}, },
getViewKey() {
let viewKey = this.viewKey;
if (this.objectViewKey) {
viewKey = this.objectViewKey;
}
return viewKey;
},
getViewProvider() { getViewProvider() {
let provider = this.openmct.objectViews.getByProviderKey(this.viewKey);
let provider = this.openmct.objectViews.getByProviderKey(this.getViewKey());
if (!provider) { if (!provider) {
let objectPath = this.currentObjectPath || this.objectPath; provider = this.openmct.objectViews.get(this.domainObject)[0];
provider = this.openmct.objectViews.get(this.domainObject, objectPath)[0];
if (!provider) { if (!provider) {
return; return;
} }
@ -330,11 +316,10 @@ export default {
return provider; return provider;
}, },
editIfEditable(event) { editIfEditable(event) {
let objectPath = this.currentObjectPath || this.objectPath;
let provider = this.getViewProvider(); let provider = this.getViewProvider();
if (provider if (provider
&& provider.canEdit && provider.canEdit
&& provider.canEdit(this.domainObject, objectPath) && provider.canEdit(this.domainObject)
&& this.isEditingAllowed() && this.isEditingAllowed()
&& !this.openmct.editor.isEditing()) { && !this.openmct.editor.isEditing()) {
this.openmct.editor.edit(); this.openmct.editor.edit();

View File

@ -1,166 +0,0 @@
<template>
<div ref="axisHolder"
class="c-timesystem-axis"
>
<div class="nowMarker"><span class="icon-arrow-down"></span></div>
</div>
</template>
<script>
import * as d3Selection from 'd3-selection';
import * as d3Axis from 'd3-axis';
import * as d3Scale from 'd3-scale';
import utcMultiTimeFormat from '@/plugins/timeConductor/utcMultiTimeFormat';
//TODO: UI direction needed for the following property values
const PADDING = 1;
const RESIZE_POLL_INTERVAL = 200;
const PIXELS_PER_TICK = 100;
const PIXELS_PER_TICK_WIDE = 200;
//This offset needs to be re-considered
export default {
inject: ['openmct', 'domainObject'],
props: {
bounds: {
type: Object,
default() {
return {};
}
},
timeSystem: {
type: Object,
default() {
return {};
}
},
contentHeight: {
type: Number,
default() {
return 0;
}
},
renderingEngine: {
type: String,
default() {
return 'svg';
}
},
offset: {
type: Number,
default() {
return 0;
}
}
},
watch: {
bounds(newBounds) {
this.drawAxis(newBounds, this.timeSystem);
},
timeSystem(newTimeSystem) {
this.drawAxis(this.bounds, newTimeSystem);
}
},
mounted() {
if (this.renderingEngine === 'svg') {
this.useSVG = true;
}
this.container = d3Selection.select(this.$refs.axisHolder);
this.svgElement = this.container.append("svg:svg");
// draw x axis with labels. CSS is used to position them.
this.axisElement = this.svgElement.append("g")
.attr("class", "axis")
.attr('font-size', '1.3em')
.attr("transform", "translate(0,20)");
this.setDimensions();
this.drawAxis(this.bounds, this.timeSystem);
this.resizeTimer = setInterval(this.resize, RESIZE_POLL_INTERVAL);
},
destroyed() {
clearInterval(this.resizeTimer);
},
methods: {
resize() {
if (this.$refs.axisHolder.clientWidth !== this.width) {
this.setDimensions();
this.drawAxis(this.bounds, this.timeSystem);
this.updateNowMarker();
}
},
updateNowMarker() {
if (this.openmct.time.clock() === undefined) {
let nowMarker = document.querySelector('.nowMarker');
if (nowMarker) {
nowMarker.parentNode.removeChild(nowMarker);
}
} else {
let nowMarker = document.querySelector('.nowMarker');
if (nowMarker) {
const svgEl = d3Selection.select(this.svgElement).node();
let height = svgEl.style('height').replace('px', '');
height = Number(height) + this.contentHeight;
nowMarker.style.height = height + 'px';
const now = this.xScale(Date.now());
nowMarker.style.left = now + this.offset + 'px';
}
}
},
setDimensions() {
const axisHolder = this.$refs.axisHolder;
this.width = axisHolder.clientWidth;
this.offsetWidth = this.width - this.offset;
this.height = Math.round(axisHolder.getBoundingClientRect().height);
if (this.useSVG) {
this.svgElement.attr("width", this.width);
this.svgElement.attr("height", this.height);
} else {
this.svgElement.attr("height", 50);
}
},
drawAxis(bounds, timeSystem) {
this.setScale(bounds, timeSystem);
this.setAxis(bounds);
this.axisElement.call(this.xAxis);
this.updateNowMarker();
},
setScale(bounds, timeSystem) {
if (!this.width) {
return;
}
if (timeSystem === undefined) {
timeSystem = this.openmct.time.timeSystem();
}
if (timeSystem.isUTCBased) {
this.xScale = d3Scale.scaleUtc();
this.xScale.domain(
[new Date(bounds.start), new Date(bounds.end)]
);
} else {
this.xScale = d3Scale.scaleLinear();
this.xScale.domain(
[bounds.start, bounds.end]
);
}
this.xScale.range([PADDING, this.offsetWidth - PADDING * 2]);
},
setAxis() {
this.xAxis = d3Axis.axisTop(this.xScale);
this.xAxis.tickFormat(utcMultiTimeFormat);
if (this.width > 1800) {
this.xAxis.ticks(this.offsetWidth / PIXELS_PER_TICK_WIDE);
} else {
this.xAxis.ticks(this.offsetWidth / PIXELS_PER_TICK);
}
}
}
};
</script>

View File

@ -1,76 +0,0 @@
<template>
<div class="u-contents"
:class="{'c-swimlane': !isNested}"
>
<div class="c-swimlane__lane-label c-object-label"
:class="{'c-swimlane__lane-label--span-cols': (!spanRowsCount && !isNested)}"
:style="gridRowSpan"
>
<div v-if="iconClass"
class="c-object-label__type-icon"
:class="iconClass"
>
</div>
<div class="c-object-label__name">
<slot name="label"></slot>
</div>
</div>
<div class="c-swimlane__lane-object"
:style="{'min-height': minHeight}"
:class="{'u-contents': showUcontents}"
data-selectable
>
<slot name="object"></slot>
</div>
</div>
</template>
<script>
export default {
props: {
iconClass: {
type: String,
default() {
return '';
}
},
minHeight: {
type: String,
default() {
return '';
}
},
showUcontents: {
type: Boolean,
default() {
return false;
}
},
isNested: {
type: Boolean,
default() {
return false;
}
},
spanRowsCount: {
type: Number,
default() {
return 0;
}
}
},
computed: {
gridRowSpan() {
if (this.spanRowsCount) {
return `grid-row: span ${this.spanRowsCount}`;
} else {
return '';
}
}
}
};
</script>

View File

@ -1,26 +0,0 @@
.c-swimlane {
display: grid;
grid-template-columns: 100px 100px 1fr;
grid-column-gap: 1px;
grid-row-gap: 1px;
margin-bottom: 1px;
width: 100%;
[class*='__lane-label'] {
background: rgba($colorBodyFg, 0.2);
color: $colorBodyFg;
padding: $interiorMarginSm;
}
[class*='--span-cols'] {
grid-column: span 2;
}
&__lane-object {
background: rgba(black, 0.1);
.c-plan {
display: contents;
}
}
}

View File

@ -1,42 +0,0 @@
.c-timesystem-axis {
$h: 30px;
height: $h;
svg {
$lineC: rgba($colorBodyFg, 0.3) !important;
text-rendering: geometricPrecision;
width: 100%;
height: 100%;
.domain {
stroke: $lineC;
}
.tick {
line {
stroke: $lineC;
}
text {
// Tick labels
fill: $colorBodyFg;
paint-order: stroke;
font-weight: bold;
}
}
}
.nowMarker {
width: 2px;
position: absolute;
z-index: 10;
background: gray;
& .icon-arrow-down {
font-size: large;
position: absolute;
top: -8px;
left: -8px;
}
}
}

View File

@ -1,96 +0,0 @@
<template>
<li
draggable="true"
@dragstart="emitDragStartEvent"
@dragenter="onDragenter"
@dragover="onDragover"
@dragleave="onDragleave"
@drop="emitDropEvent"
>
<div
class="c-tree__item c-elements-pool__item"
:class="{
'is-context-clicked': contextClickActive,
'hover': hover
}"
>
<span
class="c-elements-pool__grippy c-grippy c-grippy--vertical-drag"
></span>
<object-label
:domain-object="elementObject"
:object-path="[elementObject, parentObject]"
@context-click-active="setContextClickState"
/>
</div>
</li>
</template>
<script>
import ObjectLabel from '../components/ObjectLabel.vue';
export default {
components: {
ObjectLabel
},
props: {
index: {
type: Number,
required: true,
default: () => {
return 0;
}
},
elementObject: {
type: Object,
required: true,
default: () => {
return {};
}
},
parentObject: {
type: Object,
required: true,
default: () => {
return {};
}
},
allowDrop: {
type: Boolean
}
},
data() {
return {
contextClickActive: false,
hover: false
};
},
methods: {
onDragover(event) {
event.preventDefault();
},
emitDropEvent(event) {
this.$emit('drop-custom', this.index);
this.hover = false;
},
emitDragStartEvent(event) {
this.$emit('dragstart-custom', this.index);
},
onDragenter(event) {
if (this.allowDrop) {
this.hover = true;
this.dragElement = event.target.parentElement;
}
},
onDragleave(event) {
if (event.target.parentElement === this.dragElement) {
this.hover = false;
delete this.dragElement;
}
},
setContextClickState(state) {
this.contextClickActive = state;
}
}
};
</script>

View File

@ -8,22 +8,34 @@
/> />
<div <div
class="c-elements-pool__elements" class="c-elements-pool__elements"
:class="{'is-dragging': isDragging}"
> >
<ul <ul
v-if="elements.length > 0" v-if="elements.length > 0"
id="inspector-elements-tree" id="inspector-elements-tree"
class="c-tree c-elements-pool__tree" class="c-tree c-elements-pool__tree"
> >
<element-item <li
v-for="(element, index) in elements" v-for="(element, index) in elements"
:key="element.identifier.key" :key="element.identifier.key"
:index="index" @drop="moveTo(index)"
:element-object="element" @dragover="allowDrop"
:parent-object="parentObject" >
:allow-drop="allowDrop" <div
@dragstart-custom="moveFrom(index)" class="c-tree__item c-elements-pool__item"
@drop-custom="moveTo(index)" draggable="true"
/> @dragstart="moveFrom(index)"
>
<span
v-if="elements.length > 1 && isEditing"
class="c-elements-pool__grippy c-grippy c-grippy--vertical-drag"
></span>
<object-label
:domain-object="element"
:object-path="[element, parentObject]"
/>
</div>
</li>
<li <li
class="js-last-place" class="js-last-place"
@drop="moveToIndex(elements.length)" @drop="moveToIndex(elements.length)"
@ -39,12 +51,12 @@
<script> <script>
import _ from 'lodash'; import _ from 'lodash';
import Search from '../components/search.vue'; import Search from '../components/search.vue';
import ElementItem from './ElementItem.vue'; import ObjectLabel from '../components/ObjectLabel.vue';
export default { export default {
components: { components: {
'Search': Search, 'Search': Search,
'ElementItem': ElementItem 'ObjectLabel': ObjectLabel
}, },
inject: ['openmct'], inject: ['openmct'],
data() { data() {
@ -53,9 +65,8 @@ export default {
isEditing: this.openmct.editor.isEditing(), isEditing: this.openmct.editor.isEditing(),
parentObject: undefined, parentObject: undefined,
currentSearch: '', currentSearch: '',
selection: [], isDragging: false,
contextClickTracker: {}, selection: []
allowDrop: false
}; };
}, },
mounted() { mounted() {
@ -137,15 +148,20 @@ export default {
&& element.name.toLowerCase().search(this.currentSearch) !== -1; && element.name.toLowerCase().search(this.currentSearch) !== -1;
}); });
}, },
allowDrop(event) {
event.preventDefault();
},
moveTo(moveToIndex) { moveTo(moveToIndex) {
if (this.allowDrop) { this.composition.reorder(this.moveFromIndex, moveToIndex);
this.composition.reorder(this.moveFromIndex, moveToIndex);
this.allowDrop = false;
}
}, },
moveFrom(index) { moveFrom(index) {
this.allowDrop = true; this.isDragging = true;
this.moveFromIndex = index; this.moveFromIndex = index;
document.addEventListener('dragend', this.hideDragStyling);
},
hideDragStyling() {
this.isDragging = false;
document.removeEventListener('dragend', this.hideDragStyling);
} }
} }
}; };

View File

@ -29,7 +29,7 @@
handle="before" handle="before"
label="Elements" label="Elements"
> >
<elements-pool /> <elements />
</pane> </pane>
</multipane> </multipane>
<multipane <multipane
@ -55,7 +55,7 @@
<script> <script>
import multipane from '../layout/multipane.vue'; import multipane from '../layout/multipane.vue';
import pane from '../layout/pane.vue'; import pane from '../layout/pane.vue';
import ElementsPool from './ElementsPool.vue'; import Elements from './Elements.vue';
import Location from './Location.vue'; import Location from './Location.vue';
import Properties from './Properties.vue'; import Properties from './Properties.vue';
import ObjectName from './ObjectName.vue'; import ObjectName from './ObjectName.vue';
@ -71,7 +71,7 @@ export default {
SavedStylesInspectorView, SavedStylesInspectorView,
multipane, multipane,
pane, pane,
ElementsPool, Elements,
Properties, Properties,
ObjectName, ObjectName,
Location, Location,

View File

@ -15,6 +15,9 @@
&__elements { &__elements {
flex: 1 1 auto; flex: 1 1 auto;
overflow: auto; overflow: auto;
&.is-dragging {
li { opacity: 0.2; }
}
} }
.c-grippy { .c-grippy {
@ -24,16 +27,8 @@
transform: translateY(-2px); transform: translateY(-2px);
width: $d; height: $d; width: $d; height: $d;
} }
&.is-context-clicked {
box-shadow: inset $colorItemTreeSelectedBg 0 0 0 1px;
}
.hover {
background-color: $colorItemTreeSelectedBg;
}
} }
.js-last-place { .js-last-place {
height: 10px; height: 10px;
} }

View File

@ -159,14 +159,10 @@ export default {
return this.views.filter(v => v.key === this.viewKey)[0] || {}; return this.views.filter(v => v.key === this.viewKey)[0] || {};
}, },
views() { views() {
if (this.domainObject && (this.openmct.router.started !== true)) {
return [];
}
return this return this
.openmct .openmct
.objectViews .objectViews
.get(this.domainObject, this.openmct.router.path) .get(this.domainObject)
.map((p) => { .map((p) => {
return { return {
key: p.key, key: p.key,
@ -201,7 +197,7 @@ export default {
if (currentViewKey !== undefined) { if (currentViewKey !== undefined) {
let currentViewProvider = this.openmct.objectViews.getByProviderKey(currentViewKey); let currentViewProvider = this.openmct.objectViews.getByProviderKey(currentViewKey);
return currentViewProvider.canEdit && currentViewProvider.canEdit(this.domainObject, this.openmct.router.path); return currentViewProvider.canEdit && currentViewProvider.canEdit(this.domainObject);
} }
return false; return false;

View File

@ -235,12 +235,6 @@ export default {
}, },
watch: { watch: {
syncTreeNavigation() { syncTreeNavigation() {
// if there is an abort controller, then a search is in progress and will need to be canceled
if (this.abortController) {
this.abortController.abort();
delete this.abortController;
}
this.searchValue = ''; this.searchValue = '';
if (!this.openmct.router.path) { if (!this.openmct.router.path) {
@ -691,55 +685,35 @@ export default {
// clear any previous search results // clear any previous search results
this.searchResultItems = []; this.searchResultItems = [];
// an abort controller will be passed in that will be used const promises = this.openmct.objects.search(this.searchValue)
// to cancel an active searches if necessary
this.abortController = new AbortController();
const abortSignal = this.abortController.signal;
const promises = this.openmct.objects.search(this.searchValue, abortSignal)
.map(promise => promise .map(promise => promise
.then(results => this.aggregateSearchResults(results, abortSignal))); .then(results => this.aggregateSearchResults(results)));
Promise.all(promises).then(() => { Promise.all(promises).then(() => {
this.searchLoading = false; this.searchLoading = false;
}).catch(reason => {
// search aborted
}).finally(() => {
if (this.abortController) {
delete this.abortController;
}
}); });
}, },
async aggregateSearchResults(results, abortSignal) { async aggregateSearchResults(results) {
for (const result of results) { for (const result of results) {
if (!abortSignal.aborted) { const objectPath = await this.openmct.objects.getOriginalPath(result.identifier);
const objectPath = await this.openmct.objects.getOriginalPath(result.identifier);
// removing the item itself, as the path we pass to buildTreeItem is a parent path // removing the item itself, as the path we pass to buildTreeItem is a parent path
objectPath.shift(); objectPath.shift();
// if root, remove, we're not using in object path for tree // if root, remove, we're not using in object path for tree
let lastObject = objectPath.length ? objectPath[objectPath.length - 1] : false; let lastObject = objectPath.length ? objectPath[objectPath.length - 1] : false;
if (lastObject && lastObject.type === 'root') { if (lastObject && lastObject.type === 'root') {
objectPath.pop(); objectPath.pop();
}
// we reverse the objectPath in the tree, so have to do it here first,
// since this one is already in the correct direction
let resultObject = this.buildTreeItem(result, objectPath.reverse());
this.searchResultItems.push(resultObject);
} }
// we reverse the objectPath in the tree, so have to do it here first,
// since this one is already in the correct direction
let resultObject = this.buildTreeItem(result, objectPath.reverse());
this.searchResultItems.push(resultObject);
} }
}, },
searchTree(value) { searchTree(value) {
// if an abort controller exists, regardless of the value passed in,
// there is an active search that should be cancled
if (this.abortController) {
this.abortController.abort();
delete this.abortController;
}
this.searchValue = value; this.searchValue = value;
this.searchLoading = true; this.searchLoading = true;

View File

@ -58,7 +58,7 @@ export default {
}; };
}, },
mounted() { mounted() {
this.views = this.openmct.objectViews.get(this.domainObject, this.objectPath).map((view) => { this.views = this.openmct.objectViews.get(this.domainObject).map((view) => {
view.callBack = () => { view.callBack = () => {
return this.setView(view); return this.setView(view);
}; };

View File

@ -39,16 +39,10 @@ define(['EventEmitter'], function (EventEmitter) {
/** /**
* @private for platform-internal use * @private for platform-internal use
* @param {*} item the object to be viewed * @param {*} item the object to be viewed
* @param {array} objectPath - The current contextual object path of the view object
* eg current domainObject is located under MyItems which is under Root
* @returns {module:openmct.ViewProvider[]} any providers * @returns {module:openmct.ViewProvider[]} any providers
* which can provide views of this object * which can provide views of this object
*/ */
ViewRegistry.prototype.get = function (item, objectPath) { ViewRegistry.prototype.get = function (item) {
if (objectPath === undefined) {
throw "objectPath must be provided to get applicable views for an object";
}
function byPriority(providerA, providerB) { function byPriority(providerA, providerB) {
let priorityA = providerA.priority ? providerA.priority(item) : DEFAULT_VIEW_PRIORITY; let priorityA = providerA.priority ? providerA.priority(item) : DEFAULT_VIEW_PRIORITY;
let priorityB = providerB.priority ? providerB.priority(item) : DEFAULT_VIEW_PRIORITY; let priorityB = providerB.priority ? providerB.priority(item) : DEFAULT_VIEW_PRIORITY;
@ -58,7 +52,7 @@ define(['EventEmitter'], function (EventEmitter) {
return this.getAllProviders() return this.getAllProviders()
.filter(function (provider) { .filter(function (provider) {
return provider.canView(item, objectPath); return provider.canView(item);
}).sort(byPriority); }).sort(byPriority);
}; };
@ -187,8 +181,6 @@ define(['EventEmitter'], function (EventEmitter) {
* @memberof module:openmct.ViewProvider# * @memberof module:openmct.ViewProvider#
* @param {module:openmct.DomainObject} domainObject the domain object * @param {module:openmct.DomainObject} domainObject the domain object
* to be viewed * to be viewed
* @param {array} objectPath - The current contextual object path of the view object
* eg current domainObject is located under MyItems which is under Root
* @returns {boolean} 'true' if the view applies to the provided object, * @returns {boolean} 'true' if the view applies to the provided object,
* otherwise 'false'. * otherwise 'false'.
*/ */
@ -209,8 +201,6 @@ define(['EventEmitter'], function (EventEmitter) {
* @memberof module:openmct.ViewProvider# * @memberof module:openmct.ViewProvider#
* @param {module:openmct.DomainObject} domainObject the domain object * @param {module:openmct.DomainObject} domainObject the domain object
* to be edited * to be edited
* @param {array} objectPath - The current contextual object path of the view object
* eg current domainObject is located under MyItems which is under Root
* @returns {boolean} 'true' if the view can be used to edit the provided object, * @returns {boolean} 'true' if the view can be used to edit the provided object,
* otherwise 'false'. * otherwise 'false'.
*/ */

View File

@ -100,13 +100,13 @@ define([
document.title = browseObject.name; //change document title to current object in main view document.title = browseObject.name; //change document title to current object in main view
if (currentProvider && currentProvider.canView(browseObject, openmct.router.path)) { if (currentProvider && currentProvider.canView(browseObject)) {
viewObject(browseObject, currentProvider); viewObject(browseObject, currentProvider);
return; return;
} }
let defaultProvider = openmct.objectViews.get(browseObject, openmct.router.path)[0]; let defaultProvider = openmct.objectViews.get(browseObject)[0];
if (defaultProvider) { if (defaultProvider) {
openmct.router.updateParams({ openmct.router.updateParams({
view: defaultProvider.key view: defaultProvider.key