Compare commits
27 Commits
progress-i
...
yield-requ
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15926cc9a2 | ||
|
|
dad9f12a5c | ||
|
|
aa5edb0b83 | ||
|
|
d647e287fd | ||
|
|
b315803180 | ||
|
|
7998ee3f61 | ||
|
|
e7d94d9447 | ||
|
|
336babdb15 | ||
|
|
0b4624fc8f | ||
|
|
62c54e732d | ||
|
|
e63393b0c4 | ||
|
|
02c97ae10b | ||
|
|
5b499368ae | ||
|
|
dd556c3335 | ||
|
|
1a71708b63 | ||
|
|
ab425ed04e | ||
|
|
514181c2fd | ||
|
|
757ff1c85a | ||
|
|
e7f21654e0 | ||
|
|
c895869cbf | ||
|
|
5d40a55a20 | ||
|
|
54477f2bb9 | ||
|
|
eb894894b5 | ||
|
|
3f06bc5c48 | ||
|
|
c3238e50ee | ||
|
|
d83e919541 | ||
|
|
5fcf0bd7de |
@@ -115,6 +115,7 @@ export class TelemetryCollection extends EventEmitter {
|
||||
|
||||
this._requestHistoricalTelemetry();
|
||||
}
|
||||
|
||||
/**
|
||||
* If a historical provider exists, then historical requests will be made
|
||||
* @private
|
||||
@@ -126,20 +127,25 @@ export class TelemetryCollection extends EventEmitter {
|
||||
|
||||
let historicalData;
|
||||
|
||||
this.options.onPartialResponse = this._processNewTelemetry.bind(this);
|
||||
|
||||
try {
|
||||
this.requestAbort = new AbortController();
|
||||
this.options.signal = this.requestAbort.signal;
|
||||
historicalData = await this.historicalProvider.request(this.domainObject, this.options);
|
||||
this.requestAbort = undefined;
|
||||
} catch (error) {
|
||||
console.error('Error requesting telemetry data...');
|
||||
this.requestAbort = undefined;
|
||||
this._error(error);
|
||||
if (error.name !== 'AbortError') {
|
||||
console.error('Error requesting telemetry data...');
|
||||
this._error(error);
|
||||
}
|
||||
}
|
||||
|
||||
this.requestAbort = undefined;
|
||||
|
||||
this._processNewTelemetry(historicalData);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* This uses the built in subscription function from Telemetry API
|
||||
* @private
|
||||
|
||||
73
src/plugins/imagery/ImageryTimestripViewProvider.js
Normal file
73
src/plugins/imagery/ImageryTimestripViewProvider.js
Normal file
@@ -0,0 +1,73 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
import ImageryTimeView from './components/ImageryTimeView.vue';
|
||||
import Vue from "vue";
|
||||
|
||||
export default function ImageryTimestripViewProvider(openmct) {
|
||||
const type = 'example.imagery.time-strip.view';
|
||||
|
||||
function hasImageTelemetry(domainObject) {
|
||||
const metadata = openmct.telemetry.getMetadata(domainObject);
|
||||
if (!metadata) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return metadata.valuesForHints(['image']).length > 0;
|
||||
}
|
||||
|
||||
return {
|
||||
key: type,
|
||||
name: 'Imagery Timestrip View',
|
||||
cssClass: 'icon-image',
|
||||
canView: function (domainObject, objectPath) {
|
||||
let isChildOfTimeStrip = objectPath.find(object => object.type === 'time-strip');
|
||||
|
||||
return hasImageTelemetry(domainObject) && isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath);
|
||||
},
|
||||
view: function (domainObject, objectPath) {
|
||||
let component;
|
||||
|
||||
return {
|
||||
show: function (element) {
|
||||
component = new Vue({
|
||||
el: element,
|
||||
components: {
|
||||
ImageryTimeView
|
||||
},
|
||||
provide: {
|
||||
openmct: openmct,
|
||||
domainObject: domainObject,
|
||||
objectPath: objectPath
|
||||
},
|
||||
template: '<imagery-time-view></imagery-time-view>'
|
||||
|
||||
});
|
||||
},
|
||||
|
||||
destroy: function () {
|
||||
component.$destroy();
|
||||
component = undefined;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import ImageryViewLayout from './components/ImageryViewLayout.vue';
|
||||
import ImageryViewComponent from './components/ImageryView.vue';
|
||||
|
||||
import Vue from 'vue';
|
||||
|
||||
@@ -14,7 +14,7 @@ export default class ImageryView {
|
||||
this.component = new Vue({
|
||||
el: element,
|
||||
components: {
|
||||
ImageryViewLayout
|
||||
'imagery-view': ImageryViewComponent
|
||||
},
|
||||
provide: {
|
||||
openmct: this.openmct,
|
||||
@@ -22,7 +22,8 @@ export default class ImageryView {
|
||||
objectPath: this.objectPath,
|
||||
currentView: this
|
||||
},
|
||||
template: '<imagery-view-layout ref="ImageryLayout"></imagery-view-layout>'
|
||||
template: '<imagery-view ref="ImageryContainer"></imagery-view>'
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -37,8 +37,10 @@ export default function ImageryViewProvider(openmct) {
|
||||
key: type,
|
||||
name: 'Imagery Layout',
|
||||
cssClass: 'icon-image',
|
||||
canView: function (domainObject) {
|
||||
return hasImageTelemetry(domainObject);
|
||||
canView: function (domainObject, objectPath) {
|
||||
let isChildOfTimeStrip = objectPath.find(object => object.type === 'time-strip');
|
||||
|
||||
return hasImageTelemetry(domainObject) && (!isChildOfTimeStrip || openmct.router.isNavigatedObject(objectPath));
|
||||
},
|
||||
view: function (domainObject, objectPath) {
|
||||
return new ImageryView(openmct, domainObject, objectPath);
|
||||
|
||||
476
src/plugins/imagery/components/ImageryTimeView.vue
Normal file
476
src/plugins/imagery/components/ImageryTimeView.vue
Normal file
@@ -0,0 +1,476 @@
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div ref="imagery"
|
||||
class="c-imagery-tsv c-timeline-holder"
|
||||
>
|
||||
<div ref="imageryHolder"
|
||||
class="c-imagery-tsv__contents u-contents"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as d3Scale from 'd3-scale';
|
||||
import SwimLane from "@/ui/components/swim-lane/SwimLane.vue";
|
||||
import Vue from "vue";
|
||||
import imageryData from "../../imagery/mixins/imageryData";
|
||||
import PreviewAction from "@/ui/preview/PreviewAction";
|
||||
import _ from "lodash";
|
||||
|
||||
const PADDING = 1;
|
||||
const RESIZE_POLL_INTERVAL = 200;
|
||||
const ROW_HEIGHT = 100;
|
||||
const IMAGE_WIDTH_THRESHOLD = 40;
|
||||
|
||||
export default {
|
||||
mixins: [imageryData],
|
||||
inject: ['openmct', 'domainObject', 'objectPath'],
|
||||
data() {
|
||||
let timeSystem = this.openmct.time.timeSystem();
|
||||
this.metadata = {};
|
||||
this.requestCount = 0;
|
||||
|
||||
return {
|
||||
viewBounds: undefined,
|
||||
height: 0,
|
||||
durationFormatter: undefined,
|
||||
imageHistory: [],
|
||||
timeSystem: timeSystem,
|
||||
keyString: undefined
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
imageHistorySize() {
|
||||
return this.imageHistory.length;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
imageHistorySize(newSize, oldSize) {
|
||||
this.updatePlotImagery();
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.previewAction = new PreviewAction(this.openmct);
|
||||
|
||||
this.canvas = this.$refs.imagery.appendChild(document.createElement('canvas'));
|
||||
this.canvas.height = 0;
|
||||
this.canvasContext = this.canvas.getContext('2d');
|
||||
this.setDimensions();
|
||||
|
||||
this.updateViewBounds();
|
||||
|
||||
this.openmct.time.on("timeSystem", this.setScaleAndPlotImagery);
|
||||
this.openmct.time.on("bounds", this.updateViewBounds);
|
||||
|
||||
this.resize = _.debounce(this.resize, 400);
|
||||
this.imageryStripResizeObserver = new ResizeObserver(this.resize);
|
||||
this.imageryStripResizeObserver.observe(this.$refs.imagery);
|
||||
|
||||
this.unlisten = this.openmct.objects.observe(this.domainObject, '*', this.observeForChanges);
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.unsubscribe) {
|
||||
this.unsubscribe();
|
||||
delete this.unsubscribe;
|
||||
}
|
||||
|
||||
if (this.imageryStripResizeObserver) {
|
||||
this.imageryStripResizeObserver.disconnect();
|
||||
}
|
||||
|
||||
this.openmct.time.off("timeSystem", this.setScaleAndPlotImagery);
|
||||
this.openmct.time.off("bounds", this.updateViewBounds);
|
||||
if (this.unlisten) {
|
||||
this.unlisten();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
expand(index) {
|
||||
const path = this.objectPath[0];
|
||||
this.previewAction.invoke([path]);
|
||||
},
|
||||
observeForChanges(mutatedObject) {
|
||||
this.updateViewBounds();
|
||||
},
|
||||
resize() {
|
||||
let clientWidth = this.getClientWidth();
|
||||
if (clientWidth !== this.width) {
|
||||
this.setDimensions();
|
||||
this.updateViewBounds();
|
||||
}
|
||||
},
|
||||
getClientWidth() {
|
||||
let clientWidth = this.$refs.imagery.clientWidth;
|
||||
|
||||
if (!clientWidth) {
|
||||
//this is a hack - need a better way to find the parent of this component
|
||||
let parent = this.openmct.layout.$refs.browseObject.$el;
|
||||
if (parent) {
|
||||
clientWidth = parent.getBoundingClientRect().width;
|
||||
}
|
||||
}
|
||||
|
||||
return clientWidth;
|
||||
},
|
||||
updateViewBounds(bounds, isTick) {
|
||||
this.viewBounds = this.openmct.time.bounds();
|
||||
//Add a 50% padding to the end bounds to look ahead
|
||||
let timespan = (this.viewBounds.end - this.viewBounds.start);
|
||||
let padding = timespan / 2;
|
||||
this.viewBounds.end = this.viewBounds.end + padding;
|
||||
|
||||
if (this.timeSystem === undefined) {
|
||||
this.timeSystem = this.openmct.time.timeSystem();
|
||||
}
|
||||
|
||||
this.setScaleAndPlotImagery(this.timeSystem, !isTick);
|
||||
|
||||
},
|
||||
setScaleAndPlotImagery(timeSystem, clearAllImagery) {
|
||||
if (timeSystem !== undefined) {
|
||||
this.timeSystem = timeSystem;
|
||||
this.timeFormatter = this.getFormatter(this.timeSystem.key);
|
||||
}
|
||||
|
||||
this.setScale(this.timeSystem);
|
||||
this.updatePlotImagery(clearAllImagery);
|
||||
},
|
||||
getFormatter(key) {
|
||||
const metadata = this.openmct.telemetry.getMetadata(this.domainObject);
|
||||
|
||||
let metadataValue = metadata.value(key) || { format: key };
|
||||
let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);
|
||||
|
||||
return valueFormatter;
|
||||
},
|
||||
updatePlotImagery(clearAllImagery) {
|
||||
this.clearPreviousImagery(clearAllImagery);
|
||||
if (this.xScale) {
|
||||
this.drawImagery();
|
||||
}
|
||||
},
|
||||
clearPreviousImagery(clearAllImagery) {
|
||||
//TODO: Only clear items that are out of bounds
|
||||
let noItemsEl = this.$el.querySelectorAll(".c-imagery-tsv__no-items");
|
||||
noItemsEl.forEach(item => {
|
||||
item.remove();
|
||||
});
|
||||
let imagery = this.$el.querySelectorAll(".c-imagery-tsv__image-wrapper");
|
||||
imagery.forEach(item => {
|
||||
if (clearAllImagery) {
|
||||
item.remove();
|
||||
} else {
|
||||
const id = this.getNSAttributesForElement(item, 'id');
|
||||
if (id) {
|
||||
const timestamp = id.replace('id-', '');
|
||||
if (!this.isImageryInBounds({
|
||||
time: timestamp
|
||||
})) {
|
||||
item.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
setDimensions() {
|
||||
const imageryHolder = this.$refs.imagery;
|
||||
this.width = this.getClientWidth();
|
||||
|
||||
this.height = Math.round(imageryHolder.getBoundingClientRect().height);
|
||||
},
|
||||
setScale(timeSystem) {
|
||||
if (!this.width) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (timeSystem === undefined) {
|
||||
timeSystem = this.openmct.time.timeSystem();
|
||||
}
|
||||
|
||||
if (timeSystem.isUTCBased) {
|
||||
this.xScale = d3Scale.scaleUtc();
|
||||
this.xScale.domain(
|
||||
[new Date(this.viewBounds.start), new Date(this.viewBounds.end)]
|
||||
);
|
||||
} else {
|
||||
this.xScale = d3Scale.scaleLinear();
|
||||
this.xScale.domain(
|
||||
[this.viewBounds.start, this.viewBounds.end]
|
||||
);
|
||||
}
|
||||
|
||||
this.xScale.range([PADDING, this.width - PADDING * 2]);
|
||||
},
|
||||
isImageryInBounds(imageObj) {
|
||||
return (imageObj.time < this.viewBounds.end) && (imageObj.time > this.viewBounds.start);
|
||||
},
|
||||
getImageryContainer() {
|
||||
let svgHeight = 100;
|
||||
let svgWidth = this.imageHistory.length ? this.width : 200;
|
||||
let groupSVG;
|
||||
|
||||
let existingSVG = this.$el.querySelector(".c-imagery-tsv__contents svg");
|
||||
if (existingSVG) {
|
||||
groupSVG = existingSVG;
|
||||
this.setNSAttributesForElement(groupSVG, {
|
||||
width: svgWidth
|
||||
});
|
||||
} else {
|
||||
let component = new Vue({
|
||||
components: {
|
||||
SwimLane
|
||||
},
|
||||
provide: {
|
||||
openmct: this.openmct
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isNested: true,
|
||||
height: svgHeight,
|
||||
width: svgWidth
|
||||
};
|
||||
},
|
||||
template: `<swim-lane :is-nested="isNested" :hide-label="true"><template slot="object"><svg class="c-imagery-tsv-container" :height="height" :width="width"></svg></template></swim-lane>`
|
||||
});
|
||||
|
||||
this.$refs.imageryHolder.appendChild(component.$mount().$el);
|
||||
|
||||
groupSVG = component.$el.querySelector('svg');
|
||||
|
||||
groupSVG.addEventListener('mouseout', (event) => {
|
||||
if (event.target.nodeName === 'svg' || event.target.nodeName === 'use') {
|
||||
this.removeFromForeground();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return groupSVG;
|
||||
},
|
||||
isImageryWidthAcceptable() {
|
||||
// We're calculating if there is enough space between images to show the thumbnails.
|
||||
// This algorithm could probably be enhanced to check the x co-ordinate distance between 2 consecutive images, but
|
||||
// we will go with this for now assuming imagery is not sorted by asc time so it's difficult to calculate.
|
||||
// TODO: Use telemetry.requestCollection to get sorted telemetry
|
||||
const currentStart = this.viewBounds.start;
|
||||
const currentEnd = this.viewBounds.end;
|
||||
const rectX = this.xScale(currentStart);
|
||||
const rectY = this.xScale(currentEnd);
|
||||
const imageContainerWidth = this.imageHistory.length ? (rectY - rectX) / this.imageHistory.length : 0;
|
||||
|
||||
return imageContainerWidth < IMAGE_WIDTH_THRESHOLD;
|
||||
},
|
||||
drawImagery() {
|
||||
let groupSVG = this.getImageryContainer();
|
||||
const showImagePlaceholders = this.isImageryWidthAcceptable();
|
||||
|
||||
if (this.imageHistory.length) {
|
||||
this.imageHistory.forEach((currentImageObject, index) => {
|
||||
if (this.isImageryInBounds(currentImageObject)) {
|
||||
this.plotImagery(currentImageObject, showImagePlaceholders, groupSVG, index);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.plotNoItems(groupSVG);
|
||||
}
|
||||
},
|
||||
plotNoItems(svgElement) {
|
||||
let textElement = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
||||
this.setNSAttributesForElement(textElement, {
|
||||
x: "10",
|
||||
y: "20",
|
||||
class: "c-imagery-tsv__no-items"
|
||||
});
|
||||
textElement.innerHTML = 'No images within timeframe';
|
||||
|
||||
svgElement.appendChild(textElement);
|
||||
},
|
||||
setNSAttributesForElement(element, attributes) {
|
||||
Object.keys(attributes).forEach((key) => {
|
||||
if (key === 'url') {
|
||||
element.setAttributeNS('http://www.w3.org/1999/xlink', 'href', attributes[key]);
|
||||
} else {
|
||||
element.setAttributeNS(null, key, attributes[key]);
|
||||
}
|
||||
});
|
||||
},
|
||||
getNSAttributesForElement(element, attribute) {
|
||||
return element.getAttributeNS(null, attribute);
|
||||
},
|
||||
getImageWrapper(item) {
|
||||
const id = `id-${item.time}`;
|
||||
|
||||
return this.$el.querySelector(`.c-imagery-tsv__contents g[id=${id}]`);
|
||||
},
|
||||
plotImagery(item, showImagePlaceholders, svgElement, index) {
|
||||
//TODO: Placeholder image
|
||||
let existingImageWrapper = this.getImageWrapper(item);
|
||||
//imageWrapper wraps the vertical tick rect and the image
|
||||
if (existingImageWrapper) {
|
||||
this.updateExistingImageWrapper(existingImageWrapper, item, showImagePlaceholders);
|
||||
} else {
|
||||
let imageWrapper = this.createImageWrapper(index, item, showImagePlaceholders, svgElement);
|
||||
svgElement.appendChild(imageWrapper);
|
||||
}
|
||||
},
|
||||
updateExistingImageWrapper(existingImageWrapper, item, showImagePlaceholders) {
|
||||
//Update the x co-ordinates of the handle and image elements and the url of image
|
||||
//this is to avoid tearing down all elements completely and re-drawing them
|
||||
this.setNSAttributesForElement(existingImageWrapper, {
|
||||
showImagePlaceholders
|
||||
});
|
||||
let imageTickElement = existingImageWrapper.querySelector('rect.c-imagery-tsv__image-handle');
|
||||
this.setNSAttributesForElement(imageTickElement, {
|
||||
x: this.xScale(item.time)
|
||||
});
|
||||
|
||||
let imageRect = existingImageWrapper.querySelector('rect.c-imagery-tsv__image-placeholder');
|
||||
this.setNSAttributesForElement(imageRect, {
|
||||
x: this.xScale(item.time) + 2
|
||||
});
|
||||
|
||||
let imageElement = existingImageWrapper.querySelector('image');
|
||||
const selector = `href*=${existingImageWrapper.id}`;
|
||||
let hoverEl = this.$el.querySelector(`.c-imagery-tsv__contents use[${selector}]`);
|
||||
const hideImageUrl = (showImagePlaceholders && !hoverEl);
|
||||
this.setNSAttributesForElement(imageElement, {
|
||||
x: this.xScale(item.time) + 2,
|
||||
url: hideImageUrl ? '' : item.url
|
||||
});
|
||||
},
|
||||
createImageWrapper(index, item, showImagePlaceholders, svgElement) {
|
||||
const id = `id-${item.time}`;
|
||||
const imgSize = String(ROW_HEIGHT - 15);
|
||||
let imageWrapper = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
||||
this.setNSAttributesForElement(imageWrapper, {
|
||||
id,
|
||||
class: 'c-imagery-tsv__image-wrapper',
|
||||
showImagePlaceholders
|
||||
});
|
||||
//create image tick indicator
|
||||
let imageTickElement = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
||||
this.setNSAttributesForElement(imageTickElement, {
|
||||
class: 'c-imagery-tsv__image-handle',
|
||||
x: this.xScale(item.time),
|
||||
y: 5,
|
||||
rx: 0,
|
||||
width: 2,
|
||||
height: String(ROW_HEIGHT - 10)
|
||||
});
|
||||
imageWrapper.appendChild(imageTickElement);
|
||||
|
||||
let imageRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
||||
this.setNSAttributesForElement(imageRect, {
|
||||
class: 'c-imagery-tsv__image-placeholder',
|
||||
x: this.xScale(item.time) + 2,
|
||||
y: 10,
|
||||
rx: 0,
|
||||
width: imgSize,
|
||||
height: imgSize,
|
||||
mask: `#image-${item.time}`
|
||||
});
|
||||
imageWrapper.appendChild(imageRect);
|
||||
|
||||
let imageElement = document.createElementNS('http://www.w3.org/2000/svg', 'image');
|
||||
this.setNSAttributesForElement(imageElement, {
|
||||
id: `image-${item.time}`,
|
||||
x: this.xScale(item.time) + 2,
|
||||
y: 10,
|
||||
rx: 0,
|
||||
width: imgSize,
|
||||
height: imgSize,
|
||||
url: showImagePlaceholders ? '' : item.url
|
||||
});
|
||||
imageWrapper.appendChild(imageElement);
|
||||
|
||||
//TODO: Don't add the hover listener if the width is too small
|
||||
imageWrapper.addEventListener('mouseover', this.bringToForeground.bind(this, svgElement, imageWrapper, index, item.url));
|
||||
|
||||
return imageWrapper;
|
||||
},
|
||||
bringToForeground(svgElement, imageWrapper, index, url, event) {
|
||||
const selector = `href*=${imageWrapper.id}`;
|
||||
let hoverEls = this.$el.querySelectorAll(`.c-imagery-tsv__contents use:not([${selector}])`);
|
||||
if (hoverEls.length > 0) {
|
||||
this.removeFromForeground(hoverEls);
|
||||
}
|
||||
|
||||
hoverEls = this.$el.querySelectorAll(`.c-imagery-tsv__contents use[${selector}]`);
|
||||
if (hoverEls.length) {
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let imageElement = imageWrapper.querySelector('image');
|
||||
this.setNSAttributesForElement(imageElement, {
|
||||
url: url,
|
||||
fill: 'none'
|
||||
});
|
||||
let hoverElement = document.createElementNS('http://www.w3.org/2000/svg', 'use');
|
||||
this.setNSAttributesForElement(hoverElement, {
|
||||
class: 'image-highlight',
|
||||
x: 0,
|
||||
href: `#${imageWrapper.id}`
|
||||
});
|
||||
this.setNSAttributesForElement(imageWrapper, {
|
||||
class: 'c-imagery-tsv__image-wrapper is-hovered'
|
||||
});
|
||||
// We're using mousedown here and not 'click' because 'click' doesn't seem to be triggered reliably
|
||||
hoverElement.addEventListener('mousedown', (e) => {
|
||||
if (e.button === 0) {
|
||||
this.expand(index);
|
||||
}
|
||||
});
|
||||
|
||||
svgElement.appendChild(hoverElement);
|
||||
|
||||
},
|
||||
removeFromForeground(items) {
|
||||
let hoverEls;
|
||||
if (items) {
|
||||
hoverEls = items;
|
||||
} else {
|
||||
hoverEls = this.$el.querySelectorAll(".c-imagery-tsv__contents use");
|
||||
}
|
||||
|
||||
hoverEls.forEach(item => {
|
||||
let selector = `id*=${this.getNSAttributesForElement(item, 'href').replace('#', '')}`;
|
||||
let imageWrapper = this.$el.querySelector(`.c-imagery-tsv__contents g[${selector}]`);
|
||||
this.setNSAttributesForElement(imageWrapper, {
|
||||
class: 'c-imagery-tsv__image-wrapper'
|
||||
});
|
||||
let showImagePlaceholders = this.getNSAttributesForElement(imageWrapper, 'showImagePlaceholders');
|
||||
if (showImagePlaceholders === 'true') {
|
||||
let imageElement = imageWrapper.querySelector('image');
|
||||
this.setNSAttributesForElement(imageElement, {
|
||||
url: ''
|
||||
});
|
||||
}
|
||||
|
||||
item.remove();
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -129,12 +129,11 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c-imagery__thumbs-wrapper"
|
||||
:class="[
|
||||
{ 'is-paused': isPaused },
|
||||
{ 'is-autoscroll-off': !resizingWindow && !autoScroll && !isPaused }
|
||||
]"
|
||||
<div class="c-imagery__thumbs-wrapper"
|
||||
:class="[
|
||||
{ 'is-paused': isPaused },
|
||||
{ 'is-autoscroll-off': !resizingWindow && !autoScroll && !isPaused }
|
||||
]"
|
||||
>
|
||||
<div
|
||||
ref="thumbsWrapper"
|
||||
@@ -175,7 +174,8 @@ import moment from 'moment';
|
||||
import RelatedTelemetry from './RelatedTelemetry/RelatedTelemetry';
|
||||
import Compass from './Compass/Compass.vue';
|
||||
|
||||
const DEFAULT_DURATION_FORMATTER = 'duration';
|
||||
import imageryData from "../../imagery/mixins/imageryData";
|
||||
|
||||
const REFRESH_CSS_MS = 500;
|
||||
const DURATION_TRACK_MS = 1000;
|
||||
const ARROW_DOWN_DELAY_CHECK_MS = 400;
|
||||
@@ -197,30 +197,29 @@ export default {
|
||||
components: {
|
||||
Compass
|
||||
},
|
||||
mixins: [imageryData],
|
||||
inject: ['openmct', 'domainObject', 'objectPath', 'currentView'],
|
||||
data() {
|
||||
let timeSystem = this.openmct.time.timeSystem();
|
||||
this.metadata = {};
|
||||
this.requestCount = 0;
|
||||
|
||||
return {
|
||||
autoScroll: true,
|
||||
durationFormatter: undefined,
|
||||
imageHistory: [],
|
||||
timeSystem: timeSystem,
|
||||
keyString: undefined,
|
||||
autoScroll: true,
|
||||
filters: {
|
||||
brightness: 100,
|
||||
contrast: 100
|
||||
},
|
||||
imageHistory: [],
|
||||
thumbnailClick: THUMBNAIL_CLICKED,
|
||||
isPaused: false,
|
||||
metadata: {},
|
||||
requestCount: 0,
|
||||
timeSystem: timeSystem,
|
||||
timeFormatter: undefined,
|
||||
refreshCSS: false,
|
||||
keyString: undefined,
|
||||
focusedImageIndex: undefined,
|
||||
focusedImageRelatedTelemetry: {},
|
||||
numericDuration: undefined,
|
||||
metadataEndpoints: {},
|
||||
relatedTelemetry: {},
|
||||
latestRelatedTelemetry: {},
|
||||
focusedImageNaturalAspectRatio: undefined,
|
||||
@@ -231,6 +230,9 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
imageHistorySize() {
|
||||
return this.imageHistory.length;
|
||||
},
|
||||
compassRoseSizingClasses() {
|
||||
let compassRoseSizingClasses = '';
|
||||
if (this.sizedImageDimensions.width < 300) {
|
||||
@@ -258,9 +260,6 @@ export default {
|
||||
canTrackDuration() {
|
||||
return this.openmct.time.clock() && this.timeSystem.isUTCBased;
|
||||
},
|
||||
focusedImageDownloadName() {
|
||||
return this.getImageDownloadName(this.focusedImage);
|
||||
},
|
||||
isNextDisabled() {
|
||||
let disabled = false;
|
||||
|
||||
@@ -383,6 +382,10 @@ export default {
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
imageHistorySize(newSize, oldSize) {
|
||||
this.setFocusedImage(newSize - 1, false);
|
||||
this.scrollToRight();
|
||||
},
|
||||
focusedImageIndex() {
|
||||
this.trackDuration();
|
||||
this.resetAgeCSS();
|
||||
@@ -391,18 +394,9 @@ export default {
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
// listen
|
||||
this.openmct.time.on('bounds', this.boundsChange);
|
||||
this.openmct.time.on('timeSystem', this.timeSystemChange);
|
||||
this.openmct.time.on('clock', this.clockChange);
|
||||
|
||||
// set
|
||||
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.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
|
||||
this.imageFormatter = this.openmct.telemetry.getValueFormatter(this.imageHints);
|
||||
this.imageDownloadNameHints = { ...this.metadata.valuesForHints(['imageDownloadName'])[0]};
|
||||
//listen
|
||||
this.openmct.time.on('timeSystem', this.trackDuration);
|
||||
this.openmct.time.on('clock', this.trackDuration);
|
||||
|
||||
// related telemetry keys
|
||||
this.spacecraftPositionKeys = ['positionX', 'positionY', 'positionZ'];
|
||||
@@ -410,56 +404,49 @@ export default {
|
||||
this.cameraKeys = ['cameraPan', 'cameraTilt'];
|
||||
this.sunKeys = ['sunOrientation'];
|
||||
|
||||
// initialize
|
||||
this.timeKey = this.timeSystem.key;
|
||||
this.timeFormatter = this.getFormatter(this.timeKey);
|
||||
|
||||
// kickoff
|
||||
this.subscribe();
|
||||
this.requestHistory();
|
||||
|
||||
// related telemetry
|
||||
await this.initializeRelatedTelemetry();
|
||||
this.updateRelatedTelemetryForFocusedImage();
|
||||
await this.updateRelatedTelemetryForFocusedImage();
|
||||
this.trackLatestRelatedTelemetry();
|
||||
|
||||
// for scrolling through images quickly and resizing the object view
|
||||
_.debounce(this.updateRelatedTelemetryForFocusedImage, 400);
|
||||
_.debounce(this.resizeImageContainer, 400);
|
||||
this.updateRelatedTelemetryForFocusedImage = _.debounce(this.updateRelatedTelemetryForFocusedImage, 400);
|
||||
|
||||
this.imageContainerResizeObserver = new ResizeObserver(this.resizeImageContainer);
|
||||
this.imageContainerResizeObserver.observe(this.$refs.imageBG);
|
||||
// for resizing the object view
|
||||
this.resizeImageContainer = _.debounce(this.resizeImageContainer, 400);
|
||||
|
||||
if (this.$refs.imageBG) {
|
||||
this.imageContainerResizeObserver = new ResizeObserver(this.resizeImageContainer);
|
||||
this.imageContainerResizeObserver.observe(this.$refs.imageBG);
|
||||
}
|
||||
|
||||
// For adjusting scroll bar size and position when resizing thumbs wrapper
|
||||
this.handleScroll = _.debounce(this.handleScroll, SCROLL_LATENCY);
|
||||
this.handleThumbWindowResizeEnded = _.debounce(this.handleThumbWindowResizeEnded, SCROLL_LATENCY);
|
||||
this.handleThumbWindowResizeStart = _.debounce(this.handleThumbWindowResizeStart, SCROLL_LATENCY);
|
||||
|
||||
if (this.$refs.thumbsWrapper) {
|
||||
this.thumbWrapperResizeObserver = new ResizeObserver(this.handleThumbWindowResizeStart);
|
||||
this.thumbWrapperResizeObserver.observe(this.$refs.thumbsWrapper);
|
||||
}
|
||||
|
||||
this.thumbWrapperResizeObserver = new ResizeObserver(this.handleThumbWindowResizeStart);
|
||||
this.thumbWrapperResizeObserver.observe(this.$refs.thumbsWrapper);
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.unsubscribe) {
|
||||
this.unsubscribe();
|
||||
delete this.unsubscribe;
|
||||
this.openmct.time.off('timeSystem', this.trackDuration);
|
||||
this.openmct.time.off('clock', this.trackDuration);
|
||||
|
||||
if (this.thumbWrapperResizeObserver) {
|
||||
this.thumbWrapperResizeObserver.disconnect();
|
||||
}
|
||||
|
||||
if (this.imageContainerResizeObserver) {
|
||||
this.imageContainerResizeObserver.disconnect();
|
||||
}
|
||||
|
||||
if (this.thumbWrapperResizeObserver) {
|
||||
this.thumbWrapperResizeObserver.disconnect();
|
||||
}
|
||||
|
||||
if (this.relatedTelemetry.hasRelatedTelemetry) {
|
||||
this.relatedTelemetry.destroy();
|
||||
}
|
||||
|
||||
this.stopDurationTracking();
|
||||
this.openmct.time.off('bounds', this.boundsChange);
|
||||
this.openmct.time.off('timeSystem', this.timeSystemChange);
|
||||
this.openmct.time.off('clock', this.clockChange);
|
||||
|
||||
// unsubscribe from related telemetry
|
||||
if (this.relatedTelemetry.hasRelatedTelemetry) {
|
||||
for (let key of this.relatedTelemetry.keys) {
|
||||
@@ -576,56 +563,6 @@ export default {
|
||||
focusElement() {
|
||||
this.$el.focus();
|
||||
},
|
||||
datumIsNotValid(datum) {
|
||||
if (this.imageHistory.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const datumURL = this.formatImageUrl(datum);
|
||||
const lastHistoryURL = this.formatImageUrl(this.imageHistory.slice(-1)[0]);
|
||||
|
||||
// datum is not valid if it matches the last datum in history,
|
||||
// or it is before the last datum in the history
|
||||
const datumTimeCheck = this.parseTime(datum);
|
||||
const historyTimeCheck = this.parseTime(this.imageHistory.slice(-1)[0]);
|
||||
const matchesLast = (datumTimeCheck === historyTimeCheck) && (datumURL === lastHistoryURL);
|
||||
const isStale = datumTimeCheck < historyTimeCheck;
|
||||
|
||||
return matchesLast || isStale;
|
||||
},
|
||||
formatImageUrl(datum) {
|
||||
if (!datum) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.imageFormatter.format(datum);
|
||||
},
|
||||
formatTime(datum) {
|
||||
if (!datum) {
|
||||
return;
|
||||
}
|
||||
|
||||
let dateTimeStr = this.timeFormatter.format(datum);
|
||||
|
||||
// Replace ISO "T" with a space to allow wrapping
|
||||
return dateTimeStr.replace("T", " ");
|
||||
},
|
||||
getImageDownloadName(datum) {
|
||||
let imageDownloadName = '';
|
||||
if (datum) {
|
||||
const key = this.imageDownloadNameHints.key;
|
||||
imageDownloadName = datum[key];
|
||||
}
|
||||
|
||||
return imageDownloadName;
|
||||
},
|
||||
parseTime(datum) {
|
||||
if (!datum) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.timeFormatter.parse(datum);
|
||||
},
|
||||
handleScroll() {
|
||||
const thumbsWrapper = this.$refs.thumbsWrapper;
|
||||
if (!thumbsWrapper || this.resizingWindow) {
|
||||
@@ -683,6 +620,10 @@ export default {
|
||||
setFocusedImage(index, thumbnailClick = false) {
|
||||
if (this.isPaused && !thumbnailClick) {
|
||||
this.nextImageIndex = index;
|
||||
//this could happen if bounds changes
|
||||
if (this.focusedImageIndex > this.imageHistory.length - 1) {
|
||||
this.focusedImageIndex = index;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -693,70 +634,6 @@ export default {
|
||||
this.paused(true);
|
||||
}
|
||||
},
|
||||
boundsChange(bounds, isTick) {
|
||||
if (!isTick) {
|
||||
this.requestHistory();
|
||||
}
|
||||
},
|
||||
async requestHistory() {
|
||||
let bounds = this.openmct.time.bounds();
|
||||
this.requestCount++;
|
||||
const requestId = this.requestCount;
|
||||
this.imageHistory = [];
|
||||
|
||||
let data = await this.openmct.telemetry
|
||||
.request(this.domainObject, bounds) || [];
|
||||
|
||||
if (this.requestCount === requestId) {
|
||||
data.forEach((datum, index) => {
|
||||
this.updateHistory(datum, index === data.length - 1);
|
||||
});
|
||||
}
|
||||
},
|
||||
timeSystemChange(system) {
|
||||
this.timeSystem = this.openmct.time.timeSystem();
|
||||
this.timeKey = this.timeSystem.key;
|
||||
this.timeFormatter = this.getFormatter(this.timeKey);
|
||||
this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
|
||||
this.trackDuration();
|
||||
},
|
||||
clockChange(clock) {
|
||||
this.trackDuration();
|
||||
},
|
||||
subscribe() {
|
||||
this.unsubscribe = this.openmct.telemetry
|
||||
.subscribe(this.domainObject, (datum) => {
|
||||
let parsedTimestamp = this.parseTime(datum);
|
||||
let bounds = this.openmct.time.bounds();
|
||||
|
||||
if (parsedTimestamp >= bounds.start && parsedTimestamp <= bounds.end) {
|
||||
this.updateHistory(datum);
|
||||
}
|
||||
});
|
||||
},
|
||||
updateHistory(datum, setFocused = true) {
|
||||
if (this.datumIsNotValid(datum)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let image = { ...datum };
|
||||
image.formattedTime = this.formatTime(datum);
|
||||
image.url = this.formatImageUrl(datum);
|
||||
image.time = datum[this.timeKey];
|
||||
image.imageDownloadName = this.getImageDownloadName(datum);
|
||||
|
||||
this.imageHistory.push(image);
|
||||
if (setFocused) {
|
||||
this.setFocusedImage(this.imageHistory.length - 1);
|
||||
this.scrollToRight();
|
||||
}
|
||||
},
|
||||
getFormatter(key) {
|
||||
let metadataValue = this.metadata.value(key) || { format: key };
|
||||
let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);
|
||||
|
||||
return valueFormatter;
|
||||
},
|
||||
trackDuration() {
|
||||
if (this.canTrackDuration) {
|
||||
this.stopDurationTracking();
|
||||
@@ -876,6 +753,10 @@ export default {
|
||||
}, { once: true });
|
||||
},
|
||||
resizeImageContainer() {
|
||||
if (!this.$refs.imageBG) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.$refs.imageBG.clientWidth !== this.imageContainerWidth) {
|
||||
this.imageContainerWidth = this.$refs.imageBG.clientWidth;
|
||||
}
|
||||
@@ -312,3 +312,34 @@
|
||||
@include cArrowButtonSizing($dimOuter: 32px);
|
||||
}
|
||||
}
|
||||
|
||||
/*************************************** IMAGERY IN TIMESTRIP VIEWS */
|
||||
.c-imagery-tsv {
|
||||
g.c-imagery-tsv__image-wrapper {
|
||||
cursor: pointer;
|
||||
|
||||
&.is-hovered {
|
||||
filter: brightness(1) contrast(1) !important;
|
||||
[class*='__image-handle'] {
|
||||
fill: $colorBodyFg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__no-items {
|
||||
fill: $colorBodyFg !important;
|
||||
}
|
||||
|
||||
&__image-handle {
|
||||
fill: rgba($colorBodyFg, 0.5);
|
||||
}
|
||||
|
||||
&__image-placeholder {
|
||||
fill: pushBack($colorBodyBg, 0.3);
|
||||
}
|
||||
|
||||
&:hover g.c-imagery-tsv__image-wrapper {
|
||||
// TODO CH: convert to theme constants
|
||||
filter: brightness(0.5) contrast(0.7);
|
||||
}
|
||||
}
|
||||
174
src/plugins/imagery/mixins/imageryData.js
Normal file
174
src/plugins/imagery/mixins/imageryData.js
Normal file
@@ -0,0 +1,174 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
|
||||
const DEFAULT_DURATION_FORMATTER = 'duration';
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'domainObject', 'objectPath'],
|
||||
mounted() {
|
||||
// listen
|
||||
this.openmct.time.on('bounds', this.boundsChange);
|
||||
this.openmct.time.on('timeSystem', this.timeSystemChange);
|
||||
|
||||
// set
|
||||
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.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
|
||||
this.imageFormatter = this.openmct.telemetry.getValueFormatter(this.imageHints);
|
||||
this.imageDownloadNameHints = { ...this.metadata.valuesForHints(['imageDownloadName'])[0]};
|
||||
|
||||
// initialize
|
||||
this.timeKey = this.timeSystem.key;
|
||||
this.timeFormatter = this.getFormatter(this.timeKey);
|
||||
|
||||
// kickoff
|
||||
this.subscribe();
|
||||
this.requestHistory();
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.unsubscribe) {
|
||||
this.unsubscribe();
|
||||
delete this.unsubscribe;
|
||||
}
|
||||
|
||||
this.openmct.time.off('bounds', this.boundsChange);
|
||||
this.openmct.time.off('timeSystem', this.timeSystemChange);
|
||||
},
|
||||
methods: {
|
||||
datumIsNotValid(datum) {
|
||||
if (this.imageHistory.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const datumURL = this.formatImageUrl(datum);
|
||||
const lastHistoryURL = this.formatImageUrl(this.imageHistory.slice(-1)[0]);
|
||||
|
||||
// datum is not valid if it matches the last datum in history,
|
||||
// or it is before the last datum in the history
|
||||
const datumTimeCheck = this.parseTime(datum);
|
||||
const historyTimeCheck = this.parseTime(this.imageHistory.slice(-1)[0]);
|
||||
const matchesLast = (datumTimeCheck === historyTimeCheck) && (datumURL === lastHistoryURL);
|
||||
const isStale = datumTimeCheck < historyTimeCheck;
|
||||
|
||||
return matchesLast || isStale;
|
||||
},
|
||||
formatImageUrl(datum) {
|
||||
if (!datum) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.imageFormatter.format(datum);
|
||||
},
|
||||
formatTime(datum) {
|
||||
if (!datum) {
|
||||
return;
|
||||
}
|
||||
|
||||
let dateTimeStr = this.timeFormatter.format(datum);
|
||||
|
||||
// Replace ISO "T" with a space to allow wrapping
|
||||
return dateTimeStr.replace("T", " ");
|
||||
},
|
||||
getImageDownloadName(datum) {
|
||||
let imageDownloadName = '';
|
||||
if (datum) {
|
||||
const key = this.imageDownloadNameHints.key;
|
||||
imageDownloadName = datum[key];
|
||||
}
|
||||
|
||||
return imageDownloadName;
|
||||
},
|
||||
parseTime(datum) {
|
||||
if (!datum) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.timeFormatter.parse(datum);
|
||||
},
|
||||
boundsChange(bounds, isTick) {
|
||||
if (!isTick) {
|
||||
this.requestHistory();
|
||||
}
|
||||
},
|
||||
async requestHistory() {
|
||||
let bounds = this.openmct.time.bounds();
|
||||
this.requestCount++;
|
||||
const requestId = this.requestCount;
|
||||
this.imageHistory = [];
|
||||
|
||||
let data = await this.openmct.telemetry
|
||||
.request(this.domainObject, bounds) || [];
|
||||
|
||||
if (this.requestCount === requestId) {
|
||||
let imagery = [];
|
||||
data.forEach((datum) => {
|
||||
let image = this.normalizeDatum(datum);
|
||||
if (image) {
|
||||
imagery.push(image);
|
||||
}
|
||||
});
|
||||
//this is to optimize anything that reacts to imageHistory length
|
||||
this.imageHistory = imagery;
|
||||
}
|
||||
},
|
||||
timeSystemChange() {
|
||||
this.timeSystem = this.openmct.time.timeSystem();
|
||||
this.timeKey = this.timeSystem.key;
|
||||
this.timeFormatter = this.getFormatter(this.timeKey);
|
||||
this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
|
||||
},
|
||||
subscribe() {
|
||||
this.unsubscribe = this.openmct.telemetry
|
||||
.subscribe(this.domainObject, (datum) => {
|
||||
let parsedTimestamp = this.parseTime(datum);
|
||||
let bounds = this.openmct.time.bounds();
|
||||
|
||||
if (parsedTimestamp >= bounds.start && parsedTimestamp <= bounds.end) {
|
||||
let image = this.normalizeDatum(datum);
|
||||
if (image) {
|
||||
this.imageHistory.push(image);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
normalizeDatum(datum) {
|
||||
if (this.datumIsNotValid(datum)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let image = { ...datum };
|
||||
image.formattedTime = this.formatTime(datum);
|
||||
image.url = this.formatImageUrl(datum);
|
||||
image.time = datum[this.timeKey];
|
||||
image.imageDownloadName = this.getImageDownloadName(datum);
|
||||
|
||||
return image;
|
||||
},
|
||||
getFormatter(key) {
|
||||
let metadataValue = this.metadata.value(key) || { format: key };
|
||||
let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);
|
||||
|
||||
return valueFormatter;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -21,10 +21,12 @@
|
||||
*****************************************************************************/
|
||||
|
||||
import ImageryViewProvider from './ImageryViewProvider';
|
||||
import ImageryTimestripViewProvider from './ImageryTimestripViewProvider';
|
||||
|
||||
export default function () {
|
||||
return function install(openmct) {
|
||||
openmct.objectViews.addProvider(new ImageryViewProvider(openmct));
|
||||
openmct.objectViews.addProvider(new ImageryTimestripViewProvider(openmct));
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -84,12 +84,14 @@ function generateTelemetry(start, count) {
|
||||
return telemetry;
|
||||
}
|
||||
|
||||
describe("The Imagery View Layout", () => {
|
||||
describe("The Imagery View Layouts", () => {
|
||||
const imageryKey = 'example.imagery';
|
||||
const imageryForTimeStripKey = 'example.imagery.time-strip.view';
|
||||
const START = Date.now();
|
||||
const COUNT = 10;
|
||||
|
||||
let resolveFunction;
|
||||
let originalRouterPath;
|
||||
|
||||
let openmct;
|
||||
let appHolder;
|
||||
@@ -116,51 +118,51 @@ describe("The Imagery View Layout", () => {
|
||||
"image": 1,
|
||||
"priority": 3
|
||||
},
|
||||
"source": "url",
|
||||
"relatedTelemetry": {
|
||||
"heading": {
|
||||
"comparisonFunction": comparisonFunction,
|
||||
"historical": {
|
||||
"telemetryObjectId": "heading",
|
||||
"valueKey": "value"
|
||||
}
|
||||
},
|
||||
"roll": {
|
||||
"comparisonFunction": comparisonFunction,
|
||||
"historical": {
|
||||
"telemetryObjectId": "roll",
|
||||
"valueKey": "value"
|
||||
}
|
||||
},
|
||||
"pitch": {
|
||||
"comparisonFunction": comparisonFunction,
|
||||
"historical": {
|
||||
"telemetryObjectId": "pitch",
|
||||
"valueKey": "value"
|
||||
}
|
||||
},
|
||||
"cameraPan": {
|
||||
"comparisonFunction": comparisonFunction,
|
||||
"historical": {
|
||||
"telemetryObjectId": "cameraPan",
|
||||
"valueKey": "value"
|
||||
}
|
||||
},
|
||||
"cameraTilt": {
|
||||
"comparisonFunction": comparisonFunction,
|
||||
"historical": {
|
||||
"telemetryObjectId": "cameraTilt",
|
||||
"valueKey": "value"
|
||||
}
|
||||
},
|
||||
"sunOrientation": {
|
||||
"comparisonFunction": comparisonFunction,
|
||||
"historical": {
|
||||
"telemetryObjectId": "sunOrientation",
|
||||
"valueKey": "value"
|
||||
}
|
||||
}
|
||||
}
|
||||
"source": "url"
|
||||
// "relatedTelemetry": {
|
||||
// "heading": {
|
||||
// "comparisonFunction": comparisonFunction,
|
||||
// "historical": {
|
||||
// "telemetryObjectId": "heading",
|
||||
// "valueKey": "value"
|
||||
// }
|
||||
// },
|
||||
// "roll": {
|
||||
// "comparisonFunction": comparisonFunction,
|
||||
// "historical": {
|
||||
// "telemetryObjectId": "roll",
|
||||
// "valueKey": "value"
|
||||
// }
|
||||
// },
|
||||
// "pitch": {
|
||||
// "comparisonFunction": comparisonFunction,
|
||||
// "historical": {
|
||||
// "telemetryObjectId": "pitch",
|
||||
// "valueKey": "value"
|
||||
// }
|
||||
// },
|
||||
// "cameraPan": {
|
||||
// "comparisonFunction": comparisonFunction,
|
||||
// "historical": {
|
||||
// "telemetryObjectId": "cameraPan",
|
||||
// "valueKey": "value"
|
||||
// }
|
||||
// },
|
||||
// "cameraTilt": {
|
||||
// "comparisonFunction": comparisonFunction,
|
||||
// "historical": {
|
||||
// "telemetryObjectId": "cameraTilt",
|
||||
// "valueKey": "value"
|
||||
// }
|
||||
// },
|
||||
// "sunOrientation": {
|
||||
// "comparisonFunction": comparisonFunction,
|
||||
// "historical": {
|
||||
// "telemetryObjectId": "sunOrientation",
|
||||
// "valueKey": "value"
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
},
|
||||
{
|
||||
"name": "Name",
|
||||
@@ -220,6 +222,8 @@ describe("The Imagery View Layout", () => {
|
||||
spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([]));
|
||||
spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({}));
|
||||
|
||||
originalRouterPath = openmct.router.path;
|
||||
|
||||
openmct.on('start', done);
|
||||
openmct.start(appHolder);
|
||||
});
|
||||
@@ -229,10 +233,34 @@ describe("The Imagery View Layout", () => {
|
||||
start: 0,
|
||||
end: 1
|
||||
});
|
||||
openmct.router.path = originalRouterPath;
|
||||
|
||||
return resetApplicationState(openmct);
|
||||
});
|
||||
|
||||
it("should provide an imagery time strip view when in a time strip", () => {
|
||||
openmct.router.path = [{
|
||||
identifier: {
|
||||
key: 'test-timestrip',
|
||||
namespace: ''
|
||||
},
|
||||
type: 'time-strip'
|
||||
}];
|
||||
|
||||
let applicableViews = openmct.objectViews.get(imageryObject, [imageryObject, {
|
||||
identifier: {
|
||||
key: 'test-timestrip',
|
||||
namespace: ''
|
||||
},
|
||||
type: 'time-strip'
|
||||
}]);
|
||||
let imageryView = applicableViews.find(
|
||||
viewProvider => viewProvider.key === imageryForTimeStripKey
|
||||
);
|
||||
|
||||
expect(imageryView).toBeDefined();
|
||||
});
|
||||
|
||||
it("should provide an imagery view only for imagery producing objects", () => {
|
||||
let applicableViews = openmct.objectViews.get(imageryObject, []);
|
||||
let imageryView = applicableViews.find(
|
||||
@@ -242,6 +270,46 @@ describe("The Imagery View Layout", () => {
|
||||
expect(imageryView).toBeDefined();
|
||||
});
|
||||
|
||||
it("should not provide an imagery view when in a time strip", () => {
|
||||
openmct.router.path = [{
|
||||
identifier: {
|
||||
key: 'test-timestrip',
|
||||
namespace: ''
|
||||
},
|
||||
type: 'time-strip'
|
||||
}];
|
||||
|
||||
let applicableViews = openmct.objectViews.get(imageryObject, [imageryObject, {
|
||||
identifier: {
|
||||
key: 'test-timestrip',
|
||||
namespace: ''
|
||||
},
|
||||
type: 'time-strip'
|
||||
}]);
|
||||
let imageryView = applicableViews.find(
|
||||
viewProvider => viewProvider.key === imageryKey
|
||||
);
|
||||
|
||||
expect(imageryView).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should provide an imagery view when navigated to in the composition of a time strip", () => {
|
||||
openmct.router.path = [imageryObject];
|
||||
|
||||
let applicableViews = openmct.objectViews.get(imageryObject, [imageryObject, {
|
||||
identifier: {
|
||||
key: 'test-timestrip',
|
||||
namespace: ''
|
||||
},
|
||||
type: 'time-strip'
|
||||
}]);
|
||||
let imageryView = applicableViews.find(
|
||||
viewProvider => viewProvider.key === imageryKey
|
||||
);
|
||||
|
||||
expect(imageryView).toBeDefined();
|
||||
});
|
||||
|
||||
describe("imagery view", () => {
|
||||
let applicableViews;
|
||||
let imageryViewProvider;
|
||||
@@ -367,18 +435,18 @@ describe("The Imagery View Layout", () => {
|
||||
});
|
||||
it ('shows an auto scroll button when scroll to left', async () => {
|
||||
// to mock what a scroll would do
|
||||
imageryView._getInstance().$refs.ImageryLayout.autoScroll = false;
|
||||
imageryView._getInstance().$refs.ImageryContainer.autoScroll = false;
|
||||
await Vue.nextTick();
|
||||
let autoScrollButton = parent.querySelector('.c-imagery__auto-scroll-resume-button');
|
||||
expect(autoScrollButton).toBeTruthy();
|
||||
});
|
||||
it ('scrollToRight is called when clicking on auto scroll button', async () => {
|
||||
// use spyon to spy the scroll function
|
||||
spyOn(imageryView._getInstance().$refs.ImageryLayout, 'scrollToRight');
|
||||
imageryView._getInstance().$refs.ImageryLayout.autoScroll = false;
|
||||
spyOn(imageryView._getInstance().$refs.ImageryContainer, 'scrollToRight');
|
||||
imageryView._getInstance().$refs.ImageryContainer.autoScroll = false;
|
||||
await Vue.nextTick();
|
||||
parent.querySelector('.c-imagery__auto-scroll-resume-button').click();
|
||||
expect(imageryView._getInstance().$refs.ImageryLayout.scrollToRight).toHaveBeenCalledWith('reset');
|
||||
expect(imageryView._getInstance().$refs.ImageryContainer.scrollToRight).toHaveBeenCalledWith('reset');
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -143,10 +143,12 @@ export default {
|
||||
},
|
||||
updateViewBounds() {
|
||||
this.viewBounds = this.openmct.time.bounds();
|
||||
if (!this.options.compact) {
|
||||
//Add a 50% padding to the end bounds to look ahead
|
||||
let timespan = (this.viewBounds.end - this.viewBounds.start);
|
||||
let padding = timespan / 2;
|
||||
this.viewBounds.end = this.viewBounds.end + padding;
|
||||
let timespan = (this.viewBounds.end - this.viewBounds.start);
|
||||
let padding = timespan / 2;
|
||||
this.viewBounds.end = this.viewBounds.end + padding;
|
||||
}
|
||||
|
||||
if (this.timeSystem === undefined) {
|
||||
this.timeSystem = this.openmct.time.timeSystem();
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
class="u-contents"
|
||||
:default-object="item.domainObject"
|
||||
:object-path="item.objectPath"
|
||||
@change-action-collection="setActionCollection"
|
||||
/>
|
||||
</swim-lane>
|
||||
</template>
|
||||
@@ -106,6 +107,9 @@ export default {
|
||||
this.$el, this.context);
|
||||
}
|
||||
});
|
||||
},
|
||||
setActionCollection(actionCollection) {
|
||||
this.openmct.menus.actionsToMenuItems(actionCollection.getVisibleActions(), actionCollection.objectPath, actionCollection.view);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -53,7 +53,7 @@ export default class ViewLargeAction {
|
||||
const parentElement = view.parentElement;
|
||||
const element = parentElement && parentElement.firstChild;
|
||||
const viewLargeAction = element && !element.classList.contains('js-main-container')
|
||||
&& !this._isNavigatedObject(objectPath);
|
||||
&& !this.openmct.router.isNavigatedObject(objectPath);
|
||||
|
||||
return viewLargeAction;
|
||||
}
|
||||
@@ -85,11 +85,4 @@ export default class ViewLargeAction {
|
||||
|
||||
return preview.$mount().$el;
|
||||
}
|
||||
|
||||
_isNavigatedObject(objectPath) {
|
||||
let targetObject = objectPath[0];
|
||||
let navigatedObject = this.openmct.router.path[0];
|
||||
|
||||
return this.openmct.objects.areIdsEqual(targetObject.identifier, navigatedObject.identifier);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
@import "../plugins/folderView/components/grid-view.scss";
|
||||
@import "../plugins/folderView/components/list-item.scss";
|
||||
@import "../plugins/folderView/components/list-view.scss";
|
||||
@import "../plugins/imagery/components/imagery-view-layout.scss";
|
||||
@import "../plugins/imagery/components/imagery-view.scss";
|
||||
@import "../plugins/imagery/components/Compass/compass.scss";
|
||||
@import "../plugins/telemetryTable/components/table-row.scss";
|
||||
@import "../plugins/telemetryTable/components/table-footer-indicator.scss";
|
||||
|
||||
@@ -122,8 +122,10 @@ export default {
|
||||
}
|
||||
},
|
||||
drawAxis(bounds, timeSystem) {
|
||||
this.setScale(bounds, timeSystem);
|
||||
this.setAxis(bounds);
|
||||
let viewBounds = Object.assign({}, bounds);
|
||||
|
||||
this.setScale(viewBounds, timeSystem);
|
||||
this.setAxis(viewBounds);
|
||||
this.axisElement.call(this.xAxis);
|
||||
this.updateNowMarker();
|
||||
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
:class="{'c-swimlane': !isNested}"
|
||||
>
|
||||
|
||||
<div class="c-swimlane__lane-label c-object-label"
|
||||
<div v-if="hideLabel === false"
|
||||
class="c-swimlane__lane-label c-object-label"
|
||||
:class="{'c-swimlane__lane-label--span-cols': (!spanRowsCount && !isNested)}"
|
||||
:style="gridRowSpan"
|
||||
>
|
||||
@@ -49,6 +50,12 @@ export default {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
hideLabel: {
|
||||
type: Boolean,
|
||||
default() {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
isNested: {
|
||||
type: Boolean,
|
||||
default() {
|
||||
|
||||
@@ -1,3 +1,25 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
|
||||
<template>
|
||||
<li
|
||||
draggable="true"
|
||||
|
||||
@@ -1,3 +1,25 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
|
||||
<template>
|
||||
<div class="c-elements-pool">
|
||||
<Search
|
||||
|
||||
@@ -1,3 +1,25 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
|
||||
<template>
|
||||
<div class="c-inspector">
|
||||
<object-name />
|
||||
@@ -19,7 +41,9 @@
|
||||
type="vertical"
|
||||
>
|
||||
<pane class="c-inspector__properties">
|
||||
<properties />
|
||||
<Properties
|
||||
v-if="!activity"
|
||||
/>
|
||||
<location />
|
||||
<inspector-views />
|
||||
</pane>
|
||||
@@ -57,7 +81,7 @@ import multipane from '../layout/multipane.vue';
|
||||
import pane from '../layout/pane.vue';
|
||||
import ElementsPool from './ElementsPool.vue';
|
||||
import Location from './Location.vue';
|
||||
import Properties from './Properties.vue';
|
||||
import Properties from './details/Properties.vue';
|
||||
import ObjectName from './ObjectName.vue';
|
||||
import InspectorViews from './InspectorViews.vue';
|
||||
import _ from "lodash";
|
||||
@@ -98,7 +122,8 @@ export default {
|
||||
key: '__styles',
|
||||
name: 'Styles'
|
||||
}],
|
||||
currentTabbedView: {}
|
||||
currentTabbedView: {},
|
||||
activity: undefined
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
@@ -111,9 +136,12 @@ export default {
|
||||
methods: {
|
||||
updateInspectorViews(selection) {
|
||||
this.refreshComposition(selection);
|
||||
|
||||
if (this.openmct.types.get('conditionSet')) {
|
||||
this.refreshTabs(selection);
|
||||
}
|
||||
|
||||
this.setActivity(selection);
|
||||
},
|
||||
refreshComposition(selection) {
|
||||
if (selection.length > 0 && selection[0].length > 0) {
|
||||
@@ -150,6 +178,12 @@ export default {
|
||||
},
|
||||
isCurrent(view) {
|
||||
return _.isEqual(this.currentTabbedView, view);
|
||||
},
|
||||
setActivity(selection) {
|
||||
this.activity = selection
|
||||
&& selection.length
|
||||
&& selection[0].length
|
||||
&& selection[0][0].activity;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
166
src/ui/inspector/InspectorDetailsSpec.js
Normal file
166
src/ui/inspector/InspectorDetailsSpec.js
Normal file
@@ -0,0 +1,166 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
|
||||
import {
|
||||
createOpenMct,
|
||||
resetApplicationState
|
||||
} from 'utils/testing';
|
||||
import Vue from 'vue';
|
||||
|
||||
const INSPECTOR_SELECTOR_PREFIX = '.c-inspect-properties__';
|
||||
|
||||
describe('the inspector', () => {
|
||||
let appHolder;
|
||||
let openmct;
|
||||
let folderItem;
|
||||
let selection;
|
||||
|
||||
beforeEach((done) => {
|
||||
folderItem = {
|
||||
name: 'folder',
|
||||
type: 'folder',
|
||||
id: 'mock-folder-key',
|
||||
identifier: {
|
||||
namespace: '',
|
||||
key: 'mock-folder-key'
|
||||
},
|
||||
created: 1592851063871
|
||||
};
|
||||
|
||||
selection = [
|
||||
{
|
||||
context: {
|
||||
item: folderItem
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
appHolder = document.createElement('div');
|
||||
|
||||
openmct = createOpenMct();
|
||||
openmct.on('start', done);
|
||||
openmct.start(appHolder);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
return resetApplicationState(openmct);
|
||||
});
|
||||
|
||||
it('displays default details for selection', async () => {
|
||||
openmct.selection.select(selection);
|
||||
await Vue.nextTick();
|
||||
|
||||
const details = getDetails();
|
||||
const [
|
||||
title,
|
||||
type,
|
||||
timestamp
|
||||
] = details;
|
||||
|
||||
expect(title.name)
|
||||
.toEqual('Title');
|
||||
expect(title.value.toLowerCase())
|
||||
.toEqual(folderItem.name);
|
||||
|
||||
expect(type.name)
|
||||
.toEqual('Type');
|
||||
expect(type.value.toLowerCase())
|
||||
.toEqual(folderItem.type);
|
||||
|
||||
expect(timestamp.name)
|
||||
.toEqual('Created');
|
||||
expect(new Date(timestamp.value).toString())
|
||||
.toEqual(new Date(folderItem.created).toString());
|
||||
});
|
||||
|
||||
it('details show modified date', async () => {
|
||||
const modifiedTimestamp = folderItem.created + 1000;
|
||||
folderItem.modified = modifiedTimestamp;
|
||||
|
||||
openmct.selection.select(selection);
|
||||
await Vue.nextTick();
|
||||
|
||||
const details = getDetails();
|
||||
const timestamp = details[details.length - 1];
|
||||
|
||||
expect(timestamp.name)
|
||||
.toEqual('Modified');
|
||||
expect(new Date(timestamp.value).toString())
|
||||
.toEqual(new Date(folderItem.modified).toString());
|
||||
});
|
||||
|
||||
it('displays custom details if provided through context', async () => {
|
||||
const NAME_PREFIX = 'Custom Name';
|
||||
const VALUE_PREFIX = 'Custom Value';
|
||||
const indexes = [0, 1, 2, 3, 4, 5, 6, 7, 8];
|
||||
const customDetails = indexes.map(index => {
|
||||
return {
|
||||
name: `${NAME_PREFIX} ${index}`,
|
||||
value: `${VALUE_PREFIX} ${index}`
|
||||
};
|
||||
});
|
||||
|
||||
selection[0].context.details = customDetails;
|
||||
|
||||
openmct.selection.select(selection);
|
||||
await Vue.nextTick();
|
||||
|
||||
const details = getDetails();
|
||||
|
||||
expect(details.length)
|
||||
.toEqual(customDetails.length);
|
||||
|
||||
details.forEach((detail, index) => {
|
||||
expect(detail.name)
|
||||
.toEqual(customDetails[index].name);
|
||||
expect(detail.value)
|
||||
.toEqual(customDetails[index].value);
|
||||
});
|
||||
});
|
||||
|
||||
function getDetailsElements() {
|
||||
const inspectorDetailsSection = appHolder
|
||||
.querySelector(`${INSPECTOR_SELECTOR_PREFIX}section`);
|
||||
const details = inspectorDetailsSection
|
||||
.querySelectorAll(`${INSPECTOR_SELECTOR_PREFIX}row`);
|
||||
|
||||
return details;
|
||||
}
|
||||
|
||||
function getDetails() {
|
||||
const detailsElements = getDetailsElements();
|
||||
const details = Array.from(detailsElements)
|
||||
.map(element => {
|
||||
return {
|
||||
name: getText(element, 'label'),
|
||||
value: getText(element, 'value')
|
||||
};
|
||||
});
|
||||
|
||||
return details;
|
||||
}
|
||||
|
||||
function getText(element, selectorSuffix) {
|
||||
return element.querySelector(`${INSPECTOR_SELECTOR_PREFIX}${selectorSuffix}`)
|
||||
.textContent.trim();
|
||||
}
|
||||
});
|
||||
@@ -1,3 +1,25 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
|
||||
export const mockTelemetryTableSelection = [
|
||||
[{
|
||||
context: {
|
||||
|
||||
@@ -1,10 +1,29 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
inject: ['openmct'],
|
||||
|
||||
@@ -1,3 +1,25 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
|
||||
<template>
|
||||
<div class="c-inspect-properties c-inspect-properties--location">
|
||||
<div
|
||||
|
||||
@@ -1,3 +1,25 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
|
||||
<template>
|
||||
<div class="c-inspector__header">
|
||||
<div v-if="!multiSelect"
|
||||
|
||||
@@ -1,171 +0,0 @@
|
||||
<template>
|
||||
<div v-if="!activity"
|
||||
class="c-inspector__properties c-inspect-properties"
|
||||
>
|
||||
<div class="c-inspect-properties__header">
|
||||
Details
|
||||
</div>
|
||||
<ul
|
||||
v-if="!multiSelect && !singleSelectNonObject"
|
||||
class="c-inspect-properties__section"
|
||||
>
|
||||
<li class="c-inspect-properties__row">
|
||||
<div class="c-inspect-properties__label">
|
||||
Title
|
||||
</div>
|
||||
<div class="c-inspect-properties__value">
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</li>
|
||||
<li class="c-inspect-properties__row">
|
||||
<div class="c-inspect-properties__label">
|
||||
Type
|
||||
</div>
|
||||
<div class="c-inspect-properties__value">
|
||||
{{ typeName }}
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
v-if="item.created"
|
||||
class="c-inspect-properties__row"
|
||||
>
|
||||
<div class="c-inspect-properties__label">
|
||||
Created
|
||||
</div>
|
||||
<div class="c-inspect-properties__value">
|
||||
{{ formatTime(item.created) }}
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
v-if="item.modified"
|
||||
class="c-inspect-properties__row"
|
||||
>
|
||||
<div class="c-inspect-properties__label">
|
||||
Modified
|
||||
</div>
|
||||
<div class="c-inspect-properties__value">
|
||||
{{ formatTime(item.modified) }}
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
v-for="prop in typeProperties"
|
||||
:key="prop.name"
|
||||
class="c-inspect-properties__row"
|
||||
>
|
||||
<div class="c-inspect-properties__label">
|
||||
{{ prop.name }}
|
||||
</div>
|
||||
<div class="c-inspect-properties__value">
|
||||
{{ prop.value }}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div
|
||||
v-if="multiSelect"
|
||||
class="c-inspect-properties__row--span-all"
|
||||
>
|
||||
No properties to display for multiple items
|
||||
</div>
|
||||
<div
|
||||
v-if="singleSelectNonObject"
|
||||
class="c-inspect-properties__row--span-all"
|
||||
>
|
||||
No properties to display for this item
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Moment from "moment";
|
||||
|
||||
export default {
|
||||
inject: ['openmct'],
|
||||
data() {
|
||||
return {
|
||||
domainObject: {},
|
||||
activity: undefined,
|
||||
multiSelect: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
item() {
|
||||
return this.domainObject || {};
|
||||
},
|
||||
type() {
|
||||
return this.openmct.types.get(this.item.type);
|
||||
},
|
||||
typeName() {
|
||||
if (!this.type) {
|
||||
return `Unknown: ${this.item.type}`;
|
||||
}
|
||||
|
||||
return this.type.definition.name;
|
||||
},
|
||||
typeProperties() {
|
||||
if (!this.type) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let definition = this.type.definition;
|
||||
if (!definition.form || definition.form.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return definition.form
|
||||
.map((field) => {
|
||||
let path = field.property;
|
||||
if (typeof path === 'string') {
|
||||
path = [path];
|
||||
}
|
||||
|
||||
return {
|
||||
name: field.name,
|
||||
path
|
||||
};
|
||||
})
|
||||
.filter(field => Array.isArray(field.path))
|
||||
.map((field) => {
|
||||
return {
|
||||
name: field.name,
|
||||
value: field.path.reduce((object, key) => {
|
||||
return object[key];
|
||||
}, this.item)
|
||||
};
|
||||
});
|
||||
},
|
||||
singleSelectNonObject() {
|
||||
return !this.item.identifier && !this.multiSelect;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.openmct.selection.on('change', this.updateSelection);
|
||||
this.updateSelection(this.openmct.selection.get());
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.openmct.selection.off('change', this.updateSelection);
|
||||
},
|
||||
methods: {
|
||||
updateSelection(selection) {
|
||||
if (selection.length === 0 || selection[0].length === 0) {
|
||||
this.domainObject = {};
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (selection.length > 1) {
|
||||
this.multiSelect = true;
|
||||
this.domainObject = {};
|
||||
|
||||
return;
|
||||
} else {
|
||||
this.multiSelect = false;
|
||||
this.domainObject = selection[0][0].context.item;
|
||||
this.activity = selection[0][0].context.activity;
|
||||
}
|
||||
},
|
||||
formatTime(unixTime) {
|
||||
return Moment.utc(unixTime).format('YYYY-MM-DD[\n]HH:mm:ss') + ' UTC';
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
43
src/ui/inspector/details/DetailText.vue
Normal file
43
src/ui/inspector/details/DetailText.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
|
||||
<template>
|
||||
<li class="c-inspect-properties__row">
|
||||
<div class="c-inspect-properties__label">
|
||||
{{ detail.name }}
|
||||
</div>
|
||||
<div class="c-inspect-properties__value">
|
||||
{{ detail.value }}
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
detail: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
201
src/ui/inspector/details/Properties.vue
Normal file
201
src/ui/inspector/details/Properties.vue
Normal file
@@ -0,0 +1,201 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
|
||||
<template>
|
||||
<div class="c-inspector__properties c-inspect-properties">
|
||||
<div class="c-inspect-properties__header">
|
||||
Details
|
||||
</div>
|
||||
<ul
|
||||
v-if="hasDetails"
|
||||
class="c-inspect-properties__section"
|
||||
>
|
||||
<Component
|
||||
:is="getComponent(detail)"
|
||||
v-for="detail in details"
|
||||
:key="detail.name"
|
||||
:detail="detail"
|
||||
/>
|
||||
|
||||
</ul>
|
||||
<div
|
||||
v-else
|
||||
class="c-inspect-properties__row--span-all"
|
||||
>
|
||||
{{ noDetailsMessage }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Moment from 'moment';
|
||||
import DetailText from './DetailText.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DetailText
|
||||
},
|
||||
inject: ['openmct'],
|
||||
data() {
|
||||
return {
|
||||
selection: undefined
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
details() {
|
||||
return this.customDetails ? this.customDetails : this.domainObjectDetails;
|
||||
},
|
||||
customDetails() {
|
||||
if (this.context === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.context.details;
|
||||
},
|
||||
domainObject() {
|
||||
if (this.context === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.context.item;
|
||||
},
|
||||
type() {
|
||||
if (this.domainObject === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.openmct.types.get(this.domainObject.type);
|
||||
},
|
||||
domainObjectDetails() {
|
||||
if (this.domainObject === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const title = this.domainObject.name;
|
||||
const typeName = this.type ? this.type.definition.name : `Unknown: ${this.domainObject.type}`;
|
||||
const timestampLabel = this.domainObject.modified ? 'Modified' : 'Created';
|
||||
const timestamp = this.domainObject.modified ? this.domainObject.modified : this.domainObject.created;
|
||||
|
||||
const details = [
|
||||
{
|
||||
name: 'Title',
|
||||
value: title
|
||||
},
|
||||
{
|
||||
name: 'Type',
|
||||
value: typeName
|
||||
}
|
||||
];
|
||||
|
||||
if (timestamp !== undefined) {
|
||||
const formattedTimestamp = Moment.utc(timestamp)
|
||||
.format('YYYY-MM-DD[\n]HH:mm:ss')
|
||||
+ ' UTC';
|
||||
|
||||
details.push(
|
||||
{
|
||||
name: timestampLabel,
|
||||
value: formattedTimestamp
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return [...details, ...this.typeProperties];
|
||||
},
|
||||
context() {
|
||||
if (
|
||||
!this.selection
|
||||
|| !this.selection.length
|
||||
|| !this.selection[0].length
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.selection[0][0].context;
|
||||
},
|
||||
hasDetails() {
|
||||
return Boolean(
|
||||
this.details
|
||||
&& this.details.length
|
||||
&& !this.multiSelection
|
||||
);
|
||||
},
|
||||
multiSelection() {
|
||||
return this.selection && this.selection.length > 1;
|
||||
},
|
||||
noDetailsMessage() {
|
||||
return this.multiSelection
|
||||
? 'No properties to display for multiple items'
|
||||
: 'No properties to display for this item';
|
||||
},
|
||||
typeProperties() {
|
||||
if (!this.type) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let definition = this.type.definition;
|
||||
if (!definition.form || definition.form.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return definition.form
|
||||
.map((field) => {
|
||||
let path = field.property;
|
||||
if (typeof path === 'string') {
|
||||
path = [path];
|
||||
}
|
||||
|
||||
return {
|
||||
name: field.name,
|
||||
path
|
||||
};
|
||||
})
|
||||
.filter(field => Array.isArray(field.path))
|
||||
.map((field) => {
|
||||
return {
|
||||
name: field.name,
|
||||
value: field.path.reduce((object, key) => {
|
||||
return object[key];
|
||||
}, this.domainObject)
|
||||
};
|
||||
});
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.openmct.selection.on('change', this.updateSelection);
|
||||
this.updateSelection(this.openmct.selection.get());
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.openmct.selection.off('change', this.updateSelection);
|
||||
},
|
||||
methods: {
|
||||
getComponent(detail) {
|
||||
const component = detail.component ? detail.component : 'text';
|
||||
|
||||
return `detail-${component}`;
|
||||
},
|
||||
updateSelection(selection) {
|
||||
this.selection = selection;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -81,17 +81,10 @@ export default class PreviewAction {
|
||||
const isObjectView = parentElement && parentElement.classList.contains('js-object-view');
|
||||
|
||||
return !PreviewAction.isVisible
|
||||
&& !this._isNavigatedObject(objectPath)
|
||||
&& !this._openmct.router.isNavigatedObject(objectPath)
|
||||
&& !isObjectView;
|
||||
}
|
||||
|
||||
_isNavigatedObject(objectPath) {
|
||||
let targetObject = objectPath[0];
|
||||
let navigatedObject = this._openmct.router.path[0];
|
||||
|
||||
return this._openmct.objects.areIdsEqual(targetObject.identifier, navigatedObject.identifier);
|
||||
}
|
||||
|
||||
_preventPreview(objectPath) {
|
||||
const noPreviewTypes = ['folder'];
|
||||
|
||||
|
||||
@@ -136,6 +136,13 @@ class ApplicationRouter extends EventEmitter {
|
||||
this.handleLocationChange(hash.substring(1));
|
||||
}
|
||||
|
||||
isNavigatedObject(objectPath) {
|
||||
let targetObject = objectPath[0];
|
||||
let navigatedObject = this.path[0];
|
||||
|
||||
return this.openmct.objects.areIdsEqual(targetObject.identifier, navigatedObject.identifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add routes listeners
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user