mirror of
https://github.com/nasa/openmct.git
synced 2025-05-23 10:44:06 +00:00
more scaffolding
This commit is contained in:
parent
3159de08b1
commit
680b0953b2
@ -21,10 +21,10 @@
|
|||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
import mount from 'utils/mount';
|
import mount from 'utils/mount';
|
||||||
|
|
||||||
import EventTimeView from './components/EventTimeView.vue';
|
import EventTimelineView from './components/EventTimelineView.vue';
|
||||||
|
|
||||||
export default function EventTimestripViewProvider(openmct) {
|
export default function EventTimestripViewProvider(openmct) {
|
||||||
const type = 'event.time-strip.view';
|
const type = 'event.time-line.view';
|
||||||
|
|
||||||
function hasEventTelemetry(domainObject) {
|
function hasEventTelemetry(domainObject) {
|
||||||
const metadata = openmct.telemetry.getMetadata(domainObject);
|
const metadata = openmct.telemetry.getMetadata(domainObject);
|
||||||
@ -39,7 +39,7 @@ export default function EventTimestripViewProvider(openmct) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
key: type,
|
key: type,
|
||||||
name: 'Event Timestrip View',
|
name: 'Event Timeline View',
|
||||||
cssClass: 'icon-event',
|
cssClass: 'icon-event',
|
||||||
canView: function (domainObject, objectPath) {
|
canView: function (domainObject, objectPath) {
|
||||||
let isChildOfTimeStrip = objectPath.find((object) => object.type === 'time-strip');
|
let isChildOfTimeStrip = objectPath.find((object) => object.type === 'time-strip');
|
||||||
@ -60,14 +60,14 @@ export default function EventTimestripViewProvider(openmct) {
|
|||||||
{
|
{
|
||||||
el: element,
|
el: element,
|
||||||
components: {
|
components: {
|
||||||
EventTimeView
|
EventTimelineView
|
||||||
},
|
},
|
||||||
provide: {
|
provide: {
|
||||||
openmct: openmct,
|
openmct: openmct,
|
||||||
domainObject: domainObject,
|
domainObject: domainObject,
|
||||||
objectPath: objectPath
|
objectPath: objectPath
|
||||||
},
|
},
|
||||||
template: '<event-time-view ref="root"></event-time-view>'
|
template: '<event-timeline-view ref="root"></event-timeline-view>'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
app: openmct.app,
|
app: openmct.app,
|
@ -1,413 +0,0 @@
|
|||||||
<!--
|
|
||||||
Open MCT, Copyright (c) 2014-2024, 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="events" class="c-event-tsv c-timeline-holder">
|
|
||||||
<div ref="imageryHolder" class="c-imagery-tsv__contents u-contents"></div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { scaleLinear, scaleUtc } from 'd3-scale';
|
|
||||||
import _ from 'lodash';
|
|
||||||
import mount from 'utils/mount';
|
|
||||||
|
|
||||||
import SwimLane from '@/ui/components/swim-lane/SwimLane.vue';
|
|
||||||
import { PREVIEW_ACTION_KEY } from '@/ui/preview/PreviewAction.js';
|
|
||||||
|
|
||||||
const PADDING = 1;
|
|
||||||
const ROW_HEIGHT = 100;
|
|
||||||
const IMAGE_SIZE = 85;
|
|
||||||
const IMAGE_WIDTH_THRESHOLD = 25;
|
|
||||||
const CONTAINER_CLASS = 'c-imagery-tsv-container';
|
|
||||||
const NO_ITEMS_CLASS = 'c-imagery-tsv__no-items';
|
|
||||||
const IMAGE_WRAPPER_CLASS = 'c-imagery-tsv__image-wrapper';
|
|
||||||
const ID_PREFIX = 'wrapper-';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
inject: ['openmct', 'domainObject', 'objectPath'],
|
|
||||||
data() {
|
|
||||||
let timeSystem = this.openmct.time.getTimeSystem();
|
|
||||||
this.metadata = {};
|
|
||||||
this.requestCount = 0;
|
|
||||||
|
|
||||||
return {
|
|
||||||
viewBounds: undefined,
|
|
||||||
height: 0,
|
|
||||||
durationFormatter: undefined,
|
|
||||||
imageHistory: [],
|
|
||||||
timeSystem: timeSystem,
|
|
||||||
keyString: undefined
|
|
||||||
};
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
imageHistory: {
|
|
||||||
handler(newHistory, oldHistory) {
|
|
||||||
this.updatePlotImagery();
|
|
||||||
},
|
|
||||||
deep: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.previewAction = this.openmct.actions.getAction(PREVIEW_ACTION_KEY);
|
|
||||||
|
|
||||||
this.canvas = this.$refs.imagery.appendChild(document.createElement('canvas'));
|
|
||||||
this.canvas.height = 0;
|
|
||||||
this.canvasContext = this.canvas.getContext('2d');
|
|
||||||
this.setDimensions();
|
|
||||||
|
|
||||||
this.setScaleAndPlotImagery = this.setScaleAndPlotImagery.bind(this);
|
|
||||||
this.updateViewBounds = this.updateViewBounds.bind(this);
|
|
||||||
this.setTimeContext = this.setTimeContext.bind(this);
|
|
||||||
this.setTimeContext();
|
|
||||||
|
|
||||||
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);
|
|
||||||
},
|
|
||||||
beforeUnmount() {
|
|
||||||
if (this.imageryStripResizeObserver) {
|
|
||||||
this.imageryStripResizeObserver.disconnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.stopFollowingTimeContext();
|
|
||||||
if (this.unlisten) {
|
|
||||||
this.unlisten();
|
|
||||||
}
|
|
||||||
if (this.destroyImageryContainer) {
|
|
||||||
this.destroyImageryContainer();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
setTimeContext() {
|
|
||||||
this.stopFollowingTimeContext();
|
|
||||||
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
|
|
||||||
this.timeContext.on('timeSystem', this.setScaleAndPlotImagery);
|
|
||||||
this.timeContext.on('boundsChanged', this.updateViewBounds);
|
|
||||||
},
|
|
||||||
stopFollowingTimeContext() {
|
|
||||||
if (this.timeContext) {
|
|
||||||
this.timeContext.off('timeSystem', this.setScaleAndPlotImagery);
|
|
||||||
this.timeContext.off('boundsChanged', this.updateViewBounds);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
expand(imageTimestamp) {
|
|
||||||
const path = this.objectPath[0];
|
|
||||||
this.previewAction.invoke([path], {
|
|
||||||
timestamp: imageTimestamp,
|
|
||||||
objectPath: this.objectPath
|
|
||||||
});
|
|
||||||
},
|
|
||||||
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.timeContext.getBounds();
|
|
||||||
|
|
||||||
if (this.timeSystem === undefined) {
|
|
||||||
this.timeSystem = this.timeContext.getTimeSystem();
|
|
||||||
}
|
|
||||||
|
|
||||||
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(`.${NO_ITEMS_CLASS}`);
|
|
||||||
noItemsEl.forEach((item) => {
|
|
||||||
item.remove();
|
|
||||||
});
|
|
||||||
let imagery = this.$el.querySelectorAll(`.${IMAGE_WRAPPER_CLASS}`);
|
|
||||||
imagery.forEach((imageElm) => {
|
|
||||||
if (clearAllImagery) {
|
|
||||||
imageElm.remove();
|
|
||||||
} else {
|
|
||||||
const id = imageElm.getAttributeNS(null, 'id');
|
|
||||||
if (id) {
|
|
||||||
const timestamp = id.replace(ID_PREFIX, '');
|
|
||||||
if (
|
|
||||||
!this.isImageryInBounds({
|
|
||||||
time: timestamp
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
imageElm.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.timeContext.getTimeSystem();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (timeSystem.isUTCBased) {
|
|
||||||
this.xScale = scaleUtc();
|
|
||||||
this.xScale.domain([new Date(this.viewBounds.start), new Date(this.viewBounds.end)]);
|
|
||||||
} else {
|
|
||||||
this.xScale = 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 containerHeight = 100;
|
|
||||||
let containerWidth = this.imageHistory.length ? this.width : 200;
|
|
||||||
let imageryContainer;
|
|
||||||
|
|
||||||
let existingContainer = this.$el.querySelector(`.${CONTAINER_CLASS}`);
|
|
||||||
if (existingContainer) {
|
|
||||||
imageryContainer = existingContainer;
|
|
||||||
imageryContainer.style.maxWidth = `${containerWidth}px`;
|
|
||||||
} else {
|
|
||||||
if (this.destroyImageryContainer) {
|
|
||||||
this.destroyImageryContainer();
|
|
||||||
}
|
|
||||||
const { vNode, destroy } = mount(
|
|
||||||
{
|
|
||||||
components: {
|
|
||||||
SwimLane
|
|
||||||
},
|
|
||||||
provide: {
|
|
||||||
openmct: this.openmct
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
isNested: true
|
|
||||||
};
|
|
||||||
},
|
|
||||||
template: `<swim-lane :is-nested="isNested" :hide-label="true"><template v-slot:object><div class="c-imagery-tsv-container"></div></template></swim-lane>`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
app: this.openmct.app
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
this.destroyImageryContainer = destroy;
|
|
||||||
const component = vNode.componentInstance;
|
|
||||||
this.$refs.imageryHolder.appendChild(component.$el);
|
|
||||||
|
|
||||||
imageryContainer = component.$el.querySelector(`.${CONTAINER_CLASS}`);
|
|
||||||
imageryContainer.style.maxWidth = `${containerWidth}px`;
|
|
||||||
imageryContainer.style.height = `${containerHeight}px`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return imageryContainer;
|
|
||||||
},
|
|
||||||
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 imageryContainer = this.getImageryContainer();
|
|
||||||
const showImagePlaceholders = this.isImageryWidthAcceptable();
|
|
||||||
let index = 0;
|
|
||||||
if (this.imageHistory.length) {
|
|
||||||
this.imageHistory.forEach((currentImageObject) => {
|
|
||||||
if (this.isImageryInBounds(currentImageObject)) {
|
|
||||||
this.plotImagery(currentImageObject, showImagePlaceholders, imageryContainer, index);
|
|
||||||
index = index + 1;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.plotNoItems(imageryContainer);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
plotNoItems(containerElement) {
|
|
||||||
let textElement = document.createElement('text');
|
|
||||||
textElement.classList.add(NO_ITEMS_CLASS);
|
|
||||||
textElement.innerHTML = 'No images within timeframe';
|
|
||||||
|
|
||||||
containerElement.appendChild(textElement);
|
|
||||||
},
|
|
||||||
setNSAttributesForElement(element, attributes) {
|
|
||||||
if (!element) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.keys(attributes).forEach((key) => {
|
|
||||||
element.setAttributeNS(null, key, attributes[key]);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
setStyles(element, styles) {
|
|
||||||
if (!element) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.keys(styles).forEach((key) => {
|
|
||||||
element.style[key] = styles[key];
|
|
||||||
});
|
|
||||||
},
|
|
||||||
getImageWrapper(item) {
|
|
||||||
const id = `${ID_PREFIX}${item.time}`;
|
|
||||||
|
|
||||||
return this.$el.querySelector(`.c-imagery-tsv__contents div[id=${id}]`);
|
|
||||||
},
|
|
||||||
plotImagery(item, showImagePlaceholders, containerElement, index) {
|
|
||||||
let existingImageWrapper = this.getImageWrapper(item);
|
|
||||||
//imageWrapper wraps the vertical tick and the image
|
|
||||||
if (existingImageWrapper) {
|
|
||||||
this.updateExistingImageWrapper(existingImageWrapper, item, showImagePlaceholders);
|
|
||||||
} else {
|
|
||||||
let imageWrapper = this.createImageWrapper(index, item, showImagePlaceholders);
|
|
||||||
containerElement.appendChild(imageWrapper);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setImageDisplay(imageElement, showImagePlaceholders) {
|
|
||||||
if (showImagePlaceholders) {
|
|
||||||
imageElement.style.display = 'none';
|
|
||||||
} else {
|
|
||||||
imageElement.style.display = 'block';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
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(image.time)}px`;
|
|
||||||
|
|
||||||
let imageElement = existingImageWrapper.querySelector('img');
|
|
||||||
this.setNSAttributesForElement(imageElement, {
|
|
||||||
src: image.thumbnailUrl || image.url
|
|
||||||
});
|
|
||||||
this.setImageDisplay(imageElement, showImagePlaceholders);
|
|
||||||
},
|
|
||||||
createImageWrapper(index, image, showImagePlaceholders) {
|
|
||||||
const id = `${ID_PREFIX}${image.time}`;
|
|
||||||
let imageWrapper = document.createElement('div');
|
|
||||||
imageWrapper.ariaLabel = id;
|
|
||||||
imageWrapper.classList.add(IMAGE_WRAPPER_CLASS);
|
|
||||||
imageWrapper.style.left = `${this.xScale(image.time)}px`;
|
|
||||||
this.setNSAttributesForElement(imageWrapper, {
|
|
||||||
id,
|
|
||||||
'data-show-image-placeholders': showImagePlaceholders
|
|
||||||
});
|
|
||||||
//create image vertical tick indicator
|
|
||||||
let imageTickElement = document.createElement('div');
|
|
||||||
imageTickElement.classList.add('c-imagery-tsv__image-handle');
|
|
||||||
imageTickElement.style.width = '2px';
|
|
||||||
imageTickElement.style.height = `${String(ROW_HEIGHT - 10)}px`;
|
|
||||||
imageWrapper.appendChild(imageTickElement);
|
|
||||||
|
|
||||||
//create placeholder - this will also hold the actual image
|
|
||||||
let imagePlaceholder = document.createElement('div');
|
|
||||||
imagePlaceholder.classList.add('c-imagery-tsv__image-placeholder');
|
|
||||||
imagePlaceholder.style.width = `${IMAGE_SIZE}px`;
|
|
||||||
imagePlaceholder.style.height = `${IMAGE_SIZE}px`;
|
|
||||||
imageWrapper.appendChild(imagePlaceholder);
|
|
||||||
|
|
||||||
//create image element
|
|
||||||
let imageElement = document.createElement('img');
|
|
||||||
this.setNSAttributesForElement(imageElement, {
|
|
||||||
src: image.thumbnailUrl || image.url
|
|
||||||
});
|
|
||||||
imageElement.style.width = `${IMAGE_SIZE}px`;
|
|
||||||
imageElement.style.height = `${IMAGE_SIZE}px`;
|
|
||||||
this.setImageDisplay(imageElement, showImagePlaceholders);
|
|
||||||
|
|
||||||
//handle mousedown event to show the image in a large view
|
|
||||||
imageWrapper.addEventListener('mousedown', (e) => {
|
|
||||||
if (e.button === 0) {
|
|
||||||
this.expand(image.time);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
imagePlaceholder.appendChild(imageElement);
|
|
||||||
|
|
||||||
return imageWrapper;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
385
src/plugins/events/components/EventTimelineView.vue
Normal file
385
src/plugins/events/components/EventTimelineView.vue
Normal file
@ -0,0 +1,385 @@
|
|||||||
|
<!--
|
||||||
|
Open MCT, Copyright (c) 2014-2024, 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="events" class="c-event-tsv c-timeline-holder">
|
||||||
|
<div ref="eventsHolder" class="c-event-tsv__contents u-contents"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { scaleLinear, scaleUtc } from 'd3-scale';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import mount from 'utils/mount';
|
||||||
|
|
||||||
|
import SwimLane from '@/ui/components/swim-lane/SwimLane.vue';
|
||||||
|
import { PREVIEW_ACTION_KEY } from '@/ui/preview/PreviewAction.js';
|
||||||
|
|
||||||
|
const PADDING = 1;
|
||||||
|
const ROW_HEIGHT = 100;
|
||||||
|
const EVENT_DETAIL_SIZE = 85;
|
||||||
|
const CONTAINER_CLASS = 'c-event-tsv-container';
|
||||||
|
const NO_ITEMS_CLASS = 'c-event-tsv__no-items';
|
||||||
|
const EVENT_WRAPPER_CLASS = 'c-event-tsv__event-wrapper';
|
||||||
|
const ID_PREFIX = 'wrapper-';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
inject: ['openmct', 'domainObject', 'objectPath'],
|
||||||
|
data() {
|
||||||
|
let timeSystem = this.openmct.time.getTimeSystem();
|
||||||
|
this.metadata = {};
|
||||||
|
this.requestCount = 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
viewBounds: undefined,
|
||||||
|
height: 0,
|
||||||
|
durationFormatter: undefined,
|
||||||
|
eventHistory: [],
|
||||||
|
timeSystem: timeSystem,
|
||||||
|
keyString: undefined
|
||||||
|
};
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
eventHistory: {
|
||||||
|
handler(newHistory, oldHistory) {
|
||||||
|
this.updatePlotEvents();
|
||||||
|
},
|
||||||
|
deep: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.previewAction = this.openmct.actions.getAction(PREVIEW_ACTION_KEY);
|
||||||
|
|
||||||
|
this.canvas = this.$refs.events.appendChild(document.createElement('canvas'));
|
||||||
|
this.canvas.height = 0;
|
||||||
|
this.canvasContext = this.canvas.getContext('2d');
|
||||||
|
this.setDimensions();
|
||||||
|
|
||||||
|
this.setScaleAndPlotEvents = this.setScaleAndPlotEvents.bind(this);
|
||||||
|
this.updateViewBounds = this.updateViewBounds.bind(this);
|
||||||
|
this.setTimeContext = this.setTimeContext.bind(this);
|
||||||
|
this.setTimeContext();
|
||||||
|
|
||||||
|
this.updateViewBounds();
|
||||||
|
|
||||||
|
this.resize = _.debounce(this.resize, 400);
|
||||||
|
this.eventStripResizeObserver = new ResizeObserver(this.resize);
|
||||||
|
this.eventStripResizeObserver.observe(this.$refs.events);
|
||||||
|
|
||||||
|
this.unlisten = this.openmct.objects.observe(this.domainObject, '*', this.observeForChanges);
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
if (this.eventStripResizeObserver) {
|
||||||
|
this.eventStripResizeObserver.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.stopFollowingTimeContext();
|
||||||
|
if (this.unlisten) {
|
||||||
|
this.unlisten();
|
||||||
|
}
|
||||||
|
if (this.destroyEventContainer) {
|
||||||
|
this.destroyEventContainer();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setTimeContext() {
|
||||||
|
this.stopFollowingTimeContext();
|
||||||
|
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
|
||||||
|
this.timeContext.on('timeSystem', this.setScaleAndPlotEvents);
|
||||||
|
this.timeContext.on('boundsChanged', this.updateViewBounds);
|
||||||
|
},
|
||||||
|
stopFollowingTimeContext() {
|
||||||
|
if (this.timeContext) {
|
||||||
|
this.timeContext.off('timeSystem', this.setScaleAndPlotEvents);
|
||||||
|
this.timeContext.off('boundsChanged', this.updateViewBounds);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
expand(eventTimestamp) {
|
||||||
|
const path = this.objectPath[0];
|
||||||
|
this.previewAction.invoke([path], {
|
||||||
|
timestamp: eventTimestamp,
|
||||||
|
objectPath: this.objectPath
|
||||||
|
});
|
||||||
|
},
|
||||||
|
observeForChanges(mutatedObject) {
|
||||||
|
this.updateViewBounds();
|
||||||
|
},
|
||||||
|
resize() {
|
||||||
|
let clientWidth = this.getClientWidth();
|
||||||
|
if (clientWidth !== this.width) {
|
||||||
|
this.setDimensions();
|
||||||
|
this.updateViewBounds();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getClientWidth() {
|
||||||
|
let clientWidth = this.$refs.events.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.timeContext.getBounds();
|
||||||
|
|
||||||
|
if (this.timeSystem === undefined) {
|
||||||
|
this.timeSystem = this.timeContext.getTimeSystem();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setScaleAndPlotEvents(this.timeSystem, !isTick);
|
||||||
|
},
|
||||||
|
setScaleAndPlotEvents(timeSystem, clearAllEvents) {
|
||||||
|
if (timeSystem !== undefined) {
|
||||||
|
this.timeSystem = timeSystem;
|
||||||
|
this.timeFormatter = this.getFormatter(this.timeSystem.key);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setScale(this.timeSystem);
|
||||||
|
this.updatePlotEvents(clearAllEvents);
|
||||||
|
},
|
||||||
|
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;
|
||||||
|
},
|
||||||
|
updatePlotEvents(clearAllEvents) {
|
||||||
|
this.clearPreviousEvents(clearAllEvents);
|
||||||
|
if (this.xScale) {
|
||||||
|
this.drawEvents();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
clearPreviousEvents(clearAllEvents) {
|
||||||
|
//TODO: Only clear items that are out of bounds
|
||||||
|
let noItemsEl = this.$el.querySelectorAll(`.${NO_ITEMS_CLASS}`);
|
||||||
|
noItemsEl.forEach((item) => {
|
||||||
|
item.remove();
|
||||||
|
});
|
||||||
|
let events = this.$el.querySelectorAll(`.${EVENT_WRAPPER_CLASS}`);
|
||||||
|
events.forEach((eventElm) => {
|
||||||
|
if (clearAllEvents) {
|
||||||
|
eventElm.remove();
|
||||||
|
} else {
|
||||||
|
const id = eventElm.getAttributeNS(null, 'id');
|
||||||
|
if (id) {
|
||||||
|
const timestamp = id.replace(ID_PREFIX, '');
|
||||||
|
if (
|
||||||
|
!this.isEventInBounds({
|
||||||
|
time: timestamp
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
eventElm.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setDimensions() {
|
||||||
|
const eventsHolder = this.$refs.events;
|
||||||
|
this.width = this.getClientWidth();
|
||||||
|
|
||||||
|
this.height = Math.round(eventsHolder.getBoundingClientRect().height);
|
||||||
|
},
|
||||||
|
setScale(timeSystem) {
|
||||||
|
if (!this.width) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeSystem === undefined) {
|
||||||
|
timeSystem = this.timeContext.getTimeSystem();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeSystem.isUTCBased) {
|
||||||
|
this.xScale = scaleUtc();
|
||||||
|
this.xScale.domain([new Date(this.viewBounds.start), new Date(this.viewBounds.end)]);
|
||||||
|
} else {
|
||||||
|
this.xScale = scaleLinear();
|
||||||
|
this.xScale.domain([this.viewBounds.start, this.viewBounds.end]);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.xScale.range([PADDING, this.width - PADDING * 2]);
|
||||||
|
},
|
||||||
|
isEventInBounds(evenObj) {
|
||||||
|
return evenObj.time <= this.viewBounds.end && evenObj.time >= this.viewBounds.start;
|
||||||
|
},
|
||||||
|
getEventsContainer() {
|
||||||
|
let containerHeight = 100;
|
||||||
|
let containerWidth = this.eventHistory.length ? this.width : 200;
|
||||||
|
let eventContainer;
|
||||||
|
|
||||||
|
let existingContainer = this.$el.querySelector(`.${CONTAINER_CLASS}`);
|
||||||
|
if (existingContainer) {
|
||||||
|
eventContainer = existingContainer;
|
||||||
|
eventContainer.style.maxWidth = `${containerWidth}px`;
|
||||||
|
} else {
|
||||||
|
if (this.destroyEventsContainer) {
|
||||||
|
this.destroyEventsContainer();
|
||||||
|
}
|
||||||
|
const { vNode, destroy } = mount(
|
||||||
|
{
|
||||||
|
components: {
|
||||||
|
SwimLane
|
||||||
|
},
|
||||||
|
provide: {
|
||||||
|
openmct: this.openmct
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isNested: true
|
||||||
|
};
|
||||||
|
},
|
||||||
|
template: `<swim-lane :is-nested="isNested" :hide-label="true"><template v-slot:object><div class="c-event-tsv-container"></div></template></swim-lane>`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
app: this.openmct.app
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
this.destroyEventsContainer = destroy;
|
||||||
|
const component = vNode.componentInstance;
|
||||||
|
this.$refs.eventsHolder.appendChild(component.$el);
|
||||||
|
|
||||||
|
eventContainer = component.$el.querySelector(`.${CONTAINER_CLASS}`);
|
||||||
|
eventContainer.style.maxWidth = `${containerWidth}px`;
|
||||||
|
eventContainer.style.height = `${containerHeight}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return eventContainer;
|
||||||
|
},
|
||||||
|
drawEvents() {
|
||||||
|
let eventContainer = this.getEventsContainer();
|
||||||
|
let index = 0;
|
||||||
|
if (this.eventHistory.length) {
|
||||||
|
this.eventHistory.forEach((currentEventObject) => {
|
||||||
|
if (this.isEventsInBounds(currentEventObject)) {
|
||||||
|
this.plotEvents(currentEventObject, eventContainer, index);
|
||||||
|
index = index + 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.plotNoItems(eventContainer);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plotNoItems(containerElement) {
|
||||||
|
let textElement = document.createElement('text');
|
||||||
|
textElement.classList.add(NO_ITEMS_CLASS);
|
||||||
|
textElement.innerHTML = 'No events within timeframe';
|
||||||
|
|
||||||
|
containerElement.appendChild(textElement);
|
||||||
|
},
|
||||||
|
setNSAttributesForElement(element, attributes) {
|
||||||
|
if (!element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.keys(attributes).forEach((key) => {
|
||||||
|
element.setAttributeNS(null, key, attributes[key]);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setStyles(element, styles) {
|
||||||
|
if (!element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.keys(styles).forEach((key) => {
|
||||||
|
element.style[key] = styles[key];
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getEventWrapper(item) {
|
||||||
|
const id = `${ID_PREFIX}${item.time}`;
|
||||||
|
|
||||||
|
return this.$el.querySelector(`.c-events-tsv__contents div[id=${id}]`);
|
||||||
|
},
|
||||||
|
plotEvents(item, containerElement, index) {
|
||||||
|
let existingEventWrapper = this.getEventWrapper(item);
|
||||||
|
//eventWrapper wraps the vertical tick and the EVENT
|
||||||
|
if (existingEventWrapper) {
|
||||||
|
this.updateExistingEventWrapper(existingEventWrapper, item);
|
||||||
|
} else {
|
||||||
|
let eventWrapper = this.createEventWrapper(index, item);
|
||||||
|
containerElement.appendChild(eventWrapper);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setEventDisplay(eventElement) {
|
||||||
|
eventElement.style.display = 'block';
|
||||||
|
},
|
||||||
|
updateExistingEventWrapper(existingEventWrapper, event) {
|
||||||
|
//Update the x co-ordinates of the event wrapper and the url of event
|
||||||
|
//this is to avoid tearing down all elements completely and re-drawing them
|
||||||
|
existingEventWrapper.style.left = `${this.xScale(event.time)}px`;
|
||||||
|
|
||||||
|
let eventElement = existingEventWrapper.querySelector('img');
|
||||||
|
this.setNSAttributesForElement(eventElement, {
|
||||||
|
src: event.thumbnailUrl || event.url
|
||||||
|
});
|
||||||
|
this.setEventDisplay(eventElement);
|
||||||
|
},
|
||||||
|
createEventWrapper(index, event) {
|
||||||
|
const id = `${ID_PREFIX}${event.time}`;
|
||||||
|
let eventWrapper = document.createElement('div');
|
||||||
|
eventWrapper.ariaLabel = id;
|
||||||
|
eventWrapper.classList.add(EVENT_WRAPPER_CLASS);
|
||||||
|
eventWrapper.style.left = `${this.xScale(event.time)}px`;
|
||||||
|
//create event vertical tick indicator
|
||||||
|
let eventTickElement = document.createElement('div');
|
||||||
|
eventTickElement.classList.add('c-events-tsv__event-handle');
|
||||||
|
eventTickElement.style.width = '2px';
|
||||||
|
eventTickElement.style.height = `${String(ROW_HEIGHT - 10)}px`;
|
||||||
|
eventWrapper.appendChild(eventTickElement);
|
||||||
|
|
||||||
|
//create placeholder - this will also hold the actual event
|
||||||
|
let eventPlaceholder = document.createElement('div');
|
||||||
|
eventPlaceholder.classList.add('c-events-tsv__event-placeholder');
|
||||||
|
eventPlaceholder.style.width = `${EVENT_DETAIL_SIZE}px`;
|
||||||
|
eventPlaceholder.style.height = `${EVENT_DETAIL_SIZE}px`;
|
||||||
|
eventWrapper.appendChild(eventPlaceholder);
|
||||||
|
|
||||||
|
//create event element
|
||||||
|
let eventElement = document.createElement('img');
|
||||||
|
this.setNSAttributesForElement(eventElement, {
|
||||||
|
src: event.thumbnailUrl || event.url
|
||||||
|
});
|
||||||
|
eventElement.style.width = `${EVENT_DETAIL_SIZE}px`;
|
||||||
|
eventElement.style.height = `${EVENT_DETAIL_SIZE}px`;
|
||||||
|
this.setEventDisplay(eventElement);
|
||||||
|
|
||||||
|
//handle mousedown event to show the event in a large view
|
||||||
|
eventWrapper.addEventListener('mousedown', (e) => {
|
||||||
|
if (e.button === 0) {
|
||||||
|
this.expand(event.time);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
eventPlaceholder.appendChild(eventElement);
|
||||||
|
|
||||||
|
return eventWrapper;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
@ -20,10 +20,10 @@
|
|||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
import EventTimestripViewProvider from './EventTimestripViewProvider.js';
|
import EventTimelineViewProvider from './EventTimelineViewProvider.js';
|
||||||
|
|
||||||
export default function (options) {
|
export default function (options) {
|
||||||
return function install(openmct) {
|
return function install(openmct) {
|
||||||
openmct.objectViews.addProvider(new EventTimestripViewProvider(openmct));
|
openmct.objectViews.addProvider(new EventTimelineViewProvider(openmct));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -33,10 +33,7 @@ export default function TimelineCompositionPolicy(openmct) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function hasDomainAndRange(metadata) {
|
function hasDomainAndRange(metadata) {
|
||||||
return (
|
return metadata.valuesForHints(['domain']).length > 0;
|
||||||
metadata.valuesForHints(['range']).length > 0 &&
|
|
||||||
metadata.valuesForHints(['domain']).length > 0
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasImageTelemetry(domainObject, metadata) {
|
function hasImageTelemetry(domainObject, metadata) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user