mirror of
https://github.com/nasa/openmct.git
synced 2025-05-31 22:50:49 +00:00
5853 plot annotations prototype (#6000)
* Implement new search and tagging for notebooks * Add inspector and plot annotations * Clean up inspector for plots and other views * Bump webpack defaults for windows * Notebook annotations are shown in inspector now * Only allow annotations if plot is paused or in fixed time. also do not mutate if immutable * Key off local events instead of remote (for now)
This commit is contained in:
parent
edbbebe329
commit
d1c7d133fc
@ -80,6 +80,7 @@ const config = {
|
|||||||
projectRootDir,
|
projectRootDir,
|
||||||
"src/api/objects/object-utils.js"
|
"src/api/objects/object-utils.js"
|
||||||
),
|
),
|
||||||
|
"kdbush": path.join(projectRootDir, "node_modules/kdbush/kdbush.min.js"),
|
||||||
utils: path.join(projectRootDir, "src/utils")
|
utils: path.join(projectRootDir, "src/utils")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -167,8 +168,8 @@ const config = {
|
|||||||
performance: {
|
performance: {
|
||||||
// We should eventually consider chunking to decrease
|
// We should eventually consider chunking to decrease
|
||||||
// these values
|
// these values
|
||||||
maxEntrypointSize: 25000000,
|
maxEntrypointSize: 27000000,
|
||||||
maxAssetSize: 25000000
|
maxAssetSize: 27000000
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -57,12 +57,14 @@ async function createNotebookAndEntry(page, iterations = 1) {
|
|||||||
*/
|
*/
|
||||||
async function createNotebookEntryAndTags(page, iterations = 1) {
|
async function createNotebookEntryAndTags(page, iterations = 1) {
|
||||||
const notebook = await createNotebookAndEntry(page, iterations);
|
const notebook = await createNotebookAndEntry(page, iterations);
|
||||||
|
await page.locator('text=Annotations').click();
|
||||||
|
|
||||||
for (let iteration = 0; iteration < iterations; iteration++) {
|
for (let iteration = 0; iteration < iterations; iteration++) {
|
||||||
// Hover and click "Add Tag" button
|
// Hover and click "Add Tag" button
|
||||||
// Hover is needed here to "slow down" the actions while running in headless mode
|
// Hover is needed here to "slow down" the actions while running in headless mode
|
||||||
await page.hover(`button:has-text("Add Tag") >> nth = ${iteration}`);
|
await page.locator(`[aria-label="Notebook Entry"] >> nth = ${iteration}`).click();
|
||||||
await page.locator(`button:has-text("Add Tag") >> nth = ${iteration}`).click();
|
await page.hover(`button:has-text("Add Tag")`);
|
||||||
|
await page.locator(`button:has-text("Add Tag")`).click();
|
||||||
|
|
||||||
// Click inside the tag search input
|
// Click inside the tag search input
|
||||||
await page.locator('[placeholder="Type to select tag"]').click();
|
await page.locator('[placeholder="Type to select tag"]').click();
|
||||||
@ -71,8 +73,8 @@ async function createNotebookEntryAndTags(page, iterations = 1) {
|
|||||||
|
|
||||||
// Hover and click "Add Tag" button
|
// Hover and click "Add Tag" button
|
||||||
// Hover is needed here to "slow down" the actions while running in headless mode
|
// Hover is needed here to "slow down" the actions while running in headless mode
|
||||||
await page.hover(`button:has-text("Add Tag") >> nth = ${iteration}`);
|
await page.hover(`button:has-text("Add Tag")`);
|
||||||
await page.locator(`button:has-text("Add Tag") >> nth = ${iteration}`).click();
|
await page.locator(`button:has-text("Add Tag")`).click();
|
||||||
// Click inside the tag search input
|
// Click inside the tag search input
|
||||||
await page.locator('[placeholder="Type to select tag"]').click();
|
await page.locator('[placeholder="Type to select tag"]').click();
|
||||||
// Select the "Science" tag
|
// Select the "Science" tag
|
||||||
@ -84,8 +86,10 @@ async function createNotebookEntryAndTags(page, iterations = 1) {
|
|||||||
|
|
||||||
test.describe('Tagging in Notebooks @addInit', () => {
|
test.describe('Tagging in Notebooks @addInit', () => {
|
||||||
test('Can load tags', async ({ page }) => {
|
test('Can load tags', async ({ page }) => {
|
||||||
|
|
||||||
await createNotebookAndEntry(page);
|
await createNotebookAndEntry(page);
|
||||||
|
|
||||||
|
await page.locator('text=Annotations').click();
|
||||||
|
|
||||||
await page.locator('button:has-text("Add Tag")').click();
|
await page.locator('button:has-text("Add Tag")').click();
|
||||||
|
|
||||||
await page.locator('[placeholder="Type to select tag"]').click();
|
await page.locator('[placeholder="Type to select tag"]').click();
|
||||||
@ -126,13 +130,12 @@ test.describe('Tagging in Notebooks @addInit', () => {
|
|||||||
|
|
||||||
test('Can delete tags', async ({ page }) => {
|
test('Can delete tags', async ({ page }) => {
|
||||||
await createNotebookEntryAndTags(page);
|
await createNotebookEntryAndTags(page);
|
||||||
await page.locator('[aria-label="Notebook Entries"]').click();
|
|
||||||
// Delete Driving
|
// Delete Driving
|
||||||
await page.hover('[aria-label="Tag"]:has-text("Driving")');
|
await page.hover('[aria-label="Tag"]:has-text("Driving")');
|
||||||
await page.locator('[aria-label="Tag"]:has-text("Driving") ~ .c-completed-tag-deletion').click();
|
await page.locator('[aria-label="Tag"]:has-text("Driving") ~ .c-completed-tag-deletion').click();
|
||||||
|
|
||||||
await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Science");
|
await expect(page.locator('[aria-label="Tags Inspector"]')).toContainText("Science");
|
||||||
await expect(page.locator('[aria-label="Notebook Entry"]')).not.toContainText("Driving");
|
await expect(page.locator('[aria-label="Tags Inspector"]')).not.toContainText("Driving");
|
||||||
|
|
||||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc');
|
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc');
|
||||||
await expect(page.locator('[aria-label="Search Result"]')).not.toContainText("Driving");
|
await expect(page.locator('[aria-label="Search Result"]')).not.toContainText("Driving");
|
||||||
|
@ -41,6 +41,7 @@
|
|||||||
"karma-sourcemap-loader": "0.3.8",
|
"karma-sourcemap-loader": "0.3.8",
|
||||||
"karma-spec-reporter": "0.0.36",
|
"karma-spec-reporter": "0.0.36",
|
||||||
"karma-webpack": "5.0.0",
|
"karma-webpack": "5.0.0",
|
||||||
|
"kdbush": "^3.0.0",
|
||||||
"location-bar": "3.0.1",
|
"location-bar": "3.0.1",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"mini-css-extract-plugin": "2.7.2",
|
"mini-css-extract-plugin": "2.7.2",
|
||||||
|
@ -256,6 +256,15 @@ define([
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCT's annotation API that enables
|
||||||
|
* human-created comments and categorization linked to data products
|
||||||
|
* @type {module:openmct.AnnotationAPI}
|
||||||
|
* @memberof module:openmct.MCT#
|
||||||
|
* @name annotation
|
||||||
|
*/
|
||||||
|
this.annotation = new api.AnnotationAPI(this);
|
||||||
|
|
||||||
// Plugins that are installed by default
|
// Plugins that are installed by default
|
||||||
this.install(this.plugins.Plot());
|
this.install(this.plugins.Plot());
|
||||||
this.install(this.plugins.TelemetryTable.default());
|
this.install(this.plugins.TelemetryTable.default());
|
||||||
|
@ -52,6 +52,29 @@ const ANNOTATION_LAST_CREATED = 'annotationLastCreated';
|
|||||||
* @property {String} foregroundColor eg. "#ffffff"
|
* @property {String} foregroundColor eg. "#ffffff"
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('../objects/ObjectAPI').DomainObject} DomainObject
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('../objects/ObjectAPI').Identifier} Identifier
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('../../../openmct').OpenMCT} OpenMCT
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An interface for interacting with annotations of domain objects.
|
||||||
|
* An annotation of a domain object is an operator created object for the purposes
|
||||||
|
* of further describing data in plots, notebooks, maps, etc. For example, an annotation
|
||||||
|
* could be a tag on a plot notating an interesting set of points labeled SCIENCE. It could
|
||||||
|
* also be set of notebook entries the operator has tagged DRIVING when a robot monitored by OpenMCT
|
||||||
|
* about rationals behind why the robot has taken a certain path.
|
||||||
|
* Annotations are discoverable using search, and are typically rendered in OpenMCT views to bring attention
|
||||||
|
* to other users.
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
export default class AnnotationAPI extends EventEmitter {
|
export default class AnnotationAPI extends EventEmitter {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -81,24 +104,26 @@ export default class AnnotationAPI extends EventEmitter {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create the a generic annotation
|
* Creates an annotation on a given domain object (e.g., a plot) and a set of targets (e.g., telemetry objects)
|
||||||
* @typedef {Object} CreateAnnotationOptions
|
* @typedef {Object} CreateAnnotationOptions
|
||||||
* @property {String} name a name for the new parameter
|
* @property {String} name a name for the new annotation (e.g., "Plot annnotation")
|
||||||
* @property {import('../objects/ObjectAPI').DomainObject} domainObject the domain object to create
|
* @property {DomainObject} domainObject the domain object this annotation was created with
|
||||||
* @property {ANNOTATION_TYPES} annotationType the type of annotation to create
|
* @property {ANNOTATION_TYPES} annotationType the type of annotation to create (e.g., PLOT_SPATIAL)
|
||||||
* @property {Tag[]} tags
|
* @property {Tag[]} tags tags to add to the annotation, e.g., SCIENCE for science related annotations
|
||||||
* @property {String} contentText
|
* @property {String} contentText Some text to add to the annotation, e.g. ("This annotation is about science")
|
||||||
* @property {import('../objects/ObjectAPI').Identifier[]} targets
|
* @property {Object<string, Object>} targets The targets ID keystrings and their specific properties.
|
||||||
|
* For plots, this will be a bounding box, e.g.: {maxY: 100, minY: 0, maxX: 100, minX: 0}
|
||||||
|
* For notebooks, this will be an entry ID, e.g.: {entryId: "entry-ecb158f5-d23c-45e1-a704-649b382622ba"}
|
||||||
|
* @property {DomainObject>} targetDomainObjects the targets ID keystrings and the domain objects this annotation points to (e.g., telemetry objects for a plot)
|
||||||
*/
|
*/
|
||||||
/**
|
/**
|
||||||
* @method create
|
* @method create
|
||||||
* @param {CreateAnnotationOptions} options
|
* @param {CreateAnnotationOptions} options
|
||||||
* @returns {Promise<import('../objects/ObjectAPI').DomainObject>} a promise which will resolve when the domain object
|
* @returns {Promise<DomainObject>} a promise which will resolve when the domain object
|
||||||
* has been created, or be rejected if it cannot be saved
|
* has been created, or be rejected if it cannot be saved
|
||||||
*/
|
*/
|
||||||
async create({name, domainObject, annotationType, tags, contentText, targets}) {
|
async create({name, domainObject, annotationType, tags, contentText, targets, targetDomainObjects}) {
|
||||||
if (!Object.keys(this.ANNOTATION_TYPES).includes(annotationType)) {
|
if (!Object.keys(this.ANNOTATION_TYPES).includes(annotationType)) {
|
||||||
throw new Error(`Unknown annotation type: ${annotationType}`);
|
throw new Error(`Unknown annotation type: ${annotationType}`);
|
||||||
}
|
}
|
||||||
@ -107,6 +132,10 @@ export default class AnnotationAPI extends EventEmitter {
|
|||||||
throw new Error(`At least one target is required to create an annotation`);
|
throw new Error(`At least one target is required to create an annotation`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!Object.keys(targetDomainObjects).length) {
|
||||||
|
throw new Error(`At least one targetDomainObject is required to create an annotation`);
|
||||||
|
}
|
||||||
|
|
||||||
const domainObjectKeyString = this.openmct.objects.makeKeyString(domainObject.identifier);
|
const domainObjectKeyString = this.openmct.objects.makeKeyString(domainObject.identifier);
|
||||||
const originalPathObjects = await this.openmct.objects.getOriginalPath(domainObjectKeyString);
|
const originalPathObjects = await this.openmct.objects.getOriginalPath(domainObjectKeyString);
|
||||||
const originalContextPath = this.openmct.objects.getRelativePath(originalPathObjects);
|
const originalContextPath = this.openmct.objects.getRelativePath(originalPathObjects);
|
||||||
@ -139,7 +168,9 @@ export default class AnnotationAPI extends EventEmitter {
|
|||||||
const success = await this.openmct.objects.save(createdObject);
|
const success = await this.openmct.objects.save(createdObject);
|
||||||
if (success) {
|
if (success) {
|
||||||
this.emit('annotationCreated', createdObject);
|
this.emit('annotationCreated', createdObject);
|
||||||
this.#updateAnnotationModified(domainObject);
|
Object.values(targetDomainObjects).forEach(targetDomainObject => {
|
||||||
|
this.#updateAnnotationModified(targetDomainObject);
|
||||||
|
});
|
||||||
|
|
||||||
return createdObject;
|
return createdObject;
|
||||||
} else {
|
} else {
|
||||||
@ -147,8 +178,15 @@ export default class AnnotationAPI extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#updateAnnotationModified(domainObject) {
|
#updateAnnotationModified(targetDomainObject) {
|
||||||
this.openmct.objects.mutate(domainObject, this.ANNOTATION_LAST_CREATED, Date.now());
|
// As certain telemetry objects are immutable, we'll need to check here first
|
||||||
|
// to see if we can add the annotation last created property.
|
||||||
|
// TODO: This should be removed once we have a better way to handle immutable telemetry objects
|
||||||
|
if (targetDomainObject.isMutable) {
|
||||||
|
this.openmct.objects.mutate(targetDomainObject, this.ANNOTATION_LAST_CREATED, Date.now());
|
||||||
|
} else {
|
||||||
|
this.emit('targetDomainObjectAnnotated', targetDomainObject);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -162,7 +200,7 @@ export default class AnnotationAPI extends EventEmitter {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @method isAnnotation
|
* @method isAnnotation
|
||||||
* @param {import('../objects/ObjectAPI').DomainObject} domainObject domainObject the domainObject in question
|
* @param {DomainObject} domainObject the domainObject in question
|
||||||
* @returns {Boolean} Returns true if the domain object is an annotation
|
* @returns {Boolean} Returns true if the domain object is an annotation
|
||||||
*/
|
*/
|
||||||
isAnnotation(domainObject) {
|
isAnnotation(domainObject) {
|
||||||
@ -190,56 +228,19 @@ export default class AnnotationAPI extends EventEmitter {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @method getAnnotations
|
* @method getAnnotations
|
||||||
* @param {String} query - The keystring of the domain object to search for annotations for
|
* @param {Identifier} domainObjectIdentifier - The domain object identifier to use to search for annotations. For example, a telemetry object identifier.
|
||||||
* @returns {import('../objects/ObjectAPI').DomainObject[]} Returns an array of domain objects that match the search query
|
* @returns {DomainObject[]} Returns an array of annotations that match the search query
|
||||||
*/
|
*/
|
||||||
async getAnnotations(query) {
|
async getAnnotations(domainObjectIdentifier) {
|
||||||
const searchResults = (await Promise.all(this.openmct.objects.search(query, null, this.openmct.objects.SEARCH_TYPES.ANNOTATIONS))).flat();
|
const keyStringQuery = this.openmct.objects.makeKeyString(domainObjectIdentifier);
|
||||||
|
const searchResults = (await Promise.all(this.openmct.objects.search(keyStringQuery, null, this.openmct.objects.SEARCH_TYPES.ANNOTATIONS))).flat();
|
||||||
|
|
||||||
return searchResults;
|
return searchResults;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @method addSingleAnnotationTag
|
|
||||||
* @param {import('../objects/ObjectAPI').DomainObject=} existingAnnotation - An optional annotation to add the tag to. If not specified, we will create an annotation.
|
|
||||||
* @param {import('../objects/ObjectAPI').DomainObject} targetDomainObject - The domain object the annotation will point to.
|
|
||||||
* @param {Object=} targetSpecificDetails - Optional object to add to the target object. E.g., for notebooks this would be an entryID
|
|
||||||
* @param {AnnotationType} annotationType - The type of annotation this is for.
|
|
||||||
* @returns {import('../objects/ObjectAPI').DomainObject[]} Returns the annotation that was either created or passed as an existingAnnotation
|
|
||||||
*/
|
|
||||||
async addSingleAnnotationTag(existingAnnotation, targetDomainObject, targetSpecificDetails, annotationType, tag) {
|
|
||||||
if (!existingAnnotation) {
|
|
||||||
const targets = {};
|
|
||||||
const targetKeyString = this.openmct.objects.makeKeyString(targetDomainObject.identifier);
|
|
||||||
targets[targetKeyString] = targetSpecificDetails;
|
|
||||||
const contentText = `${annotationType} tag`;
|
|
||||||
const annotationCreationArguments = {
|
|
||||||
name: contentText,
|
|
||||||
domainObject: targetDomainObject,
|
|
||||||
annotationType,
|
|
||||||
tags: [tag],
|
|
||||||
contentText,
|
|
||||||
targets
|
|
||||||
};
|
|
||||||
const newAnnotation = await this.create(annotationCreationArguments);
|
|
||||||
|
|
||||||
return newAnnotation;
|
|
||||||
} else {
|
|
||||||
if (!existingAnnotation.tags.includes(tag)) {
|
|
||||||
throw new Error(`Existing annotation did not contain tag ${tag}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existingAnnotation._deleted) {
|
|
||||||
this.unDeleteAnnotation(existingAnnotation);
|
|
||||||
}
|
|
||||||
|
|
||||||
return existingAnnotation;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @method deleteAnnotations
|
* @method deleteAnnotations
|
||||||
* @param {import('../objects/ObjectAPI').DomainObject[]} existingAnnotation - An array of annotations to delete (set _deleted to true)
|
* @param {DomainObject[]} existingAnnotation - An array of annotations to delete (set _deleted to true)
|
||||||
*/
|
*/
|
||||||
deleteAnnotations(annotations) {
|
deleteAnnotations(annotations) {
|
||||||
if (!annotations) {
|
if (!annotations) {
|
||||||
@ -255,7 +256,7 @@ export default class AnnotationAPI extends EventEmitter {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @method deleteAnnotations
|
* @method deleteAnnotations
|
||||||
* @param {import('../objects/ObjectAPI').DomainObject} existingAnnotation - An annotations to undelete (set _deleted to false)
|
* @param {DomainObject} annotation - An annotation to undelete (set _deleted to false)
|
||||||
*/
|
*/
|
||||||
unDeleteAnnotation(annotation) {
|
unDeleteAnnotation(annotation) {
|
||||||
if (!annotation) {
|
if (!annotation) {
|
||||||
@ -265,6 +266,39 @@ export default class AnnotationAPI extends EventEmitter {
|
|||||||
this.openmct.objects.mutate(annotation, '_deleted', false);
|
this.openmct.objects.mutate(annotation, '_deleted', false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getTagsFromAnnotations(annotations, filterDuplicates = true) {
|
||||||
|
if (!annotations) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
let tagsFromAnnotations = annotations.flatMap((annotation) => {
|
||||||
|
if (annotation._deleted) {
|
||||||
|
return [];
|
||||||
|
} else {
|
||||||
|
return annotation.tags;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (filterDuplicates) {
|
||||||
|
tagsFromAnnotations = tagsFromAnnotations.filter((tag, index, tagArray) => {
|
||||||
|
return tagArray.indexOf(tag) === index;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullTagModels = this.#addTagMetaInformationToTags(tagsFromAnnotations);
|
||||||
|
|
||||||
|
return fullTagModels;
|
||||||
|
}
|
||||||
|
|
||||||
|
#addTagMetaInformationToTags(tags) {
|
||||||
|
return tags.map(tagKey => {
|
||||||
|
const tagModel = this.availableTags[tagKey];
|
||||||
|
tagModel.tagID = tagKey;
|
||||||
|
|
||||||
|
return tagModel;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#getMatchingTags(query) {
|
#getMatchingTags(query) {
|
||||||
if (!query) {
|
if (!query) {
|
||||||
return [];
|
return [];
|
||||||
@ -283,12 +317,7 @@ export default class AnnotationAPI extends EventEmitter {
|
|||||||
|
|
||||||
#addTagMetaInformationToResults(results, matchingTagKeys) {
|
#addTagMetaInformationToResults(results, matchingTagKeys) {
|
||||||
const tagsAddedToResults = results.map(result => {
|
const tagsAddedToResults = results.map(result => {
|
||||||
const fullTagModels = result.tags.map(tagKey => {
|
const fullTagModels = this.#addTagMetaInformationToTags(result.tags);
|
||||||
const tagModel = this.availableTags[tagKey];
|
|
||||||
tagModel.tagID = tagKey;
|
|
||||||
|
|
||||||
return tagModel;
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
fullTagModels,
|
fullTagModels,
|
||||||
@ -338,6 +367,33 @@ export default class AnnotationAPI extends EventEmitter {
|
|||||||
return combinedResults;
|
return combinedResults;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @method #breakApartSeparateTargets
|
||||||
|
* @param {Array} results A set of search results that could have the multiple targets for the same result
|
||||||
|
* @returns {Array} The same set of results, but with each target separated out into its own result
|
||||||
|
*/
|
||||||
|
#breakApartSeparateTargets(results) {
|
||||||
|
const separateResults = [];
|
||||||
|
results.forEach(result => {
|
||||||
|
Object.keys(result.targets).forEach(targetID => {
|
||||||
|
const separatedResult = {
|
||||||
|
...result
|
||||||
|
};
|
||||||
|
separatedResult.targets = {
|
||||||
|
[targetID]: result.targets[targetID]
|
||||||
|
};
|
||||||
|
separatedResult.targetModels = result.targetModels.filter(targetModel => {
|
||||||
|
const targetKeyString = this.openmct.objects.makeKeyString(targetModel.identifier);
|
||||||
|
|
||||||
|
return targetKeyString === targetID;
|
||||||
|
});
|
||||||
|
separateResults.push(separatedResult);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return separateResults;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @method searchForTags
|
* @method searchForTags
|
||||||
* @param {String} query A query to match against tags. E.g., "dr" will match the tags "drilling" and "driving"
|
* @param {String} query A query to match against tags. E.g., "dr" will match the tags "drilling" and "driving"
|
||||||
@ -360,7 +416,8 @@ export default class AnnotationAPI extends EventEmitter {
|
|||||||
const resultsWithValidPath = appliedTargetsModels.filter(result => {
|
const resultsWithValidPath = appliedTargetsModels.filter(result => {
|
||||||
return this.openmct.objects.isReachable(result.targetModels?.[0]?.originalPath);
|
return this.openmct.objects.isReachable(result.targetModels?.[0]?.originalPath);
|
||||||
});
|
});
|
||||||
|
const breakApartSeparateTargets = this.#breakApartSeparateTargets(resultsWithValidPath);
|
||||||
|
|
||||||
return resultsWithValidPath;
|
return breakApartSeparateTargets;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -108,6 +108,7 @@ describe("The Annotation API", () => {
|
|||||||
annotationType: openmct.annotation.ANNOTATION_TYPES.NOTEBOOK,
|
annotationType: openmct.annotation.ANNOTATION_TYPES.NOTEBOOK,
|
||||||
tags: ['sometag'],
|
tags: ['sometag'],
|
||||||
contentText: "fooContext",
|
contentText: "fooContext",
|
||||||
|
targetDomainObjects: [mockDomainObject],
|
||||||
targets: {'fooTarget': {}}
|
targets: {'fooTarget': {}}
|
||||||
};
|
};
|
||||||
const annotationObject = await openmct.annotation.create(annotationCreationArguments);
|
const annotationObject = await openmct.annotation.create(annotationCreationArguments);
|
||||||
@ -124,27 +125,39 @@ describe("The Annotation API", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("Tagging", () => {
|
describe("Tagging", () => {
|
||||||
|
let tagCreationArguments;
|
||||||
|
beforeEach(() => {
|
||||||
|
tagCreationArguments = {
|
||||||
|
name: 'Test Annotation',
|
||||||
|
domainObject: mockDomainObject,
|
||||||
|
annotationType: openmct.annotation.ANNOTATION_TYPES.NOTEBOOK,
|
||||||
|
tags: ['aWonderfulTag'],
|
||||||
|
contentText: 'fooContext',
|
||||||
|
targets: {'fooNameSpace:some-object': {entryId: 'fooBarEntry'}},
|
||||||
|
targetDomainObjects: [mockDomainObject]
|
||||||
|
};
|
||||||
|
});
|
||||||
it("can create a tag", async () => {
|
it("can create a tag", async () => {
|
||||||
const annotationObject = await openmct.annotation.addSingleAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
|
const annotationObject = await openmct.annotation.create(tagCreationArguments);
|
||||||
expect(annotationObject).toBeDefined();
|
expect(annotationObject).toBeDefined();
|
||||||
expect(annotationObject.type).toEqual('annotation');
|
expect(annotationObject.type).toEqual('annotation');
|
||||||
expect(annotationObject.tags).toContain('aWonderfulTag');
|
expect(annotationObject.tags).toContain('aWonderfulTag');
|
||||||
});
|
});
|
||||||
it("can delete a tag", async () => {
|
it("can delete a tag", async () => {
|
||||||
const annotationObject = await openmct.annotation.addSingleAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
|
const annotationObject = await openmct.annotation.create(tagCreationArguments);
|
||||||
expect(annotationObject).toBeDefined();
|
expect(annotationObject).toBeDefined();
|
||||||
openmct.annotation.deleteAnnotations([annotationObject]);
|
openmct.annotation.deleteAnnotations([annotationObject]);
|
||||||
expect(annotationObject._deleted).toBeTrue();
|
expect(annotationObject._deleted).toBeTrue();
|
||||||
});
|
});
|
||||||
it("throws an error if deleting non-existent tag", async () => {
|
it("throws an error if deleting non-existent tag", async () => {
|
||||||
const annotationObject = await openmct.annotation.addSingleAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
|
const annotationObject = await openmct.annotation.create(tagCreationArguments);
|
||||||
expect(annotationObject).toBeDefined();
|
expect(annotationObject).toBeDefined();
|
||||||
expect(() => {
|
expect(() => {
|
||||||
openmct.annotation.removeAnnotationTag(annotationObject, 'ThisTagShouldNotExist');
|
openmct.annotation.removeAnnotationTag(annotationObject, 'ThisTagShouldNotExist');
|
||||||
}).toThrow();
|
}).toThrow();
|
||||||
});
|
});
|
||||||
it("can remove all tags", async () => {
|
it("can remove all tags", async () => {
|
||||||
const annotationObject = await openmct.annotation.addSingleAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
|
const annotationObject = await openmct.annotation.create(tagCreationArguments);
|
||||||
expect(annotationObject).toBeDefined();
|
expect(annotationObject).toBeDefined();
|
||||||
expect(() => {
|
expect(() => {
|
||||||
openmct.annotation.deleteAnnotations([annotationObject]);
|
openmct.annotation.deleteAnnotations([annotationObject]);
|
||||||
@ -152,13 +165,13 @@ describe("The Annotation API", () => {
|
|||||||
expect(annotationObject._deleted).toBeTrue();
|
expect(annotationObject._deleted).toBeTrue();
|
||||||
});
|
});
|
||||||
it("can add/delete/add a tag", async () => {
|
it("can add/delete/add a tag", async () => {
|
||||||
let annotationObject = await openmct.annotation.addSingleAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
|
let annotationObject = await openmct.annotation.create(tagCreationArguments);
|
||||||
expect(annotationObject).toBeDefined();
|
expect(annotationObject).toBeDefined();
|
||||||
expect(annotationObject.type).toEqual('annotation');
|
expect(annotationObject.type).toEqual('annotation');
|
||||||
expect(annotationObject.tags).toContain('aWonderfulTag');
|
expect(annotationObject.tags).toContain('aWonderfulTag');
|
||||||
openmct.annotation.deleteAnnotations([annotationObject]);
|
openmct.annotation.deleteAnnotations([annotationObject]);
|
||||||
expect(annotationObject._deleted).toBeTrue();
|
expect(annotationObject._deleted).toBeTrue();
|
||||||
annotationObject = await openmct.annotation.addSingleAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
|
annotationObject = await openmct.annotation.create(tagCreationArguments);
|
||||||
expect(annotationObject).toBeDefined();
|
expect(annotationObject).toBeDefined();
|
||||||
expect(annotationObject.type).toEqual('annotation');
|
expect(annotationObject.type).toEqual('annotation');
|
||||||
expect(annotationObject.tags).toContain('aWonderfulTag');
|
expect(annotationObject.tags).toContain('aWonderfulTag');
|
||||||
|
@ -162,10 +162,12 @@
|
|||||||
:selected-section="selectedSection"
|
:selected-section="selectedSection"
|
||||||
:read-only="false"
|
:read-only="false"
|
||||||
:is-locked="selectedPage.isLocked"
|
:is-locked="selectedPage.isLocked"
|
||||||
|
:selected-entry-id="selectedEntryId"
|
||||||
@cancelEdit="cancelTransaction"
|
@cancelEdit="cancelTransaction"
|
||||||
@editingEntry="startTransaction"
|
@editingEntry="startTransaction"
|
||||||
@deleteEntry="deleteEntry"
|
@deleteEntry="deleteEntry"
|
||||||
@updateEntry="updateEntry"
|
@updateEntry="updateEntry"
|
||||||
|
@entry-selection="entrySelection(entry)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@ -234,6 +236,7 @@ export default {
|
|||||||
sidebarCoversEntries: false,
|
sidebarCoversEntries: false,
|
||||||
filteredAndSortedEntries: [],
|
filteredAndSortedEntries: [],
|
||||||
notebookAnnotations: {},
|
notebookAnnotations: {},
|
||||||
|
selectedEntryId: '',
|
||||||
activeTransaction: false,
|
activeTransaction: false,
|
||||||
savingTransaction: false
|
savingTransaction: false
|
||||||
};
|
};
|
||||||
@ -321,6 +324,7 @@ export default {
|
|||||||
this.formatSidebar();
|
this.formatSidebar();
|
||||||
this.setSectionAndPageFromUrl();
|
this.setSectionAndPageFromUrl();
|
||||||
|
|
||||||
|
this.openmct.selection.on('change', this.updateSelection);
|
||||||
this.transaction = null;
|
this.transaction = null;
|
||||||
|
|
||||||
window.addEventListener('orientationchange', this.formatSidebar);
|
window.addEventListener('orientationchange', this.formatSidebar);
|
||||||
@ -346,6 +350,7 @@ export default {
|
|||||||
|
|
||||||
window.removeEventListener('orientationchange', this.formatSidebar);
|
window.removeEventListener('orientationchange', this.formatSidebar);
|
||||||
window.removeEventListener('hashchange', this.setSectionAndPageFromUrl);
|
window.removeEventListener('hashchange', this.setSectionAndPageFromUrl);
|
||||||
|
this.openmct.selection.off('change', this.updateSelection);
|
||||||
},
|
},
|
||||||
updated: function () {
|
updated: function () {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
@ -375,15 +380,20 @@ export default {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
updateSelection(selection) {
|
||||||
|
if (selection?.[0]?.[1]?.context?.targetDetails?.entryId === undefined) {
|
||||||
|
this.selectedEntryId = '';
|
||||||
|
}
|
||||||
|
},
|
||||||
async loadAnnotations() {
|
async loadAnnotations() {
|
||||||
if (!this.openmct.annotation.getAvailableTags().length) {
|
if (!this.openmct.annotation.getAvailableTags().length) {
|
||||||
|
// don't bother loading annotations if there are no tags
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.lastLocalAnnotationCreation = this.domainObject.annotationLastCreated ?? 0;
|
this.lastLocalAnnotationCreation = this.domainObject.annotationLastCreated ?? 0;
|
||||||
|
|
||||||
const query = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
const foundAnnotations = await this.openmct.annotation.getAnnotations(this.domainObject.identifier);
|
||||||
const foundAnnotations = await this.openmct.annotation.getAnnotations(query);
|
|
||||||
foundAnnotations.forEach((foundAnnotation) => {
|
foundAnnotations.forEach((foundAnnotation) => {
|
||||||
const targetId = Object.keys(foundAnnotation.targets)[0];
|
const targetId = Object.keys(foundAnnotation.targets)[0];
|
||||||
const entryId = foundAnnotation.targets[targetId].entryId;
|
const entryId = foundAnnotation.targets[targetId].entryId;
|
||||||
@ -941,6 +951,9 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
entrySelection(entry) {
|
||||||
|
this.selectedEntryId = entry.id;
|
||||||
|
},
|
||||||
endTransaction() {
|
endTransaction() {
|
||||||
this.openmct.objects.endTransaction();
|
this.openmct.objects.endTransaction();
|
||||||
this.transaction = null;
|
this.transaction = null;
|
||||||
|
@ -22,12 +22,13 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="c-notebook__entry c-ne has-local-controls has-tag-applier"
|
class="c-notebook__entry c-ne has-local-controls"
|
||||||
aria-label="Notebook Entry"
|
aria-label="Notebook Entry"
|
||||||
:class="{ 'locked': isLocked }"
|
:class="{ 'locked': isLocked, 'is-selected': isSelectedEntry }"
|
||||||
@dragover="changeCursor"
|
@dragover="changeCursor"
|
||||||
@drop.capture="cancelEditMode"
|
@drop.capture="cancelEditMode"
|
||||||
@drop.prevent="dropOnEntry"
|
@drop.prevent="dropOnEntry"
|
||||||
|
@click="selectEntry($event, entry)"
|
||||||
>
|
>
|
||||||
<div class="c-ne__time-and-content">
|
<div class="c-ne__time-and-content">
|
||||||
<div class="c-ne__time-and-creator">
|
<div class="c-ne__time-and-creator">
|
||||||
@ -82,13 +83,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<TagEditor
|
<div>
|
||||||
:domain-object="domainObject"
|
<div
|
||||||
:annotations="notebookAnnotations"
|
v-for="(tag, index) in entryTags"
|
||||||
:annotation-type="openmct.annotation.ANNOTATION_TYPES.NOTEBOOK"
|
:key="index"
|
||||||
:target-specific-details="{entryId: entry.id}"
|
class="c-tag"
|
||||||
@tags-updated="timestampAndUpdate"
|
:style="{ backgroundColor: tag.backgroundColor, color: tag.foregroundColor }"
|
||||||
/>
|
>
|
||||||
|
{{ tag.label }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="c-snapshots c-ne__embeds">
|
<div class="c-snapshots c-ne__embeds">
|
||||||
<NotebookEmbed
|
<NotebookEmbed
|
||||||
@ -139,7 +143,6 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import NotebookEmbed from './NotebookEmbed.vue';
|
import NotebookEmbed from './NotebookEmbed.vue';
|
||||||
import TagEditor from '../../../ui/components/tags/TagEditor.vue';
|
|
||||||
import TextHighlight from '../../../utils/textHighlight/TextHighlight.vue';
|
import TextHighlight from '../../../utils/textHighlight/TextHighlight.vue';
|
||||||
import { createNewEmbed } from '../utils/notebook-entries';
|
import { createNewEmbed } from '../utils/notebook-entries';
|
||||||
import { saveNotebookImageDomainObject, updateNamespaceOfDomainObject } from '../utils/notebook-image';
|
import { saveNotebookImageDomainObject, updateNamespaceOfDomainObject } from '../utils/notebook-image';
|
||||||
@ -151,8 +154,7 @@ const UNKNOWN_USER = 'Unknown';
|
|||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
NotebookEmbed,
|
NotebookEmbed,
|
||||||
TextHighlight,
|
TextHighlight
|
||||||
TagEditor
|
|
||||||
},
|
},
|
||||||
inject: ['openmct', 'snapshotContainer'],
|
inject: ['openmct', 'snapshotContainer'],
|
||||||
props: {
|
props: {
|
||||||
@ -203,6 +205,10 @@ export default {
|
|||||||
default() {
|
default() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
selectedEntryId: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -212,6 +218,14 @@ export default {
|
|||||||
createdOnTime() {
|
createdOnTime() {
|
||||||
return this.formatTime(this.entry.createdOn, 'HH:mm:ss');
|
return this.formatTime(this.entry.createdOn, 'HH:mm:ss');
|
||||||
},
|
},
|
||||||
|
isSelectedEntry() {
|
||||||
|
return this.selectedEntryId === this.entry.id;
|
||||||
|
},
|
||||||
|
entryTags() {
|
||||||
|
const tagsFromAnnotations = this.openmct.annotation.getTagsFromAnnotations(this.notebookAnnotations);
|
||||||
|
|
||||||
|
return tagsFromAnnotations;
|
||||||
|
},
|
||||||
entryText() {
|
entryText() {
|
||||||
let text = this.entry.text;
|
let text = this.entry.text;
|
||||||
|
|
||||||
@ -357,6 +371,38 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
this.$emit('cancelEdit');
|
this.$emit('cancelEdit');
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
selectEntry(event, entry) {
|
||||||
|
const targetDetails = {};
|
||||||
|
const keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||||
|
targetDetails[keyString] = {
|
||||||
|
entryId: entry.id
|
||||||
|
};
|
||||||
|
const targetDomainObjects = {};
|
||||||
|
targetDomainObjects[keyString] = this.domainObject;
|
||||||
|
this.openmct.selection.select(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
element: this.openmct.layout.$refs.browseObject.$el,
|
||||||
|
context: {
|
||||||
|
item: this.domainObject
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: event.currentTarget,
|
||||||
|
context: {
|
||||||
|
type: 'notebook-entry-selection',
|
||||||
|
targetDetails,
|
||||||
|
targetDomainObjects,
|
||||||
|
annotations: this.notebookAnnotations,
|
||||||
|
annotationType: this.openmct.annotation.ANNOTATION_TYPES.NOTEBOOK,
|
||||||
|
onAnnotationChange: this.timestampAndUpdate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
false);
|
||||||
|
event.stopPropagation();
|
||||||
|
this.$emit('entry-selection', this.entry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -85,7 +85,10 @@
|
|||||||
<mct-chart
|
<mct-chart
|
||||||
:rectangles="rectangles"
|
:rectangles="rectangles"
|
||||||
:highlights="highlights"
|
:highlights="highlights"
|
||||||
|
:annotated-points="annotatedPoints"
|
||||||
|
:annotation-selections="annotationSelections"
|
||||||
:show-limit-line-labels="showLimitLineLabels"
|
:show-limit-line-labels="showLimitLineLabels"
|
||||||
|
:annotation-viewing-and-editing-allowed="annotationViewingAndEditingAllowed"
|
||||||
@plotReinitializeCanvas="initCanvas"
|
@plotReinitializeCanvas="initCanvas"
|
||||||
@chartLoaded="initialize"
|
@chartLoaded="initialize"
|
||||||
/>
|
/>
|
||||||
@ -211,6 +214,7 @@ import MctTicks from "./MctTicks.vue";
|
|||||||
import MctChart from "./chart/MctChart.vue";
|
import MctChart from "./chart/MctChart.vue";
|
||||||
import XAxis from "./axis/XAxis.vue";
|
import XAxis from "./axis/XAxis.vue";
|
||||||
import YAxis from "./axis/YAxis.vue";
|
import YAxis from "./axis/YAxis.vue";
|
||||||
|
import KDBush from 'kdbush';
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
|
|
||||||
const OFFSET_THRESHOLD = 10;
|
const OFFSET_THRESHOLD = 10;
|
||||||
@ -268,6 +272,8 @@ export default {
|
|||||||
return {
|
return {
|
||||||
altPressed: false,
|
altPressed: false,
|
||||||
highlights: [],
|
highlights: [],
|
||||||
|
annotatedPoints: [],
|
||||||
|
annotationSelections: [],
|
||||||
lockHighlightPoint: false,
|
lockHighlightPoint: false,
|
||||||
tickWidth: 0,
|
tickWidth: 0,
|
||||||
yKeyOptions: [],
|
yKeyOptions: [],
|
||||||
@ -298,6 +304,10 @@ export default {
|
|||||||
isFrozen() {
|
isFrozen() {
|
||||||
return this.config.xAxis.get('frozen') === true && this.config.yAxis.get('frozen') === true;
|
return this.config.xAxis.get('frozen') === true && this.config.yAxis.get('frozen') === true;
|
||||||
},
|
},
|
||||||
|
annotationViewingAndEditingAllowed() {
|
||||||
|
// only allow annotations viewing/editing if plot is paused or in fixed time mode
|
||||||
|
return this.isFrozen || !this.isRealTime;
|
||||||
|
},
|
||||||
plotLegendPositionClass() {
|
plotLegendPositionClass() {
|
||||||
return !this.isNestedWithinAStackedPlot ? `plot-legend-${this.config.legend.get('position')}` : '';
|
return !this.isNestedWithinAStackedPlot ? `plot-legend-${this.config.legend.get('position')}` : '';
|
||||||
},
|
},
|
||||||
@ -361,16 +371,81 @@ export default {
|
|||||||
this.removeStatusListener = this.openmct.status.observe(this.domainObject.identifier, this.updateStatus);
|
this.removeStatusListener = this.openmct.status.observe(this.domainObject.identifier, this.updateStatus);
|
||||||
|
|
||||||
this.openmct.objectViews.on('clearData', this.clearData);
|
this.openmct.objectViews.on('clearData', this.clearData);
|
||||||
|
this.$on('loadingUpdated', this.loadAnnotations);
|
||||||
|
this.openmct.selection.on('change', this.updateSelection);
|
||||||
this.setTimeContext();
|
this.setTimeContext();
|
||||||
|
|
||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
|
this.openmct.selection.off('change', this.updateSelection);
|
||||||
document.removeEventListener('keydown', this.handleKeyDown);
|
document.removeEventListener('keydown', this.handleKeyDown);
|
||||||
document.removeEventListener('keyup', this.handleKeyUp);
|
document.removeEventListener('keyup', this.handleKeyUp);
|
||||||
this.destroy();
|
this.destroy();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
updateSelection(selection) {
|
||||||
|
const selectionContext = selection?.[0]?.[0]?.context?.item;
|
||||||
|
if (!selectionContext
|
||||||
|
|| this.openmct.objects.areIdsEqual(selectionContext.identifier, this.domainObject.identifier)) {
|
||||||
|
// Selection changed, but it's us, so ignoring it
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectionType = selection?.[0]?.[1]?.context?.type;
|
||||||
|
if (selectionType !== 'plot-points-selection') {
|
||||||
|
// wrong type of selection
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentXaxis = this.config.xAxis.get('displayRange');
|
||||||
|
const currentYaxis = this.config.yAxis.get('displayRange');
|
||||||
|
|
||||||
|
// when there is no plot data, the ranges can be undefined
|
||||||
|
// in which case we should not perform selection
|
||||||
|
if (!currentXaxis || !currentYaxis) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedAnnotations = selection?.[0]?.[1]?.context?.annotations;
|
||||||
|
if (selectedAnnotations?.length) {
|
||||||
|
// just use first annotation
|
||||||
|
const boundingBoxes = Object.values(selectedAnnotations[0].targets);
|
||||||
|
let minX = Number.MAX_SAFE_INTEGER;
|
||||||
|
let minY = Number.MAX_SAFE_INTEGER;
|
||||||
|
let maxX = Number.MIN_SAFE_INTEGER;
|
||||||
|
let maxY = Number.MIN_SAFE_INTEGER;
|
||||||
|
boundingBoxes.forEach(boundingBox => {
|
||||||
|
if (boundingBox.minX < minX) {
|
||||||
|
minX = boundingBox.minX;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (boundingBox.maxX > maxX) {
|
||||||
|
maxX = boundingBox.maxX;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (boundingBox.maxY > maxY) {
|
||||||
|
maxY = boundingBox.maxY;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (boundingBox.minY < minY) {
|
||||||
|
minY = boundingBox.minY;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.config.xAxis.set('displayRange', {
|
||||||
|
min: minX,
|
||||||
|
max: maxX
|
||||||
|
});
|
||||||
|
this.config.yAxis.set('displayRange', {
|
||||||
|
min: minY,
|
||||||
|
max: maxY
|
||||||
|
});
|
||||||
|
this.zoom('out', 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.prepareExistingAnnotationSelection(selectedAnnotations);
|
||||||
|
},
|
||||||
handleKeyDown(event) {
|
handleKeyDown(event) {
|
||||||
if (event.key === 'Alt') {
|
if (event.key === 'Alt') {
|
||||||
this.altPressed = true;
|
this.altPressed = true;
|
||||||
@ -445,7 +520,21 @@ export default {
|
|||||||
this.checkSameRangeValue();
|
this.checkSameRangeValue();
|
||||||
this.stopListening(plotSeries);
|
this.stopListening(plotSeries);
|
||||||
},
|
},
|
||||||
|
async loadAnnotations() {
|
||||||
|
if (!this.openmct.annotation.getAvailableTags().length) {
|
||||||
|
// don't bother loading annotations if there are no tags
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawAnnotationsForPlot = [];
|
||||||
|
await Promise.all(this.seriesModels.map(async (seriesModel) => {
|
||||||
|
const seriesAnnotations = await this.openmct.annotation.getAnnotations(seriesModel.model.identifier);
|
||||||
|
rawAnnotationsForPlot.push(...seriesAnnotations);
|
||||||
|
}));
|
||||||
|
if (rawAnnotationsForPlot) {
|
||||||
|
this.annotatedPoints = this.findAnnotationPoints(rawAnnotationsForPlot);
|
||||||
|
}
|
||||||
|
},
|
||||||
loadSeriesData(series) {
|
loadSeriesData(series) {
|
||||||
//this check ensures that duplicate requests don't happen on load
|
//this check ensures that duplicate requests don't happen on load
|
||||||
if (!this.timeContext) {
|
if (!this.timeContext) {
|
||||||
@ -469,8 +558,7 @@ export default {
|
|||||||
end: bounds.end
|
end: bounds.end
|
||||||
};
|
};
|
||||||
|
|
||||||
series.load(options)
|
series.load(options).then(this.stopLoading.bind(this));
|
||||||
.then(this.stopLoading.bind(this));
|
|
||||||
},
|
},
|
||||||
|
|
||||||
loadMoreData(range, purge) {
|
loadMoreData(range, purge) {
|
||||||
@ -662,10 +750,83 @@ export default {
|
|||||||
this.listenTo(this.canvas, 'mousemove', this.trackMousePosition, this);
|
this.listenTo(this.canvas, 'mousemove', this.trackMousePosition, this);
|
||||||
this.listenTo(this.canvas, 'mouseleave', this.untrackMousePosition, this);
|
this.listenTo(this.canvas, 'mouseleave', this.untrackMousePosition, this);
|
||||||
this.listenTo(this.canvas, 'mousedown', this.onMouseDown, this);
|
this.listenTo(this.canvas, 'mousedown', this.onMouseDown, this);
|
||||||
|
this.listenTo(this.canvas, 'click', this.selectNearbyAnnotations, this);
|
||||||
this.listenTo(this.canvas, 'wheel', this.wheelZoom, this);
|
this.listenTo(this.canvas, 'wheel', this.wheelZoom, this);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
marqueeAnnotations(annotationsToSelect) {
|
||||||
|
annotationsToSelect.forEach(annotationToSelect => {
|
||||||
|
const firstTargetKeyString = Object.keys(annotationToSelect.targets)[0];
|
||||||
|
const firstTarget = annotationToSelect.targets[firstTargetKeyString];
|
||||||
|
const rectangle = {
|
||||||
|
start: {
|
||||||
|
x: firstTarget.minX,
|
||||||
|
y: firstTarget.minY
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
x: firstTarget.maxX,
|
||||||
|
y: firstTarget.maxY
|
||||||
|
},
|
||||||
|
color: [1, 1, 1, 0.10]
|
||||||
|
};
|
||||||
|
this.rectangles.push(rectangle);
|
||||||
|
|
||||||
|
});
|
||||||
|
},
|
||||||
|
gatherNearbyAnnotations() {
|
||||||
|
const nearbyAnnotations = [];
|
||||||
|
this.config.series.models.forEach(series => {
|
||||||
|
if (series.closest.annotationsById) {
|
||||||
|
Object.values(series.closest.annotationsById).forEach(closeAnnotation => {
|
||||||
|
const addedAnnotationAlready = nearbyAnnotations.some(annotation => {
|
||||||
|
return _.isEqual(annotation.targets, closeAnnotation.targets)
|
||||||
|
&& _.isEqual(annotation.tags, closeAnnotation.tags);
|
||||||
|
});
|
||||||
|
if (!addedAnnotationAlready) {
|
||||||
|
nearbyAnnotations.push(closeAnnotation);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return nearbyAnnotations;
|
||||||
|
},
|
||||||
|
|
||||||
|
prepareExistingAnnotationSelection(annotations) {
|
||||||
|
const targetDomainObjects = {};
|
||||||
|
this.config.series.models.forEach(series => {
|
||||||
|
targetDomainObjects[series.keyString] = series.domainObject;
|
||||||
|
});
|
||||||
|
|
||||||
|
const targetDetails = {};
|
||||||
|
const uniqueBoundsAnnotations = [];
|
||||||
|
annotations.forEach(annotation => {
|
||||||
|
Object.entries(annotation.targets).forEach(([key, value]) => {
|
||||||
|
targetDetails[key] = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const boundingBoxAlreadyAdded = uniqueBoundsAnnotations.some(existingAnnotation => {
|
||||||
|
const existingBoundingBox = Object.values(existingAnnotation.targets)[0];
|
||||||
|
const newBoundingBox = Object.values(annotation.targets)[0];
|
||||||
|
|
||||||
|
return (existingBoundingBox.minX === newBoundingBox.minX
|
||||||
|
&& existingBoundingBox.minY === newBoundingBox.minY
|
||||||
|
&& existingBoundingBox.maxX === newBoundingBox.maxX
|
||||||
|
&& existingBoundingBox.maxY === newBoundingBox.maxY);
|
||||||
|
|
||||||
|
});
|
||||||
|
if (!boundingBoxAlreadyAdded) {
|
||||||
|
uniqueBoundsAnnotations.push(annotation);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.marqueeAnnotations(uniqueBoundsAnnotations);
|
||||||
|
|
||||||
|
return {
|
||||||
|
targetDomainObjects,
|
||||||
|
targetDetails
|
||||||
|
};
|
||||||
|
},
|
||||||
initialize() {
|
initialize() {
|
||||||
this.handleWindowResize = _.debounce(this.handleWindowResize, 500);
|
this.handleWindowResize = _.debounce(this.handleWindowResize, 500);
|
||||||
this.plotContainerResizeObserver = new ResizeObserver(this.handleWindowResize);
|
this.plotContainerResizeObserver = new ResizeObserver(this.handleWindowResize);
|
||||||
@ -805,7 +966,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
onMouseDown(event) {
|
onMouseDown(event) {
|
||||||
// do not monitor drag events on browser context click
|
// do not monitor drag events on browser context click
|
||||||
if (event.ctrlKey) {
|
if (event.ctrlKey) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -817,10 +978,12 @@ export default {
|
|||||||
const isFrozen = this.config.xAxis.get('frozen') === true && this.config.yAxis.get('frozen') === true;
|
const isFrozen = this.config.xAxis.get('frozen') === true && this.config.yAxis.get('frozen') === true;
|
||||||
this.isFrozenOnMouseDown = isFrozen;
|
this.isFrozenOnMouseDown = isFrozen;
|
||||||
|
|
||||||
if (event.altKey) {
|
if (event.altKey && !event.shiftKey) {
|
||||||
return this.startPan(event);
|
return this.startPan(event);
|
||||||
|
} else if (this.annotationViewingAndEditingAllowed && event.altKey && event.shiftKey) {
|
||||||
|
return this.startMarquee(event, true);
|
||||||
} else {
|
} else {
|
||||||
return this.startMarquee(event);
|
return this.startMarquee(event, false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -828,7 +991,7 @@ export default {
|
|||||||
this.stopListening(window, 'mouseup', this.onMouseUp, this);
|
this.stopListening(window, 'mouseup', this.onMouseUp, this);
|
||||||
this.stopListening(window, 'mousemove', this.trackMousePosition, this);
|
this.stopListening(window, 'mousemove', this.trackMousePosition, this);
|
||||||
|
|
||||||
if (this.isMouseClick()) {
|
if (this.isMouseClick() && event.shiftKey) {
|
||||||
this.lockHighlightPoint = !this.lockHighlightPoint;
|
this.lockHighlightPoint = !this.lockHighlightPoint;
|
||||||
this.$emit('lockHighlightPoint', this.lockHighlightPoint);
|
this.$emit('lockHighlightPoint', this.lockHighlightPoint);
|
||||||
}
|
}
|
||||||
@ -869,7 +1032,9 @@ export default {
|
|||||||
this.marquee.endPixels = this.positionOverElement;
|
this.marquee.endPixels = this.positionOverElement;
|
||||||
},
|
},
|
||||||
|
|
||||||
startMarquee(event) {
|
startMarquee(event, annotationEvent) {
|
||||||
|
this.rectangles = [];
|
||||||
|
this.annotationSelections = [];
|
||||||
this.canvas.classList.remove('plot-drag');
|
this.canvas.classList.remove('plot-drag');
|
||||||
this.canvas.classList.add('plot-marquee');
|
this.canvas.classList.add('plot-marquee');
|
||||||
|
|
||||||
@ -883,12 +1048,153 @@ export default {
|
|||||||
end: this.positionOverPlot,
|
end: this.positionOverPlot,
|
||||||
color: [1, 1, 1, 0.5]
|
color: [1, 1, 1, 0.5]
|
||||||
};
|
};
|
||||||
|
if (annotationEvent) {
|
||||||
|
this.marquee.annotationEvent = true;
|
||||||
|
}
|
||||||
|
|
||||||
this.rectangles.push(this.marquee);
|
this.rectangles.push(this.marquee);
|
||||||
this.trackHistory();
|
this.trackHistory();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
selectNearbyAnnotations(event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
endMarquee() {
|
if (!this.annotationViewingAndEditingAllowed || this.annotationSelections.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nearbyAnnotations = this.gatherNearbyAnnotations();
|
||||||
|
const { targetDomainObjects, targetDetails } = this.prepareExistingAnnotationSelection(nearbyAnnotations);
|
||||||
|
this.selectPlotAnnotations({
|
||||||
|
targetDetails,
|
||||||
|
targetDomainObjects,
|
||||||
|
annotations: nearbyAnnotations
|
||||||
|
});
|
||||||
|
},
|
||||||
|
selectPlotAnnotations({targetDetails, targetDomainObjects, annotations}) {
|
||||||
|
const selection =
|
||||||
|
[
|
||||||
|
{
|
||||||
|
element: this.openmct.layout.$refs.browseObject.$el,
|
||||||
|
context: {
|
||||||
|
item: this.domainObject
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: this.$el,
|
||||||
|
context: {
|
||||||
|
type: 'plot-points-selection',
|
||||||
|
targetDetails,
|
||||||
|
targetDomainObjects,
|
||||||
|
annotations,
|
||||||
|
annotationType: this.openmct.annotation.ANNOTATION_TYPES.PLOT_SPATIAL,
|
||||||
|
onAnnotationChange: this.onAnnotationChange
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
this.openmct.selection.select(selection, true);
|
||||||
|
},
|
||||||
|
selectNewPlotAnnotations(minX, minY, maxX, maxY, pointsInBox, event) {
|
||||||
|
const boundingBox = {
|
||||||
|
minX,
|
||||||
|
minY,
|
||||||
|
maxX,
|
||||||
|
maxY
|
||||||
|
};
|
||||||
|
let targetDomainObjects = {};
|
||||||
|
let targetDetails = {};
|
||||||
|
let annotations = {};
|
||||||
|
pointsInBox.forEach(pointInBox => {
|
||||||
|
if (pointInBox.length) {
|
||||||
|
const seriesID = pointInBox[0].series.keyString;
|
||||||
|
targetDetails[seriesID] = boundingBox;
|
||||||
|
targetDomainObjects[seriesID] = pointInBox[0].series.domainObject;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.selectPlotAnnotations({
|
||||||
|
targetDetails,
|
||||||
|
targetDomainObjects,
|
||||||
|
annotations
|
||||||
|
});
|
||||||
|
},
|
||||||
|
findAnnotationPoints(rawAnnotations) {
|
||||||
|
const annotationsByPoints = [];
|
||||||
|
rawAnnotations.forEach(rawAnnotation => {
|
||||||
|
if (rawAnnotation.targets) {
|
||||||
|
const targetValues = Object.values(rawAnnotation.targets);
|
||||||
|
if (targetValues && targetValues.length) {
|
||||||
|
// just get the first one
|
||||||
|
const boundingBox = Object.values(targetValues)?.[0];
|
||||||
|
const pointsInBox = this.getPointsInBox(boundingBox, rawAnnotation);
|
||||||
|
if (pointsInBox && pointsInBox.length) {
|
||||||
|
annotationsByPoints.push(pointsInBox.flat());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return annotationsByPoints.flat();
|
||||||
|
},
|
||||||
|
getPointsInBox(boundingBox, rawAnnotation) {
|
||||||
|
// load series models in KD-Trees
|
||||||
|
const seriesKDTrees = [];
|
||||||
|
this.seriesModels.forEach(seriesModel => {
|
||||||
|
const seriesData = seriesModel.getSeriesData();
|
||||||
|
if (seriesData && seriesData.length) {
|
||||||
|
const kdTree = new KDBush(seriesData,
|
||||||
|
(point) => {
|
||||||
|
return seriesModel.getXVal(point);
|
||||||
|
},
|
||||||
|
(point) => {
|
||||||
|
return seriesModel.getYVal(point);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const searchResults = [];
|
||||||
|
const rangeResults = kdTree.range(boundingBox.minX, boundingBox.minY, boundingBox.maxX, boundingBox.maxY);
|
||||||
|
rangeResults.forEach(id => {
|
||||||
|
const seriesDatum = seriesData[id];
|
||||||
|
if (seriesDatum) {
|
||||||
|
const result = {
|
||||||
|
series: seriesModel,
|
||||||
|
point: seriesDatum
|
||||||
|
};
|
||||||
|
searchResults.push(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rawAnnotation) {
|
||||||
|
if (!seriesDatum.annotationsById) {
|
||||||
|
seriesDatum.annotationsById = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const annotationKeyString = this.openmct.objects.makeKeyString(rawAnnotation.identifier);
|
||||||
|
seriesDatum.annotationsById[annotationKeyString] = rawAnnotation;
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
if (searchResults.length) {
|
||||||
|
seriesKDTrees.push(searchResults);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return seriesKDTrees;
|
||||||
|
},
|
||||||
|
endAnnotationMarquee(event) {
|
||||||
|
const minX = Math.min(this.marquee.start.x, this.marquee.end.x);
|
||||||
|
const minY = Math.min(this.marquee.start.y, this.marquee.end.y);
|
||||||
|
const maxX = Math.max(this.marquee.start.x, this.marquee.end.x);
|
||||||
|
const maxY = Math.max(this.marquee.start.y, this.marquee.end.y);
|
||||||
|
const boundingBox = {
|
||||||
|
minX,
|
||||||
|
minY,
|
||||||
|
maxX,
|
||||||
|
maxY
|
||||||
|
};
|
||||||
|
const pointsInBox = this.getPointsInBox(boundingBox);
|
||||||
|
this.annotationSelections = pointsInBox.flat();
|
||||||
|
this.selectNewPlotAnnotations(minX, minY, maxX, maxY, pointsInBox, event);
|
||||||
|
},
|
||||||
|
endZoomMarquee() {
|
||||||
const startPixels = this.marquee.startPixels;
|
const startPixels = this.marquee.startPixels;
|
||||||
const endPixels = this.marquee.endPixels;
|
const endPixels = this.marquee.endPixels;
|
||||||
const marqueeDistance = Math.sqrt(
|
const marqueeDistance = Math.sqrt(
|
||||||
@ -911,9 +1217,25 @@ export default {
|
|||||||
// if marquee zoom doesn't occur.
|
// if marquee zoom doesn't occur.
|
||||||
this.plotHistory.pop();
|
this.plotHistory.pop();
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
endMarquee(event) {
|
||||||
|
if (this.marquee.annotationEvent) {
|
||||||
|
this.endAnnotationMarquee(event);
|
||||||
|
} else {
|
||||||
|
this.endZoomMarquee();
|
||||||
|
this.rectangles = [];
|
||||||
|
}
|
||||||
|
|
||||||
this.rectangles = [];
|
this.marquee = null;
|
||||||
this.marquee = undefined;
|
},
|
||||||
|
|
||||||
|
onAnnotationChange(annotations) {
|
||||||
|
if (this.marquee) {
|
||||||
|
this.marquee.annotationEvent = false;
|
||||||
|
this.endMarquee();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loadAnnotations();
|
||||||
},
|
},
|
||||||
|
|
||||||
zoom(zoomDirection, zoomFactor) {
|
zoom(zoomDirection, zoomFactor) {
|
||||||
|
@ -50,10 +50,11 @@ import Vue from 'vue';
|
|||||||
|
|
||||||
const MARKER_SIZE = 6.0;
|
const MARKER_SIZE = 6.0;
|
||||||
const HIGHLIGHT_SIZE = MARKER_SIZE * 2.0;
|
const HIGHLIGHT_SIZE = MARKER_SIZE * 2.0;
|
||||||
|
const ANNOTATION_SIZE = MARKER_SIZE * 3.0;
|
||||||
const CLEARANCE = 15;
|
const CLEARANCE = 15;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
inject: ['openmct', 'domainObject'],
|
inject: ['openmct', 'domainObject', 'path'],
|
||||||
props: {
|
props: {
|
||||||
rectangles: {
|
rectangles: {
|
||||||
type: Array,
|
type: Array,
|
||||||
@ -67,11 +68,27 @@ export default {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
annotatedPoints: {
|
||||||
|
type: Array,
|
||||||
|
default() {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
annotationSelections: {
|
||||||
|
type: Array,
|
||||||
|
default() {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
showLimitLineLabels: {
|
showLimitLineLabels: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default() {
|
default() {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
annotationViewingAndEditingAllowed: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@ -83,6 +100,12 @@ export default {
|
|||||||
highlights() {
|
highlights() {
|
||||||
this.scheduleDraw();
|
this.scheduleDraw();
|
||||||
},
|
},
|
||||||
|
annotatedPoints() {
|
||||||
|
this.scheduleDraw();
|
||||||
|
},
|
||||||
|
annotationSelections() {
|
||||||
|
this.scheduleDraw();
|
||||||
|
},
|
||||||
rectangles() {
|
rectangles() {
|
||||||
this.scheduleDraw();
|
this.scheduleDraw();
|
||||||
},
|
},
|
||||||
@ -148,10 +171,22 @@ export default {
|
|||||||
this.listenTo(series, 'change:alarmMarkers', this.changeAlarmMarkers, this);
|
this.listenTo(series, 'change:alarmMarkers', this.changeAlarmMarkers, this);
|
||||||
this.listenTo(series, 'change:limitLines', this.changeLimitLines, this);
|
this.listenTo(series, 'change:limitLines', this.changeLimitLines, this);
|
||||||
this.listenTo(series, 'change', this.scheduleDraw);
|
this.listenTo(series, 'change', this.scheduleDraw);
|
||||||
this.listenTo(series, 'add', this.scheduleDraw);
|
this.listenTo(series, 'add', this.onAddPoint);
|
||||||
this.makeChartElement(series);
|
this.makeChartElement(series);
|
||||||
this.makeLimitLines(series);
|
this.makeLimitLines(series);
|
||||||
},
|
},
|
||||||
|
onAddPoint(point, insertIndex, series) {
|
||||||
|
const xRange = this.config.xAxis.get('displayRange');
|
||||||
|
const yRange = this.config.yAxis.get('displayRange');
|
||||||
|
const xValue = series.getXVal(point);
|
||||||
|
const yValue = series.getYVal(point);
|
||||||
|
|
||||||
|
// if user is not looking at data within the current bounds, don't draw the point
|
||||||
|
if ((xValue > xRange.min) && (xValue < xRange.max)
|
||||||
|
&& (yValue > yRange.min) && (yValue < yRange.max)) {
|
||||||
|
this.scheduleDraw();
|
||||||
|
}
|
||||||
|
},
|
||||||
changeInterpolate(mode, o, series) {
|
changeInterpolate(mode, o, series) {
|
||||||
if (mode === o) {
|
if (mode === o) {
|
||||||
return;
|
return;
|
||||||
@ -439,6 +474,12 @@ export default {
|
|||||||
this.drawSeries();
|
this.drawSeries();
|
||||||
this.drawRectangles();
|
this.drawRectangles();
|
||||||
this.drawHighlights();
|
this.drawHighlights();
|
||||||
|
|
||||||
|
// only draw these in fixed time mode or plot is paused
|
||||||
|
if (this.annotationViewingAndEditingAllowed) {
|
||||||
|
this.drawAnnotatedPoints();
|
||||||
|
this.drawAnnotationSelections();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updateViewport() {
|
updateViewport() {
|
||||||
@ -584,6 +625,65 @@ export default {
|
|||||||
disconnected
|
disconnected
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
annotatedPointWithinRange(annotatedPoint, xRange, yRange) {
|
||||||
|
const xValue = annotatedPoint.series.getXVal(annotatedPoint.point);
|
||||||
|
const yValue = annotatedPoint.series.getYVal(annotatedPoint.point);
|
||||||
|
|
||||||
|
return ((xValue > xRange.min) && (xValue < xRange.max)
|
||||||
|
&& (yValue > yRange.min) && (yValue < yRange.max));
|
||||||
|
},
|
||||||
|
drawAnnotatedPoints() {
|
||||||
|
// we should do this by series, and then plot all the points at once instead
|
||||||
|
// of doing it one by one
|
||||||
|
if (this.annotatedPoints && this.annotatedPoints.length) {
|
||||||
|
const uniquePointsToDraw = [];
|
||||||
|
const xRange = this.config.xAxis.get('displayRange');
|
||||||
|
const yRange = this.config.yAxis.get('displayRange');
|
||||||
|
this.annotatedPoints.forEach((annotatedPoint) => {
|
||||||
|
// if the annotation is outside the range, don't draw it
|
||||||
|
if (this.annotatedPointWithinRange(annotatedPoint, xRange, yRange)) {
|
||||||
|
const canvasXValue = this.offset.xVal(annotatedPoint.point, annotatedPoint.series);
|
||||||
|
const canvasYValue = this.offset.yVal(annotatedPoint.point, annotatedPoint.series);
|
||||||
|
const pointToDraw = new Float32Array([canvasXValue, canvasYValue]);
|
||||||
|
const drawnPoint = uniquePointsToDraw.some((rawPoint) => {
|
||||||
|
return rawPoint[0] === pointToDraw[0] && rawPoint[1] === pointToDraw[1];
|
||||||
|
});
|
||||||
|
if (!drawnPoint) {
|
||||||
|
uniquePointsToDraw.push(pointToDraw);
|
||||||
|
this.drawAnnotatedPoint(annotatedPoint, pointToDraw);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
drawAnnotatedPoint(annotatedPoint, pointToDraw) {
|
||||||
|
if (annotatedPoint.point && annotatedPoint.series) {
|
||||||
|
const color = annotatedPoint.series.get('color').asRGBAArray();
|
||||||
|
// set transparency
|
||||||
|
color[3] = 0.15;
|
||||||
|
const pointCount = 1;
|
||||||
|
const shape = annotatedPoint.series.get('markerShape');
|
||||||
|
|
||||||
|
this.drawAPI.drawPoints(pointToDraw, color, pointCount, ANNOTATION_SIZE, shape);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
drawAnnotationSelections() {
|
||||||
|
if (this.annotationSelections && this.annotationSelections.length) {
|
||||||
|
this.annotationSelections.forEach(this.drawAnnotationSelection, this);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
drawAnnotationSelection(annotationSelection) {
|
||||||
|
const points = new Float32Array([
|
||||||
|
this.offset.xVal(annotationSelection.point, annotationSelection.series),
|
||||||
|
this.offset.yVal(annotationSelection.point, annotationSelection.series)
|
||||||
|
]);
|
||||||
|
|
||||||
|
const color = [255, 255, 255, 1]; // white
|
||||||
|
const pointCount = 1;
|
||||||
|
const shape = annotationSelection.series.get('markerShape');
|
||||||
|
|
||||||
|
this.drawAPI.drawPoints(points, color, pointCount, ANNOTATION_SIZE, shape);
|
||||||
|
},
|
||||||
drawHighlights() {
|
drawHighlights() {
|
||||||
if (this.highlights && this.highlights.length) {
|
if (this.highlights && this.highlights.length) {
|
||||||
this.highlights.forEach(this.drawHighlight, this);
|
this.highlights.forEach(this.drawHighlight, this);
|
||||||
|
@ -29,7 +29,7 @@ import LegendModel from "./LegendModel";
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* PlotConfiguration model stores the configuration of a plot and some
|
* PlotConfiguration model stores the configuration of a plot and some
|
||||||
* limited state. The indiidual parts of the plot configuration model
|
* limited state. The individual parts of the plot configuration model
|
||||||
* handle setting defaults and updating in response to various changes.
|
* handle setting defaults and updating in response to various changes.
|
||||||
*
|
*
|
||||||
* @extends {Model<PlotConfigModelType, PlotConfigModelOptions>}
|
* @extends {Model<PlotConfigModelType, PlotConfigModelOptions>}
|
||||||
|
@ -83,6 +83,10 @@ export default class PlotSeries extends Model {
|
|||||||
// Model.apply(this, arguments);
|
// Model.apply(this, arguments);
|
||||||
this.onXKeyChange(this.get('xKey'));
|
this.onXKeyChange(this.get('xKey'));
|
||||||
this.onYKeyChange(this.get('yKey'));
|
this.onYKeyChange(this.get('yKey'));
|
||||||
|
this.xRangeMin = Number.MIN_SAFE_INTEGER;
|
||||||
|
this.yRangeMin = Number.MIN_SAFE_INTEGER;
|
||||||
|
this.xRangeMax = Number.MAX_SAFE_INTEGER;
|
||||||
|
this.yRangeMax = Number.MAX_SAFE_INTEGER;
|
||||||
|
|
||||||
this.unPlottableValues = [undefined, Infinity, -Infinity];
|
this.unPlottableValues = [undefined, Infinity, -Infinity];
|
||||||
}
|
}
|
||||||
@ -378,6 +382,7 @@ export default class PlotSeries extends Model {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a point to the data array while maintaining the sort order of
|
* Add a point to the data array while maintaining the sort order of
|
||||||
* the array and preventing insertion of points with a duplicate x
|
* the array and preventing insertion of points with a duplicate x
|
||||||
|
@ -178,10 +178,10 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
isNestedWithinAStackedPlot() {
|
isNestedWithinAStackedPlot() {
|
||||||
return this.path.find((pathObject, pathObjIndex) => pathObjIndex > 0 && pathObject.type === 'telemetry.plot.stacked');
|
return this.path.find((pathObject, pathObjIndex) => pathObjIndex > 0 && pathObject?.type === 'telemetry.plot.stacked');
|
||||||
},
|
},
|
||||||
isStackedPlotObject() {
|
isStackedPlotObject() {
|
||||||
return this.path.find((pathObject, pathObjIndex) => pathObjIndex === 0 && pathObject.type === 'telemetry.plot.stacked');
|
return this.path.find((pathObject, pathObjIndex) => pathObjIndex === 0 && pathObject?.type === 'telemetry.plot.stacked');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
@ -137,7 +137,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: rgba($colorKey, 0.2);
|
background: rgba($colorBodyFg, 0.1); //$colorInteriorBorder;
|
||||||
//color: $colorBodyFg;
|
//color: $colorBodyFg;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -283,6 +283,14 @@
|
|||||||
@include discreteItem();
|
@include discreteItem();
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: $interiorMarginSm $interiorMarginSm $interiorMarginSm $interiorMargin;
|
padding: $interiorMarginSm $interiorMarginSm $interiorMarginSm $interiorMargin;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba($colorBodyFg, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-selected {
|
||||||
|
background: rgba($colorKey, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
&__text,
|
&__text,
|
||||||
&__local-controls {
|
&__local-controls {
|
||||||
|
@ -42,6 +42,7 @@
|
|||||||
@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";
|
||||||
|
@import "../ui/inspector/annotations/annotation-inspector.scss";
|
||||||
@import "../ui/layout/app-logo.scss";
|
@import "../ui/layout/app-logo.scss";
|
||||||
@import "../ui/layout/create-button.scss";
|
@import "../ui/layout/create-button.scss";
|
||||||
@import "../ui/layout/layout.scss";
|
@import "../ui/layout/layout.scss";
|
||||||
|
@ -57,17 +57,28 @@ export default {
|
|||||||
},
|
},
|
||||||
annotationType: {
|
annotationType: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: false,
|
||||||
},
|
default: null
|
||||||
targetSpecificDetails: {
|
|
||||||
type: Object,
|
|
||||||
required: true
|
|
||||||
},
|
},
|
||||||
domainObject: {
|
domainObject: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default() {
|
required: true,
|
||||||
return null;
|
default: null
|
||||||
}
|
},
|
||||||
|
targets: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
targetDomainObjects: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
onTagChange: {
|
||||||
|
type: Function,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@ -99,7 +110,7 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
annotationsChanged() {
|
annotationsChanged() {
|
||||||
if (this.annotations && this.annotations.length) {
|
if (this.annotations) {
|
||||||
this.tagsChanged();
|
this.tagsChanged();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -141,27 +152,47 @@ export default {
|
|||||||
this.userAddingTag = true;
|
this.userAddingTag = true;
|
||||||
},
|
},
|
||||||
async tagRemoved(tagToRemove) {
|
async tagRemoved(tagToRemove) {
|
||||||
// Soft delete annotations that match tag instead
|
// Soft delete annotations that match tag instead (that aren't already deleted)
|
||||||
const annotationsToDelete = this.annotations.filter((annotation) => {
|
const annotationsToDelete = this.annotations.filter((annotation) => {
|
||||||
return annotation.tags.includes(tagToRemove);
|
return annotation.tags.includes(tagToRemove) && !annotation._deleted;
|
||||||
});
|
});
|
||||||
if (annotationsToDelete) {
|
if (annotationsToDelete) {
|
||||||
await this.openmct.annotation.deleteAnnotations(annotationsToDelete);
|
await this.openmct.annotation.deleteAnnotations(annotationsToDelete);
|
||||||
this.$emit('tags-updated', annotationsToDelete);
|
this.$emit('tags-updated', annotationsToDelete);
|
||||||
|
if (this.onTagChange) {
|
||||||
|
this.onTagChange(this.annotations);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async tagAdded(newTag) {
|
async tagAdded(newTag) {
|
||||||
// Either undelete an annotation, or create one (1) new annotation
|
// Either undelete an annotation, or create one (1) new annotation
|
||||||
const existingAnnotation = this.annotations.find((annotation) => {
|
let existingAnnotation = this.annotations.find((annotation) => {
|
||||||
return annotation.tags.includes(newTag);
|
return annotation.tags.includes(newTag);
|
||||||
});
|
});
|
||||||
|
|
||||||
const createdAnnotation = await this.openmct.annotation.addSingleAnnotationTag(existingAnnotation,
|
if (!existingAnnotation) {
|
||||||
this.domainObject, this.targetSpecificDetails, this.annotationType, newTag);
|
const contentText = `${this.annotationType} tag`;
|
||||||
|
const annotationCreationArguments = {
|
||||||
|
name: contentText,
|
||||||
|
existingAnnotation,
|
||||||
|
contentText: contentText,
|
||||||
|
targets: this.targets,
|
||||||
|
targetDomainObjects: this.targetDomainObjects,
|
||||||
|
domainObject: this.domainObject,
|
||||||
|
annotationType: this.annotationType,
|
||||||
|
tags: [newTag]
|
||||||
|
};
|
||||||
|
existingAnnotation = await this.openmct.annotation.create(annotationCreationArguments);
|
||||||
|
} else if (existingAnnotation._deleted) {
|
||||||
|
this.openmct.annotation.unDeleteAnnotation(existingAnnotation);
|
||||||
|
}
|
||||||
|
|
||||||
this.userAddingTag = false;
|
this.userAddingTag = false;
|
||||||
|
|
||||||
this.$emit('tags-updated', createdAnnotation);
|
this.$emit('tags-updated', existingAnnotation);
|
||||||
|
if (this.onTagChange) {
|
||||||
|
this.onTagChange([existingAnnotation]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -35,6 +35,7 @@
|
|||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="c-tag"
|
class="c-tag"
|
||||||
|
:class="{'c-tag-edit': !readOnly}"
|
||||||
:style="{ background: selectedBackgroundColor, color: selectedForegroundColor }"
|
:style="{ background: selectedBackgroundColor, color: selectedForegroundColor }"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@ -42,6 +43,7 @@
|
|||||||
aria-label="Tag"
|
aria-label="Tag"
|
||||||
>{{ selectedTagLabel }} </div>
|
>{{ selectedTagLabel }} </div>
|
||||||
<button
|
<button
|
||||||
|
v-show="!readOnly"
|
||||||
class="c-completed-tag-deletion c-tag__remove-btn icon-x-in-circle"
|
class="c-completed-tag-deletion c-tag__remove-btn icon-x-in-circle"
|
||||||
@click="removeTag"
|
@click="removeTag"
|
||||||
></button>
|
></button>
|
||||||
@ -77,6 +79,12 @@ export default {
|
|||||||
default() {
|
default() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
readOnly: {
|
||||||
|
type: Boolean,
|
||||||
|
default() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
|
@ -36,7 +36,6 @@
|
|||||||
>
|
>
|
||||||
{{ tabbedView.name }}
|
{{ tabbedView.name }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="c-inspector__content">
|
<div class="c-inspector__content">
|
||||||
<multipane
|
<multipane
|
||||||
@ -44,10 +43,12 @@
|
|||||||
type="vertical"
|
type="vertical"
|
||||||
>
|
>
|
||||||
<pane class="c-inspector__properties">
|
<pane class="c-inspector__properties">
|
||||||
<Properties
|
<Properties v-if="!activity" />
|
||||||
v-if="!activity"
|
<div
|
||||||
/>
|
v-if="!multiSelect"
|
||||||
<location />
|
class="c-inspect-properties c-inspect-properties--location"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
<inspector-views />
|
<inspector-views />
|
||||||
</pane>
|
</pane>
|
||||||
<pane
|
<pane
|
||||||
@ -75,39 +76,49 @@
|
|||||||
<SavedStylesInspectorView :is-editing="isEditing" />
|
<SavedStylesInspectorView :is-editing="isEditing" />
|
||||||
</pane>
|
</pane>
|
||||||
</multipane>
|
</multipane>
|
||||||
|
<multipane
|
||||||
|
v-show="currentTabbedView.key === '__annotations'"
|
||||||
|
type="vertical"
|
||||||
|
>
|
||||||
|
<pane class="c-inspector__annotations">
|
||||||
|
<AnnotationsInspectorView
|
||||||
|
@annotationCreated="updateCurrentTab(tabbedViews[2])"
|
||||||
|
/>
|
||||||
|
</pane>
|
||||||
|
</multipane>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<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 ElementsPool from "./ElementsPool.vue";
|
||||||
import Location from './Location.vue';
|
import Properties from "./details/Properties.vue";
|
||||||
import Properties from './details/Properties.vue';
|
import ObjectName from "./ObjectName.vue";
|
||||||
import ObjectName from './ObjectName.vue';
|
import InspectorViews from "./InspectorViews.vue";
|
||||||
import InspectorViews from './InspectorViews.vue';
|
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import stylesManager from '@/ui/inspector/styles/StylesManager';
|
import stylesManager from "@/ui/inspector/styles/StylesManager";
|
||||||
import StylesInspectorView from '@/ui/inspector/styles/StylesInspectorView.vue';
|
import StylesInspectorView from "@/ui/inspector/styles/StylesInspectorView.vue";
|
||||||
import SavedStylesInspectorView from '@/ui/inspector/styles/SavedStylesInspectorView.vue';
|
import SavedStylesInspectorView from "@/ui/inspector/styles/SavedStylesInspectorView.vue";
|
||||||
|
import AnnotationsInspectorView from "./annotations/AnnotationsInspectorView.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
StylesInspectorView,
|
StylesInspectorView,
|
||||||
SavedStylesInspectorView,
|
SavedStylesInspectorView,
|
||||||
|
AnnotationsInspectorView,
|
||||||
multipane,
|
multipane,
|
||||||
pane,
|
pane,
|
||||||
ElementsPool,
|
ElementsPool,
|
||||||
Properties,
|
Properties,
|
||||||
ObjectName,
|
ObjectName,
|
||||||
Location,
|
|
||||||
InspectorViews
|
InspectorViews
|
||||||
},
|
},
|
||||||
provide: {
|
provide: {
|
||||||
stylesManager: stylesManager
|
stylesManager: stylesManager
|
||||||
},
|
},
|
||||||
inject: ['openmct'],
|
inject: ["openmct"],
|
||||||
props: {
|
props: {
|
||||||
isEditing: {
|
isEditing: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@ -117,40 +128,64 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
hasComposition: false,
|
hasComposition: false,
|
||||||
|
multiSelect: false,
|
||||||
showStyles: false,
|
showStyles: false,
|
||||||
tabbedViews: [{
|
tabbedViews: [
|
||||||
key: '__properties',
|
{
|
||||||
name: 'Properties'
|
key: "__properties",
|
||||||
}, {
|
name: "Properties"
|
||||||
key: '__styles',
|
},
|
||||||
name: 'Styles'
|
{
|
||||||
}],
|
key: "__styles",
|
||||||
|
name: "Styles"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "__annotations",
|
||||||
|
name: "Annotations"
|
||||||
|
}
|
||||||
|
],
|
||||||
currentTabbedView: {},
|
currentTabbedView: {},
|
||||||
activity: undefined
|
activity: undefined
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.excludeObjectTypes = ['folder', 'webPage', 'conditionSet', 'summary-widget', 'hyperlink'];
|
this.excludeObjectTypes = [
|
||||||
this.openmct.selection.on('change', this.updateInspectorViews);
|
"folder",
|
||||||
|
"webPage",
|
||||||
|
"conditionSet",
|
||||||
|
"summary-widget",
|
||||||
|
"hyperlink"
|
||||||
|
];
|
||||||
|
this.openmct.selection.on("change", this.updateInspectorViews);
|
||||||
},
|
},
|
||||||
destroyed() {
|
destroyed() {
|
||||||
this.openmct.selection.off('change', this.updateInspectorViews);
|
this.openmct.selection.off("change", this.updateInspectorViews);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
updateInspectorViews(selection) {
|
updateInspectorViews(selection) {
|
||||||
this.refreshComposition(selection);
|
this.refreshComposition(selection);
|
||||||
|
|
||||||
if (this.openmct.types.get('conditionSet')) {
|
if (this.openmct.types.get("conditionSet")) {
|
||||||
this.refreshTabs(selection);
|
this.refreshTabs(selection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (selection.length > 1) {
|
||||||
|
this.multiSelect = true;
|
||||||
|
|
||||||
|
// return;
|
||||||
|
} else {
|
||||||
|
this.multiSelect = false;
|
||||||
|
}
|
||||||
|
|
||||||
this.setActivity(selection);
|
this.setActivity(selection);
|
||||||
},
|
},
|
||||||
refreshComposition(selection) {
|
refreshComposition(selection) {
|
||||||
if (selection.length > 0 && selection[0].length > 0) {
|
if (selection.length > 0 && selection[0].length > 0) {
|
||||||
let parentObject = selection[0][0].context.item;
|
let parentObject = selection[0][0].context.item;
|
||||||
|
|
||||||
this.hasComposition = Boolean(parentObject && this.openmct.composition.get(parentObject));
|
this.hasComposition = Boolean(
|
||||||
|
parentObject && this.openmct.composition.get(parentObject)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
refreshTabs(selection) {
|
refreshTabs(selection) {
|
||||||
@ -160,21 +195,33 @@ export default {
|
|||||||
let object = selection[0][0].context.item;
|
let object = selection[0][0].context.item;
|
||||||
if (object) {
|
if (object) {
|
||||||
let type = this.openmct.types.get(object.type);
|
let type = this.openmct.types.get(object.type);
|
||||||
this.showStyles = this.isLayoutObject(selection[0], object.type) || this.isCreatableObject(object, type);
|
this.showStyles =
|
||||||
|
this.isLayoutObject(selection[0], object.type)
|
||||||
|
|| this.isCreatableObject(object, type);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.currentTabbedView.key || (!this.showStyles && this.currentTabbedView.key === this.tabbedViews[1].key)) {
|
if (
|
||||||
|
!this.currentTabbedView.key
|
||||||
|
|| (!this.showStyles
|
||||||
|
&& this.currentTabbedView.key === this.tabbedViews[1].key)
|
||||||
|
) {
|
||||||
this.updateCurrentTab(this.tabbedViews[0]);
|
this.updateCurrentTab(this.tabbedViews[0]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
isLayoutObject(selection, objectType) {
|
isLayoutObject(selection, objectType) {
|
||||||
//we allow conditionSets to be styled if they're part of a layout
|
//we allow conditionSets to be styled if they're part of a layout
|
||||||
return selection.length > 1
|
return (
|
||||||
&& ((objectType === 'conditionSet') || (this.excludeObjectTypes.indexOf(objectType) < 0));
|
selection.length > 1
|
||||||
|
&& (objectType === "conditionSet"
|
||||||
|
|| this.excludeObjectTypes.indexOf(objectType) < 0)
|
||||||
|
);
|
||||||
},
|
},
|
||||||
isCreatableObject(object, type) {
|
isCreatableObject(object, type) {
|
||||||
return (this.excludeObjectTypes.indexOf(object.type) < 0) && type.definition.creatable;
|
return (
|
||||||
|
this.excludeObjectTypes.indexOf(object.type) < 0
|
||||||
|
&& type.definition.creatable
|
||||||
|
);
|
||||||
},
|
},
|
||||||
updateCurrentTab(view) {
|
updateCurrentTab(view) {
|
||||||
this.currentTabbedView = view;
|
this.currentTabbedView = view;
|
||||||
@ -183,10 +230,11 @@ export default {
|
|||||||
return _.isEqual(this.currentTabbedView, view);
|
return _.isEqual(this.currentTabbedView, view);
|
||||||
},
|
},
|
||||||
setActivity(selection) {
|
setActivity(selection) {
|
||||||
this.activity = selection
|
this.activity =
|
||||||
&& selection.length
|
selection
|
||||||
&& selection[0].length
|
&& selection.length
|
||||||
&& selection[0][0].activity;
|
&& selection[0].length
|
||||||
|
&& selection[0][0].activity;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
83
src/ui/inspector/annotations/AnnotationEditor.vue
Normal file
83
src/ui/inspector/annotations/AnnotationEditor.vue
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2022, 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-annotation__row">
|
||||||
|
<textarea
|
||||||
|
v-model="contentModel"
|
||||||
|
class="c-annotation__text_area"
|
||||||
|
type="text"
|
||||||
|
></textarea>
|
||||||
|
<div>
|
||||||
|
<span>{{ modifiedOnDate }}</span>
|
||||||
|
<span>{{ modifiedOnTime }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Moment from 'moment';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
inject: ['openmct'],
|
||||||
|
props: {
|
||||||
|
annotation: {
|
||||||
|
type: Object,
|
||||||
|
default() {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
contentModel: {
|
||||||
|
get() {
|
||||||
|
return this.annotation.contentText;
|
||||||
|
},
|
||||||
|
set(contentText) {
|
||||||
|
console.debug(`Set tag called with ${contentText}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifiedOnDate() {
|
||||||
|
return this.formatTime(this.annotation.modified, 'YYYY-MM-DD');
|
||||||
|
},
|
||||||
|
modifiedOnTime() {
|
||||||
|
return this.formatTime(this.annotation.modified, 'HH:mm:ss');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getAvailableTagByID(tagID) {
|
||||||
|
return this.openmct.annotation.getAvailableTags().find(tag => {
|
||||||
|
return tag.id === tagID;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
formatTime(unixTime, timeFormat) {
|
||||||
|
return Moment.utc(unixTime).format(timeFormat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
213
src/ui/inspector/annotations/AnnotationsInspectorView.vue
Normal file
213
src/ui/inspector/annotations/AnnotationsInspectorView.vue
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2022, 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-inspector__properties c-inspect-properties has-tag-applier"
|
||||||
|
aria-label="Tags Inspector"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="c-inspect-properties__header"
|
||||||
|
>
|
||||||
|
Tags
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="shouldShowTagsEditor"
|
||||||
|
class="c-inspect-properties__section"
|
||||||
|
>
|
||||||
|
<TagEditor
|
||||||
|
:targets="targetDetails"
|
||||||
|
:target-domain-objects="targetDomainObjects"
|
||||||
|
:domain-object="domainObject"
|
||||||
|
:annotations="loadedAnnotations"
|
||||||
|
:annotation-type="annotationType"
|
||||||
|
:on-tag-change="onAnnotationChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="c-inspect-properties__row--span-all"
|
||||||
|
>
|
||||||
|
{{ noTagsMessage }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import TagEditor from '../../components/tags/TagEditor.vue';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
TagEditor
|
||||||
|
},
|
||||||
|
inject: ['openmct'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
selection: null,
|
||||||
|
lastLocalAnnotationCreations: {},
|
||||||
|
unobserveEntries: {},
|
||||||
|
loadedAnnotations: []
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
hasAnnotations() {
|
||||||
|
return Boolean(
|
||||||
|
this.loadedAnnotations
|
||||||
|
&& this.loadedAnnotations.length
|
||||||
|
);
|
||||||
|
},
|
||||||
|
nonTagAnnotations() {
|
||||||
|
if (!this.loadedAnnotations) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.loadedAnnotations.filter(annotation => {
|
||||||
|
return !annotation.tags && !annotation._deleted;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
tagAnnotations() {
|
||||||
|
if (!this.loadedAnnotations) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.loadedAnnotations.filter(annotation => {
|
||||||
|
return !annotation.tags && !annotation._deleted;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
multiSelection() {
|
||||||
|
return this.selection && this.selection.length > 1;
|
||||||
|
},
|
||||||
|
noAnnotationsMessage() {
|
||||||
|
return this.multiSelection
|
||||||
|
? 'No annotations to display for multiple items'
|
||||||
|
: 'No annotations to display for this item';
|
||||||
|
},
|
||||||
|
noTagsMessage() {
|
||||||
|
return this.multiSelection
|
||||||
|
? 'No tags to display for multiple items'
|
||||||
|
: 'No tags to display for this item';
|
||||||
|
},
|
||||||
|
domainObject() {
|
||||||
|
return this?.selection?.[0]?.[0]?.context?.item;
|
||||||
|
},
|
||||||
|
targetDetails() {
|
||||||
|
return this?.selection?.[0]?.[1]?.context?.targetDetails ?? {};
|
||||||
|
},
|
||||||
|
shouldShowTagsEditor() {
|
||||||
|
return Object.keys(this.targetDetails).length > 0;
|
||||||
|
},
|
||||||
|
targetDomainObjects() {
|
||||||
|
return this?.selection?.[0]?.[1]?.context?.targetDomainObjects ?? {};
|
||||||
|
},
|
||||||
|
selectedAnnotations() {
|
||||||
|
return this?.selection?.[0]?.[1]?.context?.annotations;
|
||||||
|
},
|
||||||
|
annotationType() {
|
||||||
|
return this?.selection?.[0]?.[1]?.context?.annotationType;
|
||||||
|
},
|
||||||
|
annotationFilter() {
|
||||||
|
return this?.selection?.[0]?.[1]?.context?.annotationFilter;
|
||||||
|
},
|
||||||
|
onAnnotationChange() {
|
||||||
|
return this?.selection?.[0]?.[1]?.context?.onAnnotationChange;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
this.openmct.annotation.on('targetDomainObjectAnnotated', this.loadAnnotationForTargetObject);
|
||||||
|
this.openmct.selection.on('change', this.updateSelection);
|
||||||
|
await this.updateSelection(this.openmct.selection.get());
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.openmct.selection.off('change', this.updateSelection);
|
||||||
|
const unobserveEntryFunctions = Object.values(this.unobserveEntries);
|
||||||
|
unobserveEntryFunctions.forEach(unobserveEntry => {
|
||||||
|
unobserveEntry();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
loadNewAnnotations(annotationsToLoad) {
|
||||||
|
if (!annotationsToLoad || !annotationsToLoad.length) {
|
||||||
|
this.loadedAnnotations.splice(0);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortedAnnotations = annotationsToLoad.sort((annotationA, annotationB) => {
|
||||||
|
return annotationB.modified - annotationA.modified;
|
||||||
|
});
|
||||||
|
|
||||||
|
const mutableAnnotations = sortedAnnotations.map((annotation) => {
|
||||||
|
return this.openmct.objects.toMutable(annotation);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (sortedAnnotations.length < this.loadedAnnotations.length) {
|
||||||
|
this.loadedAnnotations = this.loadedAnnotations.slice(0, mutableAnnotations.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let index = 0; index < mutableAnnotations.length; index += 1) {
|
||||||
|
this.$set(this.loadedAnnotations, index, mutableAnnotations[index]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateSelection(selection) {
|
||||||
|
const unobserveEntryFunctions = Object.values(this.unobserveEntries);
|
||||||
|
unobserveEntryFunctions.forEach(unobserveEntry => {
|
||||||
|
unobserveEntry();
|
||||||
|
});
|
||||||
|
this.unobserveEntries = {};
|
||||||
|
|
||||||
|
this.selection = selection;
|
||||||
|
const targetKeys = Object.keys(this.targetDomainObjects);
|
||||||
|
targetKeys.forEach(targetKey => {
|
||||||
|
const targetObject = this.targetDomainObjects[targetKey];
|
||||||
|
this.lastLocalAnnotationCreations[targetKey] = targetObject?.annotationLastCreated ?? 0;
|
||||||
|
if (!this.unobserveEntries[targetKey]) {
|
||||||
|
this.unobserveEntries[targetKey] = this.openmct.objects.observe(targetObject, '*', this.targetObjectChanged);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.loadNewAnnotations(this.selectedAnnotations);
|
||||||
|
},
|
||||||
|
async targetObjectChanged(target) {
|
||||||
|
const targetID = this.openmct.objects.makeKeyString(target.identifier);
|
||||||
|
const lastLocalAnnotationCreation = this.lastLocalAnnotationCreations[targetID] ?? 0;
|
||||||
|
if (lastLocalAnnotationCreation < target.annotationLastCreated) {
|
||||||
|
this.lastLocalAnnotationCreations[targetID] = target.annotationLastCreated;
|
||||||
|
await this.loadAnnotationForTargetObject(target);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async loadAnnotationForTargetObject(target) {
|
||||||
|
const targetID = this.openmct.objects.makeKeyString(target.identifier);
|
||||||
|
const allAnnotationsForTarget = await this.openmct.annotation.getAnnotations(target.identifier);
|
||||||
|
const filteredAnnotationsForSelection = allAnnotationsForTarget.filter(annotation => {
|
||||||
|
const matchingTargetID = Object.keys(annotation.targets).filter(loadedTargetID => {
|
||||||
|
return targetID === loadedTargetID;
|
||||||
|
});
|
||||||
|
const fetchedTargetDetails = annotation.targets[matchingTargetID];
|
||||||
|
const selectedTargetDetails = this.targetDetails[matchingTargetID];
|
||||||
|
|
||||||
|
return _.isEqual(fetchedTargetDetails, selectedTargetDetails);
|
||||||
|
});
|
||||||
|
this.loadNewAnnotations(filteredAnnotationsForSelection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
18
src/ui/inspector/annotations/annotation-inspector.scss
Normal file
18
src/ui/inspector/annotations/annotation-inspector.scss
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
.c-inspect-annotations {
|
||||||
|
> * + * {
|
||||||
|
margin-top: $interiorMargin;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content{
|
||||||
|
> * + * {
|
||||||
|
margin-top: $interiorMargin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -106,6 +106,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.c-inspect-properties,
|
||||||
|
.c-inspect-tags {
|
||||||
|
[class*="header"] {
|
||||||
|
@include propertiesHeader();
|
||||||
|
flex: 0 0 auto;
|
||||||
|
font-size: .85em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.c-inspect-properties,
|
.c-inspect-properties,
|
||||||
.c-inspect-styles {
|
.c-inspect-styles {
|
||||||
[class*="header"] {
|
[class*="header"] {
|
||||||
@ -187,6 +196,11 @@
|
|||||||
line-height: 1.8em;
|
line-height: 1.8em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.c-location {
|
||||||
|
// Always make the location element span columns
|
||||||
|
grid-column: 1 / 3;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/********************************************* INSPECTOR PROPERTIES TAB */
|
/********************************************* INSPECTOR PROPERTIES TAB */
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
&:not(:last-child) {
|
&:not(:last-child) {
|
||||||
&:after {
|
&:after {
|
||||||
color: $colorInspectorPropName;
|
// color: $colorInspectorPropName;
|
||||||
content: $glyph-icon-arrow-right;
|
content: $glyph-icon-arrow-right;
|
||||||
font-family: symbolsfont;
|
font-family: symbolsfont;
|
||||||
font-size: 0.7em;
|
font-size: 0.7em;
|
||||||
|
@ -131,6 +131,44 @@ export default {
|
|||||||
let resultUrl = identifierToString(this.openmct, objectPath);
|
let resultUrl = identifierToString(this.openmct, objectPath);
|
||||||
|
|
||||||
this.openmct.router.navigate(resultUrl);
|
this.openmct.router.navigate(resultUrl);
|
||||||
|
if (this.result.annotationType === this.openmct.annotation.ANNOTATION_TYPES.PLOT_SPATIAL) {
|
||||||
|
//wait a beat for the navigation
|
||||||
|
setTimeout(() => {
|
||||||
|
this.clickedPlotAnnotation();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
clickedPlotAnnotation() {
|
||||||
|
const targetDetails = {};
|
||||||
|
const targetDomainObjects = {};
|
||||||
|
Object.entries(this.result.targets).forEach(([key, value]) => {
|
||||||
|
targetDetails[key] = value;
|
||||||
|
});
|
||||||
|
this.result.targetModels.forEach((targetModel) => {
|
||||||
|
const keyString = this.openmct.objects.makeKeyString(targetModel.identifier);
|
||||||
|
targetDomainObjects[keyString] = targetModel;
|
||||||
|
});
|
||||||
|
const selection =
|
||||||
|
[
|
||||||
|
{
|
||||||
|
element: this.openmct.layout.$refs.browseObject.$el,
|
||||||
|
context: {
|
||||||
|
item: this.result
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: this.$el,
|
||||||
|
context: {
|
||||||
|
type: 'plot-points-selection',
|
||||||
|
targetDetails,
|
||||||
|
targetDomainObjects,
|
||||||
|
annotations: [this.result],
|
||||||
|
annotationType: this.openmct.annotation.ANNOTATION_TYPES.PLOT_SPATIAL,
|
||||||
|
onAnnotationChange: () => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
this.openmct.selection.select(selection, true);
|
||||||
},
|
},
|
||||||
isSearchMatched(tag) {
|
isSearchMatched(tag) {
|
||||||
if (this.result.matchingTagKeys) {
|
if (this.result.matchingTagKeys) {
|
||||||
|
@ -51,7 +51,7 @@
|
|||||||
<div class="c-gsearch__results-section-title">Annotation Results</div>
|
<div class="c-gsearch__results-section-title">Annotation Results</div>
|
||||||
<annotation-search-result
|
<annotation-search-result
|
||||||
v-for="(annotationResult) in annotationResults"
|
v-for="(annotationResult) in annotationResults"
|
||||||
:key="openmct.objects.makeKeyString(annotationResult.identifier)"
|
:key="makeKeyForAnnotationResult(annotationResult)"
|
||||||
:result="annotationResult"
|
:result="annotationResult"
|
||||||
@click.native="selectedResult"
|
@click.native="selectedResult"
|
||||||
/>
|
/>
|
||||||
@ -102,6 +102,12 @@ export default {
|
|||||||
this.resultsShown = false;
|
this.resultsShown = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
makeKeyForAnnotationResult(annotationResult) {
|
||||||
|
const annotationKeyString = this.openmct.objects.makeKeyString(annotationResult.identifier);
|
||||||
|
const firstTargetKeyString = Object.keys(annotationResult.targets)[0];
|
||||||
|
|
||||||
|
return `${annotationKeyString}-${firstTargetKeyString}`;
|
||||||
|
},
|
||||||
previewChanged(changedPreviewState) {
|
previewChanged(changedPreviewState) {
|
||||||
this.previewVisible = changedPreviewState;
|
this.previewVisible = changedPreviewState;
|
||||||
},
|
},
|
||||||
|
Loading…
x
Reference in New Issue
Block a user