mirror of
https://github.com/nasa/openmct.git
synced 2025-01-18 18:57:01 +00:00
34 - Image Pan and Zoom (#4736)
This commit is contained in:
parent
0cf30940c8
commit
0f9e727675
217
e2e/tests/plugins/imagery/exampleImagery.e2e.spec.js
Normal file
217
e2e/tests/plugins/imagery/exampleImagery.e2e.spec.js
Normal file
@ -0,0 +1,217 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
This test suite is dedicated to tests which verify the basic operations surrounding imagery,
|
||||
but only assume that example imagery is present.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('@playwright/test');
|
||||
|
||||
test.describe('Example Imagery', () => {
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
page.on('console', msg => console.log(msg.text()))
|
||||
//Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
//Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
// Click text=Example Imagery
|
||||
await page.click('text=Example Imagery');
|
||||
|
||||
// Click text=OK
|
||||
await Promise.all([
|
||||
page.waitForNavigation(/*{ url: 'http://localhost:8080/#/browse/mine/dab945d4-5a84-480e-8180-222b4aa730fa?tc.mode=fixed&tc.startBound=1639696164435&tc.endBound=1639697964435&tc.timeSystem=utc&view=conditionSet.view' }*/),
|
||||
page.click('text=OK')
|
||||
]);
|
||||
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery');
|
||||
});
|
||||
|
||||
const backgroundImageSelector = '.c-imagery__main-image__background-image';
|
||||
test('Can use Mouse Wheel to zoom in and out of latest image', async ({ page }) => {
|
||||
const bgImageLocator = await page.locator(backgroundImageSelector);
|
||||
const deltaYStep = 100; //equivalent to 1x zoom
|
||||
await bgImageLocator.hover();
|
||||
const originalImageDimensions = await page.locator(backgroundImageSelector).boundingBox();
|
||||
// zoom in
|
||||
await bgImageLocator.hover();
|
||||
await page.mouse.wheel(0, deltaYStep * 2);
|
||||
// wait for zoom animation to finish
|
||||
await bgImageLocator.hover();
|
||||
const imageMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox();
|
||||
// zoom out
|
||||
await bgImageLocator.hover();
|
||||
await page.mouse.wheel(0, -deltaYStep);
|
||||
// wait for zoom animation to finish
|
||||
await bgImageLocator.hover();
|
||||
const imageMouseZoomedOut = await page.locator(backgroundImageSelector).boundingBox();
|
||||
|
||||
expect(imageMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height);
|
||||
expect(imageMouseZoomedIn.width).toBeGreaterThan(originalImageDimensions.width);
|
||||
expect(imageMouseZoomedOut.height).toBeLessThan(imageMouseZoomedIn.height);
|
||||
expect(imageMouseZoomedOut.width).toBeLessThan(imageMouseZoomedIn.width);
|
||||
|
||||
});
|
||||
|
||||
test('Can use alt+drag to move around image once zoomed in', async ({ page }) => {
|
||||
const deltaYStep = 100; //equivalent to 1x zoom
|
||||
|
||||
const bgImageLocator = await page.locator(backgroundImageSelector);
|
||||
await bgImageLocator.hover();
|
||||
// zoom in
|
||||
await page.mouse.wheel(0, deltaYStep * 2);
|
||||
await bgImageLocator.hover();
|
||||
const zoomedBoundingBox = await bgImageLocator.boundingBox();
|
||||
const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2;
|
||||
const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2;
|
||||
// move to the right
|
||||
|
||||
// center the mouse pointer
|
||||
await page.mouse.move(imageCenterX, imageCenterY);
|
||||
|
||||
// pan right
|
||||
await page.keyboard.down('Alt');
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(imageCenterX - 200, imageCenterY, 10);
|
||||
await page.mouse.up();
|
||||
await page.keyboard.up('Alt');
|
||||
const afterRightPanBoundingBox = await bgImageLocator.boundingBox();
|
||||
expect(zoomedBoundingBox.x).toBeGreaterThan(afterRightPanBoundingBox.x);
|
||||
|
||||
// pan left
|
||||
await page.keyboard.down('Alt');
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(imageCenterX, imageCenterY, 10);
|
||||
await page.mouse.up();
|
||||
await page.keyboard.up('Alt');
|
||||
const afterLeftPanBoundingBox = await bgImageLocator.boundingBox();
|
||||
expect(afterRightPanBoundingBox.x).toBeLessThan(afterLeftPanBoundingBox.x);
|
||||
|
||||
// pan up
|
||||
await page.mouse.move(imageCenterX, imageCenterY);
|
||||
await page.keyboard.down('Alt');
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(imageCenterX, imageCenterY + 200, 10);
|
||||
await page.mouse.up();
|
||||
await page.keyboard.up('Alt');
|
||||
const afterUpPanBoundingBox = await bgImageLocator.boundingBox();
|
||||
expect(afterUpPanBoundingBox.y).toBeGreaterThan(afterLeftPanBoundingBox.y);
|
||||
|
||||
// pan down
|
||||
await page.keyboard.down('Alt');
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(imageCenterX, imageCenterY - 200, 10);
|
||||
await page.mouse.up();
|
||||
await page.keyboard.up('Alt');
|
||||
const afterDownPanBoundingBox = await bgImageLocator.boundingBox();
|
||||
expect(afterDownPanBoundingBox.y).toBeLessThan(afterUpPanBoundingBox.y);
|
||||
|
||||
});
|
||||
|
||||
test('Can use + - buttons to zoom on the image', async ({ page }) => {
|
||||
const bgImageLocator = await page.locator(backgroundImageSelector);
|
||||
await bgImageLocator.hover();
|
||||
const zoomInBtn = await page.locator('.t-btn-zoom-in');
|
||||
const zoomOutBtn = await page.locator('.t-btn-zoom-out');
|
||||
const initialBoundingBox = await bgImageLocator.boundingBox();
|
||||
|
||||
await zoomInBtn.click();
|
||||
await zoomInBtn.click();
|
||||
// wait for zoom animation to finish
|
||||
await bgImageLocator.hover();
|
||||
const zoomedInBoundingBox = await bgImageLocator.boundingBox();
|
||||
expect(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height);
|
||||
expect(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width);
|
||||
|
||||
await zoomOutBtn.click();
|
||||
// wait for zoom animation to finish
|
||||
await bgImageLocator.hover();
|
||||
const zoomedOutBoundingBox = await bgImageLocator.boundingBox();
|
||||
expect(zoomedOutBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height);
|
||||
expect(zoomedOutBoundingBox.width).toBeLessThan(zoomedInBoundingBox.width);
|
||||
|
||||
});
|
||||
|
||||
test('Can use the reset button to reset the image', async ({ page }) => {
|
||||
const bgImageLocator = await page.locator(backgroundImageSelector);
|
||||
await bgImageLocator.hover();
|
||||
const zoomInBtn = await page.locator('.t-btn-zoom-in');
|
||||
const zoomResetBtn = await page.locator('.t-btn-zoom-reset');
|
||||
const initialBoundingBox = await bgImageLocator.boundingBox();
|
||||
|
||||
await zoomInBtn.click();
|
||||
await zoomInBtn.click();
|
||||
// wait for zoom animation to finish
|
||||
await bgImageLocator.hover();
|
||||
const zoomedInBoundingBox = await bgImageLocator.boundingBox();
|
||||
expect(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height);
|
||||
expect(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width);
|
||||
|
||||
await zoomResetBtn.click();
|
||||
await bgImageLocator.hover();
|
||||
|
||||
const resetBoundingBox = await bgImageLocator.boundingBox();
|
||||
expect(resetBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height);
|
||||
expect(resetBoundingBox.width).toBeLessThan(zoomedInBoundingBox.width);
|
||||
|
||||
expect(resetBoundingBox.height).toEqual(initialBoundingBox.height);
|
||||
expect(resetBoundingBox.width).toEqual(initialBoundingBox.width);
|
||||
});
|
||||
|
||||
//test('Can use Mouse Wheel to zoom in and out of previous image');
|
||||
//test('Can zoom into the latest image and the real-time/fixed-time imagery will pause');
|
||||
//test.skip('Can zoom into a previous image from thumbstrip in real-time or fixed-time');
|
||||
//test.skip('Clicking on the left arrow should pause the imagery and go to previous image');
|
||||
//test.skip('If the imagery view is in pause mode, it should not be updated when new images come in');
|
||||
//test.skip('If the imagery view is not in pause mode, it should be updated when new images come in');
|
||||
});
|
||||
|
||||
test.describe('Example Imagery in Display layout', () => {
|
||||
test.skip('Can use Mouse Wheel to zoom in and out of previous image');
|
||||
test.skip('Can use alt+drag to move around image once zoomed in');
|
||||
test.skip('Can zoom into the latest image and the real-time/fixed-time imagery will pause');
|
||||
test.skip('Clicking on the left arrow should pause the imagery and go to previous image');
|
||||
test.skip('If the imagery view is in pause mode, it should not be updated when new images come in');
|
||||
test.skip('If the imagery view is not in pause mode, it should be updated when new images come in');
|
||||
});
|
||||
|
||||
test.describe('Example Imagery in Flexible layout', () => {
|
||||
test.skip('Can use Mouse Wheel to zoom in and out of previous image');
|
||||
test.skip('Can use alt+drag to move around image once zoomed in');
|
||||
test.skip('Can zoom into the latest image and the real-time/fixed-time imagery will pause');
|
||||
test.skip('Clicking on the left arrow should pause the imagery and go to previous image');
|
||||
test.skip('If the imagery view is in pause mode, it should not be updated when new images come in');
|
||||
test.skip('If the imagery view is not in pause mode, it should be updated when new images come in');
|
||||
});
|
||||
|
||||
test.describe('Example Imagery in Tabs view', () => {
|
||||
test.skip('Can use Mouse Wheel to zoom in and out of previous image');
|
||||
test.skip('Can use alt+drag to move around image once zoomed in');
|
||||
test.skip('Can zoom into the latest image and the real-time/fixed-time imagery will pause');
|
||||
test.skip('Can zoom into a previous image from thumbstrip in real-time or fixed-time');
|
||||
test.skip('Clicking on the left arrow should pause the imagery and go to previous image');
|
||||
test.skip('If the imagery view is in pause mode, it should not be updated when new images come in');
|
||||
test.skip('If the imagery view is not in pause mode, it should be updated when new images come in');
|
||||
});
|
273
src/plugins/imagery/components/ImageControls.vue
Normal file
273
src/plugins/imagery/components/ImageControls.vue
Normal file
@ -0,0 +1,273 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
<template>
|
||||
<div class="h-local-controls h-local-controls--overlay-content c-local-controls--show-on-hover c-image-controls__controls">
|
||||
<div class="c-image-controls__control c-image-controls__zoom icon-magnify">
|
||||
<div class="c-button-set c-button-set--strip-h">
|
||||
<button
|
||||
class="c-button t-btn-zoom-out icon-minus"
|
||||
title="Zoom out"
|
||||
@click="zoomOut"
|
||||
></button>
|
||||
|
||||
<button
|
||||
class="c-button t-btn-zoom-in icon-plus"
|
||||
title="Zoom in"
|
||||
@click="zoomIn"
|
||||
></button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="c-button t-btn-zoom-lock"
|
||||
title="Lock current zoom and pan across all images"
|
||||
:class="{'icon-unlocked': !panZoomLocked, 'icon-lock': panZoomLocked}"
|
||||
@click="toggleZoomLock"
|
||||
></button>
|
||||
|
||||
<button
|
||||
class="c-button icon-reset t-btn-zoom-reset"
|
||||
title="Remove zoom and pan"
|
||||
@click="handleResetImage"
|
||||
></button>
|
||||
|
||||
<span class="c-image-controls__zoom-factor">x{{ formattedZoomFactor }}</span>
|
||||
</div>
|
||||
<div class="c-image-controls__control c-image-controls__brightness-contrast">
|
||||
<span
|
||||
class="c-image-controls__sliders"
|
||||
draggable="true"
|
||||
@dragstart.stop.prevent
|
||||
>
|
||||
<div class="c-image-controls__input icon-brightness">
|
||||
<input
|
||||
v-model="filters.contrast"
|
||||
type="range"
|
||||
min="0"
|
||||
max="500"
|
||||
@change="notifyFiltersChanged"
|
||||
>
|
||||
</div>
|
||||
<div class="c-image-controls__input icon-contrast">
|
||||
<input
|
||||
v-model="filters.brightness"
|
||||
type="range"
|
||||
min="0"
|
||||
max="500"
|
||||
@change="notifyFiltersChanged"
|
||||
>
|
||||
</div>
|
||||
</span>
|
||||
<span class="t-reset-btn-holder c-imagery__lc__reset-btn c-image-controls__btn-reset">
|
||||
<button
|
||||
class="c-icon-link icon-reset t-btn-reset"
|
||||
@click="handleResetFilters"
|
||||
></button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash';
|
||||
|
||||
const DEFAULT_FILTER_VALUES = {
|
||||
brightness: '100',
|
||||
contrast: '100'
|
||||
};
|
||||
|
||||
const ZOOM_LIMITS_MAX_DEFAULT = 20;
|
||||
const ZOOM_LIMITS_MIN_DEFAULT = 1;
|
||||
const ZOOM_STEP = 1;
|
||||
const ZOOM_WHEEL_SENSITIVITY_REDUCTION = 0.01;
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'domainObject'],
|
||||
props: {
|
||||
zoomFactor: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
imageUrl: String
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
altPressed: false,
|
||||
shiftPressed: false,
|
||||
metaPressed: false,
|
||||
panZoomLocked: false,
|
||||
wheelZooming: false,
|
||||
filters: {
|
||||
brightness: 100,
|
||||
contrast: 100
|
||||
}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
formattedZoomFactor() {
|
||||
return Number.parseFloat(this.zoomFactor).toPrecision(2);
|
||||
},
|
||||
cursorStates() {
|
||||
const isPannable = this.altPressed && this.zoomFactor > 1;
|
||||
const showCursorZoomIn = this.metaPressed && !this.shiftPressed;
|
||||
const showCursorZoomOut = this.metaPressed && this.shiftPressed;
|
||||
const modifierKeyPressed = Boolean(this.metaPressed || this.shiftPressed || this.altPressed);
|
||||
|
||||
return {
|
||||
isPannable,
|
||||
showCursorZoomIn,
|
||||
showCursorZoomOut,
|
||||
modifierKeyPressed
|
||||
};
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
imageUrl(newUrl, oldUrl) {
|
||||
// reset image pan/zoom if newUrl only if not locked
|
||||
if (newUrl && !this.panZoomLocked) {
|
||||
this.$emit('resetImage');
|
||||
}
|
||||
},
|
||||
cursorStates(states) {
|
||||
this.$emit('cursorsUpdated', states);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
document.addEventListener('keydown', this.handleKeyDown);
|
||||
document.addEventListener('keyup', this.handleKeyUp);
|
||||
this.clearWheelZoom = _.debounce(this.clearWheelZoom, 600);
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.removeEventListener('keydown', this.handleKeyDown);
|
||||
document.removeEventListener('keyup', this.handleKeyUp);
|
||||
},
|
||||
methods: {
|
||||
handleResetImage() {
|
||||
this.$emit('resetImage');
|
||||
},
|
||||
handleUpdatePanZoom(options) {
|
||||
this.$emit('panZoomUpdated', options);
|
||||
},
|
||||
toggleZoomLock() {
|
||||
this.panZoomLocked = !this.panZoomLocked;
|
||||
},
|
||||
notifyFiltersChanged() {
|
||||
this.$emit('filtersUpdated', this.filters);
|
||||
},
|
||||
handleResetFilters() {
|
||||
this.filters = DEFAULT_FILTER_VALUES;
|
||||
this.notifyFiltersChanged();
|
||||
},
|
||||
limitZoomRange(factor) {
|
||||
return Math.min(Math.max(ZOOM_LIMITS_MIN_DEFAULT, factor), ZOOM_LIMITS_MAX_DEFAULT);
|
||||
},
|
||||
// used to increment the zoom without knowledge of current level
|
||||
processZoom(increment, userCoordX, userCoordY) {
|
||||
const newFactor = this.limitZoomRange(this.zoomFactor + increment);
|
||||
this.zoomImage(newFactor, userCoordX, userCoordY);
|
||||
},
|
||||
zoomImage(newScaleFactor, screenClientX, screenClientY) {
|
||||
if (!(newScaleFactor || Number.isInteger(newScaleFactor))) {
|
||||
console.error('Scale factor provided is invalid');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (newScaleFactor > ZOOM_LIMITS_MAX_DEFAULT) {
|
||||
newScaleFactor = ZOOM_LIMITS_MAX_DEFAULT;
|
||||
}
|
||||
|
||||
if (newScaleFactor <= 0 || newScaleFactor <= ZOOM_LIMITS_MIN_DEFAULT) {
|
||||
return this.handleResetImage();
|
||||
}
|
||||
|
||||
this.handleUpdatePanZoom({
|
||||
newScaleFactor,
|
||||
screenClientX,
|
||||
screenClientY
|
||||
});
|
||||
},
|
||||
wheelZoom(e) {
|
||||
// only use x,y coordinates on scrolling in
|
||||
if (this.wheelZooming === false && e.deltaY > 0) {
|
||||
this.wheelZooming = true;
|
||||
// grab first x,y coordinates
|
||||
this.processZoom(e.deltaY * ZOOM_WHEEL_SENSITIVITY_REDUCTION, e.clientX, e.clientY);
|
||||
} else {
|
||||
// ignore subsequent event x,y so scroll drift doesn't occur
|
||||
this.processZoom(e.deltaY * ZOOM_WHEEL_SENSITIVITY_REDUCTION);
|
||||
}
|
||||
|
||||
// debounced method that will only fire after the scroll series is complete
|
||||
this.clearWheelZoom();
|
||||
},
|
||||
/* debounced method so that wheelZooming state will
|
||||
** remain true through a zoom event series
|
||||
*/
|
||||
clearWheelZoom() {
|
||||
this.wheelZooming = false;
|
||||
},
|
||||
handleKeyDown(event) {
|
||||
if (event.key === 'Alt') {
|
||||
this.altPressed = true;
|
||||
}
|
||||
|
||||
if (event.metaKey) {
|
||||
this.metaPressed = true;
|
||||
}
|
||||
|
||||
if (event.shiftKey) {
|
||||
this.shiftPressed = true;
|
||||
}
|
||||
},
|
||||
handleKeyUp(event) {
|
||||
if (event.key === 'Alt') {
|
||||
this.altPressed = false;
|
||||
}
|
||||
|
||||
this.shiftPressed = false;
|
||||
if (!event.metaKey) {
|
||||
this.metaPressed = false;
|
||||
}
|
||||
},
|
||||
zoomIn() {
|
||||
this.processZoom(ZOOM_STEP);
|
||||
},
|
||||
zoomOut() {
|
||||
this.processZoom(-ZOOM_STEP);
|
||||
},
|
||||
// attached to onClick listener in ImageryView
|
||||
handlePanZoomClick(e) {
|
||||
if (this.altPressed) {
|
||||
return this.$emit('startPan', e);
|
||||
}
|
||||
|
||||
if (!(this.metaPressed && e.button === 0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newScaleFactor = this.zoomFactor + (this.shiftPressed ? -ZOOM_STEP : ZOOM_STEP);
|
||||
this.zoomImage(newScaleFactor, e.clientX, e.clientY);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
@ -29,59 +29,78 @@
|
||||
@mouseover="focusElement"
|
||||
>
|
||||
<div class="c-imagery__main-image-wrapper has-local-controls">
|
||||
<div class="h-local-controls h-local-controls--overlay-content c-local-controls--show-on-hover c-image-controls__controls">
|
||||
<span
|
||||
class="c-image-controls__sliders"
|
||||
draggable="true"
|
||||
@dragstart="startDrag"
|
||||
>
|
||||
<div class="c-image-controls__slider-wrapper icon-brightness">
|
||||
<input
|
||||
v-model="filters.brightness"
|
||||
type="range"
|
||||
min="0"
|
||||
max="500"
|
||||
>
|
||||
</div>
|
||||
<div class="c-image-controls__slider-wrapper icon-contrast">
|
||||
<input
|
||||
v-model="filters.contrast"
|
||||
type="range"
|
||||
min="0"
|
||||
max="500"
|
||||
>
|
||||
</div>
|
||||
</span>
|
||||
<span class="t-reset-btn-holder c-imagery__lc__reset-btn c-image-controls__btn-reset">
|
||||
<a
|
||||
class="s-icon-button icon-reset t-btn-reset"
|
||||
@click="filters={brightness: 100, contrast: 100}"
|
||||
></a>
|
||||
</span>
|
||||
</div>
|
||||
<ImageControls
|
||||
ref="imageControls"
|
||||
:zoom-factor="zoomFactor"
|
||||
:image-url="imageUrl"
|
||||
@resetImage="resetImage"
|
||||
@panZoomUpdated="handlePanZoomUpdate"
|
||||
@filtersUpdated="setFilters"
|
||||
@cursorsUpdated="setCursorStates"
|
||||
@startPan="startPan"
|
||||
/>
|
||||
|
||||
<div
|
||||
ref="imageBG"
|
||||
class="c-imagery__main-image__bg"
|
||||
:class="{'paused unnsynced': isPaused && !isFixed,'stale':false }"
|
||||
:class="{
|
||||
'paused unnsynced': isPaused && !isFixed,
|
||||
'stale': false,
|
||||
'pannable': cursorStates.isPannable,
|
||||
'cursor-zoom-in': cursorStates.showCursorZoomIn,
|
||||
'cursor-zoom-out': cursorStates.showCursorZoomOut
|
||||
}"
|
||||
@click="expand"
|
||||
>
|
||||
<div
|
||||
v-if="zoomFactor > 1"
|
||||
class="c-imagery__hints"
|
||||
>Alt-drag to pan</div>
|
||||
<div
|
||||
ref="focusedImageWrapper"
|
||||
class="image-wrapper"
|
||||
:style="{
|
||||
'width': `${sizedImageDimensions.width}px`,
|
||||
'height': `${sizedImageDimensions.height}px`
|
||||
'width': `${sizedImageWidth}px`,
|
||||
'height': `${sizedImageHeight}px`
|
||||
}"
|
||||
@mousedown="handlePanZoomClick"
|
||||
>
|
||||
<img
|
||||
ref="focusedImage"
|
||||
class="c-imagery__main-image__image js-imageryView-image"
|
||||
class="c-imagery__main-image__image js-imageryView-image "
|
||||
:src="imageUrl"
|
||||
:draggable="!isSelectable"
|
||||
:style="{
|
||||
'filter': `brightness(${filters.brightness}%) contrast(${filters.contrast}%)`
|
||||
}"
|
||||
:data-openmct-image-timestamp="time"
|
||||
:data-openmct-object-keystring="keyString"
|
||||
>
|
||||
<div
|
||||
v-if="imageUrl"
|
||||
ref="focusedImageElement"
|
||||
class="c-imagery__main-image__background-image"
|
||||
:draggable="!isSelectable"
|
||||
:style="{
|
||||
'filter': `brightness(${filters.brightness}%) contrast(${filters.contrast}%)`,
|
||||
'background-image':
|
||||
`${imageUrl ? (
|
||||
`url(${imageUrl}),
|
||||
repeating-linear-gradient(
|
||||
45deg,
|
||||
transparent,
|
||||
transparent 4px,
|
||||
rgba(125,125,125,.2) 4px,
|
||||
rgba(125,125,125,.2) 8px
|
||||
)`
|
||||
) : ''}`,
|
||||
'transform': `scale(${zoomFactor}) translate(${imageTranslateX}px, ${imageTranslateY}px)`,
|
||||
'transition': `${!pan && animateZoom ? 'transform 250ms ease-in' : 'initial'}`,
|
||||
'width': `${sizedImageWidth}px`,
|
||||
'height': `${sizedImageHeight}px`,
|
||||
|
||||
}"
|
||||
></div>
|
||||
<Compass
|
||||
v-if="shouldDisplayCompass"
|
||||
:compass-rose-sizing-classes="compassRoseSizingClasses"
|
||||
@ -134,7 +153,7 @@
|
||||
v-if="!isFixed"
|
||||
class="c-button icon-pause pause-play"
|
||||
:class="{'is-paused': isPaused}"
|
||||
@click="paused(!isPaused, 'button')"
|
||||
@click="paused(!isPaused)"
|
||||
></button>
|
||||
</div>
|
||||
</div>
|
||||
@ -156,7 +175,7 @@
|
||||
:key="image.url + image.time"
|
||||
class="c-imagery__thumb c-thumb"
|
||||
:class="{ selected: focusedImageIndex === index && isPaused }"
|
||||
@click="setFocusedImage(index, thumbnailClick)"
|
||||
@click="thumbnailClicked(index)"
|
||||
>
|
||||
<a
|
||||
href=""
|
||||
@ -182,12 +201,13 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import eventHelpers from '../lib/eventHelpers';
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
|
||||
import RelatedTelemetry from './RelatedTelemetry/RelatedTelemetry';
|
||||
import Compass from './Compass/Compass.vue';
|
||||
|
||||
import ImageControls from './ImageControls.vue';
|
||||
import imageryData from "../../imagery/mixins/imageryData";
|
||||
|
||||
const REFRESH_CSS_MS = 500;
|
||||
@ -207,9 +227,12 @@ const ARROW_LEFT = 37;
|
||||
|
||||
const SCROLL_LATENCY = 250;
|
||||
|
||||
const ZOOM_SCALE_DEFAULT = 1;
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Compass
|
||||
Compass,
|
||||
ImageControls
|
||||
},
|
||||
mixins: [imageryData],
|
||||
inject: ['openmct', 'domainObject', 'objectPath', 'currentView'],
|
||||
@ -232,10 +255,6 @@ export default {
|
||||
timeSystem: timeSystem,
|
||||
keyString: undefined,
|
||||
autoScroll: true,
|
||||
filters: {
|
||||
brightness: 100,
|
||||
contrast: 100
|
||||
},
|
||||
thumbnailClick: THUMBNAIL_CLICKED,
|
||||
isPaused: false,
|
||||
refreshCSS: false,
|
||||
@ -247,19 +266,37 @@ export default {
|
||||
focusedImageNaturalAspectRatio: undefined,
|
||||
imageContainerWidth: undefined,
|
||||
imageContainerHeight: undefined,
|
||||
sizedImageWidth: 0,
|
||||
sizedImageHeight: 0,
|
||||
lockCompass: true,
|
||||
resizingWindow: false,
|
||||
timeContext: undefined
|
||||
timeContext: undefined,
|
||||
zoomFactor: ZOOM_SCALE_DEFAULT,
|
||||
filters: {
|
||||
brightness: 100,
|
||||
contrast: 100
|
||||
},
|
||||
cursorStates: {
|
||||
isPannable: false,
|
||||
showCursorZoomIn: false,
|
||||
showCursorZoomOut: false,
|
||||
modifierKeyPressed: false
|
||||
},
|
||||
imageTranslateX: 0,
|
||||
imageTranslateY: 0,
|
||||
pan: undefined,
|
||||
animateZoom: true,
|
||||
imagePanned: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
compassRoseSizingClasses() {
|
||||
let compassRoseSizingClasses = '';
|
||||
if (this.sizedImageDimensions.width < 300) {
|
||||
if (this.sizedImageWidth < 300) {
|
||||
compassRoseSizingClasses = '--rose-small --rose-min';
|
||||
} else if (this.sizedImageDimensions.width < 500) {
|
||||
} else if (this.sizedImageWidth < 500) {
|
||||
compassRoseSizingClasses = '--rose-small';
|
||||
} else if (this.sizedImageDimensions.width > 1000) {
|
||||
} else if (this.sizedImageWidth > 1000) {
|
||||
compassRoseSizingClasses = '--rose-max';
|
||||
}
|
||||
|
||||
@ -328,10 +365,18 @@ export default {
|
||||
return result;
|
||||
},
|
||||
shouldDisplayCompass() {
|
||||
return this.focusedImage !== undefined
|
||||
const imageHeightAndWidth = this.sizedImageHeight !== 0
|
||||
&& this.sizedImageWidth !== 0;
|
||||
|
||||
const display = this.focusedImage !== undefined
|
||||
&& this.focusedImageNaturalAspectRatio !== undefined
|
||||
&& this.imageContainerWidth !== undefined
|
||||
&& this.imageContainerHeight !== undefined;
|
||||
&& this.imageContainerHeight !== undefined
|
||||
&& imageHeightAndWidth
|
||||
&& this.zoomFactor === 1
|
||||
&& this.imagePanned !== true;
|
||||
|
||||
return display;
|
||||
},
|
||||
isSpacecraftPositionFresh() {
|
||||
let isFresh = undefined;
|
||||
@ -393,20 +438,6 @@ export default {
|
||||
|
||||
return isFresh;
|
||||
},
|
||||
sizedImageDimensions() {
|
||||
let sizedImageDimensions = {};
|
||||
if ((this.imageContainerWidth / this.imageContainerHeight) > this.focusedImageNaturalAspectRatio) {
|
||||
// container is wider than image
|
||||
sizedImageDimensions.width = this.imageContainerHeight * this.focusedImageNaturalAspectRatio;
|
||||
sizedImageDimensions.height = this.imageContainerHeight;
|
||||
} else {
|
||||
// container is taller than image
|
||||
sizedImageDimensions.width = this.imageContainerWidth;
|
||||
sizedImageDimensions.height = this.imageContainerWidth / this.focusedImageNaturalAspectRatio;
|
||||
}
|
||||
|
||||
return sizedImageDimensions;
|
||||
},
|
||||
isFixed() {
|
||||
let clock;
|
||||
if (this.timeContext) {
|
||||
@ -416,6 +447,16 @@ export default {
|
||||
}
|
||||
|
||||
return clock === undefined;
|
||||
},
|
||||
isSelectable() {
|
||||
return true;
|
||||
|
||||
},
|
||||
sizedImageDimensions() {
|
||||
return {
|
||||
width: this.sizedImageWidth,
|
||||
height: this.sizedImageHeight
|
||||
};
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@ -424,16 +465,35 @@ export default {
|
||||
const newSize = newHistory.length;
|
||||
let imageIndex;
|
||||
if (this.focusedImageTimestamp !== undefined) {
|
||||
const foundImageIndex = this.imageHistory.findIndex(image => {
|
||||
return image.time === this.focusedImageTimestamp;
|
||||
});
|
||||
imageIndex = foundImageIndex > -1 ? foundImageIndex : newSize - 1;
|
||||
const foundImageIndex = newHistory.findIndex(img => img.time === this.focusedImageTimestamp);
|
||||
imageIndex = foundImageIndex > -1
|
||||
? foundImageIndex
|
||||
: newSize - 1;
|
||||
} else {
|
||||
imageIndex = newSize > 0 ? newSize - 1 : undefined;
|
||||
imageIndex = newSize > 0
|
||||
? newSize - 1
|
||||
: undefined;
|
||||
}
|
||||
|
||||
this.setFocusedImage(imageIndex, false);
|
||||
this.nextImageIndex = imageIndex;
|
||||
|
||||
if (this.previousFocusedImage && newHistory.length) {
|
||||
const matchIndex = this.matchIndexOfPreviousImage(
|
||||
this.previousFocusedImage,
|
||||
newHistory
|
||||
);
|
||||
|
||||
if (matchIndex > -1) {
|
||||
this.setFocusedImage(matchIndex);
|
||||
} else {
|
||||
this.paused();
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.isPaused) {
|
||||
this.setFocusedImage(imageIndex);
|
||||
this.scrollToRight();
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
@ -445,6 +505,10 @@ export default {
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
eventHelpers.extend(this);
|
||||
this.focusedImageWrapper = this.$refs.focusedImageWrapper;
|
||||
this.focusedImageElement = this.$refs.focusedImageElement;
|
||||
|
||||
//We only need to use this till the user focuses an image manually
|
||||
if (this.focusedImageTimestamp !== undefined) {
|
||||
this.isPaused = true;
|
||||
@ -485,6 +549,7 @@ export default {
|
||||
this.thumbWrapperResizeObserver.observe(this.$refs.thumbsWrapper);
|
||||
}
|
||||
|
||||
this.listenTo(this.focusedImageWrapper, 'wheel', this.wheelZoom, this);
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.stopFollowingTimeContext();
|
||||
@ -509,6 +574,9 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.stopListening(this.focusedImageWrapper, 'wheel', this.wheelZoom, this);
|
||||
|
||||
},
|
||||
methods: {
|
||||
setTimeContext() {
|
||||
@ -525,6 +593,11 @@ export default {
|
||||
}
|
||||
},
|
||||
expand() {
|
||||
// check for modifier keys so it doesnt interfere with the layout
|
||||
if (this.cursorStates.modifierKeyPressed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const actionCollection = this.openmct.actions.getActionsCollection(this.objectPath, this.currentView);
|
||||
const visibleActions = actionCollection.getVisibleActions();
|
||||
const viewLargeAction = visibleActions
|
||||
@ -630,6 +703,7 @@ export default {
|
||||
focusElement() {
|
||||
this.$el.focus();
|
||||
},
|
||||
|
||||
handleScroll() {
|
||||
const thumbsWrapper = this.$refs.thumbsWrapper;
|
||||
if (!thumbsWrapper || this.resizingWindow) {
|
||||
@ -640,20 +714,15 @@ export default {
|
||||
const disableScroll = scrollWidth > Math.ceil(scrollLeft + clientWidth);
|
||||
this.autoScroll = !disableScroll;
|
||||
},
|
||||
paused(state, type) {
|
||||
this.isPaused = state;
|
||||
paused(state) {
|
||||
this.isPaused = Boolean(state);
|
||||
|
||||
if (type === 'button') {
|
||||
this.setFocusedImage(this.imageHistory.length - 1);
|
||||
}
|
||||
|
||||
if (this.nextImageIndex) {
|
||||
if (!state) {
|
||||
this.previousFocusedImage = null;
|
||||
this.setFocusedImage(this.nextImageIndex);
|
||||
delete this.nextImageIndex;
|
||||
}
|
||||
|
||||
this.autoScroll = true;
|
||||
this.scrollToRight();
|
||||
}
|
||||
},
|
||||
scrollToFocused() {
|
||||
const thumbsWrapper = this.$refs.thumbsWrapper;
|
||||
@ -692,51 +761,24 @@ export default {
|
||||
&& x.time === previous.time
|
||||
));
|
||||
},
|
||||
setFocusedImage(index, thumbnailClick = false) {
|
||||
let focusedIndex = index;
|
||||
thumbnailClicked(index) {
|
||||
this.setFocusedImage(index);
|
||||
this.paused(true);
|
||||
|
||||
this.setPreviousFocusedImage(index);
|
||||
},
|
||||
setPreviousFocusedImage(index) {
|
||||
this.focusedImageTimestamp = undefined;
|
||||
this.previousFocusedImage = this.imageHistory[index]
|
||||
? JSON.parse(JSON.stringify(this.imageHistory[index]))
|
||||
: undefined;
|
||||
},
|
||||
setFocusedImage(index) {
|
||||
if (!(Number.isInteger(index) && index > -1)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (thumbnailClick) {
|
||||
//We use the props till the user changes what they want to see
|
||||
this.focusedImageTimestamp = undefined;
|
||||
//set the previousFocusedImage when a user chooses an image
|
||||
this.previousFocusedImage = this.imageHistory[focusedIndex] ? JSON.parse(JSON.stringify(this.imageHistory[focusedIndex])) : undefined;
|
||||
}
|
||||
|
||||
if (this.previousFocusedImage) {
|
||||
// determine if the previous image exists in the new bounds of imageHistory
|
||||
if (!thumbnailClick) {
|
||||
const matchIndex = this.matchIndexOfPreviousImage(
|
||||
this.previousFocusedImage,
|
||||
this.imageHistory
|
||||
);
|
||||
focusedIndex = matchIndex > -1 ? matchIndex : this.imageHistory.length - 1;
|
||||
}
|
||||
|
||||
if (!(this.isPaused || thumbnailClick)
|
||||
|| focusedIndex === this.imageHistory.length - 1) {
|
||||
delete this.previousFocusedImage;
|
||||
}
|
||||
}
|
||||
|
||||
this.focusedImageIndex = focusedIndex;
|
||||
|
||||
//TODO: do we even need this anymore?
|
||||
if (this.isPaused && !thumbnailClick && this.focusedImageTimestamp === undefined) {
|
||||
this.nextImageIndex = focusedIndex;
|
||||
//this could happen if bounds changes
|
||||
if (this.focusedImageIndex > this.imageHistory.length - 1) {
|
||||
this.focusedImageIndex = focusedIndex;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (thumbnailClick && !this.isPaused) {
|
||||
this.paused(true);
|
||||
}
|
||||
this.focusedImageIndex = index;
|
||||
},
|
||||
trackDuration() {
|
||||
if (this.canTrackDuration) {
|
||||
@ -774,7 +816,7 @@ export default {
|
||||
|
||||
let index = this.focusedImageIndex;
|
||||
|
||||
this.setFocusedImage(++index, THUMBNAIL_CLICKED);
|
||||
this.thumbnailClicked(++index);
|
||||
if (index === this.imageHistory.length - 1) {
|
||||
this.paused(false);
|
||||
}
|
||||
@ -787,14 +829,50 @@ export default {
|
||||
let index = this.focusedImageIndex;
|
||||
|
||||
if (index === this.imageHistory.length - 1) {
|
||||
this.setFocusedImage(this.imageHistory.length - 2, THUMBNAIL_CLICKED);
|
||||
this.thumbnailClicked(this.imageHistory.length - 2);
|
||||
} else {
|
||||
this.setFocusedImage(--index, THUMBNAIL_CLICKED);
|
||||
this.thumbnailClicked(--index);
|
||||
}
|
||||
},
|
||||
startDrag(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
resetImage() {
|
||||
this.imagePanned = false;
|
||||
this.zoomFactor = ZOOM_SCALE_DEFAULT;
|
||||
this.imageTranslateX = 0;
|
||||
this.imageTranslateY = 0;
|
||||
},
|
||||
handlePanZoomUpdate({ newScaleFactor, screenClientX, screenClientY }) {
|
||||
if (!this.isPaused) {
|
||||
this.paused(true);
|
||||
}
|
||||
|
||||
if (!(screenClientX || screenClientY)) {
|
||||
return this.updatePanZoom(newScaleFactor, 0, 0);
|
||||
}
|
||||
|
||||
// handle mouse events
|
||||
const imageRect = this.focusedImageWrapper.getBoundingClientRect();
|
||||
const imageContainerX = screenClientX - imageRect.left;
|
||||
const imageContainerY = screenClientY - imageRect.top;
|
||||
const offsetFromCenterX = (imageRect.width / 2) - imageContainerX;
|
||||
const offsetFromCenterY = (imageRect.height / 2) - imageContainerY;
|
||||
|
||||
this.updatePanZoom(newScaleFactor, offsetFromCenterX, offsetFromCenterY);
|
||||
},
|
||||
updatePanZoom(newScaleFactor, offsetFromCenterX, offsetFromCenterY) {
|
||||
const currentScale = this.zoomFactor;
|
||||
const previousTranslateX = this.imageTranslateX;
|
||||
const previousTranslateY = this.imageTranslateY;
|
||||
|
||||
const offsetXInOriginalScale = offsetFromCenterX / currentScale;
|
||||
const offsetYInOriginalScale = offsetFromCenterY / currentScale;
|
||||
const translateX = offsetXInOriginalScale + previousTranslateX;
|
||||
const translateY = offsetYInOriginalScale + previousTranslateY;
|
||||
this.imageTranslateX = translateX;
|
||||
this.imageTranslateY = translateY;
|
||||
this.zoomFactor = newScaleFactor;
|
||||
},
|
||||
handlePanZoomClick(e) {
|
||||
this.$refs.imageControls.handlePanZoomClick(e);
|
||||
},
|
||||
arrowDownHandler(event) {
|
||||
let key = event.keyCode;
|
||||
@ -857,7 +935,7 @@ export default {
|
||||
|
||||
// TODO - should probably cache this
|
||||
img.addEventListener('load', () => {
|
||||
this.focusedImageNaturalAspectRatio = img.naturalWidth / img.naturalHeight;
|
||||
this.setSizedImageDimensions();
|
||||
}, { once: true });
|
||||
},
|
||||
resizeImageContainer() {
|
||||
@ -872,6 +950,21 @@ export default {
|
||||
if (this.$refs.imageBG.clientHeight !== this.imageContainerHeight) {
|
||||
this.imageContainerHeight = this.$refs.imageBG.clientHeight;
|
||||
}
|
||||
|
||||
this.setSizedImageDimensions();
|
||||
},
|
||||
setSizedImageDimensions() {
|
||||
this.focusedImageNaturalAspectRatio = this.$refs.focusedImage.naturalWidth / this.$refs.focusedImage.naturalHeight;
|
||||
|
||||
if ((this.imageContainerWidth / this.imageContainerHeight) > this.focusedImageNaturalAspectRatio) {
|
||||
// container is wider than image
|
||||
this.sizedImageWidth = this.imageContainerHeight * this.focusedImageNaturalAspectRatio;
|
||||
this.sizedImageHeight = this.imageContainerHeight;
|
||||
} else {
|
||||
// container is taller than image
|
||||
this.sizedImageWidth = this.imageContainerWidth;
|
||||
this.sizedImageHeight = this.imageContainerWidth / this.focusedImageNaturalAspectRatio;
|
||||
}
|
||||
},
|
||||
handleThumbWindowResizeStart() {
|
||||
if (!this.autoScroll) {
|
||||
@ -890,6 +983,73 @@ export default {
|
||||
this.$nextTick(() => {
|
||||
this.resizingWindow = false;
|
||||
});
|
||||
},
|
||||
// debounced method
|
||||
clearWheelZoom() {
|
||||
this.$refs.imageControls.clearWheelZoom();
|
||||
},
|
||||
wheelZoom(e) {
|
||||
e.preventDefault();
|
||||
if (!this.isPaused) {
|
||||
this.paused(true);
|
||||
}
|
||||
|
||||
this.$refs.imageControls.wheelZoom(e);
|
||||
},
|
||||
startPan(e) {
|
||||
e.preventDefault();
|
||||
if (!this.pan && this.zoomFactor > 1) {
|
||||
this.animateZoom = false;
|
||||
this.imagePanned = true;
|
||||
this.pan = {
|
||||
x: e.clientX,
|
||||
y: e.clientY
|
||||
};
|
||||
this.listenTo(window, 'mouseup', this.onMouseUp, this);
|
||||
this.listenTo(window, 'mousemove', this.trackMousePosition, this);
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
trackMousePosition(e) {
|
||||
if (!e.altKey) {
|
||||
return this.onMouseUp(e);
|
||||
}
|
||||
|
||||
this.updatePan(e);
|
||||
e.preventDefault();
|
||||
|
||||
},
|
||||
updatePan(e) {
|
||||
if (!this.pan) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dX = e.clientX - this.pan.x;
|
||||
const dY = e.clientY - this.pan.y;
|
||||
this.pan = {
|
||||
x: e.clientX,
|
||||
y: e.clientY
|
||||
};
|
||||
this.updatePanZoom(this.zoomFactor, dX, dY);
|
||||
},
|
||||
endPan() {
|
||||
this.pan = undefined;
|
||||
this.animateZoom = true;
|
||||
},
|
||||
onMouseUp(event) {
|
||||
this.stopListening(window, 'mouseup', this.onMouseUp, this);
|
||||
this.stopListening(window, 'mousemove', this.trackMousePosition, this);
|
||||
|
||||
if (this.pan) {
|
||||
return this.endPan(event);
|
||||
}
|
||||
},
|
||||
setFilters(filtersObj) {
|
||||
this.filters = filtersObj;
|
||||
},
|
||||
setCursorStates(states) {
|
||||
this.cursorStates = states;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -18,6 +18,11 @@
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.image-wrapper {
|
||||
overflow: visible clip;
|
||||
background-image: repeating-linear-gradient(45deg, transparent, transparent 4px, rgba(125, 125, 125, 0.2) 4px, rgba(125, 125, 125, 0.2) 8px);
|
||||
}
|
||||
|
||||
&__main-image {
|
||||
&__bg {
|
||||
background-color: $colorPlotBg;
|
||||
@ -27,18 +32,46 @@
|
||||
justify-content: center;
|
||||
flex: 1 1 auto;
|
||||
height: 0;
|
||||
|
||||
overflow: hidden;
|
||||
&.unnsynced{
|
||||
@include sUnsynced();
|
||||
}
|
||||
&.cursor-zoom-in {
|
||||
cursor: zoom-in;
|
||||
}
|
||||
&.cursor-zoom-out {
|
||||
cursor: zoom-out;
|
||||
}
|
||||
&.pannable {
|
||||
@include cursorGrab();
|
||||
|
||||
}
|
||||
}
|
||||
&__background-image {
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
}
|
||||
&__image {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
visibility: hidden;
|
||||
display: contents;
|
||||
}
|
||||
}
|
||||
|
||||
&__hints {
|
||||
$m: $interiorMargin;
|
||||
background: rgba(black, 0.2);
|
||||
border-radius: $smallCr;
|
||||
padding: 2px $interiorMargin;
|
||||
position: absolute;
|
||||
right: $m;
|
||||
top: $m;
|
||||
opacity: 0.9;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
&__control-bar,
|
||||
&__time {
|
||||
display: flex;
|
||||
@ -177,8 +210,8 @@
|
||||
z-index: 2;
|
||||
background: $colorLocalControlOvrBg;
|
||||
border-radius: $basicCr;
|
||||
max-width: 200px;
|
||||
min-width: 70px;
|
||||
max-width: 250px;
|
||||
min-width: 170px;
|
||||
width: 35%;
|
||||
align-items: center;
|
||||
padding: $interiorMargin $interiorMarginLg;
|
||||
@ -202,6 +235,7 @@
|
||||
|
||||
&__lc {
|
||||
&__reset-btn {
|
||||
// Span that holds bracket graphics and button
|
||||
$bc: $scrollbarTrackColorBg;
|
||||
|
||||
&:before,
|
||||
@ -222,18 +256,50 @@
|
||||
border-bottom: 1px solid $bc;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.c-icon-link {
|
||||
color: $colorBtnFg;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.c-image-controls {
|
||||
// Brightness/contrast
|
||||
|
||||
&__controls {
|
||||
// Sliders and reset element
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
|
||||
> * + * {
|
||||
margin-top: $interiorMargin;
|
||||
}
|
||||
|
||||
[class*='c-button'] { flex: 0 0 auto; }
|
||||
}
|
||||
|
||||
&__control,
|
||||
&__input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: $interiorMargin; // Need some extra space due to proximity to close button
|
||||
width: 100%;
|
||||
|
||||
&:before {
|
||||
color: rgba($colorMenuFg, 0.5);
|
||||
margin-right: $interiorMarginSm;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&__input {
|
||||
// A wrapper is needed to add the type icon to left of each control
|
||||
|
||||
input[type='range'] {
|
||||
//width: 100%; // Do we need this?
|
||||
}
|
||||
}
|
||||
|
||||
&__zoom {
|
||||
> * + * { margin-left: $interiorMargin; }
|
||||
}
|
||||
|
||||
&__sliders {
|
||||
@ -246,21 +312,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
&__slider-wrapper {
|
||||
// A wrapper is needed to add the type icon to left of each range input
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&:before {
|
||||
color: rgba($colorMenuFg, 0.5);
|
||||
margin-right: $interiorMarginSm;
|
||||
}
|
||||
|
||||
input[type='range'] {
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
&__btn-reset {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
98
src/plugins/imagery/lib/eventHelpers.js
Normal file
98
src/plugins/imagery/lib/eventHelpers.js
Normal file
@ -0,0 +1,98 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, 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.
|
||||
*****************************************************************************/
|
||||
|
||||
define([], function () {
|
||||
const helperFunctions = {
|
||||
listenTo: function (object, event, callback, context) {
|
||||
if (!this._listeningTo) {
|
||||
this._listeningTo = [];
|
||||
}
|
||||
|
||||
const listener = {
|
||||
object: object,
|
||||
event: event,
|
||||
callback: callback,
|
||||
context: context,
|
||||
_cb: context ? callback.bind(context) : callback
|
||||
};
|
||||
if (object.$watch && event.indexOf('change:') === 0) {
|
||||
const scopePath = event.replace('change:', '');
|
||||
listener.unlisten = object.$watch(scopePath, listener._cb, true);
|
||||
} else if (object.$on) {
|
||||
listener.unlisten = object.$on(event, listener._cb);
|
||||
} else if (object.addEventListener) {
|
||||
object.addEventListener(event, listener._cb);
|
||||
} else {
|
||||
object.on(event, listener._cb);
|
||||
}
|
||||
|
||||
this._listeningTo.push(listener);
|
||||
},
|
||||
|
||||
stopListening: function (object, event, callback, context) {
|
||||
if (!this._listeningTo) {
|
||||
this._listeningTo = [];
|
||||
}
|
||||
|
||||
this._listeningTo.filter(function (listener) {
|
||||
if (object && object !== listener.object) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (event && event !== listener.event) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (callback && callback !== listener.callback) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (context && context !== listener.context) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
.map(function (listener) {
|
||||
if (listener.unlisten) {
|
||||
listener.unlisten();
|
||||
} else if (listener.object.removeEventListener) {
|
||||
listener.object.removeEventListener(listener.event, listener._cb);
|
||||
} else {
|
||||
listener.object.off(listener.event, listener._cb);
|
||||
}
|
||||
|
||||
return listener;
|
||||
})
|
||||
.forEach(function (listener) {
|
||||
this._listeningTo.splice(this._listeningTo.indexOf(listener), 1);
|
||||
}, this);
|
||||
},
|
||||
|
||||
extend: function (object) {
|
||||
object.listenTo = helperFunctions.listenTo;
|
||||
object.stopListening = helperFunctions.stopListening;
|
||||
}
|
||||
};
|
||||
|
||||
return helperFunctions;
|
||||
});
|
@ -483,6 +483,42 @@ describe("The Imagery View Layouts", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
xit('should change the image zoom factor when using the zoom buttons', async (done) => {
|
||||
await Vue.nextTick();
|
||||
let imageSizeBefore;
|
||||
let imageSizeAfter;
|
||||
|
||||
// test clicking the zoom in button
|
||||
imageSizeBefore = parent.querySelector('.c-imagery_main-image_background-image').getBoundingClientRect();
|
||||
parent.querySelector('.t-btn-zoom-in').click();
|
||||
await Vue.nextTick();
|
||||
imageSizeAfter = parent.querySelector('.c-imagery_main-image_background-image').getBoundingClientRect();
|
||||
expect(imageSizeAfter.height).toBeGreaterThan(imageSizeBefore.height);
|
||||
expect(imageSizeAfter.width).toBeGreaterThan(imageSizeBefore.width);
|
||||
// test clicking the zoom out button
|
||||
imageSizeBefore = parent.querySelector('.c-imagery_main-image_background-image').getBoundingClientRect();
|
||||
parent.querySelector('.t-btn-zoom-out').click();
|
||||
await Vue.nextTick();
|
||||
imageSizeAfter = parent.querySelector('.c-imagery_main-image_background-image').getBoundingClientRect();
|
||||
expect(imageSizeAfter.height).toBeLessThan(imageSizeBefore.height);
|
||||
expect(imageSizeAfter.width).toBeLessThan(imageSizeBefore.width);
|
||||
done();
|
||||
});
|
||||
xit('should reset the zoom factor on the image when clicking the zoom button', async (done) => {
|
||||
await Vue.nextTick();
|
||||
// test clicking the zoom reset button
|
||||
// zoom in to scale up the image dimensions
|
||||
parent.querySelector('.t-btn-zoom-in').click();
|
||||
await Vue.nextTick();
|
||||
let imageSizeBefore = parent.querySelector('.c-imagery_main-image_background-image').getBoundingClientRect();
|
||||
await Vue.nextTick();
|
||||
parent.querySelector('.t-btn-zoom-reset').click();
|
||||
let imageSizeAfter = parent.querySelector('.c-imagery_main-image_background-image').getBoundingClientRect();
|
||||
expect(imageSizeAfter.height).toBeLessThan(imageSizeBefore.height);
|
||||
expect(imageSizeAfter.width).toBeLessThan(imageSizeBefore.width);
|
||||
done();
|
||||
});
|
||||
|
||||
it('clear data action is installed', () => {
|
||||
expect(clearDataAction).toBeDefined();
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user