-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
PB-1204 : split CesiumMap component into multiple sub-parts
with the same logic/approach that we had with the OpenLayers counterpart, leaving only the responsibility to create and share the Cesium instance to the main component, and giving all others (interactions, layer management, feature highlighting) to sub-components.
- Loading branch information
Showing
8 changed files
with
570 additions
and
585 deletions.
There are no files selected for viewing
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,180 @@ | ||
<script setup> | ||
import { Cartesian2, Cartesian3, defined, Ellipsoid, Math as CesiumMath } from 'cesium' | ||
import { isEqual } from 'lodash' | ||
import proj4 from 'proj4' | ||
import { computed, inject, onBeforeUnmount, onMounted, watch } from 'vue' | ||
import { useStore } from 'vuex' | ||
import { | ||
CAMERA_MAX_PITCH, | ||
CAMERA_MAX_ZOOM_DISTANCE, | ||
CAMERA_MIN_PITCH, | ||
CAMERA_MIN_ZOOM_DISTANCE, | ||
} from '@/config/cesium.config' | ||
import { | ||
calculateHeight, | ||
limitCameraCenter, | ||
limitCameraPitchRoll, | ||
} from '@/modules/map/components/cesium/utils/cameraUtils' | ||
import { LV95, WGS84 } from '@/utils/coordinates/coordinateSystems' | ||
import log from '@/utils/logging' | ||
import { wrapDegrees } from '@/utils/numberUtils' | ||
const dispatcher = { | ||
dispatcher: 'useCesiumCamera.composable', | ||
} | ||
const getViewer = inject('getViewer') | ||
const store = useStore() | ||
const cameraPosition = computed(() => store.state.position.camera) | ||
onMounted(() => { | ||
initCamera() | ||
}) | ||
onBeforeUnmount(() => { | ||
const viewer = getViewer() | ||
if (viewer) { | ||
// the camera position that is for now dispatched to the store doesn't correspond where the 2D | ||
// view is looking at, as if the camera is tilted, its position will be over swaths of lands that | ||
// have nothing to do with the top-down 2D view. | ||
// here we ray trace the coordinate of where the camera is looking at, and send this "target" | ||
// to the store as the new center | ||
setCenterToCameraTarget(viewer, store) | ||
} | ||
}) | ||
watch(cameraPosition, flyToPosition, { | ||
flush: 'post', | ||
deep: true, | ||
}) | ||
function flyToPosition() { | ||
try { | ||
if (getViewer()) { | ||
log.debug( | ||
`[Cesium] Fly to camera position ${cameraPosition.value.x}, ${cameraPosition.value.y}, ${cameraPosition.value.z}` | ||
) | ||
getViewer().camera.flyTo({ | ||
destination: Cartesian3.fromDegrees( | ||
cameraPosition.value.x, | ||
cameraPosition.value.y, | ||
cameraPosition.value.z | ||
), | ||
orientation: { | ||
heading: CesiumMath.toRadians(cameraPosition.value.heading), | ||
pitch: CesiumMath.toRadians(cameraPosition.value.pitch), | ||
roll: CesiumMath.toRadians(cameraPosition.value.roll), | ||
}, | ||
duration: 1, | ||
}) | ||
} | ||
} catch (error) { | ||
log.error('[Cesium] Error while moving the camera', error, cameraPosition.value) | ||
} | ||
} | ||
function setCenterToCameraTarget() { | ||
const viewer = getViewer() | ||
if (!viewer) { | ||
return | ||
} | ||
const ray = viewer.camera.getPickRay( | ||
new Cartesian2( | ||
Math.round(viewer.scene.canvas.clientWidth / 2), | ||
Math.round(viewer.scene.canvas.clientHeight / 2) | ||
) | ||
) | ||
const cameraTarget = viewer.scene.globe.pick(ray, viewer.scene) | ||
if (defined(cameraTarget)) { | ||
const cameraTargetCartographic = Ellipsoid.WGS84.cartesianToCartographic(cameraTarget) | ||
const lat = CesiumMath.toDegrees(cameraTargetCartographic.latitude) | ||
const lon = CesiumMath.toDegrees(cameraTargetCartographic.longitude) | ||
store.dispatch('setCenter', { | ||
center: proj4(WGS84.epsg, store.state.position.projection.epsg, [lon, lat]), | ||
...dispatcher, | ||
}) | ||
} | ||
} | ||
function onCameraMoveEnd() { | ||
const viewer = getViewer() | ||
if (!viewer) { | ||
return | ||
} | ||
const camera = viewer.camera | ||
const position = camera.positionCartographic | ||
const newCameraPosition = { | ||
x: parseFloat(CesiumMath.toDegrees(position.longitude).toFixed(6)), | ||
y: parseFloat(CesiumMath.toDegrees(position.latitude).toFixed(6)), | ||
z: parseFloat(position.height.toFixed(1)), | ||
// Wrap degrees, cesium might return 360, which is internally wrapped to 0 in store. | ||
heading: wrapDegrees(parseFloat(CesiumMath.toDegrees(camera.heading).toFixed(0))), | ||
pitch: wrapDegrees(parseFloat(CesiumMath.toDegrees(camera.pitch).toFixed(0))), | ||
roll: wrapDegrees(parseFloat(CesiumMath.toDegrees(camera.roll).toFixed(0))), | ||
} | ||
if (!isEqual(newCameraPosition, cameraPosition.value)) { | ||
store.dispatch('setCameraPosition', { | ||
position: newCameraPosition, | ||
...dispatcher, | ||
}) | ||
} | ||
} | ||
function initCamera() { | ||
const viewer = getViewer() | ||
let destination | ||
let orientation | ||
if (cameraPosition.value) { | ||
// a camera position was already define in the URL, we use it | ||
log.debug('[Cesium] Existing camera position found at startup, using', cameraPosition.value) | ||
destination = Cartesian3.fromDegrees( | ||
cameraPosition.value.x, | ||
cameraPosition.value.y, | ||
cameraPosition.value.z | ||
) | ||
orientation = { | ||
heading: CesiumMath.toRadians(cameraPosition.value.heading), | ||
pitch: CesiumMath.toRadians(cameraPosition.value.pitch), | ||
roll: CesiumMath.toRadians(cameraPosition.value.roll), | ||
} | ||
} else { | ||
// no camera position was ever calculated, so we create one using the 2D coordinates | ||
log.debug( | ||
'[Cesium] No camera initial position defined, creating one using 2D coordinates', | ||
store.getters.centerEpsg4326 | ||
) | ||
destination = Cartesian3.fromDegrees( | ||
store.getters.centerEpsg4326[0], | ||
store.getters.centerEpsg4326[1], | ||
calculateHeight(store.getters.resolution, viewer.canvas.clientWidth) | ||
) | ||
orientation = { | ||
heading: -CesiumMath.toRadians(store.state.position.rotation), | ||
pitch: -CesiumMath.PI_OVER_TWO, | ||
roll: 0, | ||
} | ||
} | ||
const sscController = viewer.scene.screenSpaceCameraController | ||
sscController.minimumZoomDistance = CAMERA_MIN_ZOOM_DISTANCE | ||
sscController.maximumZoomDistance = CAMERA_MAX_ZOOM_DISTANCE | ||
viewer.scene.postRender.addEventListener(limitCameraCenter(LV95.getBoundsAs(WGS84).flatten)) | ||
viewer.scene.postRender.addEventListener( | ||
limitCameraPitchRoll(CAMERA_MIN_PITCH, CAMERA_MAX_PITCH, 0.0, 0.0) | ||
) | ||
viewer.camera.flyTo({ | ||
destination, | ||
orientation, | ||
duration: 0, | ||
}) | ||
viewer.camera.moveEnd.addEventListener(onCameraMoveEnd) | ||
} | ||
</script> | ||
|
||
<template> | ||
<slot /> | ||
</template> |
119 changes: 119 additions & 0 deletions
119
src/modules/map/components/cesium/CesiumHighlightedFeatures.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
<script setup> | ||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' | ||
import { LineString, Point, Polygon } from 'ol/geom' | ||
import { computed, inject, onMounted, ref, watch } from 'vue' | ||
import { useStore } from 'vuex' | ||
import FeatureEdit from '@/modules/infobox/components/FeatureEdit.vue' | ||
import FeatureList from '@/modules/infobox/components/FeatureList.vue' | ||
import CesiumPopover from '@/modules/map/components/cesium/CesiumPopover.vue' | ||
import { | ||
highlightGroup, | ||
unhighlightGroup, | ||
} from '@/modules/map/components/cesium/utils/highlightUtils' | ||
import { FeatureInfoPositions } from '@/store/modules/ui.store' | ||
const dispatcher = { | ||
dispatcher: 'CesiumHighlightedFeatures.vue', | ||
} | ||
const popoverCoordinates = ref([]) | ||
const getViewer = inject('getViewer') | ||
const store = useStore() | ||
const projection = computed(() => store.state.position.projection) | ||
const selectedFeatures = computed(() => store.getters.selectedFeatures) | ||
const isFeatureInfoInTooltip = computed(() => store.getters.showFeatureInfoInTooltip) | ||
const showFeaturesPopover = computed( | ||
() => isFeatureInfoInTooltip.value && selectedFeatures.value.length > 0 | ||
) | ||
const editFeature = computed(() => selectedFeatures.value.find((feature) => feature.isEditable)) | ||
watch( | ||
selectedFeatures, | ||
(newSelectedFeatures) => { | ||
if (newSelectedFeatures.length > 0) { | ||
highlightSelectedFeatures() | ||
} | ||
}, | ||
{ | ||
deep: true, | ||
} | ||
) | ||
onMounted(() => { | ||
if (selectedFeatures.value.length > 0) { | ||
highlightSelectedFeatures() | ||
} | ||
}) | ||
function highlightSelectedFeatures() { | ||
const viewer = getViewer() | ||
if (!viewer) { | ||
return | ||
} | ||
const [firstFeature] = selectedFeatures.value | ||
const geometries = selectedFeatures.value.map((f) => { | ||
// GeoJSON and KML layers have different geometry structure | ||
if (!f.geometry.type) { | ||
let type | ||
if (f.geometry instanceof Polygon) { | ||
type = 'Polygon' | ||
} else if (f.geometry instanceof LineString) { | ||
type = 'LineString' | ||
} else if (f.geometry instanceof Point) { | ||
type = 'Point' | ||
} | ||
const coordinates = f.geometry.getCoordinates() | ||
return { | ||
type, | ||
coordinates, | ||
} | ||
} | ||
return f.geometry | ||
}) | ||
highlightGroup(viewer, geometries) | ||
popoverCoordinates.value = Array.isArray(firstFeature.coordinates[0]) | ||
? firstFeature.coordinates[firstFeature.coordinates.length - 1] | ||
: firstFeature.coordinates | ||
} | ||
function onPopupClose() { | ||
const viewer = getViewer() | ||
if (viewer) { | ||
unhighlightGroup(viewer) | ||
store.dispatch('clearAllSelectedFeatures', dispatcher) | ||
store.dispatch('clearClick', dispatcher) | ||
} | ||
} | ||
function setBottomPanelFeatureInfoPosition() { | ||
store.dispatch('setFeatureInfoPosition', { | ||
position: FeatureInfoPositions.BOTTOMPANEL, | ||
...dispatcher, | ||
}) | ||
} | ||
</script> | ||
|
||
<template> | ||
<CesiumPopover | ||
v-if="showFeaturesPopover" | ||
:coordinates="popoverCoordinates" | ||
:projection="projection" | ||
authorize-print | ||
:use-content-padding="!!editFeature" | ||
@close="onPopupClose" | ||
> | ||
<template #extra-buttons> | ||
<button | ||
class="btn btn-sm btn-light d-flex align-items-center" | ||
data-cy="toggle-floating-off" | ||
@click="setBottomPanelFeatureInfoPosition()" | ||
@mousedown.stop="" | ||
> | ||
<FontAwesomeIcon icon="angles-down" /> | ||
</button> | ||
</template> | ||
<FeatureEdit v-if="editFeature" :read-only="true" :feature="editFeature" /> | ||
<FeatureList /> | ||
</CesiumPopover> | ||
</template> |
Oops, something went wrong.