Compare commits
68 Commits
v3.0.0
...
image-tagg
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92cba55f88 | ||
|
|
def37263ea | ||
|
|
a131fd185a | ||
|
|
929b5fb8f0 | ||
|
|
01cc501f6a | ||
|
|
16dd43d1c8 | ||
|
|
6878f06b03 | ||
|
|
f61eb0a2e9 | ||
|
|
a5e3317f8e | ||
|
|
7b0d1d9c8c | ||
|
|
d32913c20f | ||
|
|
15e30f52bc | ||
|
|
076c1425a8 | ||
|
|
5be9a5d04f | ||
|
|
7de80778e3 | ||
|
|
bfa82abc25 | ||
|
|
9a0923801b | ||
|
|
11295a8042 | ||
|
|
ed7e85c8a0 | ||
|
|
ec90d4d92a | ||
|
|
cfe0c7f68e | ||
|
|
12b7c0e805 | ||
|
|
39c4e581ac | ||
|
|
8d8dd34853 | ||
|
|
c530c2998a | ||
|
|
37dd237055 | ||
|
|
c7542d0052 | ||
|
|
f4007d3dfc | ||
|
|
b51654efbb | ||
|
|
3f2c63c9d7 | ||
|
|
e35d7a085b | ||
|
|
5152583c3b | ||
|
|
fbb37ac382 | ||
|
|
71c1fdc298 | ||
|
|
efb55ecafc | ||
|
|
609dcb0460 | ||
|
|
3bff7f9f32 | ||
|
|
09df2f64f2 | ||
|
|
6b2adcb7b7 | ||
|
|
2f310a3432 | ||
|
|
02855d2c9c | ||
|
|
6dd6c87ceb | ||
|
|
8945f27eed | ||
|
|
5ddf8a8ff4 | ||
|
|
3a77efb010 | ||
|
|
bd356653db | ||
|
|
d1e8b3835d | ||
|
|
051a0adbb7 | ||
|
|
87d695a454 | ||
|
|
85482902be | ||
|
|
8015aceaa7 | ||
|
|
a381673f21 | ||
|
|
7a808622ae | ||
|
|
5fb78a9604 | ||
|
|
629e884c9b | ||
|
|
a8949d39bf | ||
|
|
91ad130f8b | ||
|
|
72d8779736 | ||
|
|
e34093eda7 | ||
|
|
501fdf902b | ||
|
|
cb32dd94f8 | ||
|
|
2d868cdb58 | ||
|
|
424a2b30ac | ||
|
|
693b8804ba | ||
|
|
9018dcd319 | ||
|
|
0aaa7998f5 | ||
|
|
d426ae86b8 | ||
|
|
002d8d11e8 |
@@ -67,7 +67,6 @@ const config = {
|
||||
MCT: path.join(projectRootDir, 'src/MCT'),
|
||||
testUtils: path.join(projectRootDir, 'src/utils/testUtils.js'),
|
||||
objectUtils: path.join(projectRootDir, 'src/api/objects/object-utils.js'),
|
||||
kdbush: path.join(projectRootDir, 'node_modules/kdbush/kdbush.min.js'),
|
||||
utils: path.join(projectRootDir, 'src/utils')
|
||||
}
|
||||
},
|
||||
|
||||
@@ -23,5 +23,5 @@ module.exports = merge(common, {
|
||||
__OPENMCT_ROOT_RELATIVE__: '""'
|
||||
})
|
||||
],
|
||||
devtool: 'source-map'
|
||||
devtool: 'eval-source-map'
|
||||
});
|
||||
|
||||
@@ -205,6 +205,71 @@ test.describe('Display Layout', () => {
|
||||
|
||||
expect(await page.locator('.l-layout .l-layout__frame').count()).toEqual(0);
|
||||
});
|
||||
|
||||
test('When multiple plots are contained in a layout, we only ask for annotations once @couchdb', async ({
|
||||
page
|
||||
}) => {
|
||||
// Create another Sine Wave Generator
|
||||
const anotherSineWaveObject = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator'
|
||||
});
|
||||
// Create a Display Layout
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Display Layout',
|
||||
name: 'Test Display Layout'
|
||||
});
|
||||
// Edit Display Layout
|
||||
await page.locator('[title="Edit"]').click();
|
||||
|
||||
// Expand the 'My Items' folder in the left tree
|
||||
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click();
|
||||
// Add the Sine Wave Generator to the Display Layout and save changes
|
||||
const treePane = page.getByRole('tree', {
|
||||
name: 'Main Tree'
|
||||
});
|
||||
const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
|
||||
name: new RegExp(sineWaveObject.name)
|
||||
});
|
||||
|
||||
let layoutGridHolder = page.locator('.l-layout__grid-holder');
|
||||
// eslint-disable-next-line playwright/no-force-option
|
||||
await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder, { force: true });
|
||||
|
||||
await page.getByText('View type').click();
|
||||
await page.getByText('Overlay Plot').click();
|
||||
|
||||
const anotherSineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
|
||||
name: new RegExp(anotherSineWaveObject.name)
|
||||
});
|
||||
layoutGridHolder = page.locator('.l-layout__grid-holder');
|
||||
// eslint-disable-next-line playwright/no-force-option
|
||||
await anotherSineWaveGeneratorTreeItem.dragTo(layoutGridHolder, { force: true });
|
||||
|
||||
await page.getByText('View type').click();
|
||||
await page.getByText('Overlay Plot').click();
|
||||
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
// Time to inspect some network traffic
|
||||
let networkRequests = [];
|
||||
page.on('request', (request) => {
|
||||
const searchRequest = request.url().endsWith('_find');
|
||||
const fetchRequest = request.resourceType() === 'fetch';
|
||||
if (searchRequest && fetchRequest) {
|
||||
networkRequests.push(request);
|
||||
}
|
||||
});
|
||||
|
||||
await page.reload();
|
||||
|
||||
// wait for annotations requests to be batched and requested
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Network requests for the composite telemetry with multiple items should be:
|
||||
// 1. a single batched request for annotations
|
||||
expect(networkRequests.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -30,6 +30,7 @@ const { test, expect } = require('../../../../pluginFixtures');
|
||||
const { createDomainObjectWithDefaults } = require('../../../../appActions');
|
||||
const backgroundImageSelector = '.c-imagery__main-image__background-image';
|
||||
const panHotkey = process.platform === 'linux' ? ['Shift', 'Alt'] : ['Alt'];
|
||||
const tagHotkey = ['Shift', 'Alt'];
|
||||
const expectedAltText = process.platform === 'linux' ? 'Shift+Alt drag to pan' : 'Alt drag to pan';
|
||||
const thumbnailUrlParamsRegexp = /\?w=100&h=100/;
|
||||
|
||||
@@ -44,7 +45,7 @@ test.describe('Example Imagery Object', () => {
|
||||
|
||||
// Verify that the created object is focused
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText(exampleImagery.name);
|
||||
await page.locator(backgroundImageSelector).hover({ trial: true });
|
||||
await page.locator('.c-imagery__main-image__bg').hover({ trial: true });
|
||||
});
|
||||
|
||||
test('Can use Mouse Wheel to zoom in and out of latest image', async ({ page }) => {
|
||||
@@ -72,11 +73,11 @@ test.describe('Example Imagery Object', () => {
|
||||
test('Can use alt+drag to move around image once zoomed in', async ({ page }) => {
|
||||
const deltaYStep = 100; //equivalent to 1x zoom
|
||||
|
||||
await page.locator(backgroundImageSelector).hover({ trial: true });
|
||||
await page.locator('.c-imagery__main-image__bg').hover({ trial: true });
|
||||
|
||||
// zoom in
|
||||
await page.mouse.wheel(0, deltaYStep * 2);
|
||||
await page.locator(backgroundImageSelector).hover({ trial: true });
|
||||
await page.locator('.c-imagery__main-image__bg').hover({ trial: true });
|
||||
const zoomedBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2;
|
||||
const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2;
|
||||
@@ -131,6 +132,36 @@ test.describe('Example Imagery Object', () => {
|
||||
expect(afterDownPanBoundingBox.y).toBeLessThan(afterUpPanBoundingBox.y);
|
||||
});
|
||||
|
||||
test('Can use alt+shift+drag to create a tag', async ({ page }) => {
|
||||
const canvas = page.locator('canvas');
|
||||
await canvas.hover({ trial: true });
|
||||
|
||||
const canvasBoundingBox = await canvas.boundingBox();
|
||||
const canvasCenterX = canvasBoundingBox.x + canvasBoundingBox.width / 2;
|
||||
const canvasCenterY = canvasBoundingBox.y + canvasBoundingBox.height / 2;
|
||||
|
||||
await Promise.all(tagHotkey.map((x) => page.keyboard.down(x)));
|
||||
await page.mouse.down();
|
||||
// steps not working for me here
|
||||
await page.mouse.move(canvasCenterX - 20, canvasCenterY - 20);
|
||||
await page.mouse.move(canvasCenterX - 100, canvasCenterY - 100);
|
||||
await page.mouse.up();
|
||||
await Promise.all(tagHotkey.map((x) => page.keyboard.up(x)));
|
||||
|
||||
//Wait for canvas to stablize.
|
||||
await canvas.hover({ trial: true });
|
||||
|
||||
// add some tags
|
||||
await page.getByText('Annotations').click();
|
||||
await page.getByRole('button', { name: /Add Tag/ }).click();
|
||||
await page.getByPlaceholder('Type to select tag').click();
|
||||
await page.getByText('Driving').click();
|
||||
|
||||
await page.getByRole('button', { name: /Add Tag/ }).click();
|
||||
await page.getByPlaceholder('Type to select tag').click();
|
||||
await page.getByText('Science').click();
|
||||
});
|
||||
|
||||
test('Can use + - buttons to zoom on the image @unstable', async ({ page }) => {
|
||||
await buttonZoomOnImageAndAssert(page);
|
||||
});
|
||||
@@ -692,7 +723,6 @@ async function panZoomAndAssertImageProperties(page) {
|
||||
async function mouseZoomOnImageAndAssert(page, factor = 2) {
|
||||
// Zoom in
|
||||
const originalImageDimensions = await page.locator(backgroundImageSelector).boundingBox();
|
||||
await page.locator(backgroundImageSelector).hover({ trial: true });
|
||||
const deltaYStep = 100; // equivalent to 1x zoom
|
||||
await page.mouse.wheel(0, deltaYStep * factor);
|
||||
const zoomedBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
@@ -703,7 +733,7 @@ async function mouseZoomOnImageAndAssert(page, factor = 2) {
|
||||
await page.mouse.move(imageCenterX, imageCenterY);
|
||||
|
||||
// Wait for zoom animation to finish
|
||||
await page.locator(backgroundImageSelector).hover({ trial: true });
|
||||
await page.locator('.c-imagery__main-image__bg').hover({ trial: true });
|
||||
const imageMouseZoomed = await page.locator(backgroundImageSelector).boundingBox();
|
||||
|
||||
if (factor > 0) {
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
"eslint-plugin-you-dont-need-lodash-underscore": "6.12.0",
|
||||
"eventemitter3": "1.2.0",
|
||||
"file-saver": "2.0.5",
|
||||
"flatbush": "4.1.0",
|
||||
"git-rev-sync": "3.0.2",
|
||||
"html2canvas": "1.4.1",
|
||||
"imports-loader": "4.0.1",
|
||||
@@ -44,7 +45,6 @@
|
||||
"karma-sourcemap-loader": "0.4.0",
|
||||
"karma-spec-reporter": "0.0.36",
|
||||
"karma-webpack": "5.0.0",
|
||||
"kdbush": "3.0.0",
|
||||
"location-bar": "3.0.1",
|
||||
"lodash": "4.17.21",
|
||||
"mini-css-extract-plugin": "2.7.6",
|
||||
|
||||
@@ -76,6 +76,9 @@ const ANNOTATION_LAST_CREATED = 'annotationLastCreated';
|
||||
* @constructor
|
||||
*/
|
||||
export default class AnnotationAPI extends EventEmitter {
|
||||
/** @type {Map<ANNOTATION_TYPES, Array<(a, b) => boolean >>} */
|
||||
#targetComparatorMap;
|
||||
|
||||
/**
|
||||
* @param {OpenMCT} openmct
|
||||
*/
|
||||
@@ -84,6 +87,7 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
this.openmct = openmct;
|
||||
this.availableTags = {};
|
||||
this.namespaceToSaveAnnotations = '';
|
||||
this.#targetComparatorMap = new Map();
|
||||
|
||||
this.ANNOTATION_TYPES = ANNOTATION_TYPES;
|
||||
this.ANNOTATION_TYPE = ANNOTATION_TYPE;
|
||||
@@ -246,15 +250,16 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
/**
|
||||
* @method getAnnotations
|
||||
* @param {Identifier} domainObjectIdentifier - The domain object identifier to use to search for annotations. For example, a telemetry object identifier.
|
||||
* @param {AbortSignal} abortSignal - An abort signal to cancel the search
|
||||
* @returns {DomainObject[]} Returns an array of annotations that match the search query
|
||||
*/
|
||||
async getAnnotations(domainObjectIdentifier) {
|
||||
async getAnnotations(domainObjectIdentifier, abortSignal = null) {
|
||||
const keyStringQuery = this.openmct.objects.makeKeyString(domainObjectIdentifier);
|
||||
const searchResults = (
|
||||
await Promise.all(
|
||||
this.openmct.objects.search(
|
||||
keyStringQuery,
|
||||
null,
|
||||
abortSignal,
|
||||
this.openmct.objects.SEARCH_TYPES.ANNOTATIONS
|
||||
)
|
||||
)
|
||||
@@ -384,7 +389,8 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
const combinedResults = [];
|
||||
results.forEach((currentAnnotation) => {
|
||||
const existingAnnotation = combinedResults.find((annotationToFind) => {
|
||||
return _.isEqual(currentAnnotation.targets, annotationToFind.targets);
|
||||
const { annotationType, targets } = currentAnnotation;
|
||||
return this.areAnnotationTargetsEqual(annotationType, targets, annotationToFind.targets);
|
||||
});
|
||||
if (!existingAnnotation) {
|
||||
combinedResults.push(currentAnnotation);
|
||||
@@ -460,4 +466,35 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
|
||||
return breakApartSeparateTargets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a comparator function for a given annotation type.
|
||||
* The comparator functions will be used to determine if two annotations
|
||||
* have the same target.
|
||||
* @param {ANNOTATION_TYPES} annotationType
|
||||
* @param {(t1, t2) => boolean} comparator
|
||||
*/
|
||||
addTargetComparator(annotationType, comparator) {
|
||||
const comparatorList = this.#targetComparatorMap.get(annotationType) ?? [];
|
||||
comparatorList.push(comparator);
|
||||
this.#targetComparatorMap.set(annotationType, comparatorList);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two sets of targets to see if they are equal. First checks if
|
||||
* any targets comparators evaluate to true, then falls back to a deep
|
||||
* equality check.
|
||||
* @param {ANNOTATION_TYPES} annotationType
|
||||
* @param {*} targets
|
||||
* @param {*} otherTargets
|
||||
* @returns true if the targets are equal, false otherwise
|
||||
*/
|
||||
areAnnotationTargetsEqual(annotationType, targets, otherTargets) {
|
||||
const targetComparatorList = this.#targetComparatorMap.get(annotationType);
|
||||
return (
|
||||
(targetComparatorList?.length &&
|
||||
targetComparatorList.some((targetComparator) => targetComparator(targets, otherTargets))) ||
|
||||
_.isEqual(targets, otherTargets)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,4 +265,52 @@ describe('The Annotation API', () => {
|
||||
expect(results.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Target Comparators', () => {
|
||||
let targets;
|
||||
let otherTargets;
|
||||
let comparator;
|
||||
|
||||
beforeEach(() => {
|
||||
targets = {
|
||||
fooTarget: {
|
||||
foo: 42
|
||||
}
|
||||
};
|
||||
otherTargets = {
|
||||
fooTarget: {
|
||||
bar: 42
|
||||
}
|
||||
};
|
||||
comparator = (t1, t2) => t1.fooTarget.foo === t2.fooTarget.bar;
|
||||
});
|
||||
|
||||
it('can add a comparator function', () => {
|
||||
const notebookAnnotationType = openmct.annotation.ANNOTATION_TYPES.NOTEBOOK;
|
||||
expect(
|
||||
openmct.annotation.areAnnotationTargetsEqual(notebookAnnotationType, targets, otherTargets)
|
||||
).toBeFalse(); // without a comparator, these should NOT be equal
|
||||
// Register a comparator function for the notebook annotation type
|
||||
openmct.annotation.addTargetComparator(notebookAnnotationType, comparator);
|
||||
expect(
|
||||
openmct.annotation.areAnnotationTargetsEqual(notebookAnnotationType, targets, otherTargets)
|
||||
).toBeTrue(); // the comparator should make these equal
|
||||
});
|
||||
|
||||
it('falls back to deep equality check if no comparator functions', () => {
|
||||
const annotationTypeWithoutComparator = openmct.annotation.ANNOTATION_TYPES.GEOSPATIAL;
|
||||
const areEqual = openmct.annotation.areAnnotationTargetsEqual(
|
||||
annotationTypeWithoutComparator,
|
||||
targets,
|
||||
targets
|
||||
);
|
||||
const areNotEqual = openmct.annotation.areAnnotationTargetsEqual(
|
||||
annotationTypeWithoutComparator,
|
||||
targets,
|
||||
otherTargets
|
||||
);
|
||||
expect(areEqual).toBeTrue();
|
||||
expect(areNotEqual).toBeFalse();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
378
src/plugins/imagery/components/AnnotationsCanvas.vue
Normal file
378
src/plugins/imagery/components/AnnotationsCanvas.vue
Normal file
@@ -0,0 +1,378 @@
|
||||
<!--
|
||||
Open MCT, Copyright (c) 2014-2023, 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>
|
||||
<canvas
|
||||
ref="canvas"
|
||||
class="c-image-canvas"
|
||||
style="width: 100%; height: 100%"
|
||||
@mousedown="clearSelectedAnnotations"
|
||||
@mousemove="trackAnnotationDrag"
|
||||
@click="selectOrCreateAnnotation"
|
||||
></canvas>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Flatbush from 'flatbush';
|
||||
const EXISTING_ANNOTATION_STROKE_STYLE = '#D79078';
|
||||
const EXISTING_ANNOTATION_FILL_STYLE = 'rgba(202, 202, 142, 0.2)';
|
||||
const SELECTED_ANNOTATION_STROKE_COLOR = '#BD8ECC';
|
||||
const SELECTED_ANNOTATION_FILL_STYLE = 'rgba(199, 87, 231, 0.2)';
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'domainObject', 'objectPath'],
|
||||
props: {
|
||||
image: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
imageryAnnotations: {
|
||||
type: Array,
|
||||
default() {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dragging: false,
|
||||
mouseDown: false,
|
||||
newAnnotationRectangle: {},
|
||||
keyString: null,
|
||||
context: null,
|
||||
canvas: null,
|
||||
annotationsIndex: null,
|
||||
selectedAnnotations: [],
|
||||
indexToAnnotationMap: {}
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
imageryAnnotations() {
|
||||
this.buildAnnotationIndex();
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.canvas = this.$refs.canvas;
|
||||
this.context = this.canvas.getContext('2d');
|
||||
|
||||
// adjust canvas size for retina displays
|
||||
const scale = window.devicePixelRatio;
|
||||
this.canvas.width = Math.floor(this.canvas.width * scale);
|
||||
this.canvas.height = Math.floor(this.canvas.height * scale);
|
||||
|
||||
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
this.openmct.selection.on('change', this.updateSelection);
|
||||
this.buildAnnotationIndex();
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.openmct.selection.off('change', this.updateSelection);
|
||||
document.body.removeEventListener('click', this.cancelSelection);
|
||||
},
|
||||
methods: {
|
||||
buildAnnotationIndex() {
|
||||
if (this.imageryAnnotations.length) {
|
||||
// create a flatbush index for the annotations
|
||||
this.annotationsIndex = new Flatbush(this.imageryAnnotations.length);
|
||||
this.imageryAnnotations.forEach((annotation) => {
|
||||
const annotationRectangle = annotation.targets[this.keyString].rectangle;
|
||||
const indexNumber = this.annotationsIndex.add(
|
||||
annotationRectangle.x,
|
||||
annotationRectangle.y,
|
||||
annotationRectangle.x + annotationRectangle.width,
|
||||
annotationRectangle.y + annotationRectangle.height
|
||||
);
|
||||
this.indexToAnnotationMap[indexNumber] = annotation;
|
||||
});
|
||||
this.annotationsIndex.finish();
|
||||
|
||||
this.drawAnnotations();
|
||||
}
|
||||
},
|
||||
onAnnotationChange(annotations) {
|
||||
this.selectedAnnotations = annotations;
|
||||
this.$emit('annotationsChanged', annotations);
|
||||
},
|
||||
updateSelection(selection) {
|
||||
const selectionContext = selection?.[0]?.[0]?.context?.item;
|
||||
const selectionType = selection?.[0]?.[0]?.context?.type;
|
||||
const validSelectionTypes = ['clicked-on-image-selection'];
|
||||
|
||||
if (!validSelectionTypes.includes(selectionType)) {
|
||||
// wrong type of selection
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
selectionContext &&
|
||||
this.openmct.objects.areIdsEqual(selectionContext.identifier, this.domainObject.identifier)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const incomingSelectedAnnotations = selection?.[0]?.[0]?.context?.annotations;
|
||||
|
||||
this.prepareExistingAnnotationSelection(incomingSelectedAnnotations);
|
||||
},
|
||||
prepareExistingAnnotationSelection(annotations) {
|
||||
const targetDomainObjects = {};
|
||||
targetDomainObjects[this.keyString] = this.domainObject;
|
||||
|
||||
const targetDetails = {};
|
||||
annotations.forEach((annotation) => {
|
||||
Object.entries(annotation.targets).forEach(([key, value]) => {
|
||||
targetDetails[key] = value;
|
||||
});
|
||||
});
|
||||
this.selectedAnnotations = annotations;
|
||||
this.drawAnnotations();
|
||||
|
||||
return {
|
||||
targetDomainObjects,
|
||||
targetDetails
|
||||
};
|
||||
},
|
||||
clearSelectedAnnotations() {
|
||||
if (!this.openmct.annotation.getAvailableTags().length) {
|
||||
// don't bother with new annotations if there are no tags
|
||||
return;
|
||||
}
|
||||
|
||||
this.mouseDown = true;
|
||||
this.selectedAnnotations = [];
|
||||
},
|
||||
drawRectInCanvas(rectangle, fillStyle, strokeStyle) {
|
||||
this.context.beginPath();
|
||||
this.context.lineWidth = 1;
|
||||
this.context.fillStyle = fillStyle;
|
||||
this.context.strokeStyle = strokeStyle;
|
||||
this.context.rect(rectangle.x, rectangle.y, rectangle.width, rectangle.height);
|
||||
this.context.fill();
|
||||
this.context.stroke();
|
||||
},
|
||||
trackAnnotationDrag(event) {
|
||||
if (this.mouseDown && !this.dragging && event.shiftKey && event.altKey) {
|
||||
this.startAnnotationDrag(event);
|
||||
} else if (this.dragging) {
|
||||
const boundingRect = this.canvas.getBoundingClientRect();
|
||||
const scaleX = this.canvas.width / boundingRect.width;
|
||||
const scaleY = this.canvas.height / boundingRect.height;
|
||||
this.newAnnotationRectangle = {
|
||||
x: this.newAnnotationRectangle.x,
|
||||
y: this.newAnnotationRectangle.y,
|
||||
width: (event.clientX - boundingRect.left) * scaleX - this.newAnnotationRectangle.x,
|
||||
height: (event.clientY - boundingRect.top) * scaleY - this.newAnnotationRectangle.y
|
||||
};
|
||||
this.drawAnnotations();
|
||||
this.drawRectInCanvas(
|
||||
this.newAnnotationRectangle,
|
||||
SELECTED_ANNOTATION_FILL_STYLE,
|
||||
SELECTED_ANNOTATION_STROKE_COLOR
|
||||
);
|
||||
}
|
||||
},
|
||||
clearCanvas() {
|
||||
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
},
|
||||
selectImageView() {
|
||||
// should show ImageView itself if we have no annotations to display
|
||||
const selection = this.createPathSelection();
|
||||
this.openmct.selection.select(selection, true);
|
||||
},
|
||||
createSelection(annotation) {
|
||||
const selection = this.createPathSelection();
|
||||
selection[0].context = annotation;
|
||||
|
||||
return selection;
|
||||
},
|
||||
selectImageAnnotations({ targetDetails, targetDomainObjects, annotations }) {
|
||||
const annotationContext = {
|
||||
type: 'clicked-on-image-selection',
|
||||
targetDetails,
|
||||
targetDomainObjects,
|
||||
annotations,
|
||||
annotationType: this.openmct.annotation.ANNOTATION_TYPES.PIXEL_SPATIAL,
|
||||
onAnnotationChange: this.onAnnotationChange
|
||||
};
|
||||
const selection = this.createPathSelection();
|
||||
if (
|
||||
selection.length &&
|
||||
this.openmct.objects.areIdsEqual(
|
||||
selection[0].context.item.identifier,
|
||||
this.domainObject.identifier
|
||||
)
|
||||
) {
|
||||
selection[0].context = {
|
||||
...selection[0].context,
|
||||
...annotationContext
|
||||
};
|
||||
} else {
|
||||
selection.unshift({
|
||||
element: this.$el,
|
||||
context: {
|
||||
item: this.domainObject,
|
||||
...annotationContext
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.openmct.selection.select(selection, true);
|
||||
|
||||
document.body.addEventListener('click', this.cancelSelection);
|
||||
},
|
||||
cancelSelection(event) {
|
||||
if (this.$refs.canvas) {
|
||||
const clickedInsideCanvas = this.$refs.canvas.contains(event.target);
|
||||
const clickedInsideInspector = event.target.closest('.js-inspector') !== null;
|
||||
const clickedOption = event.target.closest('.js-autocomplete-options') !== null;
|
||||
if (!clickedInsideCanvas && !clickedInsideInspector && !clickedOption) {
|
||||
this.newAnnotationRectangle = {};
|
||||
this.selectedAnnotations = [];
|
||||
this.drawAnnotations();
|
||||
}
|
||||
}
|
||||
},
|
||||
createNewAnnotation() {
|
||||
this.dragging = false;
|
||||
this.selectedAnnotations = [];
|
||||
|
||||
const targetDomainObjects = {};
|
||||
targetDomainObjects[this.keyString] = this.domainObject;
|
||||
const targetDetails = {};
|
||||
targetDetails[this.keyString] = {
|
||||
rectangle: {
|
||||
x: this.newAnnotationRectangle.x,
|
||||
y: this.newAnnotationRectangle.y,
|
||||
width: this.newAnnotationRectangle.width,
|
||||
height: this.newAnnotationRectangle.height
|
||||
},
|
||||
time: this.image.time
|
||||
};
|
||||
this.selectImageAnnotations({
|
||||
targetDetails,
|
||||
targetDomainObjects,
|
||||
annotations: []
|
||||
});
|
||||
},
|
||||
attemptToSelectExistingAnnotation(event) {
|
||||
this.dragging = false;
|
||||
// use flatbush to find annotations that are close to the click
|
||||
const boundingRect = this.canvas.getBoundingClientRect();
|
||||
const scaleX = this.canvas.width / boundingRect.width;
|
||||
const scaleY = this.canvas.height / boundingRect.height;
|
||||
const x = (event.clientX - boundingRect.left) * scaleX;
|
||||
const y = (event.clientY - boundingRect.top) * scaleY;
|
||||
if (this.annotationsIndex) {
|
||||
let nearbyAnnotations = [];
|
||||
const resultIndicies = this.annotationsIndex.search(x, y, x, y);
|
||||
resultIndicies.forEach((resultIndex) => {
|
||||
const foundAnnotation = this.indexToAnnotationMap[resultIndex];
|
||||
|
||||
nearbyAnnotations.push(foundAnnotation);
|
||||
});
|
||||
//show annotations if some were found
|
||||
const { targetDomainObjects, targetDetails } =
|
||||
this.prepareExistingAnnotationSelection(nearbyAnnotations);
|
||||
this.selectImageAnnotations({
|
||||
targetDetails,
|
||||
targetDomainObjects,
|
||||
annotations: nearbyAnnotations
|
||||
});
|
||||
} else {
|
||||
// nothing selected
|
||||
this.drawAnnotations();
|
||||
}
|
||||
},
|
||||
selectOrCreateAnnotation(event) {
|
||||
event.stopPropagation();
|
||||
this.mouseDown = false;
|
||||
if (
|
||||
!this.dragging ||
|
||||
(!this.newAnnotationRectangle.width && !this.newAnnotationRectangle.height)
|
||||
) {
|
||||
this.newAnnotationRectangle = {};
|
||||
this.attemptToSelectExistingAnnotation(event);
|
||||
} else {
|
||||
this.createNewAnnotation();
|
||||
}
|
||||
},
|
||||
createPathSelection() {
|
||||
let selection = [];
|
||||
selection.unshift({
|
||||
element: this.$el,
|
||||
context: {
|
||||
item: this.domainObject
|
||||
}
|
||||
});
|
||||
this.objectPath.forEach((pathObject, index) => {
|
||||
selection.push({
|
||||
element: this.openmct.layout.$refs.browseObject.$el,
|
||||
context: {
|
||||
item: pathObject
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return selection;
|
||||
},
|
||||
startAnnotationDrag(event) {
|
||||
this.$emit('annotationMarqueed');
|
||||
this.newAnnotationRectangle = {};
|
||||
const boundingRect = this.canvas.getBoundingClientRect();
|
||||
const scaleX = this.canvas.width / boundingRect.width;
|
||||
const scaleY = this.canvas.height / boundingRect.height;
|
||||
this.newAnnotationRectangle = {
|
||||
x: (event.clientX - boundingRect.left) * scaleX,
|
||||
y: (event.clientY - boundingRect.top) * scaleY
|
||||
};
|
||||
this.dragging = true;
|
||||
},
|
||||
isSelectedAnnotation(annotation) {
|
||||
const someSelectedAnnotationExists = this.selectedAnnotations.some((selectedAnnotation) => {
|
||||
return this.openmct.objects.areIdsEqual(
|
||||
selectedAnnotation.identifier,
|
||||
annotation.identifier
|
||||
);
|
||||
});
|
||||
|
||||
return someSelectedAnnotationExists;
|
||||
},
|
||||
drawAnnotations() {
|
||||
this.clearCanvas();
|
||||
this.imageryAnnotations.forEach((annotation) => {
|
||||
if (this.isSelectedAnnotation(annotation)) {
|
||||
this.drawRectInCanvas(
|
||||
annotation.targets[this.keyString].rectangle,
|
||||
SELECTED_ANNOTATION_FILL_STYLE,
|
||||
SELECTED_ANNOTATION_STROKE_COLOR
|
||||
);
|
||||
} else {
|
||||
this.drawRectInCanvas(
|
||||
annotation.targets[this.keyString].rectangle,
|
||||
EXISTING_ANNOTATION_FILL_STYLE,
|
||||
EXISTING_ANNOTATION_STROKE_STYLE
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -19,7 +19,7 @@ $elemBg: rgba(black, 0.7);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
z-index: 3;
|
||||
@include userSelectNone;
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,11 @@
|
||||
fetchpriority="low"
|
||||
@load="imageLoadCompleted"
|
||||
/>
|
||||
<i
|
||||
v-show="showAnnotationIndicator"
|
||||
class="c-thumb__annotation-indicator icon-status-poll-edit"
|
||||
>
|
||||
</i>
|
||||
</a>
|
||||
<div v-if="viewableArea" class="c-thumb__viewable-area" :style="viewableAreaStyle"></div>
|
||||
<div class="c-thumb__timestamp">{{ image.formattedTime }}</div>
|
||||
@@ -66,6 +71,12 @@ export default {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
imageryAnnotations: {
|
||||
type: Array,
|
||||
default() {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
viewableArea: {
|
||||
type: Object,
|
||||
default: function () {
|
||||
@@ -125,6 +136,9 @@ export default {
|
||||
width: `${width}px`,
|
||||
height: `${height}px`
|
||||
};
|
||||
},
|
||||
showAnnotationIndicator() {
|
||||
return this.imageryAnnotations?.length > 0;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -88,6 +88,13 @@
|
||||
:image="focusedImage"
|
||||
:sized-image-dimensions="sizedImageDimensions"
|
||||
/>
|
||||
<AnnotationsCanvas
|
||||
v-if="shouldDisplayAnnotations"
|
||||
:image="focusedImage"
|
||||
:imagery-annotations="imageryAnnotations[focusedImage.time]"
|
||||
@annotationMarqueed="handlePauseButton(true)"
|
||||
@annotationsChanged="loadAnnotations"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -173,6 +180,7 @@
|
||||
:key="`${image.thumbnailUrl || image.url}-${image.time}-${index}`"
|
||||
:image="image"
|
||||
:active="focusedImageIndex === index"
|
||||
:imagery-annotations="imageryAnnotations[image.time]"
|
||||
:selected="focusedImageIndex === index && isPaused"
|
||||
:real-time="!isFixed"
|
||||
:viewable-area="focusedImageIndex === index ? viewableArea : null"
|
||||
@@ -200,6 +208,7 @@ import Compass from './Compass/Compass.vue';
|
||||
import ImageControls from './ImageControls.vue';
|
||||
import ImageThumbnail from './ImageThumbnail.vue';
|
||||
import imageryData from '../../imagery/mixins/imageryData';
|
||||
import AnnotationsCanvas from './AnnotationsCanvas.vue';
|
||||
|
||||
const REFRESH_CSS_MS = 500;
|
||||
const DURATION_TRACK_MS = 1000;
|
||||
@@ -232,7 +241,8 @@ export default {
|
||||
components: {
|
||||
Compass,
|
||||
ImageControls,
|
||||
ImageThumbnail
|
||||
ImageThumbnail,
|
||||
AnnotationsCanvas
|
||||
},
|
||||
mixins: [imageryData],
|
||||
inject: ['openmct', 'domainObject', 'objectPath', 'currentView', 'imageFreshnessOptions'],
|
||||
@@ -295,7 +305,8 @@ export default {
|
||||
animateZoom: true,
|
||||
imagePanned: false,
|
||||
forceShowThumbnails: false,
|
||||
animateThumbScroll: false
|
||||
animateThumbScroll: false,
|
||||
imageryAnnotations: {}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -425,6 +436,19 @@ export default {
|
||||
|
||||
return result;
|
||||
},
|
||||
shouldDisplayAnnotations() {
|
||||
const imageHeightAndWidth = this.sizedImageHeight !== 0 && this.sizedImageWidth !== 0;
|
||||
const display =
|
||||
this.focusedImage !== undefined &&
|
||||
this.focusedImageNaturalAspectRatio !== undefined &&
|
||||
this.imageContainerWidth !== undefined &&
|
||||
this.imageContainerHeight !== undefined &&
|
||||
imageHeightAndWidth &&
|
||||
this.zoomFactor === 1 &&
|
||||
this.imagePanned !== true;
|
||||
|
||||
return display;
|
||||
},
|
||||
shouldDisplayCompass() {
|
||||
const imageHeightAndWidth = this.sizedImageHeight !== 0 && this.sizedImageWidth !== 0;
|
||||
const display =
|
||||
@@ -689,6 +713,9 @@ export default {
|
||||
|
||||
this.listenTo(this.focusedImageWrapper, 'wheel', this.wheelZoom, this);
|
||||
this.loadVisibleLayers();
|
||||
this.loadAnnotations();
|
||||
|
||||
this.openmct.selection.on('change', this.updateSelection);
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.persistVisibleLayers();
|
||||
@@ -716,6 +743,15 @@ export default {
|
||||
}
|
||||
|
||||
this.stopListening(this.focusedImageWrapper, 'wheel', this.wheelZoom, this);
|
||||
|
||||
Object.keys(this.imageryAnnotations).forEach((time) => {
|
||||
const imageAnnotationsForTime = this.imageryAnnotations[time];
|
||||
imageAnnotationsForTime.forEach((imageAnnotation) => {
|
||||
this.openmct.objects.destroyMutable(imageAnnotation);
|
||||
});
|
||||
});
|
||||
|
||||
this.openmct.selection.off('change', this.updateSelection);
|
||||
},
|
||||
methods: {
|
||||
calculateViewHeight() {
|
||||
@@ -743,6 +779,24 @@ export default {
|
||||
this.timeContext.off('clock', this.trackDuration);
|
||||
}
|
||||
},
|
||||
updateSelection(selection) {
|
||||
const selectionType = selection?.[0]?.[0]?.context?.type;
|
||||
const validSelectionTypes = ['annotation-search-result'];
|
||||
|
||||
if (!validSelectionTypes.includes(selectionType)) {
|
||||
// wrong type of selection
|
||||
return;
|
||||
}
|
||||
|
||||
const incomingSelectedAnnotation = selection?.[0]?.[0]?.context?.annotations?.[0];
|
||||
console.debug(`📲 incoming search selections`, incomingSelectedAnnotation);
|
||||
// TODO in https://github.com/nasa/openmct/issues/6731
|
||||
// For incoming search results, we should:
|
||||
// 1. set the the time bounds to match the search result
|
||||
// 2. search the imageHistory for the image that matches the time of the search result
|
||||
// 3. using the index from the above, "click" on the image to select it
|
||||
// 4. pass to the annotation canvas layer the selected annotation
|
||||
},
|
||||
expand() {
|
||||
// check for modifier keys so it doesnt interfere with the layout
|
||||
if (this.cursorStates.modifierKeyPressed) {
|
||||
@@ -832,6 +886,40 @@ export default {
|
||||
});
|
||||
}
|
||||
},
|
||||
async loadAnnotations(existingAnnotations) {
|
||||
if (!this.openmct.annotation.getAvailableTags().length) {
|
||||
// don't bother loading annotations if there are no tags
|
||||
return;
|
||||
}
|
||||
let foundAnnotations = existingAnnotations;
|
||||
if (!foundAnnotations) {
|
||||
// attempt to load
|
||||
foundAnnotations = await this.openmct.annotation.getAnnotations(
|
||||
this.domainObject.identifier
|
||||
);
|
||||
}
|
||||
foundAnnotations.forEach((foundAnnotation) => {
|
||||
const targetId = Object.keys(foundAnnotation.targets)[0];
|
||||
const timeForAnnotation = foundAnnotation.targets[targetId].time;
|
||||
if (!this.imageryAnnotations[timeForAnnotation]) {
|
||||
this.$set(this.imageryAnnotations, timeForAnnotation, []);
|
||||
}
|
||||
|
||||
const annotationExtant = this.imageryAnnotations[timeForAnnotation].some(
|
||||
(existingAnnotation) => {
|
||||
return this.openmct.objects.areIdsEqual(
|
||||
existingAnnotation.identifier,
|
||||
foundAnnotation.identifier
|
||||
);
|
||||
}
|
||||
);
|
||||
if (!annotationExtant) {
|
||||
const annotationArray = this.imageryAnnotations[timeForAnnotation];
|
||||
const mutableAnnotation = this.openmct.objects.toMutable(foundAnnotation);
|
||||
annotationArray.push(mutableAnnotation);
|
||||
}
|
||||
});
|
||||
},
|
||||
persistVisibleLayers() {
|
||||
if (
|
||||
this.domainObject.configuration &&
|
||||
@@ -979,7 +1067,9 @@ export default {
|
||||
}
|
||||
|
||||
await Vue.nextTick();
|
||||
this.$refs.thumbsWrapper.scrollLeft = scrollWidth;
|
||||
if (this.$refs.thumbsWrapper) {
|
||||
this.$refs.thumbsWrapper.scrollLeft = scrollWidth;
|
||||
}
|
||||
},
|
||||
scrollHandler() {
|
||||
if (this.isPaused) {
|
||||
|
||||
@@ -293,6 +293,13 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__annotation-indicator {
|
||||
color: $colorClickIconButton;
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 8px;
|
||||
}
|
||||
|
||||
&__timestamp {
|
||||
flex: 0 0 auto;
|
||||
padding: 2px 3px;
|
||||
@@ -540,3 +547,11 @@
|
||||
align-self: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.c-image-canvas {
|
||||
pointer-events: auto; // This allows the image element to receive a browser-level context click
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
|
||||
<script>
|
||||
import TagEditor from './tags/TagEditor.vue';
|
||||
import _ from 'lodash';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -123,6 +122,7 @@ export default {
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
this.abortController = null;
|
||||
this.openmct.annotation.on('targetDomainObjectAnnotated', this.loadAnnotationForTargetObject);
|
||||
this.openmct.selection.on('change', this.updateSelection);
|
||||
await this.updateSelection(this.openmct.selection.get());
|
||||
@@ -190,20 +190,34 @@ export default {
|
||||
}
|
||||
},
|
||||
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];
|
||||
// If the user changes targets while annotations are loading,
|
||||
// abort the previous request.
|
||||
if (this.abortController !== null) {
|
||||
this.abortController.abort();
|
||||
}
|
||||
|
||||
return _.isEqual(fetchedTargetDetails, selectedTargetDetails);
|
||||
});
|
||||
this.loadNewAnnotations(filteredAnnotationsForSelection);
|
||||
this.abortController = new AbortController();
|
||||
|
||||
try {
|
||||
const allAnnotationsForTarget = await this.openmct.annotation.getAnnotations(
|
||||
target.identifier,
|
||||
this.abortController.signal
|
||||
);
|
||||
const filteredAnnotationsForSelection = allAnnotationsForTarget.filter((annotation) =>
|
||||
this.openmct.annotation.areAnnotationTargetsEqual(
|
||||
this.annotationType,
|
||||
this.targetDetails,
|
||||
annotation.targets
|
||||
)
|
||||
);
|
||||
this.loadNewAnnotations(filteredAnnotationsForSelection);
|
||||
} catch (err) {
|
||||
if (err.name !== 'AbortError') {
|
||||
throw err;
|
||||
}
|
||||
} finally {
|
||||
this.abortController = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -306,13 +306,22 @@ export default {
|
||||
this.getSearchResults = debounce(this.getSearchResults, 500);
|
||||
this.syncUrlWithPageAndSection = debounce(this.syncUrlWithPageAndSection, 100);
|
||||
},
|
||||
async mounted() {
|
||||
await this.loadAnnotations();
|
||||
async created() {
|
||||
this.transaction = null;
|
||||
this.abortController = new AbortController();
|
||||
try {
|
||||
await this.loadAnnotations();
|
||||
} catch (err) {
|
||||
if (err.name !== 'AbortError') {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.formatSidebar();
|
||||
this.setSectionAndPageFromUrl();
|
||||
|
||||
this.openmct.selection.on('change', this.updateSelection);
|
||||
this.transaction = null;
|
||||
|
||||
window.addEventListener('orientationchange', this.formatSidebar);
|
||||
window.addEventListener('hashchange', this.setSectionAndPageFromUrl);
|
||||
@@ -324,6 +333,7 @@ export default {
|
||||
);
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.abortController.abort();
|
||||
if (this.unlisten) {
|
||||
this.unlisten();
|
||||
}
|
||||
@@ -387,8 +397,10 @@ export default {
|
||||
this.lastLocalAnnotationCreation = this.domainObject.annotationLastCreated ?? 0;
|
||||
|
||||
const foundAnnotations = await this.openmct.annotation.getAnnotations(
|
||||
this.domainObject.identifier
|
||||
this.domainObject.identifier,
|
||||
this.abortController.signal
|
||||
);
|
||||
|
||||
foundAnnotations.forEach((foundAnnotation) => {
|
||||
const targetId = Object.keys(foundAnnotation.targets)[0];
|
||||
const entryId = foundAnnotation.targets[targetId].entryId;
|
||||
@@ -425,7 +437,11 @@ export default {
|
||||
: [...filteredPageEntriesByTime].reverse();
|
||||
|
||||
if (this.lastLocalAnnotationCreation < this.domainObject.annotationLastCreated) {
|
||||
this.loadAnnotations();
|
||||
this.loadAnnotations().catch((err) => {
|
||||
if (err.name !== 'AbortError') {
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
changeSelectedSection({ sectionId, pageId }) {
|
||||
|
||||
@@ -27,7 +27,13 @@
|
||||
// If the above namespace is ever resolved, we can fold this search provider
|
||||
// back into the object provider.
|
||||
|
||||
const BATCH_ANNOTATION_DEBOUNCE_MS = 100;
|
||||
|
||||
class CouchSearchProvider {
|
||||
#bulkPromise;
|
||||
#batchIds;
|
||||
#lastAbortSignal;
|
||||
|
||||
constructor(couchObjectProvider) {
|
||||
this.couchObjectProvider = couchObjectProvider;
|
||||
this.searchTypes = couchObjectProvider.openmct.objects.SEARCH_TYPES;
|
||||
@@ -36,6 +42,8 @@ class CouchSearchProvider {
|
||||
this.searchTypes.ANNOTATIONS,
|
||||
this.searchTypes.TAGS
|
||||
];
|
||||
this.#batchIds = [];
|
||||
this.#bulkPromise = null;
|
||||
}
|
||||
|
||||
supportsSearchType(searchType) {
|
||||
@@ -68,28 +76,77 @@ class CouchSearchProvider {
|
||||
return this.couchObjectProvider.getObjectsByFilter(filter, abortSignal);
|
||||
}
|
||||
|
||||
searchForAnnotations(keyString, abortSignal) {
|
||||
async #deferBatchAnnotationSearch() {
|
||||
// We until the next event loop cycle to "collect" all of the get
|
||||
// requests triggered in this iteration of the event loop
|
||||
await this.#waitForDebounce();
|
||||
const batchIdsToSearch = [...this.#batchIds];
|
||||
this.#clearBatch();
|
||||
return this.#bulkAnnotationSearch(batchIdsToSearch);
|
||||
}
|
||||
|
||||
#clearBatch() {
|
||||
this.#batchIds = [];
|
||||
this.#bulkPromise = undefined;
|
||||
}
|
||||
|
||||
#waitForDebounce() {
|
||||
let timeoutID;
|
||||
clearTimeout(timeoutID);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
timeoutID = setTimeout(() => {
|
||||
resolve();
|
||||
}, BATCH_ANNOTATION_DEBOUNCE_MS);
|
||||
});
|
||||
}
|
||||
|
||||
#bulkAnnotationSearch(batchIdsToSearch) {
|
||||
const filter = {
|
||||
selector: {
|
||||
$and: [
|
||||
{
|
||||
model: {
|
||||
targets: {}
|
||||
}
|
||||
},
|
||||
{
|
||||
'model.type': {
|
||||
$eq: 'annotation'
|
||||
}
|
||||
},
|
||||
{
|
||||
$or: []
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
filter.selector.$and[0].model.targets[keyString] = {
|
||||
$exists: true
|
||||
};
|
||||
let lastAbortSignal = null;
|
||||
// TODO: should remove duplicates from batchIds
|
||||
batchIdsToSearch.forEach(({ keyString, abortSignal }) => {
|
||||
const modelFilter = {
|
||||
model: {
|
||||
targets: {}
|
||||
}
|
||||
};
|
||||
modelFilter.model.targets[keyString] = {
|
||||
$exists: true
|
||||
};
|
||||
|
||||
return this.couchObjectProvider.getObjectsByFilter(filter, abortSignal);
|
||||
filter.selector.$and[1].$or.push(modelFilter);
|
||||
lastAbortSignal = abortSignal;
|
||||
});
|
||||
|
||||
return this.couchObjectProvider.getObjectsByFilter(filter, lastAbortSignal);
|
||||
}
|
||||
|
||||
async searchForAnnotations(keyString, abortSignal) {
|
||||
this.#batchIds.push({ keyString, abortSignal });
|
||||
if (!this.#bulkPromise) {
|
||||
this.#bulkPromise = this.#deferBatchAnnotationSearch();
|
||||
}
|
||||
|
||||
const returnedData = await this.#bulkPromise;
|
||||
// only return data that matches the keystring
|
||||
const filteredByKeyString = returnedData.filter((foundAnnotation) => {
|
||||
return foundAnnotation.targets[keyString];
|
||||
});
|
||||
return filteredByKeyString;
|
||||
}
|
||||
|
||||
searchForTags(tagsArray, abortSignal) {
|
||||
|
||||
@@ -183,7 +183,7 @@ import MctTicks from './MctTicks.vue';
|
||||
import MctChart from './chart/MctChart.vue';
|
||||
import XAxis from './axis/XAxis.vue';
|
||||
import YAxis from './axis/YAxis.vue';
|
||||
import KDBush from 'kdbush';
|
||||
import Flatbush from 'flatbush';
|
||||
import _ from 'lodash';
|
||||
|
||||
const OFFSET_THRESHOLD = 10;
|
||||
@@ -339,6 +339,9 @@ export default {
|
||||
this.cursorGuide = newCursorGuide;
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.abortController = new AbortController();
|
||||
},
|
||||
mounted() {
|
||||
this.yAxisIdVisibility = {};
|
||||
this.offsetWidth = 0;
|
||||
@@ -398,6 +401,7 @@ export default {
|
||||
this.loaded = true;
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.abortController.abort();
|
||||
this.openmct.selection.off('change', this.updateSelection);
|
||||
document.removeEventListener('keydown', this.handleKeyDown);
|
||||
document.removeEventListener('keyup', this.handleKeyUp);
|
||||
@@ -410,8 +414,8 @@ export default {
|
||||
// on clicking on a search result we highlight the annotation and zoom - we know it's an annotation result when isAnnotationSearchResult === true
|
||||
// We shouldn't zoom when we're selecting existing annotations to view them or creating new annotations.
|
||||
const selectionType = selection?.[0]?.[0]?.context?.type;
|
||||
const validSelectionTypes = ['clicked-on-plot-selection', 'plot-annotation-search-result'];
|
||||
const isAnnotationSearchResult = selectionType === 'plot-annotation-search-result';
|
||||
const validSelectionTypes = ['clicked-on-plot-selection', 'annotation-search-result'];
|
||||
const isAnnotationSearchResult = selectionType === 'annotation-search-result';
|
||||
|
||||
if (!validSelectionTypes.includes(selectionType)) {
|
||||
// wrong type of selection
|
||||
@@ -621,7 +625,8 @@ export default {
|
||||
await Promise.all(
|
||||
this.seriesModels.map(async (seriesModel) => {
|
||||
const seriesAnnotations = await this.openmct.annotation.getAnnotations(
|
||||
seriesModel.model.identifier
|
||||
seriesModel.model.identifier,
|
||||
this.abortController.signal
|
||||
);
|
||||
rawAnnotationsForPlot.push(...seriesAnnotations);
|
||||
})
|
||||
@@ -1393,6 +1398,24 @@ export default {
|
||||
|
||||
return annotationsByPoints.flat();
|
||||
},
|
||||
searchWithFlatbush(seriesData, seriesModel, boundingBox) {
|
||||
const flatbush = new Flatbush(seriesData.length);
|
||||
seriesData.forEach((point) => {
|
||||
const x = seriesModel.getXVal(point);
|
||||
const y = seriesModel.getYVal(point);
|
||||
flatbush.add(x, y, x, y);
|
||||
});
|
||||
flatbush.finish();
|
||||
|
||||
const rangeResults = flatbush.search(
|
||||
boundingBox.minX,
|
||||
boundingBox.minY,
|
||||
boundingBox.maxX,
|
||||
boundingBox.maxY
|
||||
);
|
||||
|
||||
return rangeResults;
|
||||
},
|
||||
getPointsInBox(boundingBoxPerYAxis, rawAnnotation) {
|
||||
// load series models in KD-Trees
|
||||
const seriesKDTrees = [];
|
||||
@@ -1408,22 +1431,8 @@ export default {
|
||||
|
||||
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
|
||||
);
|
||||
const rangeResults = this.searchWithFlatbush(seriesData, seriesModel, boundingBox);
|
||||
rangeResults.forEach((id) => {
|
||||
const seriesDatum = seriesData[id];
|
||||
if (seriesDatum) {
|
||||
@@ -1524,7 +1533,11 @@ export default {
|
||||
this.endMarquee();
|
||||
}
|
||||
|
||||
this.loadAnnotations();
|
||||
this.loadAnnotations().catch((err) => {
|
||||
if (err.name !== 'AbortError') {
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
zoom(zoomDirection, zoomFactor) {
|
||||
|
||||
@@ -98,6 +98,13 @@ export default {
|
||||
}
|
||||
|
||||
return 'Could not find any matching Notebook entries';
|
||||
} else if (
|
||||
this.result.annotationType === this.openmct.annotation.ANNOTATION_TYPES.GEOSPATIAL
|
||||
) {
|
||||
const targetID = Object.keys(this.result.targets)[0];
|
||||
const { layerName, name } = this.result.targets[targetID];
|
||||
|
||||
return layerName ? `${layerName} - ${name}` : name;
|
||||
} else {
|
||||
return this.result.targetModels[0].name;
|
||||
}
|
||||
@@ -115,11 +122,11 @@ export default {
|
||||
mounted() {
|
||||
this.previewAction = new PreviewAction(this.openmct);
|
||||
this.previewAction.on('isVisible', this.togglePreviewState);
|
||||
this.clickedPlotAnnotation = this.clickedPlotAnnotation.bind(this);
|
||||
this.fireAnnotationSelection = this.fireAnnotationSelection.bind(this);
|
||||
},
|
||||
destroyed() {
|
||||
this.previewAction.off('isVisible', this.togglePreviewState);
|
||||
this.openmct.selection.off('change', this.clickedPlotAnnotation);
|
||||
this.openmct.selection.off('change', this.fireAnnotationSelection);
|
||||
},
|
||||
methods: {
|
||||
clickedResult(event) {
|
||||
@@ -132,17 +139,15 @@ export default {
|
||||
if (!this.openmct.router.isNavigatedObject(objectPath)) {
|
||||
// if we're not on the correct page, navigate to the object,
|
||||
// then wait for the selection event to fire before issuing a new selection
|
||||
if (
|
||||
this.result.annotationType === this.openmct.annotation.ANNOTATION_TYPES.PLOT_SPATIAL
|
||||
) {
|
||||
this.openmct.selection.on('change', this.clickedPlotAnnotation);
|
||||
if (this.result.annotationType) {
|
||||
this.openmct.selection.on('change', this.fireAnnotationSelection);
|
||||
}
|
||||
|
||||
this.openmct.router.navigate(resultUrl);
|
||||
} else {
|
||||
// if this is the navigated object, then we are already on the correct page
|
||||
// and just need to issue the selection event
|
||||
this.clickedPlotAnnotation();
|
||||
this.fireAnnotationSelection();
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -151,8 +156,8 @@ export default {
|
||||
this.previewAction.invoke(objectPath);
|
||||
}
|
||||
},
|
||||
clickedPlotAnnotation() {
|
||||
this.openmct.selection.off('change', this.clickedPlotAnnotation);
|
||||
fireAnnotationSelection() {
|
||||
this.openmct.selection.off('change', this.fireAnnotationSelection);
|
||||
|
||||
const targetDetails = {};
|
||||
const targetDomainObjects = {};
|
||||
@@ -168,11 +173,11 @@ export default {
|
||||
element: this.$el,
|
||||
context: {
|
||||
item: this.result.targetModels[0],
|
||||
type: 'plot-annotation-search-result',
|
||||
type: 'annotation-search-result',
|
||||
targetDetails,
|
||||
targetDomainObjects,
|
||||
annotations: [this.result],
|
||||
annotationType: this.openmct.annotation.ANNOTATION_TYPES.PLOT_SPATIAL,
|
||||
annotationType: this.result.annotationType,
|
||||
onAnnotationChange: () => {}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user