diff --git a/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js b/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js
index 1311597f0a..41aea55c7c 100644
--- a/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js
+++ b/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js
@@ -25,13 +25,13 @@ This test suite is dedicated to tests which verify the basic operations surround
but only assume that example imagery is present.
*/
/* globals process */
-const { v4: uuid } = require('uuid');
const { waitForAnimations } = require('../../../../baseFixtures');
const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults } = require('../../../../appActions');
const backgroundImageSelector = '.c-imagery__main-image__background-image';
const panHotkey = process.platform === 'linux' ? ['Control', 'Alt'] : ['Alt'];
const expectedAltText = process.platform === 'linux' ? 'Ctrl+Alt drag to pan' : 'Alt drag to pan';
+const thumbnailUrlParamsRegexp = /\?w=100&h=100/;
//The following block of tests verifies the basic functionality of example imagery and serves as a template for Imagery objects embedded in other objects.
test.describe('Example Imagery Object', () => {
@@ -397,13 +397,11 @@ test.describe('Example Imagery in Time Strip', () => {
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
timeStripObject = await createDomainObjectWithDefaults(page, {
- type: 'Time Strip',
- name: 'Time Strip'.concat(' ', uuid())
+ type: 'Time Strip'
});
await createDomainObjectWithDefaults(page, {
type: 'Example Imagery',
- name: 'Example Imagery'.concat(' ', uuid()),
parent: timeStripObject.uuid
});
// Navigate to timestrip
@@ -414,17 +412,28 @@ test.describe('Example Imagery in Time Strip', () => {
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5632'
});
+
+ // Hover over the timestrip to reveal a thumbnail image
await page.locator('.c-imagery-tsv-container').hover();
- // get url of the hovered image
- const hoveredImg = page.locator('.c-imagery-tsv div.c-imagery-tsv__image-wrapper:hover img');
- const hoveredImgSrc = await hoveredImg.getAttribute('src');
- expect(hoveredImgSrc).toBeTruthy();
+
+ // Get the img src of the hovered image thumbnail
+ const hoveredThumbnailImg = page.locator('.c-imagery-tsv div.c-imagery-tsv__image-wrapper:hover img');
+ const hoveredThumbnailImgSrc = await hoveredThumbnailImg.getAttribute('src');
+
+ // Verify that imagery timestrip view uses the thumbnailUrl as img src for thumbnails
+ expect(hoveredThumbnailImgSrc).toBeTruthy();
+ expect(hoveredThumbnailImgSrc).toMatch(thumbnailUrlParamsRegexp);
+
+ // Click on the hovered thumbnail to open "View Large" view
await page.locator('.c-imagery-tsv-container').click();
- // get image of view large container
+
+ // Get the img src of the large view image
const viewLargeImg = page.locator('img.c-imagery__main-image__image');
const viewLargeImgSrc = await viewLargeImg.getAttribute('src');
expect(viewLargeImgSrc).toBeTruthy();
- expect(viewLargeImgSrc).toEqual(hoveredImgSrc);
+
+ // Verify that the image in the large view is the same as the hovered thumbnail
+ expect(viewLargeImgSrc).toEqual(hoveredThumbnailImgSrc.split('?')[0]);
});
});
@@ -441,6 +450,12 @@ test.describe('Example Imagery in Time Strip', () => {
* @param {import('@playwright/test').Page} page
*/
async function performImageryViewOperationsAndAssert(page) {
+ // Verify that imagery thumbnails use a thumbnail url
+ const thumbnailImages = page.locator('.c-thumb__image');
+ const mainImage = page.locator('.c-imagery__main-image__image');
+ await expect(thumbnailImages.first()).toHaveAttribute('src', thumbnailUrlParamsRegexp);
+ await expect(mainImage).not.toHaveAttribute('src', thumbnailUrlParamsRegexp);
+
// Click previous image button
const previousImageButton = page.locator('.c-nav--prev');
await previousImageButton.click();
diff --git a/example/imagery/plugin.js b/example/imagery/plugin.js
index e193c99afd..2eabb32361 100644
--- a/example/imagery/plugin.js
+++ b/example/imagery/plugin.js
@@ -107,6 +107,15 @@ export default function () {
}
]
},
+ {
+ name: 'Image Thumbnail',
+ key: 'thumbnail-url',
+ format: 'thumbnail',
+ hints: {
+ thumbnail: 1
+ },
+ source: 'url'
+ },
{
name: 'Image Download Name',
key: 'imageDownloadName',
@@ -143,6 +152,16 @@ export default function () {
]
});
+ const formatThumbnail = {
+ format: function (url) {
+ return `${url}?w=100&h=100`;
+ }
+ };
+
+ openmct.telemetry.addFormat({
+ key: 'thumbnail',
+ ...formatThumbnail
+ });
openmct.telemetry.addProvider(getRealtimeProvider());
openmct.telemetry.addProvider(getHistoricalProvider());
openmct.telemetry.addProvider(getLadProvider());
diff --git a/src/plugins/imagery/components/ImageThumbnail.vue b/src/plugins/imagery/components/ImageThumbnail.vue
index 99ef0febc7..1748c32bb4 100644
--- a/src/plugins/imagery/components/ImageThumbnail.vue
+++ b/src/plugins/imagery/components/ImageThumbnail.vue
@@ -39,7 +39,7 @@
diff --git a/src/plugins/imagery/components/ImageryTimeView.vue b/src/plugins/imagery/components/ImageryTimeView.vue
index ccaf7b71ba..5adfd2d476 100644
--- a/src/plugins/imagery/components/ImageryTimeView.vue
+++ b/src/plugins/imagery/components/ImageryTimeView.vue
@@ -186,17 +186,17 @@ export default {
item.remove();
});
let imagery = this.$el.querySelectorAll(`.${IMAGE_WRAPPER_CLASS}`);
- imagery.forEach(item => {
+ imagery.forEach(imageElm => {
if (clearAllImagery) {
- item.remove();
+ imageElm.remove();
} else {
- const id = item.getAttributeNS(null, 'id');
+ const id = imageElm.getAttributeNS(null, 'id');
if (id) {
const timestamp = id.replace(ID_PREFIX, '');
if (!this.isImageryInBounds({
time: timestamp
})) {
- item.remove();
+ imageElm.remove();
}
}
}
@@ -343,25 +343,25 @@ export default {
imageElement.style.display = 'block';
}
},
- updateExistingImageWrapper(existingImageWrapper, item, showImagePlaceholders) {
+ updateExistingImageWrapper(existingImageWrapper, image, showImagePlaceholders) {
//Update the x co-ordinates of the image wrapper and the url of image
//this is to avoid tearing down all elements completely and re-drawing them
this.setNSAttributesForElement(existingImageWrapper, {
'data-show-image-placeholders': showImagePlaceholders
});
- existingImageWrapper.style.left = `${this.xScale(item.time)}px`;
+ existingImageWrapper.style.left = `${this.xScale(image.time)}px`;
let imageElement = existingImageWrapper.querySelector('img');
this.setNSAttributesForElement(imageElement, {
- src: item.url
+ src: image.thumbnailUrl || image.url
});
this.setImageDisplay(imageElement, showImagePlaceholders);
},
- createImageWrapper(index, item, showImagePlaceholders) {
- const id = `${ID_PREFIX}${item.time}`;
+ createImageWrapper(index, image, showImagePlaceholders) {
+ const id = `${ID_PREFIX}${image.time}`;
let imageWrapper = document.createElement('div');
imageWrapper.classList.add(IMAGE_WRAPPER_CLASS);
- imageWrapper.style.left = `${this.xScale(item.time)}px`;
+ imageWrapper.style.left = `${this.xScale(image.time)}px`;
this.setNSAttributesForElement(imageWrapper, {
id,
'data-show-image-placeholders': showImagePlaceholders
@@ -383,7 +383,7 @@ export default {
//create image element
let imageElement = document.createElement('img');
this.setNSAttributesForElement(imageElement, {
- src: item.url
+ src: image.thumbnailUrl || image.url
});
imageElement.style.width = `${IMAGE_SIZE}px`;
imageElement.style.height = `${IMAGE_SIZE}px`;
@@ -392,7 +392,7 @@ export default {
//handle mousedown event to show the image in a large view
imageWrapper.addEventListener('mousedown', (e) => {
if (e.button === 0) {
- this.expand(item.time);
+ this.expand(image.time);
}
});
diff --git a/src/plugins/imagery/components/ImageryView.vue b/src/plugins/imagery/components/ImageryView.vue
index 5b18cd29d7..20446109aa 100644
--- a/src/plugins/imagery/components/ImageryView.vue
+++ b/src/plugins/imagery/components/ImageryView.vue
@@ -171,7 +171,7 @@
>
{
- 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;
- });
- }
+ const layersMetadata = this.imageMetadataValue.layers;
+ if (!layersMetadata) {
+ return;
+ }
+
+ this.layers = layersMetadata;
+ if (this.domainObject.configuration) {
+ const 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() {
diff --git a/src/plugins/imagery/components/imagery-view.scss b/src/plugins/imagery/components/imagery-view.scss
index ee0713244b..22e38d9040 100644
--- a/src/plugins/imagery/components/imagery-view.scss
+++ b/src/plugins/imagery/components/imagery-view.scss
@@ -29,7 +29,7 @@
flex-direction: column;
flex: 1 1 auto;
- &.unnsynced{
+ &.unsynced{
@include sUnsynced();
}
diff --git a/src/plugins/imagery/mixins/imageryData.js b/src/plugins/imagery/mixins/imageryData.js
index 94118e6813..0e51b6fd35 100644
--- a/src/plugins/imagery/mixins/imageryData.js
+++ b/src/plugins/imagery/mixins/imageryData.js
@@ -21,6 +21,9 @@
*****************************************************************************/
const DEFAULT_DURATION_FORMATTER = 'duration';
+const IMAGE_HINT_KEY = 'image';
+const IMAGE_THUMBNAIL_HINT_KEY = 'thumbnail';
+const IMAGE_DOWNLOAD_NAME_HINT_KEY = 'imageDownloadName';
export default {
inject: ['openmct', 'domainObject', 'objectPath'],
@@ -32,13 +35,20 @@ export default {
this.setDataTimeContext();
this.openmct.objectViews.on('clearData', this.dataCleared);
- // set
+ // Get metadata and formatters
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
- this.imageHints = { ...this.metadata.valuesForHints(['image'])[0] };
+
+ this.imageMetadataValue = { ...this.metadata.valuesForHints([IMAGE_HINT_KEY])[0] };
+ this.imageFormatter = this.getFormatter(this.imageMetadataValue.key);
+
+ this.imageThumbnailMetadataValue = { ...this.metadata.valuesForHints([IMAGE_THUMBNAIL_HINT_KEY])[0] };
+ this.imageThumbnailFormatter = this.imageThumbnailMetadataValue.key
+ ? this.getFormatter(this.imageThumbnailMetadataValue.key)
+ : null;
+
this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
- this.imageFormatter = this.openmct.telemetry.getValueFormatter(this.imageHints);
- this.imageDownloadNameHints = { ...this.metadata.valuesForHints(['imageDownloadName'])[0]};
+ this.imageDownloadNameMetadataValue = { ...this.metadata.valuesForHints([IMAGE_DOWNLOAD_NAME_HINT_KEY])[0]};
// initialize
this.timeKey = this.timeSystem.key;
@@ -105,12 +115,19 @@ export default {
return this.imageFormatter.format(datum);
},
+ formatImageThumbnailUrl(datum) {
+ if (!datum || !this.imageThumbnailFormatter) {
+ return;
+ }
+
+ return this.imageThumbnailFormatter.format(datum);
+ },
formatTime(datum) {
if (!datum) {
return;
}
- let dateTimeStr = this.timeFormatter.format(datum);
+ const dateTimeStr = this.timeFormatter.format(datum);
// Replace ISO "T" with a space to allow wrapping
return dateTimeStr.replace("T", " ");
@@ -118,7 +135,7 @@ export default {
getImageDownloadName(datum) {
let imageDownloadName = '';
if (datum) {
- const key = this.imageDownloadNameHints.key;
+ const key = this.imageDownloadNameMetadataValue.key;
imageDownloadName = datum[key];
}
@@ -150,6 +167,7 @@ export default {
normalizeDatum(datum) {
const formattedTime = this.formatTime(datum);
const url = this.formatImageUrl(datum);
+ const thumbnailUrl = this.formatImageThumbnailUrl(datum);
const time = this.parseTime(formattedTime);
const imageDownloadName = this.getImageDownloadName(datum);
@@ -157,13 +175,14 @@ export default {
...datum,
formattedTime,
url,
+ thumbnailUrl,
time,
imageDownloadName
};
},
getFormatter(key) {
- let metadataValue = this.metadata.value(key) || { format: key };
- let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);
+ const metadataValue = this.metadata.value(key) || { format: key };
+ const valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);
return valueFormatter;
}
diff --git a/src/plugins/imagery/pluginSpec.js b/src/plugins/imagery/pluginSpec.js
index 15a81b09e3..226de27136 100644
--- a/src/plugins/imagery/pluginSpec.js
+++ b/src/plugins/imagery/pluginSpec.js
@@ -35,6 +35,10 @@ const MAIN_IMAGE_CLASS = '.js-imageryView-image';
const NEW_IMAGE_CLASS = '.c-imagery__age.c-imagery--new';
const REFRESH_CSS_MS = 500;
+function formatThumbnail(url) {
+ return url.replace('logo-openmct.svg', 'logo-nasa.svg');
+}
+
function getImageInfo(doc) {
let imageElement = doc.querySelectorAll(MAIN_IMAGE_CLASS)[0];
let timestamp = imageElement.dataset.openmctImageTimestamp;
@@ -124,6 +128,16 @@ describe("The Imagery View Layouts", () => {
},
"source": "url"
},
+ {
+ "name": "Image Thumbnail",
+ "key": "thumbnail-url",
+ "format": "thumbnail",
+ "hints": {
+ "thumbnail": 1,
+ "priority": 3
+ },
+ "source": "url"
+ },
{
"name": "Name",
"key": "name",
@@ -200,6 +214,11 @@ describe("The Imagery View Layouts", () => {
originalRouterPath = openmct.router.path;
+ openmct.telemetry.addFormat({
+ key: 'thumbnail',
+ format: formatThumbnail
+ });
+
openmct.on('start', done);
openmct.startHeadless();
});
@@ -384,15 +403,32 @@ describe("The Imagery View Layouts", () => {
//Looks like we need Vue.nextTick here so that computed properties settle down
await Vue.nextTick();
const layerEls = parent.querySelectorAll('.js-layer-image');
- console.log(layerEls);
expect(layerEls.length).toEqual(1);
});
+ it("should use the image thumbnailUrl for thumbnails", async () => {
+ await Vue.nextTick();
+ const fullSizeImageUrl = imageTelemetry[5].url;
+ const thumbnailUrl = formatThumbnail(imageTelemetry[5].url);
+
+ // Ensure thumbnails are shown w/ thumbnail Urls
+ const thumbnails = parent.querySelectorAll(`img[src='${thumbnailUrl}']`);
+ expect(thumbnails.length).toBeGreaterThan(0);
+
+ // Click a thumbnail
+ parent.querySelectorAll(`img[src='${thumbnailUrl}']`)[0].click();
+ await Vue.nextTick();
+
+ // Ensure full size image is shown w/ full size url
+ const fullSizeImages = parent.querySelectorAll(`img[src='${fullSizeImageUrl}']`);
+ expect(fullSizeImages.length).toBeGreaterThan(0);
+ });
+
it("should show the clicked thumbnail as the main image", async () => {
//Looks like we need Vue.nextTick here so that computed properties settle down
await Vue.nextTick();
- const target = imageTelemetry[5].url;
- parent.querySelectorAll(`img[src='${target}']`)[0].click();
+ const thumbnailUrl = formatThumbnail(imageTelemetry[5].url);
+ parent.querySelectorAll(`img[src='${thumbnailUrl}']`)[0].click();
await Vue.nextTick();
const imageInfo = getImageInfo(parent);
@@ -417,7 +453,7 @@ describe("The Imagery View Layouts", () => {
it("should show that an image is not new", async () => {
await Vue.nextTick();
- const target = imageTelemetry[4].url;
+ const target = formatThumbnail(imageTelemetry[4].url);
parent.querySelectorAll(`img[src='${target}']`)[0].click();
await Vue.nextTick();