mirror of
https://github.com/nasa/openmct.git
synced 2025-06-27 11:32:13 +00:00
Compare commits
23 Commits
version-1.
...
couchdb-ob
Author | SHA1 | Date | |
---|---|---|---|
4f3317969e | |||
56780879b0 | |||
2093a9d687 | |||
ea3b7d3da4 | |||
3932ccdbad | |||
dd45fa3a7c | |||
6f3004898b | |||
44f05d16c5 | |||
813e4a5656 | |||
ea66292141 | |||
8fdb983a3d | |||
8e04d6409f | |||
91ce09217a | |||
ca6e9387c3 | |||
5734a1a69f | |||
ab3319128d | |||
46d00e6d61 | |||
56cd0cb5e1 | |||
de303d6497 | |||
64c9d29059 | |||
4794cd5711 | |||
5b3762e90f | |||
6eadddd8d2 |
@ -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"
|
||||||
|
@ -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": {
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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 = {
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
|
@ -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',
|
||||||
|
@ -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;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
|
@ -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);
|
||||||
|
@ -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", () => {
|
||||||
|
@ -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();
|
||||||
});
|
});
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
|
@ -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();
|
||||||
});
|
});
|
||||||
|
@ -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);
|
||||||
|
@ -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';
|
||||||
|
|
||||||
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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();
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
@ -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;
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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 {
|
||||||
|
@ -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 () {
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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',
|
||||||
|
@ -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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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>
|
|
@ -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;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
.c-plan {
|
|
||||||
svg {
|
|
||||||
text-rendering: geometricPrecision;
|
|
||||||
|
|
||||||
text {
|
|
||||||
stroke: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.activity-label {
|
|
||||||
&--outside-rect {
|
|
||||||
fill: $colorBodyFg !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
canvas {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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));
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
@ -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;
|
|
||||||
}
|
|
@ -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),
|
||||||
|
@ -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 () {
|
||||||
|
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -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;
|
||||||
|
@ -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
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -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 () {
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 () {
|
||||||
|
@ -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;
|
||||||
});
|
});
|
||||||
|
@ -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);
|
||||||
|
@ -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 {
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>'
|
||||||
});
|
});
|
||||||
|
@ -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));
|
||||||
|
@ -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));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
57
src/plugins/timeline/timeline-axis.scss
Normal file
57
src/plugins/timeline/timeline-axis.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +0,0 @@
|
|||||||
.c-timeline-holder {
|
|
||||||
@include abs();
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
|
@ -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";
|
||||||
|
@ -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();
|
||||||
|
@ -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>
|
|
@ -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>
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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>
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
@ -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,
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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);
|
||||||
};
|
};
|
||||||
|
@ -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'.
|
||||||
*/
|
*/
|
||||||
|
@ -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
|
||||||
|
Reference in New Issue
Block a user