Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PB-1204 : split CesiumMap component into multiple sub-parts #1170

Merged
merged 1 commit into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 180 additions & 0 deletions src/modules/map/components/cesium/CesiumCamera.vue
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 src/modules/map/components/cesium/CesiumHighlightedFeatures.vue
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>
Loading
Loading