Annotations for imagery prototype (#6624)

This commit is contained in:
Scott Bell 2023-06-20 19:12:45 +02:00 committed by GitHub
parent d305443445
commit 5a7174bf2a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 606 additions and 41 deletions

View File

@ -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')
}
},

View File

@ -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);
});
@ -713,7 +744,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();
@ -724,7 +754,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) {

View File

@ -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",

View File

@ -0,0 +1,418 @@
<!--
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,
selectedAnnotations: [],
indexToAnnotationMap: {}
};
},
computed: {
annotationsIndex() {
if (this.imageryAnnotations.length) {
// create a flatbush index for the annotations
const builtAnnotationsIndex = new Flatbush(this.imageryAnnotations.length);
this.imageryAnnotations.forEach((annotation) => {
const annotationRectangle = annotation.targets[this.keyString].rectangle;
const annotationRectangleForPixelDepth =
this.transformRectangleToPixelDense(annotationRectangle);
const indexNumber = builtAnnotationsIndex.add(
annotationRectangleForPixelDepth.x,
annotationRectangleForPixelDepth.y,
annotationRectangleForPixelDepth.x + annotationRectangleForPixelDepth.width,
annotationRectangleForPixelDepth.y + annotationRectangleForPixelDepth.height
);
this.indexToAnnotationMap[indexNumber] = annotation;
});
builtAnnotationsIndex.finish();
return builtAnnotationsIndex;
} else {
return null;
}
}
},
mounted() {
this.canvas = this.$refs.canvas;
this.context = this.canvas.getContext('2d');
// adjust canvas size for retina displays
const pixelScale = window.devicePixelRatio;
this.canvas.width = Math.floor(this.canvas.width * pixelScale);
this.canvas.height = Math.floor(this.canvas.height * pixelScale);
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.openmct.selection.on('change', this.updateSelection);
this.drawAnnotations();
},
beforeDestroy() {
this.openmct.selection.off('change', this.updateSelection);
document.body.removeEventListener('click', this.cancelSelection);
},
methods: {
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 = [];
},
/**
* Given a rectangle, returns a rectangle that conforms to the pixel density of the device
* @param {Object} rectangle without pixel density applied
* @returns {Object} transformed rectangle with pixel density applied
*/
transformRectangleToPixelDense(rectangle) {
const pixelScale = window.devicePixelRatio;
const transformedRectangle = {
x: rectangle.x * pixelScale,
y: rectangle.y * pixelScale,
width: rectangle.width * pixelScale,
height: rectangle.height * pixelScale
};
return transformedRectangle;
},
/**
* Given a rectangle, returns a rectangle that is independent of the pixel density of the device
* @param {Object} rectangle with pixel density applied
* @returns {Object} transformed rectangle without pixel density applied
*/
transformRectangleFromPixelDense(rectangle) {
const pixelScale = window.devicePixelRatio;
const transformedRectangle = {
x: rectangle.x / pixelScale,
y: rectangle.y / pixelScale,
width: rectangle.width / pixelScale,
height: rectangle.height / pixelScale
};
return transformedRectangle;
},
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 = {};
const rectangleFromCanvas = {
x: this.newAnnotationRectangle.x,
y: this.newAnnotationRectangle.y,
width: this.newAnnotationRectangle.width,
height: this.newAnnotationRectangle.height
};
const rectangleWithoutPixelScale = this.transformRectangleFromPixelDense(rectangleFromCanvas);
targetDetails[this.keyString] = {
rectangle: rectangleWithoutPixelScale,
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];
if (foundAnnotation._deleted) {
return;
}
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 (annotation._deleted) {
return;
}
const rectangleForPixelDensity = this.transformRectangleToPixelDense(
annotation.targets[this.keyString].rectangle
);
if (this.isSelectedAnnotation(annotation)) {
this.drawRectInCanvas(
rectangleForPixelDensity,
SELECTED_ANNOTATION_FILL_STYLE,
SELECTED_ANNOTATION_STROKE_COLOR
);
} else {
this.drawRectInCanvas(
rectangleForPixelDensity,
EXISTING_ANNOTATION_FILL_STYLE,
EXISTING_ANNOTATION_STROKE_STYLE
);
}
});
}
}
};
</script>

View File

@ -19,7 +19,7 @@ $elemBg: rgba(black, 0.7);
position: absolute;
left: 0;
top: 0;
z-index: 2;
z-index: 3;
@include userSelectNone;
}

View File

@ -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,11 @@ export default {
width: `${width}px`,
height: `${height}px`
};
},
showAnnotationIndicator() {
return this.imageryAnnotations.some((annotation) => {
return !annotation._deleted;
});
}
},
methods: {

View File

@ -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 =
@ -631,6 +655,9 @@ export default {
}
}
},
created() {
this.abortController = new AbortController();
},
async mounted() {
eventHelpers.extend(this);
this.focusedImageWrapper = this.$refs.focusedImageWrapper;
@ -689,8 +716,12 @@ export default {
this.listenTo(this.focusedImageWrapper, 'wheel', this.wheelZoom, this);
this.loadVisibleLayers();
this.loadAnnotations();
this.openmct.selection.on('change', this.updateSelection);
},
beforeDestroy() {
this.abortController.abort();
this.persistVisibleLayers();
this.stopFollowingTimeContext();
@ -716,6 +747,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 +783,15 @@ 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;
}
},
expand() {
// check for modifier keys so it doesnt interfere with the layout
if (this.cursorStates.modifierKeyPressed) {
@ -832,6 +881,41 @@ 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,
this.abortController.signal
);
}
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 +1063,9 @@ export default {
}
await Vue.nextTick();
this.$refs.thumbsWrapper.scrollLeft = scrollWidth;
if (this.$refs.thumbsWrapper) {
this.$refs.thumbsWrapper.scrollLeft = scrollWidth;
}
},
scrollHandler() {
if (this.isPaused) {

View File

@ -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;
}

View File

@ -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;
@ -414,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
@ -1398,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 = [];
@ -1413,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) {

View File

@ -122,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) {
@ -139,18 +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.result.annotationType === this.openmct.annotation.ANNOTATION_TYPES.GEOSPATIAL
) {
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();
}
}
},
@ -159,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 = {};
@ -176,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: () => {}
}
}