From 7088d98df47ce018521f998165efdd9a2596e872 Mon Sep 17 00:00:00 2001 From: Pascal Barth Date: Mon, 10 Jun 2024 11:47:59 +0200 Subject: [PATCH] PB-583 : use backend service to get LV03 <> LV95 conversion right Using proj4 isn't accurate enough on a geodesic point of view, as the conversion isn't linear. There's a backend service that does the computation in a proper manner, so that's what is being used here when showing a LV03 value in the location popup. Tracking the mouse position while moving the cursor can still be done with a simple matrix transform, it's "good enough" --- src/api/lv03Reframe.api.js | 49 +++++++++++++++++++ .../map/components/LocationPopupPosition.vue | 20 +++++++- src/utils/components/CoordinateCopySlot.vue | 10 +++- tests/cypress/tests-e2e/mouseposition.cy.js | 6 ++- 4 files changed, 80 insertions(+), 5 deletions(-) create mode 100644 src/api/lv03Reframe.api.js diff --git a/src/api/lv03Reframe.api.js b/src/api/lv03Reframe.api.js new file mode 100644 index 000000000..105aac4b9 --- /dev/null +++ b/src/api/lv03Reframe.api.js @@ -0,0 +1,49 @@ +import axios from 'axios' +import proj4 from 'proj4' + +import { LV03, LV95 } from '@/utils/coordinates/coordinateSystems' +import log from '@/utils/logging' + +const REFRAME_BASE_URL = 'https://geodesy.geo.admin.ch/reframe/' + +/** + * Re-frames LV95 coordinate taking all LV03 -> LV95 deformation into account (they are not stable, + * so using "simple" proj4 matrices isn't enough to get a very accurate result) + * + * @param {[Number, Number]} lv95coordinate LV95 coordinate that we want expressed in LV03 + * @returns {Promise<[Number, Number]>} Input LV95 coordinate re-framed by the backend service into + * LV03 coordinate + * @see https://www.swisstopo.admin.ch/en/rest-api-geoservices-reframe-web + * @see https://github.com/geoadmin/mf-geoadmin3/blob/master/src/components/ReframeService.js + */ +export default function reframe(lv95coordinate) { + return new Promise((resolve, reject) => { + if (!Array.isArray(lv95coordinate) || lv95coordinate.length !== 2) { + reject(new Error('lv95coordinate must be an array with length of 2')) + } + axios({ + method: 'GET', + url: `${REFRAME_BASE_URL}lv95tolv03`, + params: { + easting: lv95coordinate[0], + northing: lv95coordinate[1], + }, + }) + .then((response) => { + if (response.data?.coordinates) { + resolve(response.data.coordinates) + } else { + log.error( + 'Error while re-framing coordinate', + lv95coordinate, + 'fallback to proj4' + ) + resolve(proj4(LV95.epsg, LV03.epsg, lv95coordinate)) + } + }) + .catch((error) => { + log.error('Error while re-framing coordinate', lv95coordinate, error) + reject(error) + }) + }) +} diff --git a/src/modules/map/components/LocationPopupPosition.vue b/src/modules/map/components/LocationPopupPosition.vue index 88f6aa442..c97785411 100644 --- a/src/modules/map/components/LocationPopupPosition.vue +++ b/src/modules/map/components/LocationPopupPosition.vue @@ -6,6 +6,7 @@ import { computed, onMounted, ref, toRefs, watch } from 'vue' import { useI18n } from 'vue-i18n' import { requestHeight } from '@/api/height.api' +import reframe from '@/api/lv03Reframe.api' import { registerWhat3WordsLocation } from '@/api/what3words.api' import CoordinateCopySlot from '@/utils/components/CoordinateCopySlot.vue' import { @@ -15,7 +16,7 @@ import { UTMFormat, WGS84Format, } from '@/utils/coordinates/coordinateFormat' -import { WGS84 } from '@/utils/coordinates/coordinateSystems' +import { LV03, LV95, WGS84 } from '@/utils/coordinates/coordinateSystems' import log from '@/utils/logging' const props = defineProps({ @@ -38,6 +39,7 @@ const props = defineProps({ }) const { coordinate, clickInfo, projection, currentLang } = toRefs(props) +const lv03Coordinate = ref(null) const what3Words = ref(null) const height = ref(null) @@ -69,6 +71,7 @@ const heightInMeter = computed(() => { onMounted(() => { if (clickInfo.value) { + updateLV03Coordinate() updateWhat3Word() updateHeight() } @@ -76,6 +79,7 @@ onMounted(() => { watch(clickInfo, (newClickInfo) => { if (newClickInfo) { + updateLV03Coordinate() updateWhat3Word() updateHeight() } @@ -84,6 +88,16 @@ watch(currentLang, () => { updateWhat3Word() }) +async function updateLV03Coordinate() { + try { + const lv95coordinate = proj4(projection.value.epsg, LV95.epsg, coordinate.value) + lv03Coordinate.value = await reframe(lv95coordinate) + } catch (error) { + log.error('Failed to retrieve LV03 coordinate', error) + lv03Coordinate.value = null + } +} + async function updateWhat3Word() { try { what3Words.value = await registerWhat3WordsLocation( @@ -119,9 +133,11 @@ async function updateHeight() { {{ LV03Format.label }} diff --git a/src/utils/components/CoordinateCopySlot.vue b/src/utils/components/CoordinateCopySlot.vue index 76d561396..abeaba9c5 100644 --- a/src/utils/components/CoordinateCopySlot.vue +++ b/src/utils/components/CoordinateCopySlot.vue @@ -5,6 +5,7 @@ import { useI18n } from 'vue-i18n' import { useStore } from 'vuex' import { CoordinateFormat } from '@/utils/coordinates/coordinateFormat' +import CoordinateSystem from '@/utils/coordinates/CoordinateSystem.class' import log from '@/utils/logging' const props = defineProps({ @@ -28,8 +29,13 @@ const props = defineProps({ type: CoordinateFormat, default: null, }, + coordinateProjection: { + type: CoordinateSystem, + default: null, + }, }) -const { identifier, value, extraValue, resetDelay, coordinateFormat } = toRefs(props) +const { identifier, value, extraValue, resetDelay, coordinateFormat, coordinateProjection } = + toRefs(props) const copyButton = ref(null) const copied = ref(false) @@ -37,7 +43,7 @@ const copied = ref(false) const i18n = useI18n() const store = useStore() -const projection = computed(() => store.state.position.projection) +const projection = computed(() => coordinateProjection.value ?? store.state.position.projection) const lang = computed(() => store.state.i18n.lang) const buttonIcon = computed(() => { diff --git a/tests/cypress/tests-e2e/mouseposition.cy.js b/tests/cypress/tests-e2e/mouseposition.cy.js index 211a9f888..a644cac01 100644 --- a/tests/cypress/tests-e2e/mouseposition.cy.js +++ b/tests/cypress/tests-e2e/mouseposition.cy.js @@ -119,6 +119,9 @@ describe('Test mouse position and interactions', () => { }) it('shows the LocationPopUp when rightclick occurs on the map', () => { let shortUrl = 'https://s.geo.admin.ch/0000000' + + const fakeLV03Coordinate = [1234.56, 7890.12] + cy.intercept('**/lv95tolv03**', { coordinates: fakeLV03Coordinate }).as('reframe') cy.intercept(/^http[s]?:\/\/(sys-s\.\w+\.bgdi\.ch|s\.geo\.admin\.ch)\//, { body: { shorturl: shortUrl, success: true }, }).as('shortlink') @@ -163,10 +166,11 @@ describe('Test mouse position and interactions', () => { .then(checkXY(...centerLV95)) cy.log('it shows coordinates, correctly re-projected into LV95, in the popup') + cy.wait('@reframe') cy.get('[data-cy="location-popup-lv03"]') .invoke('text') .then(parseLV) - .then(checkXY(...centerLV03)) + .then(checkXY(...fakeLV03Coordinate)) cy.log('it shows coordinates, correctly re-projected into LV03, in the popup') cy.get('[data-cy="location-popup-wgs84"]').contains(