[Imagery Plugin] Enhancements - Compass, HUD, Freshness (#3675)
* Adds a compass rose component showing spacecraft and camera pointing direction in images, as well as sun location. * Adds a "heads up display" component that shows heading at the top of images, as well as sun direction * Adds freshness indicators for spacecraft and camera position Co-authored-by: Jamie Vigliotta <jamie.j.vigliotta@nasa.gov> Co-authored-by: David Tsay <david.e.tsay@nasa.gov> Co-authored-by: charlesh88 <charlesh88@gmail.com> Co-authored-by: Andrew Henry <akhenry@gmail.com> Co-authored-by: charlesh88 <charles.f.hacskaylo@nasa.gov> Co-authored-by: David Tsay <3614296+davetsay@users.noreply.github.com>
This commit is contained in:
@@ -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.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
import ImageryViewLayout from './components/ImageryViewLayout.vue';
|
import ImageryViewLayout from './components/ImageryViewLayout.vue';
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
|
||||||
|
|||||||
131
src/plugins/imagery/components/Compass/Compass.vue
Normal file
131
src/plugins/imagery/components/Compass/Compass.vue
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* 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-compass"
|
||||||
|
:style="compassDimensionsStyle"
|
||||||
|
>
|
||||||
|
<CompassHUD
|
||||||
|
v-if="hasCameraFieldOfView"
|
||||||
|
:heading="heading"
|
||||||
|
:sun-heading="sunHeading"
|
||||||
|
:camera-angle-of-view="cameraAngleOfView"
|
||||||
|
:camera-pan="cameraPan"
|
||||||
|
/>
|
||||||
|
<CompassRose
|
||||||
|
v-if="hasCameraFieldOfView"
|
||||||
|
:heading="heading"
|
||||||
|
:sun-heading="sunHeading"
|
||||||
|
:camera-angle-of-view="cameraAngleOfView"
|
||||||
|
:camera-pan="cameraPan"
|
||||||
|
:lock-compass="lockCompass"
|
||||||
|
@toggle-lock-compass="toggleLockCompass"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import CompassHUD from './CompassHUD.vue';
|
||||||
|
import CompassRose from './CompassRose.vue';
|
||||||
|
|
||||||
|
const CAMERA_ANGLE_OF_VIEW = 70;
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
CompassHUD,
|
||||||
|
CompassRose
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
containerWidth: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
containerHeight: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
naturalAspectRatio: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
image: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
lockCompass: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
hasCameraFieldOfView() {
|
||||||
|
return this.heading !== undefined && this.cameraPan !== undefined;
|
||||||
|
},
|
||||||
|
// compass direction from north in degrees
|
||||||
|
heading() {
|
||||||
|
return this.image.heading;
|
||||||
|
},
|
||||||
|
pitch() {
|
||||||
|
return this.image.pitch;
|
||||||
|
},
|
||||||
|
// compass direction from north in degrees
|
||||||
|
sunHeading() {
|
||||||
|
return this.image.sunOrientation;
|
||||||
|
},
|
||||||
|
// relative direction from heading in degrees
|
||||||
|
cameraPan() {
|
||||||
|
return this.image.cameraPan;
|
||||||
|
},
|
||||||
|
cameraTilt() {
|
||||||
|
return this.image.cameraTilt;
|
||||||
|
},
|
||||||
|
cameraAngleOfView() {
|
||||||
|
return CAMERA_ANGLE_OF_VIEW;
|
||||||
|
},
|
||||||
|
compassDimensionsStyle() {
|
||||||
|
const containerAspectRatio = this.containerWidth / this.containerHeight;
|
||||||
|
|
||||||
|
let width;
|
||||||
|
let height;
|
||||||
|
|
||||||
|
if (containerAspectRatio < this.naturalAspectRatio) {
|
||||||
|
width = '100%';
|
||||||
|
height = `${ this.containerWidth / this.naturalAspectRatio }px`;
|
||||||
|
} else {
|
||||||
|
width = `${ this.containerHeight * this.naturalAspectRatio }px`;
|
||||||
|
height = '100%';
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
width: width,
|
||||||
|
height: height
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toggleLockCompass() {
|
||||||
|
this.$emit('toggle-lock-compass');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
145
src/plugins/imagery/components/Compass/CompassHUD.vue
Normal file
145
src/plugins/imagery/components/Compass/CompassHUD.vue
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* 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-compass__hud c-hud"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="point in visibleCompassPoints"
|
||||||
|
:key="point.direction"
|
||||||
|
:class="point.class"
|
||||||
|
:style="point.style"
|
||||||
|
>
|
||||||
|
{{ point.direction }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="isSunInRange"
|
||||||
|
ref="sun"
|
||||||
|
class="c-hud__sun"
|
||||||
|
:style="sunPositionStyle"
|
||||||
|
></div>
|
||||||
|
<div class="c-hud__range"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
rotate,
|
||||||
|
inRange,
|
||||||
|
percentOfRange
|
||||||
|
} from './utils';
|
||||||
|
|
||||||
|
const COMPASS_POINTS = [
|
||||||
|
{
|
||||||
|
direction: 'N',
|
||||||
|
class: 'c-hud__dir',
|
||||||
|
degrees: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
direction: 'NE',
|
||||||
|
class: 'c-hud__dir--sub',
|
||||||
|
degrees: 45
|
||||||
|
},
|
||||||
|
{
|
||||||
|
direction: 'E',
|
||||||
|
class: 'c-hud__dir',
|
||||||
|
degrees: 90
|
||||||
|
},
|
||||||
|
{
|
||||||
|
direction: 'SE',
|
||||||
|
class: 'c-hud__dir--sub',
|
||||||
|
degrees: 135
|
||||||
|
},
|
||||||
|
{
|
||||||
|
direction: 'S',
|
||||||
|
class: 'c-hud__dir',
|
||||||
|
degrees: 180
|
||||||
|
},
|
||||||
|
{
|
||||||
|
direction: 'SW',
|
||||||
|
class: 'c-hud__dir--sub',
|
||||||
|
degrees: 225
|
||||||
|
},
|
||||||
|
{
|
||||||
|
direction: 'W',
|
||||||
|
class: 'c-hud__dir',
|
||||||
|
degrees: 270
|
||||||
|
},
|
||||||
|
{
|
||||||
|
direction: 'NW',
|
||||||
|
class: 'c-hud__dir--sub',
|
||||||
|
degrees: 315
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
heading: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
sunHeading: {
|
||||||
|
type: Number,
|
||||||
|
default: undefined
|
||||||
|
},
|
||||||
|
cameraAngleOfView: {
|
||||||
|
type: Number,
|
||||||
|
default: undefined
|
||||||
|
},
|
||||||
|
cameraPan: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
visibleCompassPoints() {
|
||||||
|
return COMPASS_POINTS
|
||||||
|
.filter(point => inRange(point.degrees, this.visibleRange))
|
||||||
|
.map(point => {
|
||||||
|
const percentage = percentOfRange(point.degrees, this.visibleRange);
|
||||||
|
point.style = Object.assign(
|
||||||
|
{ left: `${ percentage * 100 }%` }
|
||||||
|
);
|
||||||
|
|
||||||
|
return point;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
isSunInRange() {
|
||||||
|
return inRange(this.sunHeading, this.visibleRange);
|
||||||
|
},
|
||||||
|
sunPositionStyle() {
|
||||||
|
const percentage = percentOfRange(this.sunHeading, this.visibleRange);
|
||||||
|
|
||||||
|
return {
|
||||||
|
left: `${ percentage * 100 }%`
|
||||||
|
};
|
||||||
|
},
|
||||||
|
visibleRange() {
|
||||||
|
return [
|
||||||
|
rotate(this.heading, this.cameraPan, -this.cameraAngleOfView / 2),
|
||||||
|
rotate(this.heading, this.cameraPan, this.cameraAngleOfView / 2)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
263
src/plugins/imagery/components/Compass/CompassRose.vue
Normal file
263
src/plugins/imagery/components/Compass/CompassRose.vue
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* 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-direction-rose"
|
||||||
|
@click="toggleLockCompass"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="c-nsew"
|
||||||
|
:style="rotateFrameStyle"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="c-nsew__minor-ticks"
|
||||||
|
viewBox="0 0 100 100"
|
||||||
|
>
|
||||||
|
<rect
|
||||||
|
class="c-nsew__tick c-tick-ne"
|
||||||
|
x="49"
|
||||||
|
y="0"
|
||||||
|
width="2"
|
||||||
|
height="5"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
class="c-nsew__tick c-tick-se"
|
||||||
|
x="95"
|
||||||
|
y="49"
|
||||||
|
width="5"
|
||||||
|
height="2"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
class="c-nsew__tick c-tick-sw"
|
||||||
|
x="49"
|
||||||
|
y="95"
|
||||||
|
width="2"
|
||||||
|
height="5"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
class="c-nsew__tick c-tick-nw"
|
||||||
|
x="0"
|
||||||
|
y="49"
|
||||||
|
width="5"
|
||||||
|
height="2"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
class="c-nsew__ticks"
|
||||||
|
viewBox="0 0 100 100"
|
||||||
|
>
|
||||||
|
<polygon
|
||||||
|
class="c-nsew__tick c-tick-n"
|
||||||
|
points="50,0 57,5 43,5"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
class="c-nsew__tick c-tick-e"
|
||||||
|
x="95"
|
||||||
|
y="49"
|
||||||
|
width="5"
|
||||||
|
height="2"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
class="c-nsew__tick c-tick-w"
|
||||||
|
x="0"
|
||||||
|
y="49"
|
||||||
|
width="5"
|
||||||
|
height="2"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
class="c-nsew__tick c-tick-s"
|
||||||
|
x="49"
|
||||||
|
y="95"
|
||||||
|
width="2"
|
||||||
|
height="5"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<text
|
||||||
|
class="c-nsew__label c-label-n"
|
||||||
|
text-anchor="middle"
|
||||||
|
:transform="northTextTransform"
|
||||||
|
>N</text>
|
||||||
|
<text
|
||||||
|
class="c-nsew__label c-label-e"
|
||||||
|
text-anchor="middle"
|
||||||
|
:transform="eastTextTransform"
|
||||||
|
>E</text>
|
||||||
|
<text
|
||||||
|
class="c-nsew__label c-label-w"
|
||||||
|
text-anchor="middle"
|
||||||
|
:transform="southTextTransform"
|
||||||
|
>W</text>
|
||||||
|
<text
|
||||||
|
class="c-nsew__label c-label-s"
|
||||||
|
text-anchor="middle"
|
||||||
|
:transform="westTextTransform"
|
||||||
|
>S</text>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="c-spacecraft-body"
|
||||||
|
:style="headingStyle"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="c-sun"
|
||||||
|
:style="sunHeadingStyle"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="showCameraFOV"
|
||||||
|
class="c-cam-field"
|
||||||
|
:style="cameraHeadingStyle"
|
||||||
|
>
|
||||||
|
<div class="cam-field-half cam-field-half-l">
|
||||||
|
<div
|
||||||
|
class="cam-field-area"
|
||||||
|
:style="cameraFOVStyleLeftHalf"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<div class="cam-field-half cam-field-half-r">
|
||||||
|
<div
|
||||||
|
class="cam-field-area"
|
||||||
|
:style="cameraFOVStyleRightHalf"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { rotate } from './utils';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
heading: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
sunHeading: {
|
||||||
|
type: Number,
|
||||||
|
default: undefined
|
||||||
|
},
|
||||||
|
cameraAngleOfView: {
|
||||||
|
type: Number,
|
||||||
|
default: undefined
|
||||||
|
},
|
||||||
|
cameraPan: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
lockCompass: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
cameraHeading() {
|
||||||
|
return rotate(this.heading, this.cameraPan);
|
||||||
|
},
|
||||||
|
compassHeading() {
|
||||||
|
return this.lockCompass ? this.cameraHeading : 0;
|
||||||
|
},
|
||||||
|
north() {
|
||||||
|
return rotate(this.compassHeading, -this.cameraHeading);
|
||||||
|
},
|
||||||
|
rotateFrameStyle() {
|
||||||
|
return { transform: `rotate(${ this.north }deg)` };
|
||||||
|
},
|
||||||
|
northTextTransform() {
|
||||||
|
return this.cardinalPointsTextTransform.north;
|
||||||
|
},
|
||||||
|
eastTextTransform() {
|
||||||
|
return this.cardinalPointsTextTransform.east;
|
||||||
|
},
|
||||||
|
southTextTransform() {
|
||||||
|
return this.cardinalPointsTextTransform.south;
|
||||||
|
},
|
||||||
|
westTextTransform() {
|
||||||
|
return this.cardinalPointsTextTransform.west;
|
||||||
|
},
|
||||||
|
cardinalPointsTextTransform() {
|
||||||
|
/**
|
||||||
|
* cardinal points text must be rotated
|
||||||
|
* in the opposite direction that north is rotated
|
||||||
|
* to keep text vertically oriented
|
||||||
|
*/
|
||||||
|
const rotation = `rotate(${ -this.north })`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
north: `translate(50,15) ${ rotation }`,
|
||||||
|
east: `translate(87,50) ${ rotation }`,
|
||||||
|
south: `translate(13,50) ${ rotation }`,
|
||||||
|
west: `translate(50,87) ${ rotation }`
|
||||||
|
};
|
||||||
|
},
|
||||||
|
headingStyle() {
|
||||||
|
const rotation = rotate(this.north, this.heading);
|
||||||
|
|
||||||
|
return {
|
||||||
|
transform: `translateX(-50%) rotate(${ rotation }deg)`
|
||||||
|
};
|
||||||
|
},
|
||||||
|
cameraHeadingStyle() {
|
||||||
|
const rotation = rotate(this.north, this.cameraHeading);
|
||||||
|
|
||||||
|
return {
|
||||||
|
transform: `rotate(${ rotation }deg)`
|
||||||
|
};
|
||||||
|
},
|
||||||
|
sunHeadingStyle() {
|
||||||
|
const rotation = rotate(this.north, this.sunHeading);
|
||||||
|
|
||||||
|
return {
|
||||||
|
transform: `rotate(${ rotation }deg)`
|
||||||
|
};
|
||||||
|
},
|
||||||
|
showCameraFOV() {
|
||||||
|
return this.cameraPan !== undefined && this.cameraAngleOfView > 0;
|
||||||
|
},
|
||||||
|
// left half of camera field of view
|
||||||
|
// rotated counter-clockwise from camera field of view heading
|
||||||
|
cameraFOVStyleLeftHalf() {
|
||||||
|
return {
|
||||||
|
transform: `translateX(50%) rotate(${ -this.cameraAngleOfView / 2 }deg)`
|
||||||
|
};
|
||||||
|
},
|
||||||
|
// right half of camera field of view
|
||||||
|
// rotated clockwise from camera field of view heading
|
||||||
|
cameraFOVStyleRightHalf() {
|
||||||
|
return {
|
||||||
|
transform: `translateX(-50%) rotate(${ this.cameraAngleOfView / 2 }deg)`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toggleLockCompass() {
|
||||||
|
this.$emit('toggle-lock-compass');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
214
src/plugins/imagery/components/Compass/compass.scss
Normal file
214
src/plugins/imagery/components/Compass/compass.scss
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
/***************************** THEME/UI CONSTANTS AND MIXINS */
|
||||||
|
$interfaceKeyColor: #00B9C5;
|
||||||
|
$elemBg: rgba(black, 0.7);
|
||||||
|
|
||||||
|
@mixin sun($position: 'circle closest-side') {
|
||||||
|
$color: #ff9900;
|
||||||
|
$gradEdgePerc: 60%;
|
||||||
|
background: radial-gradient(#{$position}, $color, $color $gradEdgePerc, rgba($color, 0.4) $gradEdgePerc + 5%, transparent);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.c-compass {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
z-index: 1;
|
||||||
|
@include userSelectNone;
|
||||||
|
}
|
||||||
|
|
||||||
|
/***************************** COMPASS HUD */
|
||||||
|
.c-hud {
|
||||||
|
// To be placed within a imagery view, in the bounding box of the image
|
||||||
|
$m: 1px;
|
||||||
|
$padTB: 2px;
|
||||||
|
$padLR: $padTB;
|
||||||
|
color: $interfaceKeyColor;
|
||||||
|
font-size: 0.8em;
|
||||||
|
position: absolute;
|
||||||
|
top: $m; right: $m; left: $m;
|
||||||
|
height: 18px;
|
||||||
|
|
||||||
|
svg, div {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__display {
|
||||||
|
height: 30px;
|
||||||
|
pointer-events: all;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__range {
|
||||||
|
border: 1px solid $interfaceKeyColor;
|
||||||
|
border-top-color: transparent;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%; right: $padLR; bottom: $padTB; left: $padLR;
|
||||||
|
}
|
||||||
|
|
||||||
|
[class*="__dir"] {
|
||||||
|
// NSEW
|
||||||
|
display: inline-block;
|
||||||
|
font-weight: bold;
|
||||||
|
text-shadow: 0 1px 2px black;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%,-50%);
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
[class*="__dir--sub"] {
|
||||||
|
font-weight: normal;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__sun {
|
||||||
|
$s: 10px;
|
||||||
|
@include sun('circle farthest-side at bottom');
|
||||||
|
bottom: $padTB + 2px;
|
||||||
|
height: $s; width: $s*2;
|
||||||
|
opacity: 0.8;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/***************************** COMPASS DIRECTIONS */
|
||||||
|
.c-nsew {
|
||||||
|
$color: $interfaceKeyColor;
|
||||||
|
$inset: 7%;
|
||||||
|
$tickHeightPerc: 15%;
|
||||||
|
text-shadow: black 0 0 10px;
|
||||||
|
top: $inset; right: $inset; bottom: $inset; left: $inset;
|
||||||
|
z-index: 3;
|
||||||
|
|
||||||
|
&__tick,
|
||||||
|
&__label {
|
||||||
|
fill: $color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__minor-ticks {
|
||||||
|
opacity: 0.5;
|
||||||
|
transform-origin: center;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__label {
|
||||||
|
dominant-baseline: central;
|
||||||
|
font-size: 0.8em;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.c-label-n {
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/***************************** CAMERA FIELD ANGLE */
|
||||||
|
.c-cam-field {
|
||||||
|
$color: white;
|
||||||
|
opacity: 0.2;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 2;
|
||||||
|
|
||||||
|
.cam-field-half {
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
.cam-field-area {
|
||||||
|
background: $color;
|
||||||
|
top: -30%;
|
||||||
|
right: 0;
|
||||||
|
bottom: -30%;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// clip-paths overlap a bit to avoid a gap between halves
|
||||||
|
&-l {
|
||||||
|
clip-path: polygon(0 0, 50.5% 0, 50.5% 100%, 0 100%);
|
||||||
|
.cam-field-area {
|
||||||
|
transform-origin: left center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-r {
|
||||||
|
clip-path: polygon(49.5% 0, 100% 0, 100% 100%, 49.5% 100%);
|
||||||
|
.cam-field-area {
|
||||||
|
transform-origin: right center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/***************************** SPACECRAFT BODY */
|
||||||
|
.c-spacecraft-body {
|
||||||
|
$color: $interfaceKeyColor;
|
||||||
|
$s: 30%;
|
||||||
|
background: $color;
|
||||||
|
border-radius: 3px;
|
||||||
|
height: $s; width: $s;
|
||||||
|
left: 50%; top: 50%;
|
||||||
|
opacity: 0.4;
|
||||||
|
transform-origin: center top;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
// Direction arrow
|
||||||
|
$color: rgba(black, 0.5);
|
||||||
|
$arwPointerY: 60%;
|
||||||
|
$arwBodyOffset: 25%;
|
||||||
|
background: $color;
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 10%; right: 20%; bottom: 50%; left: 20%;
|
||||||
|
clip-path: polygon(50% 0, 100% $arwPointerY, 100%-$arwBodyOffset $arwPointerY, 100%-$arwBodyOffset 100%, $arwBodyOffset 100%, $arwBodyOffset $arwPointerY, 0 $arwPointerY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/***************************** DIRECTION ROSE */
|
||||||
|
.c-direction-rose {
|
||||||
|
$d: 100px;
|
||||||
|
$c2: rgba(white, 0.1);
|
||||||
|
background: $elemBg;
|
||||||
|
background-image: radial-gradient(circle closest-side, transparent, transparent 80%, $c2);
|
||||||
|
width: $d;
|
||||||
|
height: $d;
|
||||||
|
transform-origin: 0 0;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 10px; left: 10px;
|
||||||
|
clip-path: circle(50% at 50% 50%);
|
||||||
|
border-radius: 100%;
|
||||||
|
|
||||||
|
svg, div {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sun
|
||||||
|
.c-sun {
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
$s: 35%;
|
||||||
|
@include sun();
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0.7;
|
||||||
|
top: 0; left: 50%;
|
||||||
|
height:$s; width: $s;
|
||||||
|
transform: translate(-50%, -60%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
84
src/plugins/imagery/components/Compass/pluginSpec.js
Normal file
84
src/plugins/imagery/components/Compass/pluginSpec.js
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2020, 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 Compass from './Compass.vue';
|
||||||
|
import Vue from 'vue';
|
||||||
|
|
||||||
|
const COMPASS_ROSE_CLASS = '.c-direction-rose';
|
||||||
|
const COMPASS_HUD_CLASS = '.c-compass__hud';
|
||||||
|
|
||||||
|
describe("The Compass component", () => {
|
||||||
|
let app;
|
||||||
|
let instance;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
let imageDatum = {
|
||||||
|
heading: 100,
|
||||||
|
roll: 90,
|
||||||
|
pitch: 90,
|
||||||
|
cameraTilt: 100,
|
||||||
|
cameraPan: 90,
|
||||||
|
sunAngle: 30
|
||||||
|
};
|
||||||
|
let propsData = {
|
||||||
|
containerWidth: 600,
|
||||||
|
containerHeight: 600,
|
||||||
|
naturalAspectRatio: 0.9,
|
||||||
|
image: imageDatum
|
||||||
|
};
|
||||||
|
|
||||||
|
app = new Vue({
|
||||||
|
components: { Compass },
|
||||||
|
data() {
|
||||||
|
return propsData;
|
||||||
|
},
|
||||||
|
template: `<Compass
|
||||||
|
:container-width="containerWidth"
|
||||||
|
:container-height="containerHeight"
|
||||||
|
:natural-aspect-ratio="naturalAspectRatio"
|
||||||
|
:image="image" />`
|
||||||
|
});
|
||||||
|
instance = app.$mount();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
app.$destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when a heading value exists on the image", () => {
|
||||||
|
|
||||||
|
it("should display a compass rose", () => {
|
||||||
|
let compassRoseElement = instance.$el.querySelector(COMPASS_ROSE_CLASS
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(compassRoseElement).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should display a compass HUD", () => {
|
||||||
|
let compassHUDElement = instance.$el.querySelector(COMPASS_HUD_CLASS);
|
||||||
|
|
||||||
|
expect(compassHUDElement).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
33
src/plugins/imagery/components/Compass/utils.js
Normal file
33
src/plugins/imagery/components/Compass/utils.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
export function rotate(direction, ...rotations) {
|
||||||
|
const rotation = rotations.reduce((a, b) => a + b, 0);
|
||||||
|
|
||||||
|
return normalizeCompassDirection(direction + rotation);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeCompassDirection(degrees) {
|
||||||
|
const base = degrees % 360;
|
||||||
|
|
||||||
|
return base >= 0 ? base : 360 + base;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function inRange(degrees, [min, max]) {
|
||||||
|
return min > max
|
||||||
|
? (degrees >= min && degrees < 360) || (degrees <= max && degrees >= 0)
|
||||||
|
: degrees >= min && degrees <= max;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function percentOfRange(degrees, [min, max]) {
|
||||||
|
let distance = degrees;
|
||||||
|
let minRange = min;
|
||||||
|
let maxRange = max;
|
||||||
|
|
||||||
|
if (min > max) {
|
||||||
|
if (distance < max) {
|
||||||
|
distance += 360;
|
||||||
|
}
|
||||||
|
|
||||||
|
maxRange += 360;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (distance - minRange) / (maxRange - minRange);
|
||||||
|
}
|
||||||
@@ -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>
|
<template>
|
||||||
<div
|
<div
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@@ -36,14 +58,25 @@
|
|||||||
<div class="c-imagery__main-image__bg"
|
<div class="c-imagery__main-image__bg"
|
||||||
:class="{'paused unnsynced': isPaused,'stale':false }"
|
:class="{'paused unnsynced': isPaused,'stale':false }"
|
||||||
>
|
>
|
||||||
<div class="c-imagery__main-image__image js-imageryView-image"
|
<img
|
||||||
:style="{
|
ref="focusedImage"
|
||||||
'background-image': imageUrl ? `url(${imageUrl})` : 'none',
|
class="c-imagery__main-image__image js-imageryView-image"
|
||||||
'filter': `brightness(${filters.brightness}%) contrast(${filters.contrast}%)`
|
:src="imageUrl"
|
||||||
}"
|
:style="{
|
||||||
:data-openmct-image-timestamp="time"
|
'filter': `brightness(${filters.brightness}%) contrast(${filters.contrast}%)`
|
||||||
:data-openmct-object-keystring="keyString"
|
}"
|
||||||
></div>
|
:data-openmct-image-timestamp="time"
|
||||||
|
:data-openmct-object-keystring="keyString"
|
||||||
|
>
|
||||||
|
<Compass
|
||||||
|
v-if="shouldDisplayCompass"
|
||||||
|
:container-width="imageContainerWidth"
|
||||||
|
:container-height="imageContainerHeight"
|
||||||
|
:natural-aspect-ratio="focusedImageNaturalAspectRatio"
|
||||||
|
:image="focusedImage"
|
||||||
|
:lock-compass="lockCompass"
|
||||||
|
@toggle-lock-compass="toggleLockCompass"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="c-local-controls c-local-controls--show-on-hover c-imagery__prev-next-buttons">
|
<div class="c-local-controls c-local-controls--show-on-hover c-imagery__prev-next-buttons">
|
||||||
<button class="c-nav c-nav--prev"
|
<button class="c-nav c-nav--prev"
|
||||||
@@ -61,11 +94,25 @@
|
|||||||
<div class="c-imagery__control-bar">
|
<div class="c-imagery__control-bar">
|
||||||
<div class="c-imagery__time">
|
<div class="c-imagery__time">
|
||||||
<div class="c-imagery__timestamp u-style-receiver js-style-receiver">{{ time }}</div>
|
<div class="c-imagery__timestamp u-style-receiver js-style-receiver">{{ time }}</div>
|
||||||
|
|
||||||
|
<!-- image fresh -->
|
||||||
<div
|
<div
|
||||||
v-if="canTrackDuration"
|
v-if="canTrackDuration"
|
||||||
:class="{'c-imagery--new': isImageNew && !refreshCSS}"
|
:class="{'c-imagery--new': isImageNew && !refreshCSS}"
|
||||||
class="c-imagery__age icon-timer"
|
class="c-imagery__age icon-timer"
|
||||||
>{{ formattedDuration }}</div>
|
>{{ formattedDuration }}</div>
|
||||||
|
|
||||||
|
<!-- spacecraft position fresh -->
|
||||||
|
<div
|
||||||
|
v-if="relatedTelemetry.hasRelatedTelemetry && isSpacecraftPositionFresh"
|
||||||
|
class="c-imagery__age icon-check c-imagery--new"
|
||||||
|
>POS</div>
|
||||||
|
|
||||||
|
<!-- camera position fresh -->
|
||||||
|
<div
|
||||||
|
v-if="relatedTelemetry.hasRelatedTelemetry && isCameraPositionFresh"
|
||||||
|
class="c-imagery__age icon-check c-imagery--new"
|
||||||
|
>CAM</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="h-local-controls">
|
<div class="h-local-controls">
|
||||||
<button
|
<button
|
||||||
@@ -76,13 +123,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div ref="thumbsWrapper"
|
<div
|
||||||
class="c-imagery__thumbs-wrapper"
|
ref="thumbsWrapper"
|
||||||
:class="{'is-paused': isPaused}"
|
class="c-imagery__thumbs-wrapper"
|
||||||
@scroll="handleScroll"
|
:class="{'is-paused': isPaused}"
|
||||||
|
@scroll="handleScroll"
|
||||||
>
|
>
|
||||||
<div v-for="(datum, index) in imageHistory"
|
<div v-for="(datum, index) in imageHistory"
|
||||||
:key="datum.url"
|
:key="datum.url + datum[timeKey]"
|
||||||
class="c-imagery__thumb c-thumb"
|
class="c-imagery__thumb c-thumb"
|
||||||
:class="{ selected: focusedImageIndex === index && isPaused }"
|
:class="{ selected: focusedImageIndex === index && isPaused }"
|
||||||
@click="setFocusedImage(index, thumbnailClick)"
|
@click="setFocusedImage(index, thumbnailClick)"
|
||||||
@@ -97,7 +145,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import _ from 'lodash';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
import Compass from './Compass/Compass.vue';
|
||||||
|
import RelatedTelemetry from './RelatedTelemetry/RelatedTelemetry';
|
||||||
|
|
||||||
const DEFAULT_DURATION_FORMATTER = 'duration';
|
const DEFAULT_DURATION_FORMATTER = 'duration';
|
||||||
const REFRESH_CSS_MS = 500;
|
const REFRESH_CSS_MS = 500;
|
||||||
@@ -116,6 +167,9 @@ const ARROW_RIGHT = 39;
|
|||||||
const ARROW_LEFT = 37;
|
const ARROW_LEFT = 37;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
components: {
|
||||||
|
Compass
|
||||||
|
},
|
||||||
inject: ['openmct', 'domainObject'],
|
inject: ['openmct', 'domainObject'],
|
||||||
data() {
|
data() {
|
||||||
let timeSystem = this.openmct.time.timeSystem();
|
let timeSystem = this.openmct.time.timeSystem();
|
||||||
@@ -137,7 +191,15 @@ export default {
|
|||||||
refreshCSS: false,
|
refreshCSS: false,
|
||||||
keyString: undefined,
|
keyString: undefined,
|
||||||
focusedImageIndex: undefined,
|
focusedImageIndex: undefined,
|
||||||
numericDuration: undefined
|
focusedImageRelatedTelemetry: {},
|
||||||
|
numericDuration: undefined,
|
||||||
|
metadataEndpoints: {},
|
||||||
|
relatedTelemetry: {},
|
||||||
|
latestRelatedTelemetry: {},
|
||||||
|
focusedImageNaturalAspectRatio: undefined,
|
||||||
|
imageContainerWidth: undefined,
|
||||||
|
imageContainerHeight: undefined,
|
||||||
|
lockCompass: true
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -195,15 +257,69 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
},
|
||||||
|
shouldDisplayCompass() {
|
||||||
|
return this.focusedImage !== undefined
|
||||||
|
&& this.focusedImageNaturalAspectRatio !== undefined
|
||||||
|
&& this.imageContainerWidth !== undefined
|
||||||
|
&& this.imageContainerHeight !== undefined;
|
||||||
|
},
|
||||||
|
isSpacecraftPositionFresh() {
|
||||||
|
let isFresh = undefined;
|
||||||
|
let latest = this.latestRelatedTelemetry;
|
||||||
|
let focused = this.focusedImageRelatedTelemetry;
|
||||||
|
|
||||||
|
if (this.relatedTelemetry.hasRelatedTelemetry) {
|
||||||
|
isFresh = true;
|
||||||
|
for (let key of this.spacecraftKeys) {
|
||||||
|
if (this.relatedTelemetry[key] && latest[key] && focused[key]) {
|
||||||
|
if (!this.relatedTelemetry[key].comparisonFunction(latest[key], focused[key])) {
|
||||||
|
isFresh = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
isFresh = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return isFresh;
|
||||||
|
},
|
||||||
|
isCameraPositionFresh() {
|
||||||
|
let isFresh = undefined;
|
||||||
|
let latest = this.latestRelatedTelemetry;
|
||||||
|
let focused = this.focusedImageRelatedTelemetry;
|
||||||
|
|
||||||
|
if (this.relatedTelemetry.hasRelatedTelemetry) {
|
||||||
|
isFresh = true;
|
||||||
|
|
||||||
|
// camera freshness relies on spacecraft position freshness
|
||||||
|
if (this.isSpacecraftPositionFresh) {
|
||||||
|
for (let key of this.cameraKeys) {
|
||||||
|
if (this.relatedTelemetry[key] && latest[key] && focused[key]) {
|
||||||
|
if (!this.relatedTelemetry[key].comparisonFunction(latest[key], focused[key])) {
|
||||||
|
isFresh = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
isFresh = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
isFresh = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return isFresh;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
focusedImageIndex() {
|
focusedImageIndex() {
|
||||||
this.trackDuration();
|
this.trackDuration();
|
||||||
this.resetAgeCSS();
|
this.resetAgeCSS();
|
||||||
|
this.updateRelatedTelemetryForFocusedImage();
|
||||||
|
this.getImageNaturalDimensions();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
async mounted() {
|
||||||
// listen
|
// listen
|
||||||
this.openmct.time.on('bounds', this.boundsChange);
|
this.openmct.time.on('bounds', this.boundsChange);
|
||||||
this.openmct.time.on('timeSystem', this.timeSystemChange);
|
this.openmct.time.on('timeSystem', this.timeSystemChange);
|
||||||
@@ -212,8 +328,14 @@ export default {
|
|||||||
// set
|
// set
|
||||||
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||||
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
|
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.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
|
||||||
this.imageFormatter = this.openmct.telemetry.getValueFormatter(this.metadata.valuesForHints(['image'])[0]);
|
this.imageFormatter = this.openmct.telemetry.getValueFormatter(this.imageHints);
|
||||||
|
|
||||||
|
// related telemetry keys
|
||||||
|
this.spacecraftKeys = ['heading', 'roll', 'pitch'];
|
||||||
|
this.cameraKeys = ['cameraPan', 'cameraTilt'];
|
||||||
|
this.sunKeys = ['sunOrientation'];
|
||||||
|
|
||||||
// initialize
|
// initialize
|
||||||
this.timeKey = this.timeSystem.key;
|
this.timeKey = this.timeSystem.key;
|
||||||
@@ -222,6 +344,18 @@ export default {
|
|||||||
// kickoff
|
// kickoff
|
||||||
this.subscribe();
|
this.subscribe();
|
||||||
this.requestHistory();
|
this.requestHistory();
|
||||||
|
|
||||||
|
// related telemetry
|
||||||
|
await this.initializeRelatedTelemetry();
|
||||||
|
this.updateRelatedTelemetryForFocusedImage();
|
||||||
|
this.trackLatestRelatedTelemetry();
|
||||||
|
|
||||||
|
// for scrolling through images quickly and resizing the object view
|
||||||
|
_.debounce(this.updateRelatedTelemetryForFocusedImage, 400);
|
||||||
|
_.debounce(this.resizeImageContainer, 400);
|
||||||
|
|
||||||
|
this.imageContainerResizeObserver = new ResizeObserver(this.resizeImageContainer);
|
||||||
|
this.imageContainerResizeObserver.observe(this.$refs.focusedImage);
|
||||||
},
|
},
|
||||||
updated() {
|
updated() {
|
||||||
this.scrollToRight();
|
this.scrollToRight();
|
||||||
@@ -232,12 +366,115 @@ export default {
|
|||||||
delete this.unsubscribe;
|
delete this.unsubscribe;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.imageContainerResizeObserver.disconnect();
|
||||||
|
|
||||||
|
if (this.relatedTelemetry.hasRelatedTelemetry) {
|
||||||
|
this.relatedTelemetry.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
this.stopDurationTracking();
|
this.stopDurationTracking();
|
||||||
this.openmct.time.off('bounds', this.boundsChange);
|
this.openmct.time.off('bounds', this.boundsChange);
|
||||||
this.openmct.time.off('timeSystem', this.timeSystemChange);
|
this.openmct.time.off('timeSystem', this.timeSystemChange);
|
||||||
this.openmct.time.off('clock', this.clockChange);
|
this.openmct.time.off('clock', this.clockChange);
|
||||||
|
|
||||||
|
// unsubscribe from related telemetry
|
||||||
|
if (this.relatedTelemetry.hasRelatedTelemetry) {
|
||||||
|
for (let key of this.relatedTelemetry.keys) {
|
||||||
|
if (this.relatedTelemetry[key].unsubscribe) {
|
||||||
|
this.relatedTelemetry[key].unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
async initializeRelatedTelemetry() {
|
||||||
|
this.relatedTelemetry = new RelatedTelemetry(
|
||||||
|
this.openmct,
|
||||||
|
this.domainObject,
|
||||||
|
[...this.spacecraftKeys, ...this.cameraKeys, ...this.sunKeys]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.relatedTelemetry.hasRelatedTelemetry) {
|
||||||
|
await this.relatedTelemetry.load();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async getMostRecentRelatedTelemetry(key, targetDatum) {
|
||||||
|
if (!this.relatedTelemetry.hasRelatedTelemetry) {
|
||||||
|
throw new Error(`${this.domainObject.name} does not have any related telemetry`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.relatedTelemetry[key]) {
|
||||||
|
throw new Error(`${key} does not exist on related telemetry`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mostRecent;
|
||||||
|
let valueKey = this.relatedTelemetry[key].historical.valueKey;
|
||||||
|
let valuesOnTelemetry = this.relatedTelemetry[key].hasTelemetryOnDatum;
|
||||||
|
|
||||||
|
if (valuesOnTelemetry) {
|
||||||
|
mostRecent = targetDatum[valueKey];
|
||||||
|
|
||||||
|
if (mostRecent) {
|
||||||
|
return mostRecent;
|
||||||
|
} else {
|
||||||
|
console.warn(`Related Telemetry for ${key} does NOT exist on this telemetry datum as configuration implied.`);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mostRecent = await this.relatedTelemetry[key].requestLatestFor(targetDatum);
|
||||||
|
|
||||||
|
return mostRecent[valueKey];
|
||||||
|
},
|
||||||
|
// will subscribe to data for this key if not already done
|
||||||
|
subscribeToDataForKey(key) {
|
||||||
|
if (this.relatedTelemetry[key].isSubscribed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.relatedTelemetry[key].realtimeDomainObject) {
|
||||||
|
this.relatedTelemetry[key].unsubscribe = this.openmct.telemetry.subscribe(
|
||||||
|
this.relatedTelemetry[key].realtimeDomainObject, datum => {
|
||||||
|
this.relatedTelemetry[key].listeners.forEach(callback => {
|
||||||
|
callback(datum);
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
this.relatedTelemetry[key].isSubscribed = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async updateRelatedTelemetryForFocusedImage() {
|
||||||
|
if (!this.relatedTelemetry.hasRelatedTelemetry || !this.focusedImage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// set data ON image telemetry as well as in focusedImageRelatedTelemetry
|
||||||
|
for (let key of this.relatedTelemetry.keys) {
|
||||||
|
if (this.relatedTelemetry[key] && this.relatedTelemetry[key].historical) {
|
||||||
|
let valuesOnTelemetry = this.relatedTelemetry[key].hasTelemetryOnDatum;
|
||||||
|
let value = await this.getMostRecentRelatedTelemetry(key, this.focusedImage);
|
||||||
|
|
||||||
|
if (!valuesOnTelemetry) {
|
||||||
|
this.$set(this.imageHistory[this.focusedImageIndex], key, value); // manually add to telemetry
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$set(this.focusedImageRelatedTelemetry, key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trackLatestRelatedTelemetry() {
|
||||||
|
[...this.spacecraftKeys, ...this.cameraKeys, ...this.sunKeys].forEach(key => {
|
||||||
|
if (this.relatedTelemetry[key] && this.relatedTelemetry[key].subscribe) {
|
||||||
|
this.relatedTelemetry[key].subscribe((datum) => {
|
||||||
|
let valueKey = this.relatedTelemetry[key].realtime.valueKey;
|
||||||
|
this.$set(this.latestRelatedTelemetry, key, datum[valueKey]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
focusElement() {
|
focusElement() {
|
||||||
this.$el.focus();
|
this.$el.focus();
|
||||||
},
|
},
|
||||||
@@ -358,6 +595,7 @@ export default {
|
|||||||
this.requestCount++;
|
this.requestCount++;
|
||||||
const requestId = this.requestCount;
|
const requestId = this.requestCount;
|
||||||
this.imageHistory = [];
|
this.imageHistory = [];
|
||||||
|
|
||||||
let data = await this.openmct.telemetry
|
let data = await this.openmct.telemetry
|
||||||
.request(this.domainObject, bounds) || [];
|
.request(this.domainObject, bounds) || [];
|
||||||
|
|
||||||
@@ -509,6 +747,28 @@ export default {
|
|||||||
},
|
},
|
||||||
isLeftOrRightArrowKey(keyCode) {
|
isLeftOrRightArrowKey(keyCode) {
|
||||||
return [ARROW_RIGHT, ARROW_LEFT].includes(keyCode);
|
return [ARROW_RIGHT, ARROW_LEFT].includes(keyCode);
|
||||||
|
},
|
||||||
|
getImageNaturalDimensions() {
|
||||||
|
this.focusedImageNaturalAspectRatio = undefined;
|
||||||
|
|
||||||
|
const img = this.$refs.focusedImage;
|
||||||
|
|
||||||
|
// TODO - should probably cache this
|
||||||
|
img.addEventListener('load', () => {
|
||||||
|
this.focusedImageNaturalAspectRatio = img.naturalWidth / img.naturalHeight;
|
||||||
|
}, { once: true });
|
||||||
|
},
|
||||||
|
resizeImageContainer() {
|
||||||
|
if (this.$refs.focusedImage.clientWidth !== this.imageContainerWidth) {
|
||||||
|
this.imageContainerWidth = this.$refs.focusedImage.clientWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.$refs.focusedImage.clientHeight !== this.imageContainerHeight) {
|
||||||
|
this.imageContainerHeight = this.$refs.focusedImage.clientHeight;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toggleLockCompass() {
|
||||||
|
this.lockCompass = !this.lockCompass;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,162 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* 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.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
function copyRelatedMetadata(metadata) {
|
||||||
|
let compare = metadata.comparisonFunction;
|
||||||
|
let copiedMetadata = JSON.parse(JSON.stringify(metadata));
|
||||||
|
copiedMetadata.comparisonFunction = compare;
|
||||||
|
|
||||||
|
return copiedMetadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class RelatedTelemetry {
|
||||||
|
|
||||||
|
constructor(openmct, domainObject, telemetryKeys) {
|
||||||
|
this._openmct = openmct;
|
||||||
|
this._domainObject = domainObject;
|
||||||
|
|
||||||
|
let metadata = this._openmct.telemetry.getMetadata(this._domainObject);
|
||||||
|
let imageHints = metadata.valuesForHints(['image'])[0];
|
||||||
|
|
||||||
|
this.hasRelatedTelemetry = imageHints.relatedTelemetry !== undefined;
|
||||||
|
|
||||||
|
if (this.hasRelatedTelemetry) {
|
||||||
|
this.keys = telemetryKeys;
|
||||||
|
|
||||||
|
this._timeFormatter = undefined;
|
||||||
|
this._timeSystemChange(this._openmct.time.timeSystem());
|
||||||
|
|
||||||
|
// grab related telemetry metadata
|
||||||
|
for (let key of this.keys) {
|
||||||
|
if (imageHints.relatedTelemetry[key]) {
|
||||||
|
this[key] = copyRelatedMetadata(imageHints.relatedTelemetry[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.load = this.load.bind(this);
|
||||||
|
this._parseTime = this._parseTime.bind(this);
|
||||||
|
this._timeSystemChange = this._timeSystemChange.bind(this);
|
||||||
|
this.destroy = this.destroy.bind(this);
|
||||||
|
|
||||||
|
this._openmct.time.on('timeSystem', this._timeSystemChange);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async load() {
|
||||||
|
if (!this.hasRelatedTelemetry) {
|
||||||
|
throw new Error('This domain object does not have related telemetry, use "hasRelatedTelemetry" to check before loading.');
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
this.keys.map(async (key) => {
|
||||||
|
if (this[key].historical) {
|
||||||
|
await this._initializeHistorical(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this[key].realtime && this[key].realtime.telemetryObjectId) {
|
||||||
|
await this._intializeRealtime(key);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async _initializeHistorical(key) {
|
||||||
|
if (this[key].historical.telemetryObjectId) {
|
||||||
|
this[key].historicalDomainObject = await this._openmct.objects.get(this[key].historical.telemetryObjectId);
|
||||||
|
|
||||||
|
this[key].requestLatestFor = async (datum) => {
|
||||||
|
const options = {
|
||||||
|
start: this._openmct.time.bounds().start,
|
||||||
|
end: this._parseTime(datum),
|
||||||
|
strategy: 'latest'
|
||||||
|
};
|
||||||
|
let results = await this._openmct.telemetry
|
||||||
|
.request(this[key].historicalDomainObject, options);
|
||||||
|
|
||||||
|
return results[results.length - 1];
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
this[key].historical.hasTelemetryOnDatum = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async _intializeRealtime(key) {
|
||||||
|
this[key].realtimeDomainObject = await this._openmct.objects.get(this[key].realtime.telemetryObjectId);
|
||||||
|
this[key].listeners = [];
|
||||||
|
this[key].subscribe = (callback) => {
|
||||||
|
|
||||||
|
if (!this[key].isSubscribed) {
|
||||||
|
this._subscribeToDataForKey(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this[key].listeners.includes(callback)) {
|
||||||
|
this[key].listeners.push(callback);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
this[key].listeners.remove(callback);
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return () => {};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
_subscribeToDataForKey(key) {
|
||||||
|
if (this[key].isSubscribed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this[key].realtimeDomainObject) {
|
||||||
|
this[key].unsubscribe = this._openmct.telemetry.subscribe(
|
||||||
|
this[key].realtimeDomainObject, datum => {
|
||||||
|
this[key].listeners.forEach(callback => {
|
||||||
|
callback(datum);
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
this[key].isSubscribed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_parseTime(datum) {
|
||||||
|
return this._timeFormatter.parse(datum);
|
||||||
|
}
|
||||||
|
|
||||||
|
_timeSystemChange(system) {
|
||||||
|
let key = system.key;
|
||||||
|
let metadata = this._openmct.telemetry.getMetadata(this._domainObject);
|
||||||
|
let metadataValue = metadata.value(key) || { format: key };
|
||||||
|
this._timeFormatter = this._openmct.telemetry.getValueFormatter(metadataValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this._openmct.time.off('timeSystem', this._timeSystemChange);
|
||||||
|
for (let key of this.keys) {
|
||||||
|
if (this[key].unsubscribe) {
|
||||||
|
this[key].unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@
|
|||||||
background-color: $colorPlotBg;
|
background-color: $colorPlotBg;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
|
height: 0;
|
||||||
|
|
||||||
&.unnsynced{
|
&.unnsynced{
|
||||||
@include sUnsynced();
|
@include sUnsynced();
|
||||||
@@ -30,10 +31,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__image {
|
&__image {
|
||||||
@include abs(); // Safari fix
|
height: 100%;
|
||||||
background-position: center;
|
width: 100%;
|
||||||
background-repeat: no-repeat;
|
object-fit: contain;
|
||||||
background-size: contain;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,13 +71,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__age {
|
&__age {
|
||||||
border-radius: $controlCr;
|
border-radius: $smallCr;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
align-items: baseline;
|
align-items: center;
|
||||||
padding: 1px $interiorMarginSm;
|
padding: 2px $interiorMarginSm;
|
||||||
|
|
||||||
&:before {
|
&:before {
|
||||||
|
font-size: 0.9em;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
margin-right: $interiorMarginSm;
|
margin-right: $interiorMarginSm;
|
||||||
}
|
}
|
||||||
@@ -86,8 +87,9 @@
|
|||||||
&--new {
|
&--new {
|
||||||
// New imagery
|
// New imagery
|
||||||
$bgColor: $colorOk;
|
$bgColor: $colorOk;
|
||||||
|
color: $colorOkFg;
|
||||||
background: rgba($bgColor, 0.5);
|
background: rgba($bgColor, 0.5);
|
||||||
@include flash($animName: flashImageAge, $dur: 250ms, $valStart: rgba($colorOk, 0.7), $valEnd: rgba($colorOk, 0));
|
@include flash($animName: flashImageAge, $iter: 2, $dur: 250ms, $valStart: rgba($colorOk, 0.7), $valEnd: rgba($colorOk, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
&__thumbs-wrapper {
|
&__thumbs-wrapper {
|
||||||
|
|||||||
@@ -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.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
import ImageryViewProvider from './ImageryViewProvider';
|
import ImageryViewProvider from './ImageryViewProvider';
|
||||||
|
|
||||||
export default function () {
|
export default function () {
|
||||||
|
|||||||
@@ -32,12 +32,25 @@ const TEN_MINUTES = ONE_MINUTE * 10;
|
|||||||
const MAIN_IMAGE_CLASS = '.js-imageryView-image';
|
const MAIN_IMAGE_CLASS = '.js-imageryView-image';
|
||||||
const NEW_IMAGE_CLASS = '.c-imagery__age.c-imagery--new';
|
const NEW_IMAGE_CLASS = '.c-imagery__age.c-imagery--new';
|
||||||
const REFRESH_CSS_MS = 500;
|
const REFRESH_CSS_MS = 500;
|
||||||
|
const TOLERANCE = 0.50;
|
||||||
|
|
||||||
|
function comparisonFunction(valueOne, valueTwo) {
|
||||||
|
let larger = valueOne;
|
||||||
|
let smaller = valueTwo;
|
||||||
|
|
||||||
|
if (larger < smaller) {
|
||||||
|
larger = valueTwo;
|
||||||
|
smaller = valueOne;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (larger - smaller) < TOLERANCE;
|
||||||
|
}
|
||||||
|
|
||||||
function getImageInfo(doc) {
|
function getImageInfo(doc) {
|
||||||
let imageElement = doc.querySelectorAll(MAIN_IMAGE_CLASS)[0];
|
let imageElement = doc.querySelectorAll(MAIN_IMAGE_CLASS)[0];
|
||||||
let timestamp = imageElement.dataset.openmctImageTimestamp;
|
let timestamp = imageElement.dataset.openmctImageTimestamp;
|
||||||
let identifier = imageElement.dataset.openmctObjectKeystring;
|
let identifier = imageElement.dataset.openmctObjectKeystring;
|
||||||
let url = imageElement.style.backgroundImage;
|
let url = imageElement.src;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
timestamp,
|
timestamp,
|
||||||
@@ -63,7 +76,8 @@ function generateTelemetry(start, count) {
|
|||||||
"name": stringRep + " Imagery",
|
"name": stringRep + " Imagery",
|
||||||
"utc": start + (i * ONE_MINUTE),
|
"utc": start + (i * ONE_MINUTE),
|
||||||
"url": location.host + '/' + logo + '?time=' + stringRep,
|
"url": location.host + '/' + logo + '?time=' + stringRep,
|
||||||
"timeId": stringRep
|
"timeId": stringRep,
|
||||||
|
"value": 100
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +119,51 @@ describe("The Imagery View Layout", () => {
|
|||||||
"image": 1,
|
"image": 1,
|
||||||
"priority": 3
|
"priority": 3
|
||||||
},
|
},
|
||||||
"source": "url"
|
"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",
|
"name": "Name",
|
||||||
@@ -151,6 +209,11 @@ describe("The Imagery View Layout", () => {
|
|||||||
child = document.createElement('div');
|
child = document.createElement('div');
|
||||||
parent.appendChild(child);
|
parent.appendChild(child);
|
||||||
|
|
||||||
|
spyOn(window, 'ResizeObserver').and.returnValue({
|
||||||
|
observe() {},
|
||||||
|
disconnect() {}
|
||||||
|
});
|
||||||
|
|
||||||
spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([]));
|
spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([]));
|
||||||
|
|
||||||
imageryPlugin = new ImageryPlugin();
|
imageryPlugin = new ImageryPlugin();
|
||||||
@@ -213,6 +276,10 @@ describe("The Imagery View Layout", () => {
|
|||||||
return done();
|
return done();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
imageryView.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
it("on mount should show the the most recent image", () => {
|
it("on mount should show the the most recent image", () => {
|
||||||
const imageInfo = getImageInfo(parent);
|
const imageInfo = getImageInfo(parent);
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
@import "../plugins/folderView/components/list-item.scss";
|
@import "../plugins/folderView/components/list-item.scss";
|
||||||
@import "../plugins/folderView/components/list-view.scss";
|
@import "../plugins/folderView/components/list-view.scss";
|
||||||
@import "../plugins/imagery/components/imagery-view-layout.scss";
|
@import "../plugins/imagery/components/imagery-view-layout.scss";
|
||||||
|
@import "../plugins/imagery/components/Compass/compass.scss";
|
||||||
@import "../plugins/telemetryTable/components/table-row.scss";
|
@import "../plugins/telemetryTable/components/table-row.scss";
|
||||||
@import "../plugins/telemetryTable/components/table-footer-indicator.scss";
|
@import "../plugins/telemetryTable/components/table-footer-indicator.scss";
|
||||||
@import "../plugins/tabs/components/tabs.scss";
|
@import "../plugins/tabs/components/tabs.scss";
|
||||||
|
|||||||
Reference in New Issue
Block a user