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-722 : Cesium rework #1086

Open
wants to merge 13 commits into
base: develop
Choose a base branch
from
Open
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
15 changes: 15 additions & 0 deletions src/modules/map/components/MapPopover.vue
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ const currentHeaderHeight = computed(() => store.state.ui.headerHeight)
const isPhoneMode = computed(() => store.getters.isPhoneMode)
const isDesktopMode = computed(() => store.getters.isTraditionalDesktopSize)

const size = ref({
height: popover.value?.clientHeight ?? 0,
width: popover.value?.clientWidth ?? 0,
})

const cssPositionOnScreen = computed(() => {
if (mode.value === MapPopoverMode.FEATURE_TOOLTIP) {
return {
Expand Down Expand Up @@ -104,6 +109,12 @@ const popoverLimits = computed(() => {
})

onMounted(() => {
if (popover.value) {
size.value = {
height: popover.value.clientHeight,
width: popover.value.clientWidth,
}
}
if (mode.value === MapPopoverMode.FLOATING && popover.value && popoverHeader.value) {
useMovableElement(popover.value, {
grabElement: popoverHeader,
Expand All @@ -115,6 +126,10 @@ onMounted(() => {
function onClose() {
emits('close')
}

defineExpose({
size: size,
})
</script>

<template>
Expand Down
24 changes: 24 additions & 0 deletions src/modules/map/components/cesium/CesiumBackgroundLayer.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<script setup>
import { computed } from 'vue'
import { useStore } from 'vuex'

import CesiumInternalLayer from '@/modules/map/components/cesium/CesiumInternalLayer.vue'

const store = useStore()

const backgroundLayersFor3D = computed(() => store.getters.backgroundLayersFor3D)
</script>

<template>
<!--
z-index can be set to zero for all, as only the WMTS
background layer is an imagery layer (and requires one), all other BG layer are
primitive layer and will ignore this prop
-->
<CesiumInternalLayer
v-for="(bgLayer, index) in backgroundLayersFor3D"
:key="bgLayer.id"
:layer-config="bgLayer"
:z-index="index"
/>
</template>
199 changes: 199 additions & 0 deletions src/modules/map/components/cesium/CesiumCamera.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
<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,
})

/** @returns {CameraPosition | null} */
function getCurrentCameraPosition() {
const viewer = getViewer()
if (!viewer) {
return null
}
const camera = viewer.camera
const position = camera.positionCartographic
return {
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))),
}
}

function flyToPosition() {
const viewer = getViewer()
if (!viewer) {
return
}
const currentPosition = getCurrentCameraPosition()
if (isEqual(currentPosition, cameraPosition.value)) {
log.debug(
'[Cesium] Camera position is already at the correct location, no changes made to the Cesium viewer'
)
return
}
try {
log.debug(
`[Cesium] Fly to camera position ${cameraPosition.value.x}, ${cameraPosition.value.y}, ${cameraPosition.value.z}`
)
viewer.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 newCameraPosition = getCurrentCameraPosition()
if (newCameraPosition !== null && !isEqual(newCameraPosition, cameraPosition.value)) {
log.debug(
'[Cesium] Camera position has changed, dispatching to the store',
cameraPosition.value,
newCameraPosition
)
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>
71 changes: 38 additions & 33 deletions src/modules/map/components/cesium/CesiumGPXLayer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
GpxDataSource,
HeightReference,
} from 'cesium'
import { onBeforeUnmount, onMounted, watch } from 'vue'
import { onBeforeUnmount, onMounted, toRefs, watch } from 'vue'
import { inject } from 'vue'
import { computed } from 'vue'

Expand All @@ -21,37 +21,44 @@ const props = defineProps({
},
})

const gpxData = computed(() => props.gpxLayerConfig.gpxData)
const { gpxLayerConfig } = toRefs(props)

let gpxDataSource = null
const gpxData = computed(() => gpxLayerConfig.value.gpxData)
const opacity = computed(() => gpxLayerConfig.value.opacity)

const getViewer = inject('getViewer')

watch(gpxData, addLayer)

onMounted(addLayer)
onBeforeUnmount(removeLayer)

function addLayer() {
removeLayer()
const gpxBlob = new Blob([gpxData.value], { type: 'application/gpx+xml' })
gpxDataSource = new GpxDataSource()
gpxDataSource
.load(gpxBlob, {
clampToGround: true,
})
.then((dataSource) => {
updateStyle()
getViewer().dataSources.add(dataSource)
getViewer().scene.requestRender()
})
}
onMounted(addGpxLayer)
onBeforeUnmount(removeGpxLayer)

watch(gpxData, addGpxLayer)
watch(opacity, applyStyleToGpxEntities)

let gpxDataSource = null

function removeLayer() {
function removeGpxLayer() {
if (gpxDataSource) {
getViewer().dataSources.remove(gpxDataSource)
const viewer = getViewer()
viewer.dataSources.remove(gpxDataSource)
viewer.scene.requestRender()
gpxDataSource = null
getViewer().scene.requestRender()
}
}

function addGpxLayer() {
removeGpxLayer()
if (gpxData.value) {
new GpxDataSource()
.load(new Blob([gpxData.value], { type: 'application/gpx+xml' }), {
clampToGround: true,
})
.then((dataSource) => {
gpxDataSource = dataSource
applyStyleToGpxEntities()
const viewer = getViewer()
viewer.dataSources.add(dataSource)
viewer.scene.requestRender()
})
}
}

Expand All @@ -68,9 +75,7 @@ function createRedCircleImage(radius, opacity = 1) {
// Draw a red circle on the canvas
context.beginPath()
context.arc(radius, radius, radius, 0, 2 * Math.PI, false)

const color = `rgba(255, 0, 0, ${opacity})`
context.fillStyle = color
context.fillStyle = `rgba(255, 0, 0, ${opacity})`
context.fill()

// Return the data URL of the canvas drawing
Expand All @@ -89,10 +94,10 @@ function createRedCircleBillboard(radius, opacity = 1) {
})
}

function updateStyle() {
function applyStyleToGpxEntities() {
const radius = 8
const redCircleBillboard = createRedCircleBillboard(radius)
const redColorMaterial = new ColorMaterialProperty(Color.RED)
const redCircleBillboard = createRedCircleBillboard(radius, opacity.value)
const redColorMaterial = new ColorMaterialProperty(Color.RED.withAlpha(opacity.value))

const entities = gpxDataSource.entities.values
entities.forEach((entity) => {
Expand All @@ -110,13 +115,13 @@ function updateStyle() {

if (cesiumDefined(entity.polyline)) {
entity.polyline.material = redColorMaterial
entity.polyline.width = 1.5
entity.polyline.width = 3
}

if (cesiumDefined(entity.polygon)) {
entity.polygon.material = redColorMaterial
entity.polygon.outline = true
entity.polygon.outlineColor = Color.BLACK
entity.polygon.outlineColor = Color.BLACK.withAlpha(opacity.value)
}
})
getViewer().scene.requestRender()
Expand Down
Loading
Loading