diff --git a/.github/workflows/e2e-couchdb.yml b/.github/workflows/e2e-couchdb.yml index e442800b72b..cada516eb8f 100644 --- a/.github/workflows/e2e-couchdb.yml +++ b/.github/workflows/e2e-couchdb.yml @@ -7,15 +7,33 @@ on: - opened jobs: e2e-couchdb: - if: ${{ github.event.label.name == 'pr:e2e:couchdb' }} || ${{ github.event.action == 'opened' }} + if: github.event.label.name == 'pr:e2e:couchdb' || github.event.action == 'opened' && github.actor == 'dependabot[bot]' runs-on: ubuntu-latest + timeout-minutes: 60 steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: - node-version: 'lts/gallium' + node-version: 'lts/hydrogen' + + - name: Cache NPM dependencies + uses: actions/cache@v3 + with: + path: ~/.npm + key: ${{ runner.os }}-node-${{ hashFiles('**/package.json') }} + restore-keys: | + ${{ runner.os }}-node- + + - run: npm install --cache ~/.npm --prefer-offline --no-audit --progress=false + + - name: Login to DockerHub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - run: npx playwright@1.32.3 install - - run: npm install + - name: Start CouchDB Docker Container and Init with Setup Scripts run: | export $(cat src/plugins/persistence/couch/.env.ci | xargs) @@ -23,26 +41,31 @@ jobs: sleep 3 bash src/plugins/persistence/couch/setup-couchdb.sh bash src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.sh + - name: Run CouchDB Tests and publish to deploysentinel env: DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }} run: npm run test:e2e:couchdb + - name: Publish Results to Codecov.io env: SUPER_SECRET: ${{ secrets.CODECOV_TOKEN }} run: npm run cov:e2e:full:publish + - name: Archive test results if: success() || failure() uses: actions/upload-artifact@v3 with: path: test-results + - name: Archive html test results if: success() || failure() uses: actions/upload-artifact@v3 with: path: html-test-results + - name: Remove pr:e2e:couchdb label (if present) - if: ${{ contains(github.event.pull_request.labels.*.name, 'pr:e2e:couchdb') }} + if: always() uses: actions/github-script@v6 with: script: | @@ -56,5 +79,5 @@ jobs: name: labelToRemove }); } catch (error) { - core.warning(`Failed to remove 'pr:e2e:couchdb' label: ${error.message}`); + core.warning(`Failed to remove ' + labelToRemove + ' label: ${error.message}`); } diff --git a/.github/workflows/e2e-pr.yml b/.github/workflows/e2e-pr.yml index 62cd32dba94..0e4a8d9a9dc 100644 --- a/.github/workflows/e2e-pr.yml +++ b/.github/workflows/e2e-pr.yml @@ -7,31 +7,31 @@ on: - opened jobs: e2e-full: - if: ${{ github.event.label.name == 'pr:e2e' }} + if: github.event.label.name == 'pr:e2e' || github.event.action == 'opened' && github.actor == 'dependabot[bot]' runs-on: ${{ matrix.os }} + timeout-minutes: 60 strategy: matrix: os: - ubuntu-latest - windows-latest steps: - - name: Trigger Success - uses: actions/github-script@v6 - with: - script: | - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: "nasa", - repo: "openmct", - body: 'Started e2e Run. Follow along: https://github.com/nasa/openmct/actions/runs/' + context.runId - }) - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: - node-version: '16' + node-version: 'lts/hydrogen' + + - name: Cache NPM dependencies + uses: actions/cache@v3 + with: + path: ~/.npm + key: ${{ runner.os }}-node-${{ hashFiles('**/package.json') }} + restore-keys: | + ${{ runner.os }}-node- + - run: npx playwright@1.32.3 install - run: npx playwright install chrome-beta - - run: npm install + - run: npm install --cache ~/.npm --prefer-offline --no-audit --progress=false - run: npm run test:e2e:full -- --max-failures=40 - run: npm run cov:e2e:report || true - shell: bash @@ -44,30 +44,9 @@ jobs: uses: actions/upload-artifact@v3 with: path: test-results - - name: Test success - if: ${{ success() }} - uses: actions/github-script@v6 - with: - script: | - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: "nasa", - repo: "openmct", - body: 'Success ✅ ! Build artifacts are here: https://github.com/nasa/openmct/actions/runs/' + context.runId - }) - - name: Test failure - if: ${{ failure() }} - uses: actions/github-script@v6 - with: - script: | - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: "nasa", - repo: "openmct", - body: 'Failure ❌ ! Build artifacts are here: https://github.com/nasa/openmct/actions/runs/' + context.runId - }) + - name: Remove pr:e2e label (if present) - if: ${{ contains(github.event.pull_request.labels.*.name, 'pr:e2e') }} + if: always() uses: actions/github-script@v6 with: script: | @@ -81,5 +60,5 @@ jobs: name: labelToRemove }); } catch (error) { - core.warning(`Failed to remove 'pr:e2e' label: ${error.message}`); - } + core.warning(`Failed to remove ' + labelToRemove + ' label: ${error.message}`); + } \ No newline at end of file diff --git a/.github/workflows/npm-prerelease.yml b/.github/workflows/npm-prerelease.yml index a9321ac5692..605ebe8bba3 100644 --- a/.github/workflows/npm-prerelease.yml +++ b/.github/workflows/npm-prerelease.yml @@ -14,7 +14,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: - node-version: 16 + node-version: lts/hydrogen - run: npm install - run: | echo "//registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN" >> ~/.npmrc @@ -29,7 +29,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: - node-version: 16 + node-version: lts/hydrogen registry-url: https://registry.npmjs.org/ - run: npm install - run: npm publish --access=public --tag unstable diff --git a/.github/workflows/pr-platform.yml b/.github/workflows/pr-platform.yml index f56ff576c28..1f1cdfbaa14 100644 --- a/.github/workflows/pr-platform.yml +++ b/.github/workflows/pr-platform.yml @@ -2,12 +2,15 @@ name: 'pr-platform' on: workflow_dispatch: pull_request: - types: [labeled] + types: + - labeled + - opened jobs: - e2e-full: - if: ${{ github.event.label.name == 'pr:platform' }} + pr-platform: + if: github.event.label.name == 'pr:platform' || github.event.action == 'opened' && github.actor == 'dependabot[bot]' runs-on: ${{ matrix.os }} + timeout-minutes: 60 strategy: fail-fast: false matrix: @@ -16,18 +19,49 @@ jobs: - macos-latest - windows-latest node_version: - - 16 - - 18 + - lts/gallium + - lts/hydrogen architecture: - x64 + name: Node ${{ matrix.node_version }} - ${{ matrix.architecture }} on ${{ matrix.os }} steps: - uses: actions/checkout@v3 + - name: Setup node uses: actions/setup-node@v3 with: node-version: ${{ matrix.node_version }} architecture: ${{ matrix.architecture }} - - run: npm install + + - name: Cache NPM dependencies + uses: actions/cache@v3 + with: + path: ~/.npm + key: ${{ runner.os }}-${{ matrix.node_version }}-${{ hashFiles('**/package.json') }} + restore-keys: | + ${{ runner.os }}-${{ matrix.node_version }}- + + - run: npm install --cache ~/.npm --prefer-offline --no-audit --progress=false + - run: npm test + - run: npm run lint -- --quiet + + - name: Remove pr:platform label (if present) + if: always() + uses: actions/github-script@v6 + with: + script: | + const { owner, repo, number } = context.issue; + const labelToRemove = 'pr:platform'; + try { + await github.rest.issues.removeLabel({ + owner, + repo, + issue_number: number, + name: labelToRemove + }); + } catch (error) { + core.warning(`Failed to remove ' + labelToRemove + ' label: ${error.message}`); + } diff --git a/.prettierrc b/.prettierrc index aaae3c5c5f6..479112ec1d6 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,5 +1,6 @@ { "trailingComma": "none", "singleQuote": true, - "printWidth": 100 + "printWidth": 100, + "endOfLine": "auto" } diff --git a/.webpack/webpack.common.js b/.webpack/webpack.common.js index 797340753c6..a1e53a4dc4e 100644 --- a/.webpack/webpack.common.js +++ b/.webpack/webpack.common.js @@ -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') } }, diff --git a/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js b/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js index 5349592eef5..16792644d61 100644 --- a/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js +++ b/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js @@ -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) { diff --git a/package.json b/package.json index 536701b9ef2..bc13c420667 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,16 @@ { "name": "openmct", - "version": "2.2.5-SNAPSHOT", + "version": "2.2.6-SNAPSHOT", "description": "The Open MCT core platform", "devDependencies": { - "@babel/eslint-parser": "7.21.8", + "@babel/eslint-parser": "7.22.5", "@braintree/sanitize-url": "6.0.2", "@deploysentinel/playwright": "0.3.4", "@percy/cli": "1.26.0", "@percy/playwright": "1.0.4", "@playwright/test": "1.32.3", "@types/eventemitter3": "1.2.0", - "@types/jasmine": "4.3.1", + "@types/jasmine": "4.3.4", "@types/lodash": "4.14.192", "babel-loader": "9.1.0", "babel-plugin-istanbul": "6.1.1", @@ -21,15 +21,16 @@ "d3-axis": "3.0.0", "d3-scale": "3.3.0", "d3-selection": "3.0.0", - "eslint": "8.42.0", + "eslint": "8.43.0", "eslint-plugin-compat": "4.1.4", "eslint-config-prettier": "8.8.0", "eslint-plugin-playwright": "0.12.0", "eslint-plugin-prettier": "4.2.1", - "eslint-plugin-vue": "9.14.1", + "eslint-plugin-vue": "9.15.0", "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", @@ -61,7 +61,7 @@ "resolve-url-loader": "5.0.0", "sanitize-html": "2.10.0", "sass": "1.63.4", - "sass-loader": "13.3.1", + "sass-loader": "13.3.2", "sinon": "15.1.0", "style-loader": "3.3.3", "typescript": "5.1.3", diff --git a/src/plugins/imagery/components/AnnotationsCanvas.vue b/src/plugins/imagery/components/AnnotationsCanvas.vue new file mode 100644 index 00000000000..d750c761fb2 --- /dev/null +++ b/src/plugins/imagery/components/AnnotationsCanvas.vue @@ -0,0 +1,423 @@ + + + + + diff --git a/src/plugins/imagery/components/Compass/compass.scss b/src/plugins/imagery/components/Compass/compass.scss index 357e88754ce..368c1062ab7 100644 --- a/src/plugins/imagery/components/Compass/compass.scss +++ b/src/plugins/imagery/components/Compass/compass.scss @@ -19,7 +19,7 @@ $elemBg: rgba(black, 0.7); position: absolute; left: 0; top: 0; - z-index: 2; + z-index: 3; @include userSelectNone; } diff --git a/src/plugins/imagery/components/ImageThumbnail.vue b/src/plugins/imagery/components/ImageThumbnail.vue index 598abd61832..d15ec361a78 100644 --- a/src/plugins/imagery/components/ImageThumbnail.vue +++ b/src/plugins/imagery/components/ImageThumbnail.vue @@ -38,6 +38,11 @@ fetchpriority="low" @load="imageLoadCompleted" /> + +
{{ image.formattedTime }}
@@ -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: { diff --git a/src/plugins/imagery/components/ImageryView.vue b/src/plugins/imagery/components/ImageryView.vue index 0ac1840fec4..f1564ec0e10 100644 --- a/src/plugins/imagery/components/ImageryView.vue +++ b/src/plugins/imagery/components/ImageryView.vue @@ -88,6 +88,13 @@ :image="focusedImage" :sized-image-dimensions="sizedImageDimensions" /> + @@ -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) { diff --git a/src/plugins/imagery/components/imagery-view.scss b/src/plugins/imagery/components/imagery-view.scss index f14a6cebb09..09e8cb7d819 100644 --- a/src/plugins/imagery/components/imagery-view.scss +++ b/src/plugins/imagery/components/imagery-view.scss @@ -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; +} diff --git a/src/plugins/plot/MctPlot.vue b/src/plugins/plot/MctPlot.vue index 7d00b934fc8..899af22c72b 100644 --- a/src/plugins/plot/MctPlot.vue +++ b/src/plugins/plot/MctPlot.vue @@ -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) { diff --git a/src/ui/layout/search/AnnotationSearchResult.vue b/src/ui/layout/search/AnnotationSearchResult.vue index 1f0fe22449d..cff1f056624 100644 --- a/src/ui/layout/search/AnnotationSearchResult.vue +++ b/src/ui/layout/search/AnnotationSearchResult.vue @@ -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: () => {} } }