Imagery layers (#4968)

* Moved imagery controls to a separate component
* Zoom pan controls moved to component
* Implement adjustments to encapsulate state into ImageryControls
* Track modifier key pressed for layouts
* image control popup open/close fix
* Styling for imagery local controls

Co-authored-by: Michael Rogers <contact@mhrogers.com>
Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
Co-authored-by: Scott Bell <scott@traclabs.com>
Co-authored-by: Charles Hacskaylo <charlesh88@gmail.com>
Co-authored-by: unlikelyzero <jchill2@gmail.com>
Co-authored-by: Jamie Vigliotta <jamie.j.vigliotta@nasa.gov>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
This commit is contained in:
Nikhil 2022-06-03 18:24:43 -07:00 committed by GitHub
parent 59c0da1b57
commit 111b0d0d68
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 748 additions and 223 deletions

View File

@ -1 +1 @@
{"openmct":{"21338566-d472-4377-aed1-21b79272c8de":{"identifier":{"key":"21338566-d472-4377-aed1-21b79272c8de","namespace":""},"name":"Performance Display Layout","type":"layout","composition":[{"key":"644c2e47-2903-475f-8a4a-6be1588ee02f","namespace":""}],"configuration":{"items":[{"width":32,"height":18,"x":1,"y":1,"identifier":{"key":"644c2e47-2903-475f-8a4a-6be1588ee02f","namespace":""},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"5aeb5a71-3149-41ed-9d8a-d34b0a18b053"}],"layoutGrid":[10,10]},"modified":1652228997384,"location":"mine","persisted":1652228997384},"644c2e47-2903-475f-8a4a-6be1588ee02f":{"identifier":{"key":"644c2e47-2903-475f-8a4a-6be1588ee02f","namespace":""},"name":"Performance Example Imagery","type":"example.imagery","configuration":{"imageLocation":"","imageLoadDelayInMilliSeconds":20000,"imageSamples":[]},"telemetry":{"values":[{"name":"Name","key":"name"},{"name":"Time","key":"utc","format":"utc","hints":{"domain":2}},{"name":"Local Time","key":"local","format":"local-format","hints":{"domain":1}},{"name":"Image","key":"url","format":"image","hints":{"image":1}},{"name":"Image Download Name","key":"imageDownloadName","format":"imageDownloadName","hints":{"imageDownloadName":1}}]},"modified":1652228997375,"location":"21338566-d472-4377-aed1-21b79272c8de","persisted":1652228997375}},"rootId":"21338566-d472-4377-aed1-21b79272c8de"}
{"openmct":{"b3cee102-86dd-4c0a-8eec-4d5d276f8691":{"identifier":{"key":"b3cee102-86dd-4c0a-8eec-4d5d276f8691","namespace":""},"name":"Performance Display Layout","type":"layout","composition":[{"key":"9666e7b4-be0c-47a5-94b8-99accad7155e","namespace":""}],"configuration":{"items":[{"width":32,"height":18,"x":12,"y":9,"identifier":{"key":"9666e7b4-be0c-47a5-94b8-99accad7155e","namespace":""},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"23ca351d-a67d-46aa-a762-290eb742d2f1"}],"layoutGrid":[10,10]},"modified":1654299875432,"location":"mine","persisted":1654299878751},"9666e7b4-be0c-47a5-94b8-99accad7155e":{"identifier":{"key":"9666e7b4-be0c-47a5-94b8-99accad7155e","namespace":""},"name":"Performance Example Imagery","type":"example.imagery","configuration":{"imageLocation":"","imageLoadDelayInMilliSeconds":20000,"imageSamples":[],"layers":[{"source":"dist/imagery/example-imagery-layer-16x9.png","name":"16:9","visible":false},{"source":"dist/imagery/example-imagery-layer-safe.png","name":"Safe","visible":false},{"source":"dist/imagery/example-imagery-layer-scale.png","name":"Scale","visible":false}]},"telemetry":{"values":[{"name":"Name","key":"name"},{"name":"Time","key":"utc","format":"utc","hints":{"domain":2}},{"name":"Local Time","key":"local","format":"local-format","hints":{"domain":1}},{"name":"Image","key":"url","format":"image","hints":{"image":1},"layers":[{"source":"dist/imagery/example-imagery-layer-16x9.png","name":"16:9"},{"source":"dist/imagery/example-imagery-layer-safe.png","name":"Safe"},{"source":"dist/imagery/example-imagery-layer-scale.png","name":"Scale"}]},{"name":"Image Download Name","key":"imageDownloadName","format":"imageDownloadName","hints":{"imageDownloadName":1}}]},"modified":1654299840077,"location":"b3cee102-86dd-4c0a-8eec-4d5d276f8691","persisted":1654299840078}},"rootId":"b3cee102-86dd-4c0a-8eec-4d5d276f8691"}

View File

@ -164,7 +164,7 @@ test.describe('Performance tests', () => {
console.log('jpgResourceTiming ' + JSON.stringify(jpgResourceTiming));
// Click Close Icon
await page.locator('.c-click-icon').click();
await page.locator('[aria-label="Close"]').click();
await page.evaluate(() => window.performance.mark("view-large-close-button"));
//await client.send('HeapProfiler.enable');

View File

@ -32,7 +32,6 @@ const { 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' });
@ -61,19 +60,19 @@ test.describe('Example Imagery', () => {
test('Can use Mouse Wheel to zoom in and out of latest image', async ({ page }) => {
const bgImageLocator = page.locator(backgroundImageSelector);
const deltaYStep = 100; //equivalent to 1x zoom
await bgImageLocator.hover();
await bgImageLocator.hover({trial: true});
const originalImageDimensions = await page.locator(backgroundImageSelector).boundingBox();
// zoom in
await bgImageLocator.hover();
await bgImageLocator.hover({trial: true});
await page.mouse.wheel(0, deltaYStep * 2);
// wait for zoom animation to finish
await bgImageLocator.hover();
await bgImageLocator.hover({trial: true});
const imageMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox();
// zoom out
await bgImageLocator.hover();
await bgImageLocator.hover({trial: true});
await page.mouse.wheel(0, -deltaYStep);
// wait for zoom animation to finish
await bgImageLocator.hover();
await bgImageLocator.hover({trial: true});
const imageMouseZoomedOut = await page.locator(backgroundImageSelector).boundingBox();
expect(imageMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height);
@ -88,11 +87,11 @@ test.describe('Example Imagery', () => {
const panHotkey = process.platform === 'linux' ? ['Control', 'Alt'] : ['Alt'];
const bgImageLocator = page.locator(backgroundImageSelector);
await bgImageLocator.hover();
await bgImageLocator.hover({trial: true});
// zoom in
await page.mouse.wheel(0, deltaYStep * 2);
await bgImageLocator.hover();
await bgImageLocator.hover({trial: true});
const zoomedBoundingBox = await bgImageLocator.boundingBox();
const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2;
const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2;
@ -151,22 +150,22 @@ test.describe('Example Imagery', () => {
test('Can use + - buttons to zoom on the image', async ({ page }) => {
const bgImageLocator = page.locator(backgroundImageSelector);
await bgImageLocator.hover();
const zoomInBtn = page.locator('.t-btn-zoom-in');
const zoomOutBtn = page.locator('.t-btn-zoom-out');
await bgImageLocator.hover({trial: true});
const zoomInBtn = page.locator('.t-btn-zoom-in').nth(0);
const zoomOutBtn = page.locator('.t-btn-zoom-out').nth(0);
const initialBoundingBox = await bgImageLocator.boundingBox();
await zoomInBtn.click();
await zoomInBtn.click();
// wait for zoom animation to finish
await bgImageLocator.hover();
await bgImageLocator.hover({trial: true});
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();
await bgImageLocator.hover({trial: true});
const zoomedOutBoundingBox = await bgImageLocator.boundingBox();
expect(zoomedOutBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height);
expect(zoomedOutBoundingBox.width).toBeLessThan(zoomedInBoundingBox.width);
@ -176,18 +175,18 @@ test.describe('Example Imagery', () => {
test('Can use the reset button to reset the image', async ({ page }) => {
const bgImageLocator = page.locator(backgroundImageSelector);
// wait for zoom animation to finish
await bgImageLocator.hover();
await bgImageLocator.hover({trial: true});
const zoomInBtn = page.locator('.t-btn-zoom-in');
const zoomResetBtn = page.locator('.t-btn-zoom-reset');
const zoomInBtn = page.locator('.t-btn-zoom-in').nth(0);
const zoomResetBtn = page.locator('.t-btn-zoom-reset').nth(0);
const initialBoundingBox = await bgImageLocator.boundingBox();
await zoomInBtn.click();
// wait for zoom animation to finish
await bgImageLocator.hover();
await bgImageLocator.hover({trial: true});
await zoomInBtn.click();
// wait for zoom animation to finish
await bgImageLocator.hover();
await bgImageLocator.hover({trial: true});
const zoomedInBoundingBox = await bgImageLocator.boundingBox();
expect.soft(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height);
@ -195,7 +194,7 @@ test.describe('Example Imagery', () => {
await zoomResetBtn.click();
// wait for zoom animation to finish
await bgImageLocator.hover();
await bgImageLocator.hover({trial: true});
const resetBoundingBox = await bgImageLocator.boundingBox();
expect.soft(resetBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height);
@ -209,18 +208,18 @@ test.describe('Example Imagery', () => {
const bgImageLocator = page.locator(backgroundImageSelector);
const pausePlayButton = page.locator('.c-button.pause-play');
// wait for zoom animation to finish
await bgImageLocator.hover();
await bgImageLocator.hover({trial: true});
// open the time conductor drop down
await page.locator('.c-conductor__controls button.c-mode-button').click();
await page.locator('button:has-text("Fixed Timespan")').click();
// Click local clock
await page.locator('.icon-clock >> text=Local Clock').click();
await page.locator('[data-testid="conductor-modeOption-realtime"]').click();
await expect.soft(pausePlayButton).not.toHaveClass(/is-paused/);
const zoomInBtn = page.locator('.t-btn-zoom-in');
const zoomInBtn = page.locator('.t-btn-zoom-in').nth(0);
await zoomInBtn.click();
// wait for zoom animation to finish
await bgImageLocator.hover();
await bgImageLocator.hover({trial: true});
return expect(pausePlayButton).not.toHaveClass(/is-paused/);
});
@ -267,7 +266,7 @@ test('Example Imagery in Display layout', async ({ page }) => {
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery');
const bgImageLocator = page.locator(backgroundImageSelector);
await bgImageLocator.hover();
await bgImageLocator.hover({trial: true});
// Click previous image button
const previousImageButton = page.locator('.c-nav--prev');
@ -279,7 +278,7 @@ test('Example Imagery in Display layout', async ({ page }) => {
// Zoom in
const originalImageDimensions = await page.locator(backgroundImageSelector).boundingBox();
await bgImageLocator.hover();
await bgImageLocator.hover({trial: true});
const deltaYStep = 100; // equivalent to 1x zoom
await page.mouse.wheel(0, deltaYStep * 2);
const zoomedBoundingBox = await bgImageLocator.boundingBox();
@ -287,7 +286,7 @@ test('Example Imagery in Display layout', async ({ page }) => {
const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2;
// Wait for zoom animation to finish
await bgImageLocator.hover();
await bgImageLocator.hover({trial: true});
const imageMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox();
expect(imageMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height);
expect(imageMouseZoomedIn.width).toBeGreaterThan(originalImageDimensions.width);
@ -311,11 +310,11 @@ test('Example Imagery in Display layout', async ({ page }) => {
await page.locator('[data-testid=conductor-modeOption-realtime]').click();
// Zoom in on next image
await bgImageLocator.hover();
await bgImageLocator.hover({trial: true});
await page.mouse.wheel(0, deltaYStep * 2);
// Wait for zoom animation to finish
await bgImageLocator.hover();
await bgImageLocator.hover({trial: true});
const imageNextMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox();
expect(imageNextMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height);
expect(imageNextMouseZoomedIn.width).toBeGreaterThan(originalImageDimensions.width);

View File

@ -59,7 +59,8 @@ export default function () {
object.configuration = {
imageLocation: '',
imageLoadDelayInMilliSeconds: DEFAULT_IMAGE_LOAD_DELAY_IN_MILISECONDS,
imageSamples: []
imageSamples: [],
layers: []
};
object.telemetry = {
@ -90,7 +91,21 @@ export default function () {
format: 'image',
hints: {
image: 1
}
},
layers: [
{
source: 'dist/imagery/example-imagery-layer-16x9.png',
name: '16:9'
},
{
source: 'dist/imagery/example-imagery-layer-safe.png',
name: 'Safe'
},
{
source: 'dist/imagery/example-imagery-layer-scale.png',
name: 'Scale'
}
]
},
{
name: 'Image Download Name',

View File

@ -7,6 +7,7 @@
<div class="c-overlay__outer">
<button
v-if="dismissable"
aria-label="Close"
class="c-click-icon c-overlay__close-button icon-x"
@click="destroy"
></button>

View File

@ -25,8 +25,7 @@
class="l-layout__frame c-frame"
:class="{
'no-frame': !item.hasFrame,
'u-inspectable': inspectable,
'is-in-small-container': size.width < 600 || size.height < 600
'u-inspectable': inspectable
}"
:style="style"
>

View File

@ -9,10 +9,6 @@
> *:first-child {
flex: 1 1 auto;
}
&.is-in-small-container {
//background: rgba(blue, 0.1);
}
}
.c-frame__move-bar {

View File

@ -0,0 +1,74 @@
<template>
<div
class="c-control-menu c-menu--to-left c-menu--has-close-btn c-image-controls c-image-controls--filters"
@click="handleClose"
>
<div
class="c-image-controls__controls"
@click="$event.stopPropagation()"
>
<span class="c-image-controls__sliders">
<div class="c-image-controls__slider-wrapper icon-brightness">
<input
v-model="filters.brightness"
type="range"
min="0"
max="500"
@change="notifyFiltersChanged"
@input="notifyFiltersChanged"
>
</div>
<div class="c-image-controls__slider-wrapper icon-contrast">
<input
v-model="filters.contrast"
type="range"
min="0"
max="500"
@change="notifyFiltersChanged"
@input="notifyFiltersChanged"
>
</div>
</span>
<span class="c-image-controls__reset-btn">
<a
class="s-icon-button icon-reset t-btn-reset"
@click="resetFilters"
></a>
</span>
</div>
<button class="c-click-icon icon-x t-btn-close c-switcher-menu__close-button"></button>
</div>
</template>
<script>
export default {
inject: ['openmct'],
data() {
return {
filters: {
brightness: 100,
contrast: 100
}
};
},
methods: {
handleClose(e) {
const closeButton = e.target.classList.contains('c-switcher-menu__close-button');
if (!closeButton) {
e.stopPropagation();
}
},
notifyFiltersChanged() {
this.$emit('filterChanged', this.filters);
},
resetFilters() {
this.filters = {
brightness: 100,
contrast: 100
};
this.notifyFiltersChanged();
}
}
};
</script>

View File

@ -21,75 +21,62 @@
*****************************************************************************/
<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>
<div class="h-local-controls h-local-controls--overlay-content h-local-controls--menus-aligned c-local-controls--show-on-hover">
<imagery-view-menu-switcher
:icon-class="'icon-brightness'"
:title="'Brightness and contrast'"
>
<filter-settings @filterChanged="updateFilterValues" />
</imagery-view-menu-switcher>
<button
class="c-button t-btn-zoom-in icon-plus"
title="Zoom in"
@click="zoomIn"
></button>
</div>
<imagery-view-menu-switcher
v-if="layers.length"
:icon-class="'icon-layers'"
:title="'Layers'"
>
<layer-settings
:layers="layers"
@toggleLayerVisibility="toggleLayerVisibility"
/>
</imagery-view-menu-switcher>
<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>
<zoom-settings
class="--hide-if-less-than-220"
:pan-zoom-locked="panZoomLocked"
:zoom-factor="zoomFactor"
@zoomOut="zoomOut"
@zoomIn="zoomIn"
@toggleZoomLock="toggleZoomLock"
@handleResetImage="handleResetImage"
/>
<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>
<imagery-view-menu-switcher
class="--show-if-less-than-220"
:icon-class="'icon-magnify'"
:title="'Zoom settings'"
>
<zoom-settings
:pan-zoom-locked="panZoomLocked"
:class="'c-control-menu c-menu--has-close-btn'"
:zoom-factor="zoomFactor"
:is-menu="true"
@zoomOut="zoomOut"
@zoomIn="zoomIn"
@toggleZoomLock="toggleZoomLock"
@handleResetImage="handleResetImage"
/>
</imagery-view-menu-switcher>
</div>
</template>
<script>
import _ from 'lodash';
import FilterSettings from "./FilterSettings.vue";
import LayerSettings from "./LayerSettings.vue";
import ZoomSettings from "./ZoomSettings.vue";
import ImageryViewMenuSwitcher from "./ImageryViewMenuSwitcher.vue";
const DEFAULT_FILTER_VALUES = {
brightness: '100',
contrast: '100'
@ -101,15 +88,27 @@ const ZOOM_STEP = 1;
const ZOOM_WHEEL_SENSITIVITY_REDUCTION = 0.01;
export default {
components: {
FilterSettings,
LayerSettings,
ImageryViewMenuSwitcher,
ZoomSettings
},
inject: ['openmct', 'domainObject'],
props: {
layers: {
type: Array,
required: true
},
zoomFactor: {
type: Number,
required: true
},
imageUrl: {
type: String,
default: ''
default: () => {
return '';
}
}
},
data() {
@ -126,9 +125,6 @@ export default {
};
},
computed: {
formattedZoomFactor() {
return Number.parseFloat(this.zoomFactor).toPrecision(2);
},
cursorStates() {
const isPannable = this.altPressed && this.zoomFactor > 1;
const showCursorZoomIn = this.metaPressed && !this.shiftPressed;
@ -270,6 +266,13 @@ export default {
const newScaleFactor = this.zoomFactor + (this.shiftPressed ? -ZOOM_STEP : ZOOM_STEP);
this.zoomImage(newScaleFactor, e.clientX, e.clientY);
},
toggleLayerVisibility(index) {
this.$emit('toggleLayerVisibility', index);
},
updateFilterValues(filters) {
this.filters = filters;
this.notifyFiltersChanged();
}
}
};

View File

@ -28,34 +28,34 @@
@keydown="arrowDownHandler"
@mouseover="focusElement"
>
<div class="c-imagery__main-image-wrapper has-local-controls">
<div
class="c-imagery__main-image-wrapper has-local-controls"
:class="imageWrapperStyle"
@mousedown="handlePanZoomClick"
>
<ImageControls
ref="imageControls"
:zoom-factor="zoomFactor"
:image-url="imageUrl"
:layers="layers"
@resetImage="resetImage"
@panZoomUpdated="handlePanZoomUpdate"
@filtersUpdated="setFilters"
@cursorsUpdated="setCursorStates"
@startPan="startPan"
@toggleLayerVisibility="toggleLayerVisibility"
/>
<div
ref="imageBG"
class="c-imagery__main-image__bg"
: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"
>{{ formatImageAltText }}</div>
>
{{ formatImageAltText }}
</div>
<div
ref="focusedImageWrapper"
class="image-wrapper"
@ -65,6 +65,13 @@
}"
@mousedown="handlePanZoomClick"
>
<div
v-for="(layer, index) in visibleLayers"
:key="index"
class="layer-image s-image-layer c-imagery__layer-image js-layer-image"
:style="getVisibleLayerStyles(layer)"
>
</div>
<img
ref="focusedImage"
class="c-imagery__main-image__image js-imageryView-image "
@ -81,25 +88,7 @@
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`,
}"
:style="focusImageStyles"
></div>
<Compass
v-if="shouldDisplayCompass"
@ -260,6 +249,9 @@ export default {
this.requestCount = 0;
return {
timeFormat: '',
layers: [],
visibleLayers: [],
durationFormatter: undefined,
imageHistory: [],
timeSystem: timeSystem,
@ -323,12 +315,41 @@ export default {
displayThumbnailsSmall() {
return this.viewHeight > SHOW_THUMBS_THRESHOLD_HEIGHT && this.viewHeight <= SHOW_THUMBS_FULLSIZE_THRESHOLD_HEIGHT;
},
focusImageStyles() {
return {
'filter': `brightness(${this.filters.brightness}%) contrast(${this.filters.contrast}%)`,
'background-image':
`${this.imageUrl ? (
`url(${this.imageUrl}),
repeating-linear-gradient(
45deg,
transparent,
transparent 4px,
rgba(125,125,125,.2) 4px,
rgba(125,125,125,.2) 8px
)`
) : ''}`,
'transform': `scale(${this.zoomFactor}) translate(${this.imageTranslateX}px, ${this.imageTranslateY}px)`,
'transition': `${!this.pan && this.animateZoom ? 'transform 250ms ease-in' : 'initial'}`,
'width': `${this.sizedImageWidth}px`,
'height': `${this.sizedImageHeight}px`
};
},
time() {
return this.formatTime(this.focusedImage);
},
imageUrl() {
return this.formatImageUrl(this.focusedImage);
},
imageWrapperStyle() {
return {
'cursor-zoom-in': this.cursorStates.showCursorZoomIn,
'cursor-zoom-out': this.cursorStates.showCursorZoomOut,
'pannable': this.cursorStates.isPannable,
'paused unnsynced': this.isPaused && !this.isFixed,
'stale': false
};
},
isImageNew() {
let cutoff = FIVE_MINUTES;
if (this.imageFreshnessOptions) {
@ -593,8 +614,10 @@ export default {
}
this.listenTo(this.focusedImageWrapper, 'wheel', this.wheelZoom, this);
this.loadVisibleLayers();
},
beforeDestroy() {
this.persistVisibleLayers();
this.stopFollowingTimeContext();
if (this.thumbWrapperResizeObserver) {
@ -625,6 +648,13 @@ export default {
calculateViewHeight() {
this.viewHeight = this.$el.clientHeight;
},
getVisibleLayerStyles(layer) {
return {
'background-image': `url(${layer.source})`,
'transform': `scale(${this.zoomFactor}) translate(${this.imageTranslateX}px, ${this.imageTranslateY}px)`,
'transition': `${!this.pan && this.animateZoom ? 'transform 250ms ease-in' : 'initial'}`
};
},
setTimeContext() {
this.stopFollowingTimeContext();
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
@ -693,6 +723,37 @@ export default {
return mostRecent[valueKey];
},
loadVisibleLayers() {
const metaDataValues = this.metadata.valuesForHints(['image'])[0];
this.imageFormat = this.openmct.telemetry.getValueFormatter(metaDataValues);
let layersMetadata = metaDataValues.layers;
if (layersMetadata) {
this.layers = layersMetadata;
if (this.domainObject.configuration) {
let persistedLayers = this.domainObject.configuration.layers;
layersMetadata.forEach((layer) => {
const persistedLayer = persistedLayers.find(object => object.name === layer.name);
if (persistedLayer) {
layer.visible = persistedLayer.visible === true;
}
});
this.visibleLayers = this.layers.filter(layer => layer.visible);
} else {
this.visibleLayers = [];
this.layers.forEach((layer) => {
layer.visible = false;
});
}
}
},
persistVisibleLayers() {
if (this.domainObject.configuration) {
this.openmct.objects.mutate(this.domainObject, 'configuration.layers', this.layers);
}
this.visibleLayers = [];
this.layers = [];
},
// will subscribe to data for this key if not already done
subscribeToDataForKey(key) {
if (this.relatedTelemetry[key].isSubscribed) {
@ -1030,7 +1091,6 @@ export default {
this.resizingWindow = false;
});
},
// debounced method
clearWheelZoom() {
this.$refs.imageControls.clearWheelZoom();
},
@ -1093,6 +1153,11 @@ export default {
},
setCursorStates(states) {
this.cursorStates = states;
},
toggleLayerVisibility(index) {
let isVisible = this.layers[index].visible === true;
this.layers[index].visible = !isVisible;
this.visibleLayers = this.layers.filter(layer => layer.visible);
}
}
};

View File

@ -0,0 +1,65 @@
<template>
<div class="c-switcher-menu">
<button
:id="id"
class="c-button c-button--menu c-switcher-menu__button"
:class="iconClass"
:title="title"
@click="toggleMenu"
>
<span class="c-button__label"></span>
</button>
<div
v-show="showMenu"
class="c-switcher-menu__content"
>
<slot></slot>
</div>
</div>
</template>
<script>
import {v4 as uuid} from 'uuid';
export default {
inject: ['openmct'],
props: {
iconClass: {
type: String,
default() {
return '';
}
},
title: {
type: String,
default() {
return '';
}
}
},
data() {
return {
id: uuid(),
showMenu: false
};
},
mounted() {
document.addEventListener('click', this.hideMenu);
},
destroyed() {
document.removeEventListener('click', this.hideMenu);
},
methods: {
toggleMenu() {
this.showMenu = !this.showMenu;
},
hideMenu(e) {
if (this.id === e.target.id) {
return;
}
this.showMenu = false;
}
}
};
</script>

View File

@ -0,0 +1,59 @@
<template>
<div
class="c-control-menu c-menu--to-left c-menu--has-close-btn c-image-controls"
@click="handleClose"
>
<div class="c-checkbox-list js-checkbox-menu c-menu--to-left c-menu--has-close-btn">
<ul
@click="$event.stopPropagation()"
>
<li
v-for="(layer, index) in layers"
:key="index"
>
<input
v-if="layer.visible"
:id="index + 'LayerControl'"
checked
type="checkbox"
@change="toggleLayerVisibility(index)"
>
<input
v-else
:id="index + 'LayerControl'"
type="checkbox"
@change="toggleLayerVisibility(index)"
>
<label :for="index + 'LayerControl'">{{ layer.name }}</label>
</li>
</ul>
</div>
<button class="c-click-icon icon-x t-btn-close c-switcher-menu__close-button"></button>
</div>
</template>
<script>
export default {
inject: ['openmct'],
props: {
layers: {
type: Array,
default() {
return [];
}
}
},
methods: {
handleClose(e) {
const closeButton = e.target.classList.contains('c-switcher-menu__close-button');
if (!closeButton) {
e.stopPropagation();
}
},
toggleLayerVisibility(index) {
this.$emit('toggleLayerVisibility', index);
}
}
};
</script>

View File

@ -0,0 +1,89 @@
<template>
<div
class="c-image-controls__controls-wrapper"
@click="handleClose"
>
<div class="c-image-controls__control c-image-controls__zoom">
<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>
<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>
</div>
<div class="c-image-controls__zoom-factor">x{{ formattedZoomFactor }}</div>
</div>
<button
v-if="isMenu"
class="c-click-icon icon-x t-btn-close c-switcher-menu__close-button"
></button>
</div>
</template>
<script>
export default {
inject: ['openmct'],
props: {
zoomFactor: {
type: Number,
required: true
},
panZoomLocked: {
type: Boolean,
required: true
},
isMenu: {
type: Boolean,
required: false
}
},
data() {
return {
};
},
computed: {
formattedZoomFactor() {
return Number.parseFloat(this.zoomFactor).toPrecision(2);
}
},
methods: {
handleClose(e) {
const closeButton = e.target.classList.contains('c-switcher-menu__close-button');
if (!closeButton) {
e.stopPropagation();
}
},
handleResetImage() {
this.$emit('handleResetImage');
},
toggleZoomLock() {
this.$emit('toggleZoomLock');
},
zoomIn() {
this.$emit('zoomIn');
},
zoomOut() {
this.$emit('zoomOut');
}
}
};
</script>

View File

@ -28,6 +28,27 @@
display: flex;
flex-direction: column;
flex: 1 1 auto;
&.unnsynced{
@include sUnsynced();
}
&.cursor-zoom-in {
cursor: zoom-in;
}
&.cursor-zoom-out {
cursor: zoom-out;
}
&.pannable {
@include cursorGrab();
}
}
.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);
}
.image-wrapper {
@ -45,19 +66,6 @@
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;
@ -77,6 +85,7 @@
background: rgba(black, 0.2);
border-radius: $smallCr;
padding: 2px $interiorMargin;
pointer-events: none;
position: absolute;
right: $m;
top: $m;
@ -146,6 +155,11 @@
}
&__layer-image {
pointer-events: none;
z-index: 1;
}
&__thumbs-wrapper {
display: flex; // Uses row layout
justify-content: flex-end;
@ -179,6 +193,50 @@
font-size: 0.8em;
margin: $interiorMarginSm;
}
.c-control-menu {
// Controls on left of flex column layout, close btn on right
@include menuOuter();
border-radius: $controlCr;
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: $interiorMargin;
width: min-content;
> * + * {
margin-left: $interiorMargin;
}
}
.c-switcher-menu {
display: contents;
&__content {
// Menu panel
top: 28px;
position: absolute;
.c-so-view & {
top: 25px;
}
}
}
}
.--width-less-than-220 .--show-if-less-than-220.c-switcher-menu {
display: contents !important;
}
.s-image-layer {
position: absolute;
height: 100%;
width: 100%;
opacity: 0.5;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}
/*************************************** THUMBS */
@ -229,70 +287,36 @@
/*************************************** IMAGERY LOCAL CONTROLS*/
.c-imagery {
.h-local-controls--overlay-content {
display: flex;
flex-direction: row;
position: absolute;
left: $interiorMargin; top: $interiorMargin;
z-index: 70;
background: $colorLocalControlOvrBg;
border-radius: $basicCr;
max-width: 250px;
min-width: 170px;
width: 35%;
align-items: center;
padding: $interiorMargin $interiorMarginLg;
input[type="range"] {
display: block;
width: 100%;
&:not(:first-child) {
margin-top: $interiorMarginLg;
}
&:before {
margin-right: $interiorMarginSm;
}
}
padding: $interiorMargin $interiorMargin;
.s-status-taking-snapshot & {
display: none;
}
}
&__lc {
&__reset-btn {
// Span that holds bracket graphics and button
$bc: $scrollbarTrackColorBg;
&:before,
&:after {
border-right: 1px solid $bc;
content:'';
display: block;
width: 5px;
height: 4px;
}
&:before {
border-top: 1px solid $bc;
margin-bottom: 2px;
}
&:after {
border-bottom: 1px solid $bc;
margin-top: 2px;
}
.c-icon-link {
color: $colorBtnFg;
}
[class*='--menus-aligned'] {
> * + * {
button { margin-left: $interiorMarginSm; }
}
}
}
.c-image-controls {
&__controls-wrapper {
// Wraps __controls and __close-btn
display: flex;
}
&__controls {
display: flex;
align-items: stretch;
flex-direction: column;
> * + * {
margin-top: $interiorMargin;
@ -314,31 +338,67 @@
}
&__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; }
> * + * { margin-left: $interiorMargin; } // Is this used?
}
&__sliders {
display: flex;
flex: 1 1 auto;
flex-direction: column;
&--filters {
// Styles specific to the brightness and contrast controls
> * + * {
margin-top: 11px;
.c-image-controls {
&__sliders {
display: flex;
flex: 1 1 auto;
flex-direction: column;
min-width: 80px;
> * + * {
margin-top: 11px;
}
input[type="range"] {
display: block;
width: 100%;
}
}
&__slider-wrapper {
display: flex;
align-items: center;
&:before { margin-right: $interiorMargin; }
}
&__reset-btn {
// Span that holds bracket graphics and button
$bc: $scrollbarTrackColorBg;
flex: 0 0 auto;
&:before,
&:after {
border-right: 1px solid $bc;
content:'';
display: block;
width: 5px;
height: 4px;
}
&:before {
border-top: 1px solid $bc;
margin-bottom: 2px;
}
&:after {
border-bottom: 1px solid $bc;
margin-top: 2px;
}
.c-icon-link {
color: $colorBtnFg;
}
}
}
}
&__btn-reset {
flex: 0 0 auto;
}
}
/*************************************** BUTTONS */
@ -383,7 +443,7 @@
@include cArrowButtonSizing($dimOuter: 48px);
border-radius: $controlCr;
.is-in-small-container & {
.--width-less-than-600 & {
@include cArrowButtonSizing($dimOuter: 32px);
}
}
@ -409,10 +469,6 @@
background-color: $colorBodyFg;
}
//[class*='__image-placeholder'] {
// display: none;
//}
img {
display: block !important;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -100,12 +100,24 @@ describe("The Imagery View Layouts", () => {
location: "parentId",
modified: 0,
persisted: 0,
configuration: {
layers: [{
name: '16:9',
visible: true
}]
},
telemetry: {
values: [
{
"name": "Image",
"key": "url",
"format": "image",
"layers": [
{
source: location.host + '/images/bg-splash.jpg',
name: '16:9'
}
],
"hints": {
"image": 1,
"priority": 3
@ -366,6 +378,18 @@ describe("The Imagery View Layouts", () => {
});
});
it("on mount should show the any image layers", (done) => {
//Looks like we need Vue.nextTick here so that computed properties settle down
Vue.nextTick().then(() => {
Vue.nextTick(() => {
const layerEls = parent.querySelectorAll('.js-layer-image');
console.log(layerEls);
expect(layerEls.length).toEqual(1);
done();
});
});
});
it("should show the clicked thumbnail as the main image", (done) => {
//Looks like we need Vue.nextTick here so that computed properties settle down
Vue.nextTick(() => {

View File

@ -63,8 +63,9 @@
padding-top: 0;
padding-bottom: 0;
}
.is-in-small-container & {
display: none;
.--width-less-than-600 & {
display: none !important;
}
}
}

View File

@ -42,6 +42,17 @@
}
}
@mixin menuPositioning() {
display: flex;
flex-direction: column;
position: absolute;
z-index: 100;
> * {
flex: 0 0 auto;
}
}
@mixin menuInner() {
li {
@include cControl();
@ -479,6 +490,10 @@ select {
&__row {
> * + * { margin-left: $interiorMargin; }
}
li {
white-space: nowrap;
}
}
/******************************************************** TABS */
@ -567,6 +582,7 @@ select {
/******************************************************** MENUS */
.c-menu {
@include menuOuter();
@include menuPositioning();
@include menuInner();
&__section-hint {
@ -590,6 +606,7 @@ select {
.c-super-menu {
// Two column layout, menu items on left with detail of hover element on right
@include menuOuter();
@include menuPositioning();
display: flex;
padding: $interiorMarginLg;
flex-direction: row;
@ -1035,6 +1052,14 @@ input[type="range"] {
display: inline-flex;
align-items: center;
}
[class*='--menus-aligned'] {
// Contains top level elements that hold dropdown menus
// Top level elements use display: contents to allow their menus to compactly align
// 03-18-22: used in ImageControls.vue
display: flex;
flex-direction: row;
}
}
.c-local-controls {

View File

@ -349,3 +349,22 @@ body.desktop .has-local-controls {
pointer-events: none !important;
cursor: default !important;
}
/******************************************************** RESPONSIVE CONTAINERS */
@mixin responsiveContainerWidths($dimension) {
// 3/21/22: `--width-less-than*` classes set in ObjectView.vue
.--show-if-less-than-#{$dimension} {
// Hide anything that displays within a given width by default.
// `display` property must be set within a more specific class
// for the particular item to be displayed.
display: none !important
}
.--width-less-than-#{$dimension} {
.--hide-if-less-than-#{$dimension} { display: none; }
}
}
//.--hide-by-default { display: none !important; }
@include responsiveContainerWidths('220');
@include responsiveContainerWidths('600');

View File

@ -118,7 +118,7 @@ mct-plot {
}
}
.is-in-small-container & {
.--width-less-than-600 & {
.c-control-bar {
display: none;
}
@ -498,7 +498,7 @@ mct-plot {
margin-bottom: $interiorMarginSm;
}
.is-in-small-container & {
.--width-less-than-600 & {
&.is-legend-hidden {
display: none;
}

View File

@ -90,7 +90,7 @@ div.c-table {
flex: 1 1 auto;
}
.is-in-small-container & {
.--width-less-than-600 & {
&:not(.is-paused) {
.c-table-control-bar {
display: none;

View File

@ -21,9 +21,11 @@
*****************************************************************************/
<template>
<div
ref="soView"
class="c-so-view js-notebook-snapshot-item-wrapper"
:class="[
statusClass,
widthClass,
'c-so-view--' + domainObject.type,
{
'c-so-view--no-frame': !hasFrame,
@ -111,6 +113,7 @@ const SIMPLE_CONTENT_TYPES = [
'hyperlink',
'conditionWidget'
];
const CSS_WIDTH_LESS_STR = '--width-less-than-';
export default {
components: {
@ -150,6 +153,7 @@ export default {
return {
cssClass,
widthClass: '',
complexContent,
notebookEnabled: this.openmct.types.get('notebook'),
statusBarItems: [],
@ -168,6 +172,11 @@ export default {
if (provider) {
this.$refs.objectView.show(this.domainObject, provider.key, false, this.objectPath);
}
if (this.$refs.soView) {
this.soViewResizeObserver = new ResizeObserver(this.resizeSoView);
this.soViewResizeObserver.observe(this.$refs.soView);
}
},
beforeDestroy() {
this.removeStatusListener();
@ -175,6 +184,10 @@ export default {
if (this.actionCollection) {
this.unlistenToActionCollection();
}
if (this.soViewResizeObserver) {
this.soViewResizeObserver.disconnect();
}
},
methods: {
getSelectionContext() {
@ -207,6 +220,18 @@ export default {
},
setStatus(status) {
this.status = status;
},
resizeSoView() {
let cW = this.$refs.soView.offsetWidth;
let wClass = '';
if (cW < 220) {
wClass = CSS_WIDTH_LESS_STR + '220';
} else if (cW < 600) {
wClass = CSS_WIDTH_LESS_STR + '600';
}
this.widthClass = wClass;
}
}
};

View File

@ -43,11 +43,11 @@
flex: 0 0 auto;
}
.is-in-small-container &,
.c-fl-frame & {
.--width-less-than-220 &,
.--width-less-than-600 & {
[class*="__label"] {
// button labels
display: none;
display: none !important;
}
}
@ -141,6 +141,10 @@
&.is-status--missing {
border: $borderMissing;
}
// Leave for debugging
//&.--width-less-than-600 { background: rgba(orange, 0.2) !important; }
//&.--width-less-than-220 { background: rgba(red, 0.2) !important; }
}
.l-angular-ov-wrapper {
@ -149,3 +153,5 @@
display: block;
height: 100%;
}

View File

@ -72,6 +72,10 @@ const config = {
transform: function (content) {
return content.toString().replace(/dist\//g, '');
}
},
{
from: 'src/plugins/imagery/layers',
to: 'imagery'
}
]
}),