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,