mirror of
https://github.com/nasa/openmct.git
synced 2025-05-07 11:08:34 +00:00
Compass rose fix (#6318)
* won't mount if cameraAngleOfView undefined * fix non-gimbling camera azimuth correct pan to azimuth * reorganize shared props passing * enable hud for non-gimbling cameras * fix unit tests * rotate function needs to work with numbers * avoid -0 * fix: don't delete imagery size, update related telemetry on focusedImage change * refactor: remove unused prop * fix: ensure thumbnail key is unique * fix: watch `focusedImage`, not `focusedImageIndex` - Corrects a false assumption that if the `focusedImageIndex` changes, the `focusedImage` has changed. This was causing us to mistakenly reset a lot of display props that control whether or not the Compass shows. - For example, if an image falls out of bounds, the `focusedImageIndex` will change as the old image is removed from the array. --------- Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov> Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com> Co-authored-by: Andrew Henry <akhenry@gmail.com>
This commit is contained in:
parent
4fa9a9697b
commit
5da1c9c0d7
@ -275,7 +275,7 @@ function pointForTimestamp(timestamp, name, imageSamples, delay) {
|
|||||||
local: Math.floor(timestamp / delay) * delay,
|
local: Math.floor(timestamp / delay) * delay,
|
||||||
url,
|
url,
|
||||||
sunOrientation: getCompassValues(0, 360),
|
sunOrientation: getCompassValues(0, 360),
|
||||||
cameraPan: getCompassValues(0, 360),
|
cameraAzimuth: getCompassValues(0, 360),
|
||||||
heading: getCompassValues(0, 360),
|
heading: getCompassValues(0, 360),
|
||||||
transformations: navCamTransformations,
|
transformations: navCamTransformations,
|
||||||
imageDownloadName
|
imageDownloadName
|
||||||
|
@ -26,18 +26,23 @@
|
|||||||
:style="`width: 100%; height: 100%`"
|
:style="`width: 100%; height: 100%`"
|
||||||
>
|
>
|
||||||
<CompassHUD
|
<CompassHUD
|
||||||
v-if="showCompassHUD"
|
|
||||||
:sun-heading="sunHeading"
|
|
||||||
:camera-angle-of-view="cameraAngleOfView"
|
:camera-angle-of-view="cameraAngleOfView"
|
||||||
:camera-pan="cameraPan"
|
:heading="heading"
|
||||||
|
:camera-azimuth="cameraAzimuth"
|
||||||
|
:transformations="transformations"
|
||||||
|
:has-gimble="hasGimble"
|
||||||
|
:normalized-camera-azimuth="normalizedCameraAzimuth"
|
||||||
|
:sun-heading="sunHeading"
|
||||||
/>
|
/>
|
||||||
<CompassRose
|
<CompassRose
|
||||||
v-if="showCompassRose"
|
:camera-angle-of-view="cameraAngleOfView"
|
||||||
:camera-pan="cameraPan"
|
|
||||||
:heading="heading"
|
:heading="heading"
|
||||||
:sized-image-dimensions="sizedImageDimensions"
|
:camera-azimuth="cameraAzimuth"
|
||||||
:sun-heading="sunHeading"
|
|
||||||
:transformations="transformations"
|
:transformations="transformations"
|
||||||
|
:has-gimble="hasGimble"
|
||||||
|
:normalized-camera-azimuth="normalizedCameraAzimuth"
|
||||||
|
:sun-heading="sunHeading"
|
||||||
|
:sized-image-dimensions="sizedImageDimensions"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -45,6 +50,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import CompassHUD from './CompassHUD.vue';
|
import CompassHUD from './CompassHUD.vue';
|
||||||
import CompassRose from './CompassRose.vue';
|
import CompassRose from './CompassRose.vue';
|
||||||
|
import { rotate } from './utils';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@ -62,11 +68,14 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
showCompassHUD() {
|
hasGimble() {
|
||||||
return this.hasCameraPan && this.cameraAngleOfView > 0;
|
return this.cameraAzimuth !== undefined;
|
||||||
},
|
},
|
||||||
showCompassRose() {
|
// compass ordinal orientation of camera
|
||||||
return (this.hasCameraPan || this.hasHeading) && this.cameraAngleOfView > 0;
|
normalizedCameraAzimuth() {
|
||||||
|
return this.hasGimble
|
||||||
|
? rotate(this.cameraAzimuth)
|
||||||
|
: rotate(this.heading, -this.transformations.rotation || 0);
|
||||||
},
|
},
|
||||||
// horizontal rotation from north in degrees
|
// horizontal rotation from north in degrees
|
||||||
heading() {
|
heading() {
|
||||||
@ -80,14 +89,11 @@ export default {
|
|||||||
return this.image.sunOrientation;
|
return this.image.sunOrientation;
|
||||||
},
|
},
|
||||||
// horizontal rotation from north in degrees
|
// horizontal rotation from north in degrees
|
||||||
cameraPan() {
|
cameraAzimuth() {
|
||||||
return this.image.cameraPan;
|
return this.image.cameraPan;
|
||||||
},
|
},
|
||||||
hasCameraPan() {
|
|
||||||
return this.cameraPan !== undefined;
|
|
||||||
},
|
|
||||||
cameraAngleOfView() {
|
cameraAngleOfView() {
|
||||||
return this.transformations?.cameraAngleOfView;
|
return this.transformations.cameraAngleOfView;
|
||||||
},
|
},
|
||||||
transformations() {
|
transformations() {
|
||||||
return this.image.transformations;
|
return this.image.transformations;
|
||||||
|
@ -94,17 +94,33 @@ const COMPASS_POINTS = [
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
|
cameraAngleOfView: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
heading: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
cameraAzimuth: {
|
||||||
|
type: Number,
|
||||||
|
default: undefined
|
||||||
|
},
|
||||||
|
transformations: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
hasGimble: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
normalizedCameraAzimuth: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
sunHeading: {
|
sunHeading: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: undefined
|
default: undefined
|
||||||
},
|
|
||||||
cameraAngleOfView: {
|
|
||||||
type: Number,
|
|
||||||
default: undefined
|
|
||||||
},
|
|
||||||
cameraPan: {
|
|
||||||
type: Number,
|
|
||||||
required: true
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -130,10 +146,13 @@ export default {
|
|||||||
left: `${ percentage * 100 }%`
|
left: `${ percentage * 100 }%`
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
cameraRotation() {
|
||||||
|
return this.transformations?.rotation;
|
||||||
|
},
|
||||||
visibleRange() {
|
visibleRange() {
|
||||||
return [
|
return [
|
||||||
rotate(this.cameraPan, -this.cameraAngleOfView / 2),
|
rotate(this.normalizedCameraAzimuth, -this.cameraAngleOfView / 2),
|
||||||
rotate(this.cameraPan, this.cameraAngleOfView / 2)
|
rotate(this.normalizedCameraAzimuth, this.cameraAngleOfView / 2)
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -75,7 +75,6 @@
|
|||||||
:style="sunHeadingStyle"
|
:style="sunHeadingStyle"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Camera FOV -->
|
|
||||||
<mask
|
<mask
|
||||||
id="mask2"
|
id="mask2"
|
||||||
class="c-cr__cam-fov-l-mask"
|
class="c-cr__cam-fov-l-mask"
|
||||||
@ -117,10 +116,10 @@
|
|||||||
class="cr-vrover"
|
class="cr-vrover"
|
||||||
:style="camAngleAndPositionStyle"
|
:style="camAngleAndPositionStyle"
|
||||||
>
|
>
|
||||||
<!-- Equipment body. Rotates relative to the camera pan value for cams that gimbal. -->
|
<!-- Equipment body. Rotates relative to the camera pan value for cameras that gimble. -->
|
||||||
<path
|
<path
|
||||||
class="cr-vrover__body"
|
class="cr-vrover__body"
|
||||||
:style="camGimbalAngleStyle"
|
:style="gimbledCameraPanStyle"
|
||||||
x
|
x
|
||||||
fill-rule="evenodd"
|
fill-rule="evenodd"
|
||||||
clip-rule="evenodd"
|
clip-rule="evenodd"
|
||||||
@ -128,6 +127,7 @@
|
|||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
|
<!-- Camera FOV -->
|
||||||
<g
|
<g
|
||||||
class="c-cr__cam-fov"
|
class="c-cr__cam-fov"
|
||||||
>
|
>
|
||||||
@ -160,7 +160,7 @@
|
|||||||
<!-- NSEW and ticks -->
|
<!-- NSEW and ticks -->
|
||||||
<g
|
<g
|
||||||
class="c-cr__nsew"
|
class="c-cr__nsew"
|
||||||
:style="compassRoseStyle"
|
:style="compassDialStyle"
|
||||||
>
|
>
|
||||||
<g class="c-cr__ticks-major">
|
<g class="c-cr__ticks-major">
|
||||||
<path d="M50 3L43 10H57L50 3Z" />
|
<path d="M50 3L43 10H57L50 3Z" />
|
||||||
@ -259,23 +259,32 @@ import { throttle } from 'lodash';
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
|
cameraAngleOfView: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
heading: {
|
heading: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: true,
|
required: true
|
||||||
default() {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
sunHeading: {
|
cameraAzimuth: {
|
||||||
type: Number,
|
|
||||||
default: undefined
|
|
||||||
},
|
|
||||||
cameraPan: {
|
|
||||||
type: Number,
|
type: Number,
|
||||||
default: undefined
|
default: undefined
|
||||||
},
|
},
|
||||||
transformations: {
|
transformations: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
hasGimble: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
normalizedCameraAzimuth: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
sunHeading: {
|
||||||
|
type: Number,
|
||||||
default: undefined
|
default: undefined
|
||||||
},
|
},
|
||||||
sizedImageDimensions: {
|
sizedImageDimensions: {
|
||||||
@ -289,18 +298,6 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
cameraHeading() {
|
|
||||||
return this.cameraPan ?? this.heading;
|
|
||||||
},
|
|
||||||
cameraAngleOfView() {
|
|
||||||
const cameraAngleOfView = this.transformations?.cameraAngleOfView;
|
|
||||||
|
|
||||||
if (!cameraAngleOfView) {
|
|
||||||
console.warn('No Camera Angle of View provided');
|
|
||||||
}
|
|
||||||
|
|
||||||
return cameraAngleOfView;
|
|
||||||
},
|
|
||||||
camAngleAndPositionStyle() {
|
camAngleAndPositionStyle() {
|
||||||
const translateX = this.transformations?.translateX;
|
const translateX = this.transformations?.translateX;
|
||||||
const translateY = this.transformations?.translateY;
|
const translateY = this.transformations?.translateY;
|
||||||
@ -309,18 +306,22 @@ export default {
|
|||||||
|
|
||||||
return { transform: `translate(${translateX}%, ${translateY}%) rotate(${rotation}deg) scale(${scale})` };
|
return { transform: `translate(${translateX}%, ${translateY}%) rotate(${rotation}deg) scale(${scale})` };
|
||||||
},
|
},
|
||||||
camGimbalAngleStyle() {
|
gimbledCameraPanStyle() {
|
||||||
const rotation = rotate(this.heading);
|
if (!this.hasGimble) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const gimbledCameraPan = rotate(this.normalizedCameraAzimuth, -this.heading);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
transform: `rotate(${ rotation }deg)`
|
transform: `rotate(${ -gimbledCameraPan }deg)`
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
compassRoseStyle() {
|
compassDialStyle() {
|
||||||
return { transform: `rotate(${ this.north }deg)` };
|
return { transform: `rotate(${ this.north }deg)` };
|
||||||
},
|
},
|
||||||
north() {
|
north() {
|
||||||
return this.lockCompass ? rotate(-this.cameraHeading) : 0;
|
return this.lockCompass ? rotate(-this.normalizedCameraAzimuth) : 0;
|
||||||
},
|
},
|
||||||
cardinalTextRotateN() {
|
cardinalTextRotateN() {
|
||||||
return { transform: `translateY(-27%) rotate(${ -this.north }deg)` };
|
return { transform: `translateY(-27%) rotate(${ -this.north }deg)` };
|
||||||
@ -348,7 +349,7 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
cameraHeadingStyle() {
|
cameraHeadingStyle() {
|
||||||
const rotation = rotate(this.north, this.cameraHeading);
|
const rotation = rotate(this.north, this.normalizedCameraAzimuth);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
transform: `rotate(${ rotation }deg)`
|
transform: `rotate(${ rotation }deg)`
|
||||||
|
@ -35,8 +35,15 @@ describe("The Compass component", () => {
|
|||||||
roll: 90,
|
roll: 90,
|
||||||
pitch: 90,
|
pitch: 90,
|
||||||
cameraTilt: 100,
|
cameraTilt: 100,
|
||||||
cameraPan: 90,
|
cameraAzimuth: 90,
|
||||||
sunAngle: 30
|
sunAngle: 30,
|
||||||
|
transformations: {
|
||||||
|
translateX: 0,
|
||||||
|
translateY: 18,
|
||||||
|
rotation: 0,
|
||||||
|
scale: 0.3,
|
||||||
|
cameraAngleOfView: 70
|
||||||
|
}
|
||||||
};
|
};
|
||||||
let propsData = {
|
let propsData = {
|
||||||
naturalAspectRatio: 0.9,
|
naturalAspectRatio: 0.9,
|
||||||
@ -44,8 +51,7 @@ describe("The Compass component", () => {
|
|||||||
sizedImageDimensions: {
|
sizedImageDimensions: {
|
||||||
width: 100,
|
width: 100,
|
||||||
height: 100
|
height: 100
|
||||||
},
|
}
|
||||||
compassRoseSizingClasses: '--rose-small --rose-min'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
app = new Vue({
|
app = new Vue({
|
||||||
@ -54,7 +60,6 @@ describe("The Compass component", () => {
|
|||||||
return propsData;
|
return propsData;
|
||||||
},
|
},
|
||||||
template: `<Compass
|
template: `<Compass
|
||||||
:compass-rose-sizing-classes="compassRoseSizingClasses"
|
|
||||||
:image="image"
|
:image="image"
|
||||||
:natural-aspect-ratio="naturalAspectRatio"
|
:natural-aspect-ratio="naturalAspectRatio"
|
||||||
:sized-image-dimensions="sizedImageDimensions"
|
:sized-image-dimensions="sizedImageDimensions"
|
||||||
@ -67,7 +72,7 @@ describe("The Compass component", () => {
|
|||||||
app.$destroy();
|
app.$destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("when a heading value exists on the image", () => {
|
describe("when a heading value and cameraAngleOfView exists on the image", () => {
|
||||||
|
|
||||||
it("should display a compass rose", () => {
|
it("should display a compass rose", () => {
|
||||||
let compassRoseElement = instance.$el.querySelector(COMPASS_ROSE_CLASS
|
let compassRoseElement = instance.$el.querySelector(COMPASS_ROSE_CLASS
|
||||||
|
@ -94,7 +94,6 @@
|
|||||||
<Compass
|
<Compass
|
||||||
v-if="shouldDisplayCompass"
|
v-if="shouldDisplayCompass"
|
||||||
:image="focusedImage"
|
:image="focusedImage"
|
||||||
:natural-aspect-ratio="focusedImageNaturalAspectRatio"
|
|
||||||
:sized-image-dimensions="sizedImageDimensions"
|
:sized-image-dimensions="sizedImageDimensions"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -171,7 +170,7 @@
|
|||||||
>
|
>
|
||||||
<ImageThumbnail
|
<ImageThumbnail
|
||||||
v-for="(image, index) in imageHistory"
|
v-for="(image, index) in imageHistory"
|
||||||
:key="`${image.thumbnailUrl || image.url}${image.time}`"
|
:key="`${image.thumbnailUrl || image.url}-${image.time}-${index}`"
|
||||||
:image="image"
|
:image="image"
|
||||||
:active="focusedImageIndex === index"
|
:active="focusedImageIndex === index"
|
||||||
:selected="focusedImageIndex === index && isPaused"
|
:selected="focusedImageIndex === index && isPaused"
|
||||||
@ -430,9 +429,12 @@ export default {
|
|||||||
&& imageHeightAndWidth
|
&& imageHeightAndWidth
|
||||||
&& this.zoomFactor === 1
|
&& this.zoomFactor === 1
|
||||||
&& this.imagePanned !== true;
|
&& this.imagePanned !== true;
|
||||||
const hasCameraConfigurations = this.focusedImage?.transformations !== undefined;
|
const hasHeading = this.focusedImage?.heading !== undefined;
|
||||||
|
const hasCameraAngleOfView = this.focusedImage?.transformations?.cameraAngleOfView > 0;
|
||||||
|
|
||||||
return display && hasCameraConfigurations;
|
return display
|
||||||
|
&& hasCameraAngleOfView
|
||||||
|
&& hasHeading;
|
||||||
},
|
},
|
||||||
isSpacecraftPositionFresh() {
|
isSpacecraftPositionFresh() {
|
||||||
let isFresh = undefined;
|
let isFresh = undefined;
|
||||||
@ -582,11 +584,34 @@ export default {
|
|||||||
},
|
},
|
||||||
deep: true
|
deep: true
|
||||||
},
|
},
|
||||||
focusedImageIndex() {
|
focusedImage: {
|
||||||
this.trackDuration();
|
handler(newImage, oldImage) {
|
||||||
this.resetAgeCSS();
|
const newTime = newImage?.time;
|
||||||
this.updateRelatedTelemetryForFocusedImage();
|
const oldTime = oldImage?.time;
|
||||||
this.getImageNaturalDimensions();
|
const newUrl = newImage?.url;
|
||||||
|
const oldUrl = oldImage?.url;
|
||||||
|
|
||||||
|
// Skip if it's all falsy
|
||||||
|
if (!newTime && !oldTime && !newUrl && !oldUrl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip if it's the same image
|
||||||
|
if (newTime === oldTime && newUrl === oldUrl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update image duration and reset age CSS
|
||||||
|
this.trackDuration();
|
||||||
|
this.resetAgeCSS();
|
||||||
|
|
||||||
|
// Reset image dimensions and calculate new dimensions
|
||||||
|
// on new image load
|
||||||
|
this.getImageNaturalDimensions();
|
||||||
|
|
||||||
|
// Get the related telemetry for the new image
|
||||||
|
this.updateRelatedTelemetryForFocusedImage();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
bounds() {
|
bounds() {
|
||||||
this.scrollHandler();
|
this.scrollHandler();
|
||||||
@ -771,6 +796,10 @@ export default {
|
|||||||
this.layers = layersMetadata;
|
this.layers = layersMetadata;
|
||||||
if (this.domainObject.configuration) {
|
if (this.domainObject.configuration) {
|
||||||
const persistedLayers = this.domainObject.configuration.layers;
|
const persistedLayers = this.domainObject.configuration.layers;
|
||||||
|
if (!persistedLayers) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
layersMetadata.forEach((layer) => {
|
layersMetadata.forEach((layer) => {
|
||||||
const persistedLayer = persistedLayers.find(object => object.name === layer.name);
|
const persistedLayer = persistedLayers.find(object => object.name === layer.name);
|
||||||
if (persistedLayer) {
|
if (persistedLayer) {
|
||||||
|
@ -153,9 +153,6 @@ export default {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// forcibly reset the imageContainer size to prevent an aspect ratio distortion
|
|
||||||
delete this.imageContainerWidth;
|
|
||||||
delete this.imageContainerHeight;
|
|
||||||
this.bounds = bounds; // setting bounds for ImageryView watcher
|
this.bounds = bounds; // setting bounds for ImageryView watcher
|
||||||
},
|
},
|
||||||
timeSystemChange() {
|
timeSystemChange() {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user