Skip to content

PB-97 : implement zoom to extent in 3D #599

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

Merged
merged 4 commits into from
Jan 15, 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
119 changes: 70 additions & 49 deletions src/modules/map/components/cesium/CesiumMap.vue
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,14 @@ export default {
// see https://vuejs.org/guide/essentials/watchers.html#callback-flush-timing
flush: 'post',
},
centerEpsg4326: {
handler() {
if (this.isProjectionWebMercator && this.cameraPosition) {
this.flyToPosition()
}
},
flush: 'post',
},
},
beforeCreate() {
// Global variable required for Cesium and point to the URL where four static directories (see vite.config) are served
Expand Down Expand Up @@ -267,24 +275,11 @@ export default {
// 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
const ray = this.viewer.camera.getPickRay(
new Cartesian2(
Math.round(this.viewer.scene.canvas.clientWidth / 2),
Math.round(this.viewer.scene.canvas.clientHeight / 2)
)
)
const cameraTarget = this.viewer.scene.globe.pick(ray, this.viewer.scene)
if (defined(cameraTarget)) {
const cameraTargetCartographic =
Ellipsoid.WGS84.cartesianToCartographic(cameraTarget)
const lat = CesiumMath.toDegrees(cameraTargetCartographic.latitude)
const lon = CesiumMath.toDegrees(cameraTargetCartographic.longitude)
this.setCenter(proj4(WGS84.epsg, this.projection.epsg, [lon, lat]))
}
this.setCenterToCameraTarget()
}
},
unmounted() {
this.setCameraPosition(null)
this.setCameraPosition({ position: null, source: 'CesiumMap unmount' })
this.viewer.destroy()
delete this.viewer
},
Expand Down Expand Up @@ -415,46 +410,53 @@ export default {
: firstFeature.coordinates
},
flyToPosition() {
if (this.cameraPosition) {
this.viewer.camera.flyTo({
destination: Cartesian3.fromDegrees(
this.cameraPosition.x,
this.cameraPosition.y,
this.cameraPosition.z
),
orientation: {
heading: CesiumMath.toRadians(this.cameraPosition.heading),
pitch: CesiumMath.toRadians(this.cameraPosition.pitch),
roll: CesiumMath.toRadians(this.cameraPosition.roll),
},
duration: 0,
})
} else {
this.viewer.camera.flyTo({
destination: Cartesian3.fromDegrees(
this.centerEpsg4326[0],
this.centerEpsg4326[1],
calculateHeight(this.resolution, this.viewer.canvas.clientWidth)
),
orientation: {
heading: -this.rotation,
pitch: -CesiumMath.PI_OVER_TWO,
roll: 0,
},
duration: 0,
})
try {
if (this.cameraPosition) {
this.viewer.camera.flyTo({
destination: Cartesian3.fromDegrees(
this.cameraPosition.x,
this.cameraPosition.y,
this.cameraPosition.z
),
orientation: {
heading: CesiumMath.toRadians(this.cameraPosition.heading),
pitch: CesiumMath.toRadians(this.cameraPosition.pitch),
roll: CesiumMath.toRadians(this.cameraPosition.roll),
},
duration: 1,
})
} else {
this.viewer.camera.flyTo({
destination: Cartesian3.fromDegrees(
this.centerEpsg4326[0],
this.centerEpsg4326[1],
calculateHeight(this.resolution, this.viewer.canvas.clientWidth)
),
orientation: {
heading: -CesiumMath.toRadians(this.rotation),
pitch: -CesiumMath.PI_OVER_TWO,
roll: 0,
},
duration: 0,
})
}
} catch (error) {
log.error('Error while moving the camera', error, this.cameraPosition)
}
},
onCameraMoveEnd() {
const camera = this.viewer.camera
const position = camera.positionCartographic
this.setCameraPosition({
x: CesiumMath.toDegrees(position.longitude).toFixed(6),
y: CesiumMath.toDegrees(position.latitude).toFixed(6),
z: position.height.toFixed(0),
heading: CesiumMath.toDegrees(camera.heading).toFixed(0),
pitch: CesiumMath.toDegrees(camera.pitch).toFixed(0),
roll: CesiumMath.toDegrees(camera.roll).toFixed(0),
position: {
x: parseFloat(CesiumMath.toDegrees(position.longitude).toFixed(6)),
y: parseFloat(CesiumMath.toDegrees(position.latitude).toFixed(6)),
z: parseFloat(position.height.toFixed(1)),
heading: parseFloat(CesiumMath.toDegrees(camera.heading).toFixed(0)),
pitch: parseFloat(CesiumMath.toDegrees(camera.pitch).toFixed(0)),
roll: parseFloat(CesiumMath.toDegrees(camera.roll).toFixed(0)),
},
source: 'CesiumMap camera move end',
})
},
getCoordinateAtScreenCoordinate(x, y) {
Expand Down Expand Up @@ -560,6 +562,25 @@ export default {
clearLongPressTimer() {
clearTimeout(this.contextMenuTimeoutId)
},
setCenterToCameraTarget() {
const ray = this.viewer.camera.getPickRay(
new Cartesian2(
Math.round(this.viewer.scene.canvas.clientWidth / 2),
Math.round(this.viewer.scene.canvas.clientHeight / 2)
)
)
const cameraTarget = this.viewer.scene.globe.pick(ray, this.viewer.scene)
if (defined(cameraTarget)) {
const cameraTargetCartographic =
Ellipsoid.WGS84.cartesianToCartographic(cameraTarget)
const lat = CesiumMath.toDegrees(cameraTargetCartographic.latitude)
const lon = CesiumMath.toDegrees(cameraTargetCartographic.longitude)
this.setCenter({
center: proj4(WGS84.epsg, this.projection.epsg, [lon, lat]),
source: 'CesiumMap',
})
}
},
},
}
</script>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,11 @@ export default function useViewBasedOnProjection(map) {
if (currentView) {
const [x, y] = currentView.getCenter()
if (x !== center.value[0] || y !== center.value[1]) {
store.dispatch('setCenter', { x, y })
store.dispatch('setCenter', { center: { x, y }, source: 'OpenLayers' })
}
const currentZoom = round(currentView.getZoom(), 3)
if (currentZoom && currentZoom !== zoom.value) {
store.dispatch('setZoom', currentZoom)
store.dispatch('setZoom', { zoom: currentZoom, source: 'OpenLayers' })
}
const currentRotation = currentView.getRotation()
if (currentRotation !== rotation.value) {
Expand Down
4 changes: 3 additions & 1 deletion src/router/storeSync/CameraParamConfig.class.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ function dispatchCameraFromUrlIntoStore(store, urlParamValue) {
const promisesForAllDispatch = []
const camera = readCameraFromUrlParam(urlParamValue)
if (camera) {
promisesForAllDispatch.push(store.dispatch('setCameraPosition', camera))
promisesForAllDispatch.push(
store.dispatch('setCameraPosition', { position: camera, source: 'URL param parsing' })
)
}
return Promise.all(promisesForAllDispatch)
}
Expand Down
4 changes: 3 additions & 1 deletion src/router/storeSync/PositionParamConfig.class.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ function dispatchCenterFromUrlIntoStore(store, urlParamValue) {
const promisesForAllDispatch = []
const center = readCenterFromUrlParam(urlParamValue)
if (center) {
promisesForAllDispatch.push(store.dispatch('setCenter', center))
promisesForAllDispatch.push(
store.dispatch('setCenter', { center, source: 'URL param parsing' })
)
}
return Promise.all(promisesForAllDispatch)
}
Expand Down
37 changes: 37 additions & 0 deletions src/router/storeSync/ZoomParamConfig.class.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import AbstractParamConfig from '@/router/storeSync/abstractParamConfig.class'

export function readZoomFromUrlParam(urlParamValue) {
if (urlParamValue) {
return parseFloat(urlParamValue)
}
return null
}

function dispatchZoomFromUrlIntoStore(store, urlParamValue) {
const promisesForAllDispatch = []
const zoom = readZoomFromUrlParam(urlParamValue)
if (zoom) {
promisesForAllDispatch.push(
store.dispatch('setZoom', { zoom, source: 'URL param parsing' })
)
}
return Promise.all(promisesForAllDispatch)
}

function generateZoomUrlParamFromStoreValues(store) {
return store.state.position.zoom
}

/** Describe the zoom level of the map in the URL. */
export default class ZoomParamConfig extends AbstractParamConfig {
constructor() {
super(
'z',
'setZoom',
dispatchZoomFromUrlIntoStore,
generateZoomUrlParamFromStoreValues,
true,
Number
)
}
}
15 changes: 3 additions & 12 deletions src/router/storeSync/storeSync.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import LayerParamConfig from '@/router/storeSync/LayerParamConfig.class'
import PositionParamConfig from '@/router/storeSync/PositionParamConfig.class'
import QueryToStoreOnlyParamConfig from '@/router/storeSync/QueryToStoreOnlyParamConfig.class'
import SimpleUrlParamConfig from '@/router/storeSync/SimpleUrlParamConfig.class'
import ZoomParamConfig from '@/router/storeSync/ZoomParamConfig.class.js'

/**
* Configuration for all URL parameters of this app that need syncing with the store (and
Expand Down Expand Up @@ -35,14 +36,8 @@ const storeSyncConfig = [
// otherwise the position might be wrongly reprojected at app startup when SR is not equal
// to the default projection EPSG number
new PositionParamConfig(),
new SimpleUrlParamConfig(
'z',
'setZoom',
'setZoom',
(store) => store.state.position.zoom,
true,
Number
),
new CameraParamConfig(),
new ZoomParamConfig(),
new SimpleUrlParamConfig(
'3d',
'set3dActive',
Expand All @@ -52,10 +47,6 @@ const storeSyncConfig = [
Boolean,
false
),
// very important that this is added/defined AFTER the 3D flag param,
// so that when it is called the 3D param has already been processed (and is correctly set in the query)
// this will manage lon,lat,z and camera URL params
new CameraParamConfig(),
new SimpleUrlParamConfig(
'geolocation',
'setGeolocationActive',
Expand Down
32 changes: 18 additions & 14 deletions src/store/modules/__tests__/zoom.store.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,33 +21,35 @@ describe('Zoom level is calculated correctly in the store when using WebMercator
height: screenSize,
})
// we now then center the view on wanted coordinates
await store.dispatch('setCenter', proj4(WGS84.epsg, WEBMERCATOR.epsg, [lon, lat]))
await store.dispatch('setCenter', {
center: proj4(WGS84.epsg, WEBMERCATOR.epsg, [lon, lat]),
})
})

it("Doesn't allow negative zoom level, or non numerical value as a zoom level", async () => {
// setting the zoom at a valid value, and then setting it at an invalid value => the valid value should persist
const validZoomLevel = 10
await store.dispatch('setZoom', validZoomLevel)
await store.dispatch('setZoom', -1)
await store.dispatch('setZoom', { zoom: validZoomLevel })
await store.dispatch('setZoom', { zoom: -1 })
expect(getZoom()).to.eq(validZoomLevel, 'Should not accept negative zoom level')
// checking with non numerical (but representing a number)
await store.dispatch('setZoom', '' + (validZoomLevel - 1))
await store.dispatch('setZoom', { zoom: '' + (validZoomLevel - 1) })
expect(getZoom()).to.eq(
validZoomLevel,
'Should not accept non numerical values as zoom level'
)
await store.dispatch('setZoom', 'test')
await store.dispatch('setZoom', { zoom: 'test' })
expect(getZoom()).to.eq(
validZoomLevel,
'Should not accept non numerical values as zoom level'
)
// checking with undefined or null
await store.dispatch('setZoom', undefined)
await store.dispatch('setZoom', { zoom: undefined })
expect(getZoom()).to.eq(
validZoomLevel,
'Should not accept undefined or null value as zoom level'
)
await store.dispatch('setZoom', null)
await store.dispatch('setZoom', { zoom: null })
expect(getZoom()).to.eq(
validZoomLevel,
'Should not accept undefined or null value as zoom level'
Expand All @@ -56,20 +58,20 @@ describe('Zoom level is calculated correctly in the store when using WebMercator
it('Set zoom level correctly from what is given in "setZoom"', async () => {
// checking zoom level 0 to 24
for (let zoom = 0; zoom < 24; zoom += 1) {
await store.dispatch('setZoom', zoom)
await store.dispatch('setZoom', { zoom })
expect(getZoom()).to.eq(zoom)
}
})
it('Rounds zoom level to the third decimal if more are given', async () => {
// flooring check
await store.dispatch('setZoom', 5.4321)
await store.dispatch('setZoom', { zoom: 5.4321 })
expect(getZoom()).to.eq(5.432)
// ceiling check
await store.dispatch('setZoom', 5.6789)
await store.dispatch('setZoom', { zoom: 5.6789 })
expect(getZoom()).to.eq(5.679)
})
it('Calculate resolution from zoom levels according to OGC standard (with 0.1% error margin)', async () => {
await store.dispatch('setZoom', 10)
await store.dispatch('setZoom', { zoom: 10 })
// see https://wiki.openstreetmap.org/wiki/Zoom_levels
// at zoom level 10, resolution should be of about 152.746 meter per pixel adjusted to latitude
const resolutionAtZoom10 = 152.746
Expand All @@ -81,18 +83,20 @@ describe('Zoom level is calculated correctly in the store when using WebMercator
)

// we move to the equator so that resolution values should then match tables
await store.dispatch('setCenter', proj4(WGS84.epsg, WEBMERCATOR.epsg, [lon, 0]))
await store.dispatch('setCenter', { center: proj4(WGS84.epsg, WEBMERCATOR.epsg, [lon, 0]) })
expect(getResolution()).to.approximately(resolutionAtZoom10, toleratedDelta)

await store.dispatch('setZoom', 2)
await store.dispatch('setZoom', { zoom: 2 })
const resolutionAtZoom2 = 39103
// we tolerate a 0.1% error margin
toleratedDelta = resolutionAtZoom2 / 1000.0
expect(getZoom()).to.eq(2)
// at zoom level 2, resolution should be of about 39'103 meter per pixel at equator
expect(getResolution()).to.approximately(resolutionAtZoom2, toleratedDelta)
// let's go back to latitude 45 and check resolution again
await store.dispatch('setCenter', proj4(WGS84.epsg, WEBMERCATOR.epsg, [lon, lat]))
await store.dispatch('setCenter', {
center: proj4(WGS84.epsg, WEBMERCATOR.epsg, [lon, lat]),
})
expect(getResolution()).to.approximately(
resolutionAtZoom2 * Math.cos((lat * Math.PI) / 180.0),
toleratedDelta
Expand Down
Loading