diff --git a/src/assets/main.css b/src/assets/main.css index a4a4548a..1d9302c6 100644 --- a/src/assets/main.css +++ b/src/assets/main.css @@ -105,7 +105,7 @@ } .lux-caret { - @apply inline-block w-0 h-0 align-middle; + @apply inline-block w-0 h-0 align-middle ml-2; border-top: 4px dashed; border-top: 4px solid \9; border-right: 4px solid transparent; @@ -162,6 +162,16 @@ .lux-navbar-dropdown .lux-dropdown-list-item { @apply uppercase text-center; } + + .lux-scale-line > div { + border: 2px solid grey; + border-top: none; + color: black; + cursor: default; + font-size: 12px; + text-align: center; + margin: 1px; + } } .fa-solid { diff --git a/src/assets/ol.css b/src/assets/ol.css index af94c576..00368c7a 100644 --- a/src/assets/ol.css +++ b/src/assets/ol.css @@ -7,7 +7,8 @@ @apply absolute rounded p-1; } - .ol-control button { + .ol-control > button, + .ol-control-button { @apply w-10 h-10 text-xl text-center font-normal indent-0 block lux-btn m-[-1px] p-0 border-[1px] border-[color:var(--color-btn-ol)]; font-family: 'geoportail-icons-wc'; } @@ -47,10 +48,10 @@ } .lux-infobar-wrapper { - @apply lux-control-button bottom-12 absolute; + @apply lux-control-button bottom-2 absolute; } .lux-infobar-content { - @apply bg-white; + @apply bg-white flex; } } diff --git a/src/components/common/dropdown-list.vue b/src/components/common/dropdown-list.vue index d5d33e49..2a39fae6 100644 --- a/src/components/common/dropdown-list.vue +++ b/src/components/common/dropdown-list.vue @@ -7,7 +7,7 @@ const props = withDefaults( defineProps<{ placeholder: string options: DropdownOptionModel[] - modelValue?: string + modelValue?: string | number }>(), { options: () => [{ label: 'Default label', value: 'Default value' }], diff --git a/src/components/infobar-content/projection-selector.vue b/src/components/infobar-content/projection-selector.vue new file mode 100644 index 00000000..125f2913 --- /dev/null +++ b/src/components/infobar-content/projection-selector.vue @@ -0,0 +1,44 @@ + + + diff --git a/src/components/infobar-content/scale-selector.vue b/src/components/infobar-content/scale-selector.vue index ddc0230a..17c0a04f 100644 --- a/src/components/infobar-content/scale-selector.vue +++ b/src/components/infobar-content/scale-selector.vue @@ -1,5 +1,5 @@ + + diff --git a/src/components/map-controls/scale-line-control.vue b/src/components/map-controls/scale-line-control.vue new file mode 100644 index 00000000..6b2df9c2 --- /dev/null +++ b/src/components/map-controls/scale-line-control.vue @@ -0,0 +1,30 @@ + + + diff --git a/src/composables/control/control.composable.ts b/src/composables/control/control.composable.ts index b8855089..3bb88308 100644 --- a/src/composables/control/control.composable.ts +++ b/src/composables/control/control.composable.ts @@ -10,18 +10,23 @@ export default function useControl(ControlClass: typeof Control, options: any) { const map = useMap() const olMap = inject('olMap') as OlMap - onMounted(() => { + onMounted(() => addControlToMap()) + onUnmounted(() => removeControlFromMap()) + + function addControlToMap() { olMap.addControl(control) olMap.changed() - }) + } - onUnmounted(() => { + function removeControlFromMap() { const olMap = map.getOlMap() olMap.removeControl(control) olMap.changed() - }) + } return { control, + addControlToMap, + removeControlFromMap, } } diff --git a/src/composables/map/map.composable.ts b/src/composables/map/map.composable.ts index adbe17bb..e7730e62 100644 --- a/src/composables/map/map.composable.ts +++ b/src/composables/map/map.composable.ts @@ -7,10 +7,6 @@ import type { MapContext, } from '@/stores/map.store.model' -export const PROJECTION_WEBMERCATOR = 'EPSG:3857' -export const PROJECTION_WGS84 = 'EPSG:4326' -export const PROJECTION_LUX = 'EPSG:2169' - let map: OlMap export default function useMap() { diff --git a/src/composables/map/ol.composable.ts b/src/composables/map/ol.composable.ts index d4d43128..e5cecd11 100644 --- a/src/composables/map/ol.composable.ts +++ b/src/composables/map/ol.composable.ts @@ -13,7 +13,6 @@ import type { Layer, LayerId } from '@/stores/map.store.model' import useMap from './map.composable' import { VectorSourceDict } from '@/composables/mvt-styles/mvt-styles.model' import { statePersistorStyleService } from '@/services/state-persistor/state-persistor-bgstyle.service' -import { PROJECTION_WEBMERCATOR, PROJECTION_WGS84 } from './map.composable' import { isHiDpi, stringToBoolean } from '@/services/utils' import { storageHelper } from '@/services/state-persistor/storage/storage.helper' import { SP_KEY_IPV6 } from '@/services/state-persistor/state-persistor.model' @@ -22,6 +21,7 @@ import { TILE_MATRIX_IDS, } from '@/__fixtures__/wmts.fixture' import { useStyleStore } from '@/stores/style.store' +import { PROJECTIONS } from '@/services/projection.utils' const proxyWmsUrl = 'https://map.geoportail.lu/ogcproxywms' export const remoteProxyWms = 'https://map.geoportail.lu/httpsproxy' @@ -29,8 +29,8 @@ export const remoteProxyWms = 'https://map.geoportail.lu/httpsproxy' function getOlcsExtent() { return transformExtent( [5.31, 49.38, 6.64, 50.21], - PROJECTION_WGS84, - PROJECTION_WEBMERCATOR + PROJECTIONS.WGS84, + PROJECTIONS.WEBMERCATOR ) } @@ -62,7 +62,7 @@ function createWmsLayer(layer: Layer): ImageLayer { function createWmtsLayer(layer: Layer): TileLayer { const { name, imageType, id } = layer const hasRetina = getLayerHasRetina(layer) - const projection = getProjection(PROJECTION_WEBMERCATOR)! + const projection = getProjection(PROJECTIONS.WEBMERCATOR)! const extent = projection!.getExtent() const olLayer = new TileLayer({ diff --git a/src/composables/map/ol.synchronizer.ts b/src/composables/map/ol.synchronizer.ts index 2e181dcb..a88978a5 100644 --- a/src/composables/map/ol.synchronizer.ts +++ b/src/composables/map/ol.synchronizer.ts @@ -21,6 +21,12 @@ export class OlSynchronizer { const styleService = useMvtStyles() const openLayers = useOpenLayers() const { appliedStyle } = storeToRefs(styleStore) + const { viewZoom } = storeToRefs(mapStore) + + watch( + viewZoom, + viewZoom => viewZoom !== undefined && map.getView().setZoom(viewZoom) + ) watch( () => mapStore.layers, @@ -70,7 +76,6 @@ export class OlSynchronizer { openLayers.setBgLayer(map, bgLayer, styleStore.bgVectorSources) ) - //const appliedStyle = computed(() => watchEffect(() => { if (!styleStore.isExpertStyleActive) { // must ignore typing error (too deep) diff --git a/src/services/projection.utils.ts b/src/services/projection.utils.ts index 8756964d..35778dac 100644 --- a/src/services/projection.utils.ts +++ b/src/services/projection.utils.ts @@ -1,12 +1,25 @@ -import { get as getProjection } from 'ol/proj' +import { get as getProjection, transform } from 'ol/proj' import { register } from 'ol/proj/proj4' +import { Coordinate, format } from 'ol/coordinate' +import { padNumber } from 'ol/string' import proj4 from 'proj4' +export enum PROJECTIONS { + LUREF = 'EPSG:2169', + WGS84 = 'EPSG:4326', + WGS84DMS = 'EPSG:4326:DMS', + WGS84DM = 'EPSG:4326:DMm', + WGS84UTM3231 = 'EPSG:3263*', + WGS84UTM31 = 'EPSG:32631', + WGS84UTM32 = 'EPSG:32632', + WEBMERCATOR = 'EPSG:3857', +} + export function initProjections() { proj4.defs('EPSG:32632', '+proj=utm +zone=32 +datum=WGS84 +units=m +no_defs') proj4.defs('EPSG:32631', '+proj=utm +zone=31 +datum=WGS84 +units=m +no_defs') proj4.defs( - 'EPSG:2169', + PROJECTIONS.LUREF, '+proj=tmerc +lat_0=49.83333333333334 +lon_0=6.166666666666667 ' + '+k=1 +x_0=80000 +y_0=100000 +ellps=intl ' + '+towgs84=-189.681,18.3463,-42.7695,-0.33746,-3.09264,2.53861,0.4598 ' + @@ -21,7 +34,75 @@ export function initProjections() { getProjection('EPSG:32631')?.setExtent([ 166021.44, 0.0, 833978.55, 9329005.18, ]) - getProjection('EPSG:2169')?.setExtent([ + getProjection(PROJECTIONS.LUREF)?.setExtent([ 48225.17, 56225.6, 105842.04, 139616.4, ]) } + +export function coordinatesToString( + coordinate: Coordinate, + sourceProj: string, + destinationProj: string, + optDMS: boolean, + optDMm: boolean +) { + let formattedCoordinates: string + + if (destinationProj === PROJECTIONS.WGS84UTM3231) { + const lonlat = transform(coordinate, sourceProj, PROJECTIONS.WGS84) + destinationProj = + Math.floor(lonlat[0]) >= 6 + ? PROJECTIONS.WGS84UTM32 + : PROJECTIONS.WGS84UTM31 + } + + coordinate = transform(coordinate, sourceProj, destinationProj) + + switch (destinationProj) { + default: + case PROJECTIONS.LUREF: + formattedCoordinates = format(coordinate, '{x} E | {y} N', 0) + break + case PROJECTIONS.WGS84: + if (optDMS) { + const hdms = coordinateToStringHDMm(coordinate) + const yhdms = hdms.split(' ').slice(0, 4).join(' ') + const xhdms = hdms.split(' ').slice(4, 8).join(' ') + formattedCoordinates = xhdms + ' | ' + yhdms + } else if (optDMm) { + const hdmm = coordinateToStringHDMm(coordinate) + const yhdmm = hdmm.split(' ').slice(0, 3).join(' ') + const xhdmm = hdmm.split(' ').slice(3, 6).join(' ') + formattedCoordinates = xhdmm + ' | ' + yhdmm + } else { + formattedCoordinates = format(coordinate, ' {x} E | {y} N', 5) + } + break + case PROJECTIONS.WGS84UTM32: + formattedCoordinates = format(coordinate, '{x} | {y} (UTM32N)', 0) + break + case PROJECTIONS.WGS84UTM31: + formattedCoordinates = format(coordinate, '{x} | {y} (UTM31N)', 0) + break + } + + return formattedCoordinates +} + +function coordinateToStringHDMm(coordinate: Coordinate) { + return `${degreesToStringHDMm(coordinate[1], 'NS')} ${degreesToStringHDMm( + coordinate[0], + 'EW' + )}` +} + +function degreesToStringHDMm(degrees: number, hemispheres: string) { + const normalizedDegrees = ((degrees + 180) % 360) - 180, + x = Math.abs(3600 * normalizedDegrees), + dd = x / 3600, + m = (dd - Math.floor(dd)) * 60 + + return `${Math.floor(dd)}\u00b0 ${padNumber(Math.floor(m), 2)},${Math.floor( + (m - Math.floor(m)) * 100000 + )}\u2032 ${hemispheres.charAt(normalizedDegrees < 0 ? 1 : 0)}` +} diff --git a/src/services/state-persistor/state-persistor-map.service.ts b/src/services/state-persistor/state-persistor-map.service.ts index 49e38725..d8dceb63 100644 --- a/src/services/state-persistor/state-persistor-map.service.ts +++ b/src/services/state-persistor/state-persistor-map.service.ts @@ -3,11 +3,10 @@ import { getTransform, ProjectionLike, transform } from 'ol/proj' import { Coordinate } from 'ol/coordinate' import ObjectEventType from 'ol/ObjectEventType' -import useMap, { - PROJECTION_WEBMERCATOR, - PROJECTION_LUX, - PROJECTION_WGS84, -} from '@/composables/map/map.composable' +import useMap from '@/composables/map/map.composable' +import { debounce, stringToNumber } from '@/services/utils' +import { useMapStore } from '@/stores/map.store' + import { SP_KEY_ZOOM, SP_KEY_X, @@ -20,7 +19,7 @@ import { KeyZoomV2ToV3, V2_ZOOM_TO_V3_ZOOM_, } from './state-persistor-map.mapper' -import { debounce, stringToNumber } from '@/services/utils' +import { PROJECTIONS } from '../projection.utils' class StatePersistorMapService implements StatePersistorService { bootstrap(): void { @@ -29,10 +28,17 @@ class StatePersistorMapService implements StatePersistorService { } persistZoom() { + const mapStore = useMapStore() const view = useMap().getOlMap().getView() const fnStorageSetValueZoom = () => { - const zoom = view.getZoom() - storageHelper.setValue(SP_KEY_ZOOM, zoom ? Math.ceil(zoom) : null) + const viewZoom = view.getZoom() + const zoom = viewZoom ? Math.ceil(viewZoom) : null + + storageHelper.setValue(SP_KEY_ZOOM, zoom) + + if (zoom) { + mapStore.setViewZoom(zoom) + } } fnStorageSetValueZoom() @@ -80,8 +86,8 @@ class StatePersistorMapService implements StatePersistorService { const y = storageHelper.getValue(SP_KEY_Y, stringToNumber) const srs = storageHelper.getValue(SP_KEY_SRS) as ProjectionLike const lurefToWebMercatorFn = getTransform( - PROJECTION_LUX, - PROJECTION_WEBMERCATOR + PROJECTIONS.LUREF, + PROJECTIONS.WEBMERCATOR ) // TODO: delete params as in legacy? @@ -102,7 +108,7 @@ class StatePersistorMapService implements StatePersistorService { if (x != null && y != null) { // keep "!=" for not null AND not undefined if (version === 3 && srs != null) { - viewCenter = transform([x, y], srs, PROJECTION_WEBMERCATOR) + viewCenter = transform([x, y], srs, PROJECTIONS.WEBMERCATOR) } else { viewCenter = version === 3 ? [x, y] : lurefToWebMercatorFn([y, x], undefined, 2) @@ -110,8 +116,8 @@ class StatePersistorMapService implements StatePersistorService { } else { viewCenter = transform( [6, 49.7], - PROJECTION_WGS84, - PROJECTION_WEBMERCATOR + PROJECTIONS.WGS84, + PROJECTIONS.WEBMERCATOR ) } diff --git a/src/stores/map.store.ts b/src/stores/map.store.ts index b86d035f..5cd0d105 100644 --- a/src/stores/map.store.ts +++ b/src/stores/map.store.ts @@ -7,6 +7,7 @@ export const useMapStore = defineStore('map', () => { const map: Ref = ref({}) const layers: ShallowRef = shallowRef([]) const bgLayer: Ref = ref(undefined) // undefined => at start app | null => blank bgLayer + const mapProjection: Ref = ref(undefined) const viewZoom: Ref = ref() function setBgLayer(layer: Layer | null) { @@ -52,6 +53,7 @@ export const useMapStore = defineStore('map', () => { map, layers, bgLayer, + mapProjection, viewZoom, addLayers, removeLayers,