mirror of
https://github.com/nasa/openmct.git
synced 2024-12-27 00:31:06 +00:00
Merge branch 'master' into memory-leak-detection
This commit is contained in:
commit
4fff6b035b
33
.github/workflows/e2e-couchdb.yml
vendored
33
.github/workflows/e2e-couchdb.yml
vendored
@ -7,15 +7,33 @@ on:
|
|||||||
- opened
|
- opened
|
||||||
jobs:
|
jobs:
|
||||||
e2e-couchdb:
|
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
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 60
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
with:
|
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: npx playwright@1.32.3 install
|
||||||
- run: npm install
|
|
||||||
- name: Start CouchDB Docker Container and Init with Setup Scripts
|
- name: Start CouchDB Docker Container and Init with Setup Scripts
|
||||||
run: |
|
run: |
|
||||||
export $(cat src/plugins/persistence/couch/.env.ci | xargs)
|
export $(cat src/plugins/persistence/couch/.env.ci | xargs)
|
||||||
@ -23,26 +41,31 @@ jobs:
|
|||||||
sleep 3
|
sleep 3
|
||||||
bash src/plugins/persistence/couch/setup-couchdb.sh
|
bash src/plugins/persistence/couch/setup-couchdb.sh
|
||||||
bash src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.sh
|
bash src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.sh
|
||||||
|
|
||||||
- name: Run CouchDB Tests and publish to deploysentinel
|
- name: Run CouchDB Tests and publish to deploysentinel
|
||||||
env:
|
env:
|
||||||
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
||||||
run: npm run test:e2e:couchdb
|
run: npm run test:e2e:couchdb
|
||||||
|
|
||||||
- name: Publish Results to Codecov.io
|
- name: Publish Results to Codecov.io
|
||||||
env:
|
env:
|
||||||
SUPER_SECRET: ${{ secrets.CODECOV_TOKEN }}
|
SUPER_SECRET: ${{ secrets.CODECOV_TOKEN }}
|
||||||
run: npm run cov:e2e:full:publish
|
run: npm run cov:e2e:full:publish
|
||||||
|
|
||||||
- name: Archive test results
|
- name: Archive test results
|
||||||
if: success() || failure()
|
if: success() || failure()
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
path: test-results
|
path: test-results
|
||||||
|
|
||||||
- name: Archive html test results
|
- name: Archive html test results
|
||||||
if: success() || failure()
|
if: success() || failure()
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
path: html-test-results
|
path: html-test-results
|
||||||
|
|
||||||
- name: Remove pr:e2e:couchdb label (if present)
|
- 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
|
uses: actions/github-script@v6
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
@ -56,5 +79,5 @@ jobs:
|
|||||||
name: labelToRemove
|
name: labelToRemove
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
core.warning(`Failed to remove 'pr:e2e:couchdb' label: ${error.message}`);
|
core.warning(`Failed to remove ' + labelToRemove + ' label: ${error.message}`);
|
||||||
}
|
}
|
||||||
|
55
.github/workflows/e2e-pr.yml
vendored
55
.github/workflows/e2e-pr.yml
vendored
@ -7,31 +7,31 @@ on:
|
|||||||
- opened
|
- opened
|
||||||
jobs:
|
jobs:
|
||||||
e2e-full:
|
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 }}
|
runs-on: ${{ matrix.os }}
|
||||||
|
timeout-minutes: 60
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os:
|
os:
|
||||||
- ubuntu-latest
|
- ubuntu-latest
|
||||||
- windows-latest
|
- windows-latest
|
||||||
steps:
|
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/checkout@v3
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
with:
|
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@1.32.3 install
|
||||||
- run: npx playwright install chrome-beta
|
- 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 test:e2e:full -- --max-failures=40
|
||||||
- run: npm run cov:e2e:report || true
|
- run: npm run cov:e2e:report || true
|
||||||
- shell: bash
|
- shell: bash
|
||||||
@ -44,30 +44,9 @@ jobs:
|
|||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
path: test-results
|
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)
|
- name: Remove pr:e2e label (if present)
|
||||||
if: ${{ contains(github.event.pull_request.labels.*.name, 'pr:e2e') }}
|
if: always()
|
||||||
uses: actions/github-script@v6
|
uses: actions/github-script@v6
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
@ -81,5 +60,5 @@ jobs:
|
|||||||
name: labelToRemove
|
name: labelToRemove
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
core.warning(`Failed to remove 'pr:e2e' label: ${error.message}`);
|
core.warning(`Failed to remove ' + labelToRemove + ' label: ${error.message}`);
|
||||||
}
|
}
|
4
.github/workflows/npm-prerelease.yml
vendored
4
.github/workflows/npm-prerelease.yml
vendored
@ -14,7 +14,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 16
|
node-version: lts/hydrogen
|
||||||
- run: npm install
|
- run: npm install
|
||||||
- run: |
|
- run: |
|
||||||
echo "//registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN" >> ~/.npmrc
|
echo "//registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN" >> ~/.npmrc
|
||||||
@ -29,7 +29,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 16
|
node-version: lts/hydrogen
|
||||||
registry-url: https://registry.npmjs.org/
|
registry-url: https://registry.npmjs.org/
|
||||||
- run: npm install
|
- run: npm install
|
||||||
- run: npm publish --access=public --tag unstable
|
- run: npm publish --access=public --tag unstable
|
||||||
|
46
.github/workflows/pr-platform.yml
vendored
46
.github/workflows/pr-platform.yml
vendored
@ -2,12 +2,15 @@ name: 'pr-platform'
|
|||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
pull_request:
|
pull_request:
|
||||||
types: [labeled]
|
types:
|
||||||
|
- labeled
|
||||||
|
- opened
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
e2e-full:
|
pr-platform:
|
||||||
if: ${{ github.event.label.name == 'pr:platform' }}
|
if: github.event.label.name == 'pr:platform' || github.event.action == 'opened' && github.actor == 'dependabot[bot]'
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
|
timeout-minutes: 60
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@ -16,18 +19,49 @@ jobs:
|
|||||||
- macos-latest
|
- macos-latest
|
||||||
- windows-latest
|
- windows-latest
|
||||||
node_version:
|
node_version:
|
||||||
- 16
|
- lts/gallium
|
||||||
- 18
|
- lts/hydrogen
|
||||||
architecture:
|
architecture:
|
||||||
- x64
|
- x64
|
||||||
|
|
||||||
name: Node ${{ matrix.node_version }} - ${{ matrix.architecture }} on ${{ matrix.os }}
|
name: Node ${{ matrix.node_version }} - ${{ matrix.architecture }} on ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Setup node
|
- name: Setup node
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node_version }}
|
node-version: ${{ matrix.node_version }}
|
||||||
architecture: ${{ matrix.architecture }}
|
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 test
|
||||||
|
|
||||||
- run: npm run lint -- --quiet
|
- 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}`);
|
||||||
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"trailingComma": "none",
|
"trailingComma": "none",
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"printWidth": 100
|
"printWidth": 100,
|
||||||
|
"endOfLine": "auto"
|
||||||
}
|
}
|
||||||
|
@ -67,7 +67,6 @@ const config = {
|
|||||||
MCT: path.join(projectRootDir, 'src/MCT'),
|
MCT: path.join(projectRootDir, 'src/MCT'),
|
||||||
testUtils: path.join(projectRootDir, 'src/utils/testUtils.js'),
|
testUtils: path.join(projectRootDir, 'src/utils/testUtils.js'),
|
||||||
objectUtils: path.join(projectRootDir, 'src/api/objects/object-utils.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')
|
utils: path.join(projectRootDir, 'src/utils')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
122
e2e/tests/functional/planning/timelist.e2e.spec.js
Normal file
122
e2e/tests/functional/planning/timelist.e2e.spec.js
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* 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.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
const { test, expect } = require('../../../pluginFixtures');
|
||||||
|
const { createDomainObjectWithDefaults, createPlanFromJSON } = require('../../../appActions');
|
||||||
|
|
||||||
|
const testPlan = {
|
||||||
|
TEST_GROUP: [
|
||||||
|
{
|
||||||
|
name: 'Past event 1',
|
||||||
|
start: 1660320408000,
|
||||||
|
end: 1660343797000,
|
||||||
|
type: 'TEST-GROUP',
|
||||||
|
color: 'orange',
|
||||||
|
textColor: 'white'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Past event 2',
|
||||||
|
start: 1660406808000,
|
||||||
|
end: 1660429160000,
|
||||||
|
type: 'TEST-GROUP',
|
||||||
|
color: 'orange',
|
||||||
|
textColor: 'white'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Past event 3',
|
||||||
|
start: 1660493208000,
|
||||||
|
end: 1660503981000,
|
||||||
|
type: 'TEST-GROUP',
|
||||||
|
color: 'orange',
|
||||||
|
textColor: 'white'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Past event 4',
|
||||||
|
start: 1660579608000,
|
||||||
|
end: 1660624108000,
|
||||||
|
type: 'TEST-GROUP',
|
||||||
|
color: 'orange',
|
||||||
|
textColor: 'white'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Past event 5',
|
||||||
|
start: 1660666008000,
|
||||||
|
end: 1660681529000,
|
||||||
|
type: 'TEST-GROUP',
|
||||||
|
color: 'orange',
|
||||||
|
textColor: 'white'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
test.describe('Time List', () => {
|
||||||
|
test('Create a Time List, add a single Plan to it and verify all the activities are displayed with no milliseconds', async ({
|
||||||
|
page
|
||||||
|
}) => {
|
||||||
|
// Goto baseURL
|
||||||
|
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||||
|
|
||||||
|
const timelist = await test.step('Create a Time List', async () => {
|
||||||
|
const createdTimeList = await createDomainObjectWithDefaults(page, { type: 'Time List' });
|
||||||
|
const objectName = await page.locator('.l-browse-bar__object-name').innerText();
|
||||||
|
expect(objectName).toBe(createdTimeList.name);
|
||||||
|
|
||||||
|
return createdTimeList;
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('Create a Plan and add it to the timelist', async () => {
|
||||||
|
const createdPlan = await createPlanFromJSON(page, {
|
||||||
|
name: 'Test Plan',
|
||||||
|
json: testPlan
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto(timelist.url);
|
||||||
|
// Expand the tree to show the plan
|
||||||
|
await page.click("button[title='Show selected item in tree']");
|
||||||
|
await page.dragAndDrop(`role=treeitem[name=/${createdPlan.name}/]`, '.c-object-view');
|
||||||
|
await page.click("button[title='Save']");
|
||||||
|
await page.click("li[title='Save and Finish Editing']");
|
||||||
|
const startBound = testPlan.TEST_GROUP[0].start;
|
||||||
|
const endBound = testPlan.TEST_GROUP[testPlan.TEST_GROUP.length - 1].end;
|
||||||
|
|
||||||
|
// Switch to fixed time mode with all plan events within the bounds
|
||||||
|
await page.goto(
|
||||||
|
`${timelist.url}?tc.mode=fixed&tc.startBound=${startBound}&tc.endBound=${endBound}&tc.timeSystem=utc&view=timelist.view`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify all events are displayed
|
||||||
|
const eventCount = await page.locator('.js-list-item').count();
|
||||||
|
expect(eventCount).toEqual(testPlan.TEST_GROUP.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('Does not show milliseconds in times', async () => {
|
||||||
|
// Get the first activity
|
||||||
|
const row = await page.locator('.js-list-item').first();
|
||||||
|
// Verify that none fo the times have milliseconds displayed.
|
||||||
|
// Example: 2024-11-17T16:00:00Z is correct and 2024-11-17T16:00:00.000Z is wrong
|
||||||
|
|
||||||
|
await expect(row.locator('.--start')).not.toContainText('.');
|
||||||
|
await expect(row.locator('.--end')).not.toContainText('.');
|
||||||
|
await expect(row.locator('.--duration')).not.toContainText('.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -205,6 +205,71 @@ test.describe('Display Layout', () => {
|
|||||||
|
|
||||||
expect(await page.locator('.l-layout .l-layout__frame').count()).toEqual(0);
|
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 { createDomainObjectWithDefaults } = require('../../../../appActions');
|
||||||
const backgroundImageSelector = '.c-imagery__main-image__background-image';
|
const backgroundImageSelector = '.c-imagery__main-image__background-image';
|
||||||
const panHotkey = process.platform === 'linux' ? ['Shift', 'Alt'] : ['Alt'];
|
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 expectedAltText = process.platform === 'linux' ? 'Shift+Alt drag to pan' : 'Alt drag to pan';
|
||||||
const thumbnailUrlParamsRegexp = /\?w=100&h=100/;
|
const thumbnailUrlParamsRegexp = /\?w=100&h=100/;
|
||||||
|
|
||||||
@ -44,7 +45,7 @@ test.describe('Example Imagery Object', () => {
|
|||||||
|
|
||||||
// Verify that the created object is focused
|
// Verify that the created object is focused
|
||||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText(exampleImagery.name);
|
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 }) => {
|
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 }) => {
|
test('Can use alt+drag to move around image once zoomed in', async ({ page }) => {
|
||||||
const deltaYStep = 100; //equivalent to 1x zoom
|
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
|
// zoom in
|
||||||
await page.mouse.wheel(0, deltaYStep * 2);
|
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 zoomedBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||||
const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2;
|
const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2;
|
||||||
const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2;
|
const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2;
|
||||||
@ -131,6 +132,36 @@ test.describe('Example Imagery Object', () => {
|
|||||||
expect(afterDownPanBoundingBox.y).toBeLessThan(afterUpPanBoundingBox.y);
|
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 }) => {
|
test('Can use + - buttons to zoom on the image @unstable', async ({ page }) => {
|
||||||
await buttonZoomOnImageAndAssert(page);
|
await buttonZoomOnImageAndAssert(page);
|
||||||
});
|
});
|
||||||
@ -185,24 +216,7 @@ test.describe('Example Imagery in Display Layout', () => {
|
|||||||
displayLayout = await createDomainObjectWithDefaults(page, { type: 'Display Layout' });
|
displayLayout = await createDomainObjectWithDefaults(page, { type: 'Display Layout' });
|
||||||
await page.goto(displayLayout.url);
|
await page.goto(displayLayout.url);
|
||||||
|
|
||||||
/* Create Sine Wave Generator with minimum Image Load Delay */
|
await createImageryView(page);
|
||||||
// Click the Create button
|
|
||||||
await page.click('button:has-text("Create")');
|
|
||||||
|
|
||||||
// Click text=Example Imagery
|
|
||||||
await page.click('li[role="menuitem"]:has-text("Example Imagery")');
|
|
||||||
|
|
||||||
// Clear and set Image load delay to minimum value
|
|
||||||
await page.locator('input[type="number"]').fill('');
|
|
||||||
await page.locator('input[type="number"]').fill('5000');
|
|
||||||
|
|
||||||
// Click text=OK
|
|
||||||
await Promise.all([
|
|
||||||
page.waitForNavigation({ waitUntil: 'networkidle' }),
|
|
||||||
page.click('button:has-text("OK")'),
|
|
||||||
//Wait for Save Banner to appear
|
|
||||||
page.waitForSelector('.c-message-banner__message')
|
|
||||||
]);
|
|
||||||
|
|
||||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText(
|
await expect(page.locator('.l-browse-bar__object-name')).toContainText(
|
||||||
'Unnamed Example Imagery'
|
'Unnamed Example Imagery'
|
||||||
@ -315,9 +329,47 @@ test.describe('Example Imagery in Display Layout', () => {
|
|||||||
await page.locator('div[title="Resize object height"] > input').click();
|
await page.locator('div[title="Resize object height"] > input').click();
|
||||||
await page.locator('div[title="Resize object height"] > input').fill('100');
|
await page.locator('div[title="Resize object height"] > input').fill('100');
|
||||||
|
|
||||||
expect(thumbsWrapperLocator.isVisible()).toBeTruthy();
|
await expect(thumbsWrapperLocator).toBeVisible();
|
||||||
await expect(thumbsWrapperLocator).not.toHaveClass(/is-small-thumbs/);
|
await expect(thumbsWrapperLocator).not.toHaveClass(/is-small-thumbs/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle layer visibility checkbox by clicking on checkbox label
|
||||||
|
* - should toggle checkbox and layer visibility for that image view
|
||||||
|
* - should NOT toggle checkbox and layer visibity for the first image view in display
|
||||||
|
*/
|
||||||
|
test('Toggle layer visibility by clicking on label', async ({ page }) => {
|
||||||
|
test.info().annotations.push({
|
||||||
|
type: 'issue',
|
||||||
|
description: 'https://github.com/nasa/openmct/issues/6709'
|
||||||
|
});
|
||||||
|
await createImageryView(page);
|
||||||
|
await page.goto(displayLayout.url);
|
||||||
|
|
||||||
|
const imageElements = page.locator('.c-imagery__main-image-wrapper');
|
||||||
|
|
||||||
|
await expect(imageElements).toHaveCount(2);
|
||||||
|
|
||||||
|
const imageOne = page.locator('.c-imagery__main-image-wrapper').nth(0);
|
||||||
|
const imageTwo = page.locator('.c-imagery__main-image-wrapper').nth(1);
|
||||||
|
const imageOneWrapper = imageOne.locator('.image-wrapper');
|
||||||
|
const imageTwoWrapper = imageTwo.locator('.image-wrapper');
|
||||||
|
|
||||||
|
await imageTwo.hover();
|
||||||
|
|
||||||
|
await imageTwo.locator('button[title="Layers"]').click();
|
||||||
|
|
||||||
|
const imageTwoLayersMenuContent = imageTwo.locator('button[title="Layers"] + div');
|
||||||
|
const imageTwoLayersToggleLabel = imageTwoLayersMenuContent.locator('label').last();
|
||||||
|
|
||||||
|
await imageTwoLayersToggleLabel.click();
|
||||||
|
|
||||||
|
const imageOneLayers = imageOneWrapper.locator('.layer-image');
|
||||||
|
const imageTwoLayers = imageTwoWrapper.locator('.layer-image');
|
||||||
|
|
||||||
|
await expect(imageOneLayers).toHaveCount(0);
|
||||||
|
await expect(imageTwoLayers).toHaveCount(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe('Example Imagery in Flexible layout', () => {
|
test.describe('Example Imagery in Flexible layout', () => {
|
||||||
@ -692,7 +744,6 @@ async function panZoomAndAssertImageProperties(page) {
|
|||||||
async function mouseZoomOnImageAndAssert(page, factor = 2) {
|
async function mouseZoomOnImageAndAssert(page, factor = 2) {
|
||||||
// Zoom in
|
// Zoom in
|
||||||
const originalImageDimensions = await page.locator(backgroundImageSelector).boundingBox();
|
const originalImageDimensions = await page.locator(backgroundImageSelector).boundingBox();
|
||||||
await page.locator(backgroundImageSelector).hover({ trial: true });
|
|
||||||
const deltaYStep = 100; // equivalent to 1x zoom
|
const deltaYStep = 100; // equivalent to 1x zoom
|
||||||
await page.mouse.wheel(0, deltaYStep * factor);
|
await page.mouse.wheel(0, deltaYStep * factor);
|
||||||
const zoomedBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
const zoomedBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||||
@ -703,7 +754,7 @@ async function mouseZoomOnImageAndAssert(page, factor = 2) {
|
|||||||
await page.mouse.move(imageCenterX, imageCenterY);
|
await page.mouse.move(imageCenterX, imageCenterY);
|
||||||
|
|
||||||
// Wait for zoom animation to finish
|
// 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();
|
const imageMouseZoomed = await page.locator(backgroundImageSelector).boundingBox();
|
||||||
|
|
||||||
if (factor > 0) {
|
if (factor > 0) {
|
||||||
@ -819,3 +870,26 @@ async function resetImageryPanAndZoom(page) {
|
|||||||
await panZoomResetBtn.click();
|
await panZoomResetBtn.click();
|
||||||
await waitForAnimations(backgroundImage);
|
await waitForAnimations(backgroundImage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('@playwright/test').Page} page
|
||||||
|
*/
|
||||||
|
async function createImageryView(page) {
|
||||||
|
// Click the Create button
|
||||||
|
await page.click('button:has-text("Create")');
|
||||||
|
|
||||||
|
// Click text=Example Imagery
|
||||||
|
await page.click('li[role="menuitem"]:has-text("Example Imagery")');
|
||||||
|
|
||||||
|
// Clear and set Image load delay to minimum value
|
||||||
|
await page.locator('input[type="number"]').fill('');
|
||||||
|
await page.locator('input[type="number"]').fill('5000');
|
||||||
|
|
||||||
|
// Click text=OK
|
||||||
|
await Promise.all([
|
||||||
|
page.waitForNavigation({ waitUntil: 'networkidle' }),
|
||||||
|
page.click('button:has-text("OK")'),
|
||||||
|
//Wait for Save Banner to appear
|
||||||
|
page.waitForSelector('.c-message-banner__message')
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
28
package.json
28
package.json
@ -1,16 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "openmct",
|
"name": "openmct",
|
||||||
"version": "2.2.5-SNAPSHOT",
|
"version": "2.2.6-SNAPSHOT",
|
||||||
"description": "The Open MCT core platform",
|
"description": "The Open MCT core platform",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/eslint-parser": "7.21.8",
|
"@babel/eslint-parser": "7.22.5",
|
||||||
"@braintree/sanitize-url": "6.0.2",
|
"@braintree/sanitize-url": "6.0.2",
|
||||||
"@deploysentinel/playwright": "0.3.4",
|
"@deploysentinel/playwright": "0.3.4",
|
||||||
"@percy/cli": "1.24.2",
|
"@percy/cli": "1.26.0",
|
||||||
"@percy/playwright": "1.0.4",
|
"@percy/playwright": "1.0.4",
|
||||||
"@playwright/test": "1.32.3",
|
"@playwright/test": "1.32.3",
|
||||||
"@types/eventemitter3": "1.2.0",
|
"@types/eventemitter3": "1.2.0",
|
||||||
"@types/jasmine": "4.3.1",
|
"@types/jasmine": "4.3.4",
|
||||||
"@types/lodash": "4.14.192",
|
"@types/lodash": "4.14.192",
|
||||||
"babel-loader": "9.1.0",
|
"babel-loader": "9.1.0",
|
||||||
"babel-plugin-istanbul": "6.1.1",
|
"babel-plugin-istanbul": "6.1.1",
|
||||||
@ -21,15 +21,16 @@
|
|||||||
"d3-axis": "3.0.0",
|
"d3-axis": "3.0.0",
|
||||||
"d3-scale": "3.3.0",
|
"d3-scale": "3.3.0",
|
||||||
"d3-selection": "3.0.0",
|
"d3-selection": "3.0.0",
|
||||||
"eslint": "8.42.0",
|
"eslint": "8.43.0",
|
||||||
"eslint-plugin-compat": "4.1.4",
|
"eslint-plugin-compat": "4.1.4",
|
||||||
"eslint-config-prettier": "8.8.0",
|
"eslint-config-prettier": "8.8.0",
|
||||||
"eslint-plugin-playwright": "0.12.0",
|
"eslint-plugin-playwright": "0.12.0",
|
||||||
"eslint-plugin-prettier": "4.2.1",
|
"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",
|
"eslint-plugin-you-dont-need-lodash-underscore": "6.12.0",
|
||||||
"eventemitter3": "1.2.0",
|
"eventemitter3": "1.2.0",
|
||||||
"file-saver": "2.0.5",
|
"file-saver": "2.0.5",
|
||||||
|
"flatbush": "4.2.0",
|
||||||
"git-rev-sync": "3.0.2",
|
"git-rev-sync": "3.0.2",
|
||||||
"html2canvas": "1.4.1",
|
"html2canvas": "1.4.1",
|
||||||
"imports-loader": "4.0.1",
|
"imports-loader": "4.0.1",
|
||||||
@ -44,7 +45,6 @@
|
|||||||
"karma-sourcemap-loader": "0.4.0",
|
"karma-sourcemap-loader": "0.4.0",
|
||||||
"karma-spec-reporter": "0.0.36",
|
"karma-spec-reporter": "0.0.36",
|
||||||
"karma-webpack": "5.0.0",
|
"karma-webpack": "5.0.0",
|
||||||
"kdbush": "3.0.0",
|
|
||||||
"location-bar": "3.0.1",
|
"location-bar": "3.0.1",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"mini-css-extract-plugin": "2.7.6",
|
"mini-css-extract-plugin": "2.7.6",
|
||||||
@ -59,20 +59,20 @@
|
|||||||
"prettier": "2.8.7",
|
"prettier": "2.8.7",
|
||||||
"printj": "1.3.1",
|
"printj": "1.3.1",
|
||||||
"resolve-url-loader": "5.0.0",
|
"resolve-url-loader": "5.0.0",
|
||||||
"sanitize-html": "2.10.0",
|
"sanitize-html": "2.11.0",
|
||||||
"sass": "1.62.1",
|
"sass": "1.63.4",
|
||||||
"sass-loader": "13.3.1",
|
"sass-loader": "13.3.2",
|
||||||
"sinon": "15.1.0",
|
"sinon": "15.1.0",
|
||||||
"style-loader": "3.3.3",
|
"style-loader": "3.3.3",
|
||||||
"typescript": "5.1.3",
|
"typescript": "5.1.3",
|
||||||
"uuid": "9.0.0",
|
"uuid": "9.0.0",
|
||||||
"vue": "2.6.14",
|
"vue": "2.6.14",
|
||||||
"vue-eslint-parser": "9.3.0",
|
"vue-eslint-parser": "9.3.1",
|
||||||
"vue-loader": "15.9.8",
|
"vue-loader": "15.9.8",
|
||||||
"vue-template-compiler": "2.6.14",
|
"vue-template-compiler": "2.6.14",
|
||||||
"webpack": "5.85.1",
|
"webpack": "5.88.0",
|
||||||
"webpack-cli": "5.1.1",
|
"webpack-cli": "5.1.1",
|
||||||
"webpack-dev-server": "4.13.3",
|
"webpack-dev-server": "4.15.1",
|
||||||
"webpack-merge": "5.9.0"
|
"webpack-merge": "5.9.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -111,7 +111,7 @@
|
|||||||
"url": "https://github.com/nasa/openmct.git"
|
"url": "https://github.com/nasa/openmct.git"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16.19.1"
|
"node": ">=16.19.1 <20"
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"Firefox ESR",
|
"Firefox ESR",
|
||||||
|
@ -76,6 +76,9 @@ const ANNOTATION_LAST_CREATED = 'annotationLastCreated';
|
|||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
export default class AnnotationAPI extends EventEmitter {
|
export default class AnnotationAPI extends EventEmitter {
|
||||||
|
/** @type {Map<ANNOTATION_TYPES, Array<(a, b) => boolean >>} */
|
||||||
|
#targetComparatorMap;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {OpenMCT} openmct
|
* @param {OpenMCT} openmct
|
||||||
*/
|
*/
|
||||||
@ -84,6 +87,7 @@ export default class AnnotationAPI extends EventEmitter {
|
|||||||
this.openmct = openmct;
|
this.openmct = openmct;
|
||||||
this.availableTags = {};
|
this.availableTags = {};
|
||||||
this.namespaceToSaveAnnotations = '';
|
this.namespaceToSaveAnnotations = '';
|
||||||
|
this.#targetComparatorMap = new Map();
|
||||||
|
|
||||||
this.ANNOTATION_TYPES = ANNOTATION_TYPES;
|
this.ANNOTATION_TYPES = ANNOTATION_TYPES;
|
||||||
this.ANNOTATION_TYPE = ANNOTATION_TYPE;
|
this.ANNOTATION_TYPE = ANNOTATION_TYPE;
|
||||||
@ -246,15 +250,16 @@ export default class AnnotationAPI extends EventEmitter {
|
|||||||
/**
|
/**
|
||||||
* @method getAnnotations
|
* @method getAnnotations
|
||||||
* @param {Identifier} domainObjectIdentifier - The domain object identifier to use to search for annotations. For example, a telemetry object identifier.
|
* @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
|
* @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 keyStringQuery = this.openmct.objects.makeKeyString(domainObjectIdentifier);
|
||||||
const searchResults = (
|
const searchResults = (
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
this.openmct.objects.search(
|
this.openmct.objects.search(
|
||||||
keyStringQuery,
|
keyStringQuery,
|
||||||
null,
|
abortSignal,
|
||||||
this.openmct.objects.SEARCH_TYPES.ANNOTATIONS
|
this.openmct.objects.SEARCH_TYPES.ANNOTATIONS
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -384,7 +389,8 @@ export default class AnnotationAPI extends EventEmitter {
|
|||||||
const combinedResults = [];
|
const combinedResults = [];
|
||||||
results.forEach((currentAnnotation) => {
|
results.forEach((currentAnnotation) => {
|
||||||
const existingAnnotation = combinedResults.find((annotationToFind) => {
|
const existingAnnotation = combinedResults.find((annotationToFind) => {
|
||||||
return _.isEqual(currentAnnotation.targets, annotationToFind.targets);
|
const { annotationType, targets } = currentAnnotation;
|
||||||
|
return this.areAnnotationTargetsEqual(annotationType, targets, annotationToFind.targets);
|
||||||
});
|
});
|
||||||
if (!existingAnnotation) {
|
if (!existingAnnotation) {
|
||||||
combinedResults.push(currentAnnotation);
|
combinedResults.push(currentAnnotation);
|
||||||
@ -460,4 +466,35 @@ export default class AnnotationAPI extends EventEmitter {
|
|||||||
|
|
||||||
return breakApartSeparateTargets;
|
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);
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -28,6 +28,36 @@ import TelemetryValueFormatter from './TelemetryValueFormatter';
|
|||||||
import DefaultMetadataProvider from './DefaultMetadataProvider';
|
import DefaultMetadataProvider from './DefaultMetadataProvider';
|
||||||
import objectUtils from 'objectUtils';
|
import objectUtils from 'objectUtils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('../time/TimeContext').TimeContext} TimeContext
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describes and bounds requests for telemetry data.
|
||||||
|
*
|
||||||
|
* @typedef TelemetryRequestOptions
|
||||||
|
* @property {String} [sort] the key of the property to sort by. This may
|
||||||
|
* be prefixed with a "+" or a "-" sign to sort in ascending
|
||||||
|
* or descending order respectively. If no prefix is present,
|
||||||
|
* ascending order will be used.
|
||||||
|
* @property {Number} [start] the lower bound for values of the sorting property
|
||||||
|
* @property {Number} [end] the upper bound for values of the sorting property
|
||||||
|
* @property {String} [strategy] symbolic identifier for strategies
|
||||||
|
* (such as `latest` or `minmax`) which may be recognized by providers;
|
||||||
|
* these will be tried in order until an appropriate provider
|
||||||
|
* is found
|
||||||
|
* @property {AbortController} [signal] an AbortController which can be used
|
||||||
|
* to cancel a telemetry request
|
||||||
|
* @property {String} [domain] the domain key of the request
|
||||||
|
* @property {TimeContext} [timeContext] the time context to use for this request
|
||||||
|
* @memberof module:openmct.TelemetryAPI~
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utilities for telemetry
|
||||||
|
* @interface TelemetryAPI
|
||||||
|
* @memberof module:openmct
|
||||||
|
*/
|
||||||
export default class TelemetryAPI {
|
export default class TelemetryAPI {
|
||||||
#isGreedyLAD;
|
#isGreedyLAD;
|
||||||
|
|
||||||
@ -169,25 +199,35 @@ export default class TelemetryAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @param {TelemetryRequestOptions} options options for the telemetry request
|
||||||
* Though used in TelemetryCollection as well
|
* @returns {TelemetryRequestOptions} the options, with defaults filled in
|
||||||
*/
|
*/
|
||||||
standardizeRequestOptions(options) {
|
standardizeRequestOptions(options = {}) {
|
||||||
if (!Object.prototype.hasOwnProperty.call(options, 'start')) {
|
if (!Object.hasOwn(options, 'start')) {
|
||||||
options.start = this.openmct.time.bounds().start;
|
if (options.timeContext?.bounds()) {
|
||||||
|
options.start = options.timeContext.bounds().start;
|
||||||
|
} else {
|
||||||
|
options.start = this.openmct.time.bounds().start;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Object.prototype.hasOwnProperty.call(options, 'end')) {
|
if (!Object.hasOwn(options, 'end')) {
|
||||||
options.end = this.openmct.time.bounds().end;
|
if (options.timeContext?.bounds()) {
|
||||||
|
options.end = options.timeContext.bounds().end;
|
||||||
|
} else {
|
||||||
|
options.end = this.openmct.time.bounds().end;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Object.prototype.hasOwnProperty.call(options, 'domain')) {
|
if (!Object.hasOwn(options, 'domain')) {
|
||||||
options.domain = this.openmct.time.timeSystem().key;
|
options.domain = this.openmct.time.timeSystem().key;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Object.prototype.hasOwnProperty.call(options, 'timeContext')) {
|
if (!Object.hasOwn(options, 'timeContext')) {
|
||||||
options.timeContext = this.openmct.time;
|
options.timeContext = this.openmct.time;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -265,7 +305,7 @@ export default class TelemetryAPI {
|
|||||||
* @memberof module:openmct.TelemetryAPI~TelemetryProvider#
|
* @memberof module:openmct.TelemetryAPI~TelemetryProvider#
|
||||||
* @param {module:openmct.DomainObject} domainObject the object
|
* @param {module:openmct.DomainObject} domainObject the object
|
||||||
* which has associated telemetry
|
* which has associated telemetry
|
||||||
* @param {module:openmct.TelemetryAPI~TelemetryRequest} options
|
* @param {TelemetryRequestOptions} options
|
||||||
* options for this telemetry collection request
|
* options for this telemetry collection request
|
||||||
* @returns {TelemetryCollection} a TelemetryCollection instance
|
* @returns {TelemetryCollection} a TelemetryCollection instance
|
||||||
*/
|
*/
|
||||||
@ -283,7 +323,7 @@ export default class TelemetryAPI {
|
|||||||
* @memberof module:openmct.TelemetryAPI~TelemetryProvider#
|
* @memberof module:openmct.TelemetryAPI~TelemetryProvider#
|
||||||
* @param {module:openmct.DomainObject} domainObject the object
|
* @param {module:openmct.DomainObject} domainObject the object
|
||||||
* which has associated telemetry
|
* which has associated telemetry
|
||||||
* @param {module:openmct.TelemetryAPI~TelemetryRequest} options
|
* @param {TelemetryRequestOptions} options
|
||||||
* options for this historical request
|
* options for this historical request
|
||||||
* @returns {Promise.<object[]>} a promise for an array of
|
* @returns {Promise.<object[]>} a promise for an array of
|
||||||
* telemetry data
|
* telemetry data
|
||||||
@ -339,6 +379,7 @@ export default class TelemetryAPI {
|
|||||||
* @memberof module:openmct.TelemetryAPI~TelemetryProvider#
|
* @memberof module:openmct.TelemetryAPI~TelemetryProvider#
|
||||||
* @param {module:openmct.DomainObject} domainObject the object
|
* @param {module:openmct.DomainObject} domainObject the object
|
||||||
* which has associated telemetry
|
* which has associated telemetry
|
||||||
|
* @param {TelemetryRequestOptions} options configuration items for subscription
|
||||||
* @param {Function} callback the callback to invoke with new data, as
|
* @param {Function} callback the callback to invoke with new data, as
|
||||||
* it becomes available
|
* it becomes available
|
||||||
* @returns {Function} a function which may be called to terminate
|
* @returns {Function} a function which may be called to terminate
|
||||||
|
@ -24,6 +24,22 @@ import _ from 'lodash';
|
|||||||
import EventEmitter from 'EventEmitter';
|
import EventEmitter from 'EventEmitter';
|
||||||
import { LOADED_ERROR, TIMESYSTEM_KEY_NOTIFICATION, TIMESYSTEM_KEY_WARNING } from './constants';
|
import { LOADED_ERROR, TIMESYSTEM_KEY_NOTIFICATION, TIMESYSTEM_KEY_WARNING } from './constants';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('../objects/ObjectAPI').DomainObject} DomainObject
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('../time/TimeContext').TimeContext} TimeContext
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('./TelemetryAPI').TelemetryRequestOptions} TelemetryRequestOptions
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('../../../openmct').OpenMCT} OpenMCT
|
||||||
|
*/
|
||||||
|
|
||||||
/** Class representing a Telemetry Collection. */
|
/** Class representing a Telemetry Collection. */
|
||||||
|
|
||||||
export default class TelemetryCollection extends EventEmitter {
|
export default class TelemetryCollection extends EventEmitter {
|
||||||
@ -31,10 +47,10 @@ export default class TelemetryCollection extends EventEmitter {
|
|||||||
* Creates a Telemetry Collection
|
* Creates a Telemetry Collection
|
||||||
*
|
*
|
||||||
* @param {OpenMCT} openmct - Open MCT
|
* @param {OpenMCT} openmct - Open MCT
|
||||||
* @param {module:openmct.DomainObject} domainObject - Domain Object to use for telemetry collection
|
* @param {DomainObject} domainObject - Domain Object to use for telemetry collection
|
||||||
* @param {object} options - Any options passed in for request/subscribe
|
* @param {TelemetryRequestOptions} options - Any options passed in for request/subscribe
|
||||||
*/
|
*/
|
||||||
constructor(openmct, domainObject, options) {
|
constructor(openmct, domainObject, options = {}) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.loaded = false;
|
this.loaded = false;
|
||||||
@ -45,7 +61,7 @@ export default class TelemetryCollection extends EventEmitter {
|
|||||||
this.parseTime = undefined;
|
this.parseTime = undefined;
|
||||||
this.metadata = this.openmct.telemetry.getMetadata(domainObject);
|
this.metadata = this.openmct.telemetry.getMetadata(domainObject);
|
||||||
this.unsubscribe = undefined;
|
this.unsubscribe = undefined;
|
||||||
this.options = options;
|
this.options = this.openmct.telemetry.standardizeRequestOptions(options);
|
||||||
this.pageState = undefined;
|
this.pageState = undefined;
|
||||||
this.lastBounds = undefined;
|
this.lastBounds = undefined;
|
||||||
this.requestAbort = undefined;
|
this.requestAbort = undefined;
|
||||||
@ -62,8 +78,8 @@ export default class TelemetryCollection extends EventEmitter {
|
|||||||
this._error(LOADED_ERROR);
|
this._error(LOADED_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._setTimeSystem(this.openmct.time.timeSystem());
|
this._setTimeSystem(this.options.timeContext.timeSystem());
|
||||||
this.lastBounds = this.openmct.time.bounds();
|
this.lastBounds = this.options.timeContext.bounds();
|
||||||
|
|
||||||
this._watchBounds();
|
this._watchBounds();
|
||||||
this._watchTimeSystem();
|
this._watchTimeSystem();
|
||||||
@ -106,10 +122,10 @@ export default class TelemetryCollection extends EventEmitter {
|
|||||||
*/
|
*/
|
||||||
async _requestHistoricalTelemetry() {
|
async _requestHistoricalTelemetry() {
|
||||||
let options = { ...this.options };
|
let options = { ...this.options };
|
||||||
let historicalProvider;
|
const historicalProvider = this.openmct.telemetry.findRequestProvider(
|
||||||
|
this.domainObject,
|
||||||
this.openmct.telemetry.standardizeRequestOptions(options);
|
options
|
||||||
historicalProvider = this.openmct.telemetry.findRequestProvider(this.domainObject, options);
|
);
|
||||||
|
|
||||||
if (!historicalProvider) {
|
if (!historicalProvider) {
|
||||||
return;
|
return;
|
||||||
@ -438,7 +454,7 @@ export default class TelemetryCollection extends EventEmitter {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_watchBounds() {
|
_watchBounds() {
|
||||||
this.openmct.time.on('bounds', this._bounds, this);
|
this.options.timeContext.on('bounds', this._bounds, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -446,7 +462,7 @@ export default class TelemetryCollection extends EventEmitter {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_unwatchBounds() {
|
_unwatchBounds() {
|
||||||
this.openmct.time.off('bounds', this._bounds, this);
|
this.options.timeContext.off('bounds', this._bounds, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -454,7 +470,7 @@ export default class TelemetryCollection extends EventEmitter {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_watchTimeSystem() {
|
_watchTimeSystem() {
|
||||||
this.openmct.time.on('timeSystem', this._setTimeSystemAndFetchData, this);
|
this.options.timeContext.on('timeSystem', this._setTimeSystemAndFetchData, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -462,7 +478,7 @@ export default class TelemetryCollection extends EventEmitter {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_unwatchTimeSystem() {
|
_unwatchTimeSystem() {
|
||||||
this.openmct.time.off('timeSystem', this._setTimeSystemAndFetchData, this);
|
this.options.timeContext.off('timeSystem', this._setTimeSystemAndFetchData, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
423
src/plugins/imagery/components/AnnotationsCanvas.vue
Normal file
423
src/plugins/imagery/components/AnnotationsCanvas.vue
Normal file
@ -0,0 +1,423 @@
|
|||||||
|
<!--
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
imageryAnnotations() {
|
||||||
|
this.drawAnnotations();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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>
|
@ -19,7 +19,7 @@ $elemBg: rgba(black, 0.7);
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 2;
|
z-index: 3;
|
||||||
@include userSelectNone;
|
@include userSelectNone;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,7 +137,7 @@ export default {
|
|||||||
imageUrl(newUrl, oldUrl) {
|
imageUrl(newUrl, oldUrl) {
|
||||||
// reset image pan/zoom if newUrl only if not locked
|
// reset image pan/zoom if newUrl only if not locked
|
||||||
if (newUrl && !this.panZoomLocked) {
|
if (newUrl && !this.panZoomLocked) {
|
||||||
this.$emit('resetImage');
|
this.handleResetImage();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
cursorStates(states) {
|
cursorStates(states) {
|
||||||
|
@ -38,6 +38,11 @@
|
|||||||
fetchpriority="low"
|
fetchpriority="low"
|
||||||
@load="imageLoadCompleted"
|
@load="imageLoadCompleted"
|
||||||
/>
|
/>
|
||||||
|
<i
|
||||||
|
v-show="showAnnotationIndicator"
|
||||||
|
class="c-thumb__annotation-indicator icon-status-poll-edit"
|
||||||
|
>
|
||||||
|
</i>
|
||||||
</a>
|
</a>
|
||||||
<div v-if="viewableArea" class="c-thumb__viewable-area" :style="viewableAreaStyle"></div>
|
<div v-if="viewableArea" class="c-thumb__viewable-area" :style="viewableAreaStyle"></div>
|
||||||
<div class="c-thumb__timestamp">{{ image.formattedTime }}</div>
|
<div class="c-thumb__timestamp">{{ image.formattedTime }}</div>
|
||||||
@ -66,6 +71,12 @@ export default {
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
imageryAnnotations: {
|
||||||
|
type: Array,
|
||||||
|
default() {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
viewableArea: {
|
viewableArea: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: function () {
|
default: function () {
|
||||||
@ -125,6 +136,11 @@ export default {
|
|||||||
width: `${width}px`,
|
width: `${width}px`,
|
||||||
height: `${height}px`
|
height: `${height}px`
|
||||||
};
|
};
|
||||||
|
},
|
||||||
|
showAnnotationIndicator() {
|
||||||
|
return this.imageryAnnotations.some((annotation) => {
|
||||||
|
return !annotation._deleted;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -88,6 +88,13 @@
|
|||||||
:image="focusedImage"
|
:image="focusedImage"
|
||||||
:sized-image-dimensions="sizedImageDimensions"
|
:sized-image-dimensions="sizedImageDimensions"
|
||||||
/>
|
/>
|
||||||
|
<AnnotationsCanvas
|
||||||
|
v-if="shouldDisplayAnnotations"
|
||||||
|
:image="focusedImage"
|
||||||
|
:imagery-annotations="imageryAnnotations[focusedImage.time]"
|
||||||
|
@annotationMarqueed="handlePauseButton(true)"
|
||||||
|
@annotationsChanged="loadAnnotations"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -173,6 +180,7 @@
|
|||||||
:key="`${image.thumbnailUrl || image.url}-${image.time}-${index}`"
|
:key="`${image.thumbnailUrl || image.url}-${image.time}-${index}`"
|
||||||
:image="image"
|
:image="image"
|
||||||
:active="focusedImageIndex === index"
|
:active="focusedImageIndex === index"
|
||||||
|
:imagery-annotations="imageryAnnotations[image.time]"
|
||||||
:selected="focusedImageIndex === index && isPaused"
|
:selected="focusedImageIndex === index && isPaused"
|
||||||
:real-time="!isFixed"
|
:real-time="!isFixed"
|
||||||
:viewable-area="focusedImageIndex === index ? viewableArea : null"
|
:viewable-area="focusedImageIndex === index ? viewableArea : null"
|
||||||
@ -200,6 +208,7 @@ import Compass from './Compass/Compass.vue';
|
|||||||
import ImageControls from './ImageControls.vue';
|
import ImageControls from './ImageControls.vue';
|
||||||
import ImageThumbnail from './ImageThumbnail.vue';
|
import ImageThumbnail from './ImageThumbnail.vue';
|
||||||
import imageryData from '../../imagery/mixins/imageryData';
|
import imageryData from '../../imagery/mixins/imageryData';
|
||||||
|
import AnnotationsCanvas from './AnnotationsCanvas.vue';
|
||||||
|
|
||||||
const REFRESH_CSS_MS = 500;
|
const REFRESH_CSS_MS = 500;
|
||||||
const DURATION_TRACK_MS = 1000;
|
const DURATION_TRACK_MS = 1000;
|
||||||
@ -232,7 +241,8 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
Compass,
|
Compass,
|
||||||
ImageControls,
|
ImageControls,
|
||||||
ImageThumbnail
|
ImageThumbnail,
|
||||||
|
AnnotationsCanvas
|
||||||
},
|
},
|
||||||
mixins: [imageryData],
|
mixins: [imageryData],
|
||||||
inject: ['openmct', 'domainObject', 'objectPath', 'currentView', 'imageFreshnessOptions'],
|
inject: ['openmct', 'domainObject', 'objectPath', 'currentView', 'imageFreshnessOptions'],
|
||||||
@ -295,7 +305,8 @@ export default {
|
|||||||
animateZoom: true,
|
animateZoom: true,
|
||||||
imagePanned: false,
|
imagePanned: false,
|
||||||
forceShowThumbnails: false,
|
forceShowThumbnails: false,
|
||||||
animateThumbScroll: false
|
animateThumbScroll: false,
|
||||||
|
imageryAnnotations: {}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -425,6 +436,19 @@ export default {
|
|||||||
|
|
||||||
return result;
|
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() {
|
shouldDisplayCompass() {
|
||||||
const imageHeightAndWidth = this.sizedImageHeight !== 0 && this.sizedImageWidth !== 0;
|
const imageHeightAndWidth = this.sizedImageHeight !== 0 && this.sizedImageWidth !== 0;
|
||||||
const display =
|
const display =
|
||||||
@ -631,6 +655,9 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
created() {
|
||||||
|
this.abortController = new AbortController();
|
||||||
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
eventHelpers.extend(this);
|
eventHelpers.extend(this);
|
||||||
this.focusedImageWrapper = this.$refs.focusedImageWrapper;
|
this.focusedImageWrapper = this.$refs.focusedImageWrapper;
|
||||||
@ -689,8 +716,12 @@ export default {
|
|||||||
|
|
||||||
this.listenTo(this.focusedImageWrapper, 'wheel', this.wheelZoom, this);
|
this.listenTo(this.focusedImageWrapper, 'wheel', this.wheelZoom, this);
|
||||||
this.loadVisibleLayers();
|
this.loadVisibleLayers();
|
||||||
|
this.loadAnnotations();
|
||||||
|
|
||||||
|
this.openmct.selection.on('change', this.updateSelection);
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
|
this.abortController.abort();
|
||||||
this.persistVisibleLayers();
|
this.persistVisibleLayers();
|
||||||
this.stopFollowingTimeContext();
|
this.stopFollowingTimeContext();
|
||||||
|
|
||||||
@ -716,6 +747,15 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.stopListening(this.focusedImageWrapper, 'wheel', this.wheelZoom, this);
|
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: {
|
methods: {
|
||||||
calculateViewHeight() {
|
calculateViewHeight() {
|
||||||
@ -743,6 +783,15 @@ export default {
|
|||||||
this.timeContext.off('clock', this.trackDuration);
|
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() {
|
expand() {
|
||||||
// check for modifier keys so it doesnt interfere with the layout
|
// check for modifier keys so it doesnt interfere with the layout
|
||||||
if (this.cursorStates.modifierKeyPressed) {
|
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() {
|
persistVisibleLayers() {
|
||||||
if (
|
if (
|
||||||
this.domainObject.configuration &&
|
this.domainObject.configuration &&
|
||||||
@ -979,7 +1063,9 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await Vue.nextTick();
|
await Vue.nextTick();
|
||||||
this.$refs.thumbsWrapper.scrollLeft = scrollWidth;
|
if (this.$refs.thumbsWrapper) {
|
||||||
|
this.$refs.thumbsWrapper.scrollLeft = scrollWidth;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
scrollHandler() {
|
scrollHandler() {
|
||||||
if (this.isPaused) {
|
if (this.isPaused) {
|
||||||
|
@ -27,20 +27,14 @@
|
|||||||
<div class="c-checkbox-list js-checkbox-menu c-menu--to-left c-menu--has-close-btn">
|
<div class="c-checkbox-list js-checkbox-menu c-menu--to-left c-menu--has-close-btn">
|
||||||
<ul @click="$event.stopPropagation()">
|
<ul @click="$event.stopPropagation()">
|
||||||
<li v-for="(layer, index) in layers" :key="index">
|
<li v-for="(layer, index) in layers" :key="index">
|
||||||
<input
|
<label>
|
||||||
v-if="layer.visible"
|
<input
|
||||||
:id="index + 'LayerControl'"
|
:checked="layer.visible"
|
||||||
checked
|
type="checkbox"
|
||||||
type="checkbox"
|
@change="toggleLayerVisibility(index)"
|
||||||
@change="toggleLayerVisibility(index)"
|
/>
|
||||||
/>
|
{{ layer.name }}
|
||||||
<input
|
</label>
|
||||||
v-else
|
|
||||||
:id="index + 'LayerControl'"
|
|
||||||
type="checkbox"
|
|
||||||
@change="toggleLayerVisibility(index)"
|
|
||||||
/>
|
|
||||||
<label :for="index + 'LayerControl'">{{ layer.name }}</label>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
@ -293,6 +293,13 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__annotation-indicator {
|
||||||
|
color: $colorClickIconButton;
|
||||||
|
position: absolute;
|
||||||
|
top: 6px;
|
||||||
|
right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
&__timestamp {
|
&__timestamp {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
padding: 2px 3px;
|
padding: 2px 3px;
|
||||||
@ -540,3 +547,11 @@
|
|||||||
align-self: flex-end;
|
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;
|
||||||
|
}
|
||||||
|
@ -60,7 +60,9 @@ export default {
|
|||||||
this.timeKey = this.timeSystem.key;
|
this.timeKey = this.timeSystem.key;
|
||||||
this.timeFormatter = this.getFormatter(this.timeKey);
|
this.timeFormatter = this.getFormatter(this.timeKey);
|
||||||
|
|
||||||
this.telemetryCollection = this.openmct.telemetry.requestCollection(this.domainObject, {});
|
this.telemetryCollection = this.openmct.telemetry.requestCollection(this.domainObject, {
|
||||||
|
timeContext: this.timeContext
|
||||||
|
});
|
||||||
this.telemetryCollection.on('add', this.dataAdded);
|
this.telemetryCollection.on('add', this.dataAdded);
|
||||||
this.telemetryCollection.on('remove', this.dataRemoved);
|
this.telemetryCollection.on('remove', this.dataRemoved);
|
||||||
this.telemetryCollection.on('clear', this.dataCleared);
|
this.telemetryCollection.on('clear', this.dataCleared);
|
||||||
|
@ -41,7 +41,6 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import TagEditor from './tags/TagEditor.vue';
|
import TagEditor from './tags/TagEditor.vue';
|
||||||
import _ from 'lodash';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@ -123,6 +122,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
|
this.abortController = null;
|
||||||
this.openmct.annotation.on('targetDomainObjectAnnotated', this.loadAnnotationForTargetObject);
|
this.openmct.annotation.on('targetDomainObjectAnnotated', this.loadAnnotationForTargetObject);
|
||||||
this.openmct.selection.on('change', this.updateSelection);
|
this.openmct.selection.on('change', this.updateSelection);
|
||||||
await this.updateSelection(this.openmct.selection.get());
|
await this.updateSelection(this.openmct.selection.get());
|
||||||
@ -190,20 +190,34 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async loadAnnotationForTargetObject(target) {
|
async loadAnnotationForTargetObject(target) {
|
||||||
const targetID = this.openmct.objects.makeKeyString(target.identifier);
|
// If the user changes targets while annotations are loading,
|
||||||
const allAnnotationsForTarget = await this.openmct.annotation.getAnnotations(
|
// abort the previous request.
|
||||||
target.identifier
|
if (this.abortController !== null) {
|
||||||
);
|
this.abortController.abort();
|
||||||
const filteredAnnotationsForSelection = allAnnotationsForTarget.filter((annotation) => {
|
}
|
||||||
const matchingTargetID = Object.keys(annotation.targets).filter((loadedTargetID) => {
|
|
||||||
return targetID === loadedTargetID;
|
|
||||||
});
|
|
||||||
const fetchedTargetDetails = annotation.targets[matchingTargetID];
|
|
||||||
const selectedTargetDetails = this.targetDetails[matchingTargetID];
|
|
||||||
|
|
||||||
return _.isEqual(fetchedTargetDetails, selectedTargetDetails);
|
this.abortController = new AbortController();
|
||||||
});
|
|
||||||
this.loadNewAnnotations(filteredAnnotationsForSelection);
|
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.getSearchResults = debounce(this.getSearchResults, 500);
|
||||||
this.syncUrlWithPageAndSection = debounce(this.syncUrlWithPageAndSection, 100);
|
this.syncUrlWithPageAndSection = debounce(this.syncUrlWithPageAndSection, 100);
|
||||||
},
|
},
|
||||||
async mounted() {
|
async created() {
|
||||||
await this.loadAnnotations();
|
this.transaction = null;
|
||||||
|
this.abortController = new AbortController();
|
||||||
|
try {
|
||||||
|
await this.loadAnnotations();
|
||||||
|
} catch (err) {
|
||||||
|
if (err.name !== 'AbortError') {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
this.formatSidebar();
|
this.formatSidebar();
|
||||||
this.setSectionAndPageFromUrl();
|
this.setSectionAndPageFromUrl();
|
||||||
|
|
||||||
this.openmct.selection.on('change', this.updateSelection);
|
this.openmct.selection.on('change', this.updateSelection);
|
||||||
this.transaction = null;
|
|
||||||
|
|
||||||
window.addEventListener('orientationchange', this.formatSidebar);
|
window.addEventListener('orientationchange', this.formatSidebar);
|
||||||
window.addEventListener('hashchange', this.setSectionAndPageFromUrl);
|
window.addEventListener('hashchange', this.setSectionAndPageFromUrl);
|
||||||
@ -324,6 +333,7 @@ export default {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
|
this.abortController.abort();
|
||||||
if (this.unlisten) {
|
if (this.unlisten) {
|
||||||
this.unlisten();
|
this.unlisten();
|
||||||
}
|
}
|
||||||
@ -387,8 +397,10 @@ export default {
|
|||||||
this.lastLocalAnnotationCreation = this.domainObject.annotationLastCreated ?? 0;
|
this.lastLocalAnnotationCreation = this.domainObject.annotationLastCreated ?? 0;
|
||||||
|
|
||||||
const foundAnnotations = await this.openmct.annotation.getAnnotations(
|
const foundAnnotations = await this.openmct.annotation.getAnnotations(
|
||||||
this.domainObject.identifier
|
this.domainObject.identifier,
|
||||||
|
this.abortController.signal
|
||||||
);
|
);
|
||||||
|
|
||||||
foundAnnotations.forEach((foundAnnotation) => {
|
foundAnnotations.forEach((foundAnnotation) => {
|
||||||
const targetId = Object.keys(foundAnnotation.targets)[0];
|
const targetId = Object.keys(foundAnnotation.targets)[0];
|
||||||
const entryId = foundAnnotation.targets[targetId].entryId;
|
const entryId = foundAnnotation.targets[targetId].entryId;
|
||||||
@ -425,7 +437,11 @@ export default {
|
|||||||
: [...filteredPageEntriesByTime].reverse();
|
: [...filteredPageEntriesByTime].reverse();
|
||||||
|
|
||||||
if (this.lastLocalAnnotationCreation < this.domainObject.annotationLastCreated) {
|
if (this.lastLocalAnnotationCreation < this.domainObject.annotationLastCreated) {
|
||||||
this.loadAnnotations();
|
this.loadAnnotations().catch((err) => {
|
||||||
|
if (err.name !== 'AbortError') {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
changeSelectedSection({ sectionId, pageId }) {
|
changeSelectedSection({ sectionId, pageId }) {
|
||||||
|
@ -27,7 +27,13 @@
|
|||||||
// If the above namespace is ever resolved, we can fold this search provider
|
// If the above namespace is ever resolved, we can fold this search provider
|
||||||
// back into the object provider.
|
// back into the object provider.
|
||||||
|
|
||||||
|
const BATCH_ANNOTATION_DEBOUNCE_MS = 100;
|
||||||
|
|
||||||
class CouchSearchProvider {
|
class CouchSearchProvider {
|
||||||
|
#bulkPromise;
|
||||||
|
#batchIds;
|
||||||
|
#lastAbortSignal;
|
||||||
|
|
||||||
constructor(couchObjectProvider) {
|
constructor(couchObjectProvider) {
|
||||||
this.couchObjectProvider = couchObjectProvider;
|
this.couchObjectProvider = couchObjectProvider;
|
||||||
this.searchTypes = couchObjectProvider.openmct.objects.SEARCH_TYPES;
|
this.searchTypes = couchObjectProvider.openmct.objects.SEARCH_TYPES;
|
||||||
@ -36,6 +42,8 @@ class CouchSearchProvider {
|
|||||||
this.searchTypes.ANNOTATIONS,
|
this.searchTypes.ANNOTATIONS,
|
||||||
this.searchTypes.TAGS
|
this.searchTypes.TAGS
|
||||||
];
|
];
|
||||||
|
this.#batchIds = [];
|
||||||
|
this.#bulkPromise = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
supportsSearchType(searchType) {
|
supportsSearchType(searchType) {
|
||||||
@ -68,28 +76,77 @@ class CouchSearchProvider {
|
|||||||
return this.couchObjectProvider.getObjectsByFilter(filter, abortSignal);
|
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 = {
|
const filter = {
|
||||||
selector: {
|
selector: {
|
||||||
$and: [
|
$and: [
|
||||||
{
|
|
||||||
model: {
|
|
||||||
targets: {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
'model.type': {
|
'model.type': {
|
||||||
$eq: 'annotation'
|
$eq: 'annotation'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$or: []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
filter.selector.$and[0].model.targets[keyString] = {
|
let lastAbortSignal = null;
|
||||||
$exists: true
|
// 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) {
|
searchForTags(tagsArray, abortSignal) {
|
||||||
|
@ -183,7 +183,7 @@ import MctTicks from './MctTicks.vue';
|
|||||||
import MctChart from './chart/MctChart.vue';
|
import MctChart from './chart/MctChart.vue';
|
||||||
import XAxis from './axis/XAxis.vue';
|
import XAxis from './axis/XAxis.vue';
|
||||||
import YAxis from './axis/YAxis.vue';
|
import YAxis from './axis/YAxis.vue';
|
||||||
import KDBush from 'kdbush';
|
import Flatbush from 'flatbush';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
const OFFSET_THRESHOLD = 10;
|
const OFFSET_THRESHOLD = 10;
|
||||||
@ -339,6 +339,9 @@ export default {
|
|||||||
this.cursorGuide = newCursorGuide;
|
this.cursorGuide = newCursorGuide;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
created() {
|
||||||
|
this.abortController = new AbortController();
|
||||||
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.yAxisIdVisibility = {};
|
this.yAxisIdVisibility = {};
|
||||||
this.offsetWidth = 0;
|
this.offsetWidth = 0;
|
||||||
@ -398,6 +401,7 @@ export default {
|
|||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
|
this.abortController.abort();
|
||||||
this.openmct.selection.off('change', this.updateSelection);
|
this.openmct.selection.off('change', this.updateSelection);
|
||||||
document.removeEventListener('keydown', this.handleKeyDown);
|
document.removeEventListener('keydown', this.handleKeyDown);
|
||||||
document.removeEventListener('keyup', this.handleKeyUp);
|
document.removeEventListener('keyup', this.handleKeyUp);
|
||||||
@ -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
|
// 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.
|
// 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 selectionType = selection?.[0]?.[0]?.context?.type;
|
||||||
const validSelectionTypes = ['clicked-on-plot-selection', 'plot-annotation-search-result'];
|
const validSelectionTypes = ['clicked-on-plot-selection', 'annotation-search-result'];
|
||||||
const isAnnotationSearchResult = selectionType === 'plot-annotation-search-result';
|
const isAnnotationSearchResult = selectionType === 'annotation-search-result';
|
||||||
|
|
||||||
if (!validSelectionTypes.includes(selectionType)) {
|
if (!validSelectionTypes.includes(selectionType)) {
|
||||||
// wrong type of selection
|
// wrong type of selection
|
||||||
@ -621,7 +625,8 @@ export default {
|
|||||||
await Promise.all(
|
await Promise.all(
|
||||||
this.seriesModels.map(async (seriesModel) => {
|
this.seriesModels.map(async (seriesModel) => {
|
||||||
const seriesAnnotations = await this.openmct.annotation.getAnnotations(
|
const seriesAnnotations = await this.openmct.annotation.getAnnotations(
|
||||||
seriesModel.model.identifier
|
seriesModel.model.identifier,
|
||||||
|
this.abortController.signal
|
||||||
);
|
);
|
||||||
rawAnnotationsForPlot.push(...seriesAnnotations);
|
rawAnnotationsForPlot.push(...seriesAnnotations);
|
||||||
})
|
})
|
||||||
@ -1393,6 +1398,24 @@ export default {
|
|||||||
|
|
||||||
return annotationsByPoints.flat();
|
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) {
|
getPointsInBox(boundingBoxPerYAxis, rawAnnotation) {
|
||||||
// load series models in KD-Trees
|
// load series models in KD-Trees
|
||||||
const seriesKDTrees = [];
|
const seriesKDTrees = [];
|
||||||
@ -1408,22 +1431,8 @@ export default {
|
|||||||
|
|
||||||
const seriesData = seriesModel.getSeriesData();
|
const seriesData = seriesModel.getSeriesData();
|
||||||
if (seriesData && seriesData.length) {
|
if (seriesData && seriesData.length) {
|
||||||
const kdTree = new KDBush(
|
|
||||||
seriesData,
|
|
||||||
(point) => {
|
|
||||||
return seriesModel.getXVal(point);
|
|
||||||
},
|
|
||||||
(point) => {
|
|
||||||
return seriesModel.getYVal(point);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
const searchResults = [];
|
const searchResults = [];
|
||||||
const rangeResults = kdTree.range(
|
const rangeResults = this.searchWithFlatbush(seriesData, seriesModel, boundingBox);
|
||||||
boundingBox.minX,
|
|
||||||
boundingBox.minY,
|
|
||||||
boundingBox.maxX,
|
|
||||||
boundingBox.maxY
|
|
||||||
);
|
|
||||||
rangeResults.forEach((id) => {
|
rangeResults.forEach((id) => {
|
||||||
const seriesDatum = seriesData[id];
|
const seriesDatum = seriesData[id];
|
||||||
if (seriesDatum) {
|
if (seriesDatum) {
|
||||||
@ -1524,7 +1533,11 @@ export default {
|
|||||||
this.endMarquee();
|
this.endMarquee();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.loadAnnotations();
|
this.loadAnnotations().catch((err) => {
|
||||||
|
if (err.name !== 'AbortError') {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
zoom(zoomDirection, zoomFactor) {
|
zoom(zoomDirection, zoomFactor) {
|
||||||
|
@ -41,8 +41,10 @@ import moment from 'moment';
|
|||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
const SCROLL_TIMEOUT = 10000;
|
const SCROLL_TIMEOUT = 10000;
|
||||||
const ROW_HEIGHT = 30;
|
const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss';
|
||||||
const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss:SSS';
|
const CURRENT_CSS_SUFFIX = '--is-current';
|
||||||
|
const PAST_CSS_SUFFIX = '--is-past';
|
||||||
|
const FUTURE_CSS_SUFFIX = '--is-future';
|
||||||
const headerItems = [
|
const headerItems = [
|
||||||
{
|
{
|
||||||
defaultDirection: true,
|
defaultDirection: true,
|
||||||
@ -79,9 +81,9 @@ const headerItems = [
|
|||||||
format: function (value) {
|
format: function (value) {
|
||||||
let result;
|
let result;
|
||||||
if (value < 0) {
|
if (value < 0) {
|
||||||
result = `-${getPreciseDuration(Math.abs(value))}`;
|
result = `+${getPreciseDuration(Math.abs(value), true)}`;
|
||||||
} else if (value > 0) {
|
} else if (value > 0) {
|
||||||
result = `+${getPreciseDuration(value)}`;
|
result = `-${getPreciseDuration(value, true)}`;
|
||||||
} else {
|
} else {
|
||||||
result = 'Now';
|
result = 'Now';
|
||||||
}
|
}
|
||||||
@ -360,11 +362,12 @@ export default {
|
|||||||
groups.forEach((key) => {
|
groups.forEach((key) => {
|
||||||
activities = activities.concat(this.planData[key]);
|
activities = activities.concat(this.planData[key]);
|
||||||
});
|
});
|
||||||
activities = activities.filter(this.filterActivities);
|
// filter activities first, then sort by start time
|
||||||
|
activities = activities.filter(this.filterActivities).sort(this.sortByStartTime);
|
||||||
activities = this.applyStyles(activities);
|
activities = this.applyStyles(activities);
|
||||||
this.setScrollTop();
|
this.planActivities = activities;
|
||||||
// sort by start time
|
//We need to wait for the next tick since we need the height of the row from the DOM
|
||||||
this.planActivities = activities.sort(this.sortByStartTime);
|
this.$nextTick(this.setScrollTop);
|
||||||
},
|
},
|
||||||
updateTimeStampAndListActivities(time) {
|
updateTimeStampAndListActivities(time) {
|
||||||
this.timestamp = time;
|
this.timestamp = time;
|
||||||
@ -410,30 +413,41 @@ export default {
|
|||||||
},
|
},
|
||||||
applyStyles(activities) {
|
applyStyles(activities) {
|
||||||
let firstCurrentActivityIndex = -1;
|
let firstCurrentActivityIndex = -1;
|
||||||
|
let activityClosestToNowIndex = -1;
|
||||||
let currentActivitiesCount = 0;
|
let currentActivitiesCount = 0;
|
||||||
const styledActivities = activities.map((activity, index) => {
|
const styledActivities = activities.map((activity, index) => {
|
||||||
if (this.timestamp >= activity.start && this.timestamp <= activity.end) {
|
if (this.timestamp >= activity.start && this.timestamp <= activity.end) {
|
||||||
activity.cssClass = '--is-current';
|
activity.cssClass = CURRENT_CSS_SUFFIX;
|
||||||
if (firstCurrentActivityIndex < 0) {
|
if (firstCurrentActivityIndex < 0) {
|
||||||
firstCurrentActivityIndex = index;
|
firstCurrentActivityIndex = index;
|
||||||
}
|
}
|
||||||
|
|
||||||
currentActivitiesCount = currentActivitiesCount + 1;
|
currentActivitiesCount = currentActivitiesCount + 1;
|
||||||
} else if (this.timestamp < activity.start) {
|
} else if (this.timestamp < activity.start) {
|
||||||
activity.cssClass = '--is-future';
|
activity.cssClass = FUTURE_CSS_SUFFIX;
|
||||||
|
//the index of the first activity that's greater than the current timestamp
|
||||||
|
if (activityClosestToNowIndex < 0) {
|
||||||
|
activityClosestToNowIndex = index;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
activity.cssClass = '--is-past';
|
activity.cssClass = PAST_CSS_SUFFIX;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!activity.key) {
|
if (!activity.key) {
|
||||||
activity.key = uuid();
|
activity.key = uuid();
|
||||||
}
|
}
|
||||||
|
|
||||||
activity.duration = activity.start - this.timestamp;
|
if (activity.start < this.timestamp) {
|
||||||
|
//if the activity start time has passed, display the time to the end of the activity
|
||||||
|
activity.duration = activity.end - this.timestamp;
|
||||||
|
} else {
|
||||||
|
activity.duration = activity.start - this.timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
return activity;
|
return activity;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.activityClosestToNowIndex = activityClosestToNowIndex;
|
||||||
this.firstCurrentActivityIndex = firstCurrentActivityIndex;
|
this.firstCurrentActivityIndex = firstCurrentActivityIndex;
|
||||||
this.currentActivitiesCount = currentActivitiesCount;
|
this.currentActivitiesCount = currentActivitiesCount;
|
||||||
|
|
||||||
@ -451,13 +465,22 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.firstCurrentActivityIndex = -1;
|
this.firstCurrentActivityIndex = -1;
|
||||||
|
this.activityClosestToNowIndex = -1;
|
||||||
this.currentActivitiesCount = 0;
|
this.currentActivitiesCount = 0;
|
||||||
this.$el.parentElement?.scrollTo({ top: 0 });
|
this.$el.parentElement?.scrollTo({ top: 0 });
|
||||||
this.autoScrolled = false;
|
this.autoScrolled = false;
|
||||||
},
|
},
|
||||||
setScrollTop() {
|
setScrollTop() {
|
||||||
//scroll to somewhere mid-way of the current activities
|
//The view isn't ready yet
|
||||||
if (this.firstCurrentActivityIndex > -1) {
|
if (!this.$el.parentElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const row = this.$el.querySelector('.js-list-item');
|
||||||
|
if (row && this.firstCurrentActivityIndex > -1) {
|
||||||
|
// scroll to somewhere mid-way of the current activities
|
||||||
|
const ROW_HEIGHT = row.getBoundingClientRect().height;
|
||||||
|
|
||||||
if (this.canAutoScroll() === false) {
|
if (this.canAutoScroll() === false) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -469,7 +492,22 @@ export default {
|
|||||||
behavior: 'smooth'
|
behavior: 'smooth'
|
||||||
});
|
});
|
||||||
this.autoScrolled = false;
|
this.autoScrolled = false;
|
||||||
|
} else if (row && this.activityClosestToNowIndex > -1) {
|
||||||
|
// scroll to somewhere close to 'now'
|
||||||
|
|
||||||
|
const ROW_HEIGHT = row.getBoundingClientRect().height;
|
||||||
|
|
||||||
|
if (this.canAutoScroll() === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$el.parentElement.scrollTo({
|
||||||
|
top: ROW_HEIGHT * (this.activityClosestToNowIndex - 1),
|
||||||
|
behavior: 'smooth'
|
||||||
|
});
|
||||||
|
this.autoScrolled = false;
|
||||||
} else {
|
} else {
|
||||||
|
// scroll to the top
|
||||||
this.resetScroll();
|
this.resetScroll();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -219,10 +219,10 @@ describe('the plugin', function () {
|
|||||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua'
|
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua'
|
||||||
);
|
);
|
||||||
expect(itemValues[0].innerHTML.trim()).toEqual(
|
expect(itemValues[0].innerHTML.trim()).toEqual(
|
||||||
`${moment(twoHoursPast).format('YYYY-MM-DD HH:mm:ss:SSS')}Z`
|
`${moment(twoHoursPast).format('YYYY-MM-DD HH:mm:ss')}Z`
|
||||||
);
|
);
|
||||||
expect(itemValues[1].innerHTML.trim()).toEqual(
|
expect(itemValues[1].innerHTML.trim()).toEqual(
|
||||||
`${moment(oneHourPast).format('YYYY-MM-DD HH:mm:ss:SSS')}Z`
|
`${moment(oneHourPast).format('YYYY-MM-DD HH:mm:ss')}Z`
|
||||||
);
|
);
|
||||||
|
|
||||||
done();
|
done();
|
||||||
|
@ -98,6 +98,13 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return 'Could not find any matching Notebook entries';
|
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 {
|
} else {
|
||||||
return this.result.targetModels[0].name;
|
return this.result.targetModels[0].name;
|
||||||
}
|
}
|
||||||
@ -115,11 +122,11 @@ export default {
|
|||||||
mounted() {
|
mounted() {
|
||||||
this.previewAction = new PreviewAction(this.openmct);
|
this.previewAction = new PreviewAction(this.openmct);
|
||||||
this.previewAction.on('isVisible', this.togglePreviewState);
|
this.previewAction.on('isVisible', this.togglePreviewState);
|
||||||
this.clickedPlotAnnotation = this.clickedPlotAnnotation.bind(this);
|
this.fireAnnotationSelection = this.fireAnnotationSelection.bind(this);
|
||||||
},
|
},
|
||||||
destroyed() {
|
destroyed() {
|
||||||
this.previewAction.off('isVisible', this.togglePreviewState);
|
this.previewAction.off('isVisible', this.togglePreviewState);
|
||||||
this.openmct.selection.off('change', this.clickedPlotAnnotation);
|
this.openmct.selection.off('change', this.fireAnnotationSelection);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
clickedResult(event) {
|
clickedResult(event) {
|
||||||
@ -132,17 +139,15 @@ export default {
|
|||||||
if (!this.openmct.router.isNavigatedObject(objectPath)) {
|
if (!this.openmct.router.isNavigatedObject(objectPath)) {
|
||||||
// if we're not on the correct page, navigate to the object,
|
// 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
|
// then wait for the selection event to fire before issuing a new selection
|
||||||
if (
|
if (this.result.annotationType) {
|
||||||
this.result.annotationType === this.openmct.annotation.ANNOTATION_TYPES.PLOT_SPATIAL
|
this.openmct.selection.on('change', this.fireAnnotationSelection);
|
||||||
) {
|
|
||||||
this.openmct.selection.on('change', this.clickedPlotAnnotation);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.openmct.router.navigate(resultUrl);
|
this.openmct.router.navigate(resultUrl);
|
||||||
} else {
|
} else {
|
||||||
// if this is the navigated object, then we are already on the correct page
|
// if this is the navigated object, then we are already on the correct page
|
||||||
// and just need to issue the selection event
|
// and just need to issue the selection event
|
||||||
this.clickedPlotAnnotation();
|
this.fireAnnotationSelection();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -151,8 +156,8 @@ export default {
|
|||||||
this.previewAction.invoke(objectPath);
|
this.previewAction.invoke(objectPath);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
clickedPlotAnnotation() {
|
fireAnnotationSelection() {
|
||||||
this.openmct.selection.off('change', this.clickedPlotAnnotation);
|
this.openmct.selection.off('change', this.fireAnnotationSelection);
|
||||||
|
|
||||||
const targetDetails = {};
|
const targetDetails = {};
|
||||||
const targetDomainObjects = {};
|
const targetDomainObjects = {};
|
||||||
@ -168,11 +173,11 @@ export default {
|
|||||||
element: this.$el,
|
element: this.$el,
|
||||||
context: {
|
context: {
|
||||||
item: this.result.targetModels[0],
|
item: this.result.targetModels[0],
|
||||||
type: 'plot-annotation-search-result',
|
type: 'annotation-search-result',
|
||||||
targetDetails,
|
targetDetails,
|
||||||
targetDomainObjects,
|
targetDomainObjects,
|
||||||
annotations: [this.result],
|
annotations: [this.result],
|
||||||
annotationType: this.openmct.annotation.ANNOTATION_TYPES.PLOT_SPATIAL,
|
annotationType: this.result.annotationType,
|
||||||
onAnnotationChange: () => {}
|
onAnnotationChange: () => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -63,14 +63,16 @@ export function millisecondsToDHMS(numericDuration) {
|
|||||||
return `${dhms ? '+' : ''} ${dhms}`;
|
return `${dhms ? '+' : ''} ${dhms}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPreciseDuration(value) {
|
export function getPreciseDuration(value, excludeMilliSeconds) {
|
||||||
const ms = value || 0;
|
const ms = value || 0;
|
||||||
|
const duration = [
|
||||||
return [
|
|
||||||
toDoubleDigits(Math.floor(normalizeAge(ms / ONE_DAY))),
|
toDoubleDigits(Math.floor(normalizeAge(ms / ONE_DAY))),
|
||||||
toDoubleDigits(Math.floor(normalizeAge((ms % ONE_DAY) / ONE_HOUR))),
|
toDoubleDigits(Math.floor(normalizeAge((ms % ONE_DAY) / ONE_HOUR))),
|
||||||
toDoubleDigits(Math.floor(normalizeAge((ms % ONE_HOUR) / ONE_MINUTE))),
|
toDoubleDigits(Math.floor(normalizeAge((ms % ONE_HOUR) / ONE_MINUTE))),
|
||||||
toDoubleDigits(Math.floor(normalizeAge((ms % ONE_MINUTE) / ONE_SECOND))),
|
toDoubleDigits(Math.floor(normalizeAge((ms % ONE_MINUTE) / ONE_SECOND)))
|
||||||
toTripleDigits(Math.floor(normalizeAge(ms % ONE_SECOND)))
|
];
|
||||||
].join(':');
|
if (!excludeMilliSeconds) {
|
||||||
|
duration.push(toTripleDigits(Math.floor(normalizeAge(ms % ONE_SECOND))));
|
||||||
|
}
|
||||||
|
return duration.join(':');
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user