Skip to content

Commit 94fa9f8

Browse files
committed
WIP maplibre component
1 parent 4af6e65 commit 94fa9f8

20 files changed

+536
-12
lines changed

packages/geoadmin-coordinates/src/proj/CoordinateSystem.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import CoordinateSystemBounds from '@/proj/CoordinateSystemBounds'
1313
* could lead to initialization errors (even when initializing the constants before importing the
1414
* class). Thus we declare them here, at the root class of the coordinates systems.
1515
*/
16-
export const STANDARD_ZOOM_LEVEL_1_25000_MAP: number = 15.5
16+
export const STANDARD_ZOOM_LEVEL_1_25000_MAP: number = 8
1717
export const SWISS_ZOOM_LEVEL_1_25000_MAP: number = 8
1818

1919
/**

packages/mapviewer/.env.development

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,5 @@ VITE_API_SERVICES_BASE_URL=https://sys-map.dev.bgdi.ch/api/
99
VITE_API_SERVICE_KML_BASE_URL=https://sys-public.dev.bgdi.ch/
1010
VITE_APP_API_SERVICE_SHORTLINK_BASE_URL=https://sys-s.dev.bgdi.ch/
1111
VITE_APP_3D_TILES_BASE_URL=https://sys-3d.dev.bgdi.ch/
12-
VITE_APP_VECTORTILES_BASE_URL=https://sys-verctortiles.dev.bgdi.ch/
12+
VITE_APP_VECTORTILES_BASE_URL=https://sys-vectortiles.dev.bgdi.ch/
1313
VITE_APP_SERVICE_PROXY_BASE_URL=https://sys-proxy.dev.bgdi.ch/

packages/mapviewer/.env.integration

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,5 @@ VITE_API_SERVICES_BASE_URL=https://sys-map.int.bgdi.ch/api/
88
VITE_API_SERVICE_KML_BASE_URL=https://sys-public.int.bgdi.ch/
99
VITE_APP_API_SERVICE_SHORTLINK_BASE_URL=https://sys-s.int.bgdi.ch/
1010
VITE_APP_3D_TILES_BASE_URL=https://sys-3d.int.bgdi.ch/
11-
VITE_APP_VECTORTILES_BASE_URL=https://sys-verctortiles.int.bgdi.ch/
11+
VITE_APP_VECTORTILES_BASE_URL=https://sys-vectortiles.int.bgdi.ch/
1212
VITE_APP_SERVICE_PROXY_BASE_URL=https://sys-proxy.int.bgdi.ch/

packages/mapviewer/src/api/layers/layers.api.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ import GeoAdminWMSLayer from '@/api/layers/GeoAdminWMSLayer.class'
1010
import GeoAdminWMTSLayer from '@/api/layers/GeoAdminWMTSLayer.class'
1111
import LayerTimeConfig from '@/api/layers/LayerTimeConfig.class'
1212
import LayerTimeConfigEntry from '@/api/layers/LayerTimeConfigEntry.class'
13-
import { getApi3BaseUrl } from '@/config/baseUrl.config'
13+
import { getApi3BaseUrl, getVectorTilesBaseUrl } from '@/config/baseUrl.config'
1414
import { DEFAULT_GEOADMIN_MAX_WMTS_RESOLUTION } from '@/config/map.config'
15+
import { VECTOR_LIGHT_BASE_MAP_STYLE_ID } from '@/config/vectortiles.config.js'
1516

1617
// API file that covers the backend endpoint http://api3.geo.admin.ch/rest/services/all/MapServer/layersConfig
1718

@@ -260,3 +261,17 @@ export const loadLayersConfigFromBackend = (lang) => {
260261
}
261262
})
262263
}
264+
265+
export function loadVectorTileStyle() {
266+
return new Promise((resolve, reject) => {
267+
axios
268+
.get(`${getVectorTilesBaseUrl()}styles/${VECTOR_LIGHT_BASE_MAP_STYLE_ID}/style.json`)
269+
.then((response) => {
270+
resolve(response.data)
271+
})
272+
.catch((err) => {
273+
log.error('Unable to load vector tile style', err)
274+
reject(err)
275+
})
276+
})
277+
}

packages/mapviewer/src/config/map.config.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import { LV95 } from '@geoadmin/coordinates'
1+
import { WEBMERCATOR } from '@geoadmin/coordinates'
22

33
/**
44
* Default projection to be used throughout the application
55
*
66
* @type {CoordinateSystem}
77
*/
8-
export const DEFAULT_PROJECTION = LV95
8+
export const DEFAULT_PROJECTION = WEBMERCATOR
99

1010
/**
1111
* Default tile size to use when requesting WMS tiles with our internal WMSs (512px)

packages/mapviewer/src/config/vectortiles.config.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
*
66
* @type {string}
77
*/
8-
export const VECTOR_LIGHT_BASE_MAP_STYLE_ID = 'ch.swisstopo.leichte-basiskarte_world.vt'
8+
export const VECTOR_LIGHT_BASE_MAP_STYLE_ID = 'ch.swisstopo.basemap.vt'
99

1010
/**
1111
* Imagery base map style ID
@@ -14,4 +14,4 @@ export const VECTOR_LIGHT_BASE_MAP_STYLE_ID = 'ch.swisstopo.leichte-basiskarte_w
1414
*
1515
* @type {string}
1616
*/
17-
export const VECTOR_TILES_IMAGERY_STYLE_ID = 'ch.swisstopo.leichte-basiskarte-imagery_world.vt'
17+
export const VECTOR_TILES_IMAGERY_STYLE_ID = 'ch.swisstopo.imagerybasemap.vt'

packages/mapviewer/src/modules/map/MapModule.vue

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,17 @@ import { useStore } from 'vuex'
55
import CompareSlider from './components/CompareSlider.vue'
66
import LocationPopup from './components/LocationPopup.vue'
77
import WarningRibbon from './components/WarningRibbon.vue'
8+
89
const CesiumMap = defineAsyncComponent(() => import('./components/cesium/CesiumMap.vue'))
10+
const MapLibreMap = defineAsyncComponent(() => import('./components/maplibre/MaplibreMap.vue'))
911
const OpenLayersMap = defineAsyncComponent(
1012
() => import('./components/openlayers/OpenLayersMap.vue')
1113
)
1214
1315
const store = useStore()
1416
1517
const is3DActive = computed(() => store.state.cesium.active)
18+
const showMapLibre = computed(() => store.state.debug.showMapLibre)
1619
1720
const displayLocationPopup = computed(
1821
() => store.state.map.locationPopupCoordinates && !store.state.ui.embed
@@ -33,6 +36,11 @@ const isCompareSliderActive = computed(() => {
3336
<LocationPopup v-if="displayLocationPopup" />
3437
<slot name="footer" />
3538
</CesiumMap>
39+
<MapLibreMap v-else-if="showMapLibre">
40+
<slot />
41+
<LocationPopup v-if="displayLocationPopup" />
42+
<slot name="footer" />
43+
</MapLibreMap>
3644
<OpenLayersMap v-else>
3745
<!-- So that external modules can have access to the map instance through the provided 'getMap' -->
3846
<slot />
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<template>
2+
<div>
3+
<slot />
4+
</div>
5+
</template>
6+
<script>
7+
import axios from 'axios'
8+
9+
import transformGeoadminGeoJSONStyleIntoMapboxStyle from '@/modules/map/components/maplibre/utils/transformGeoadminGeoJSONStyleIntoMapboxStyle.js'
10+
11+
import addLayerToMaplibreMixin from './utils/addLayerToMaplibre-mixins.js'
12+
13+
export default {
14+
mixins: [addLayerToMaplibreMixin],
15+
props: {
16+
layerId: {
17+
type: String,
18+
required: true,
19+
},
20+
styleUrl: {
21+
type: String,
22+
required: true,
23+
},
24+
dataUrl: {
25+
type: String,
26+
required: true,
27+
},
28+
zIndex: {
29+
type: Number,
30+
default: -1,
31+
},
32+
},
33+
created() {
34+
axios.all([axios.get(this.dataUrl), axios.get(this.styleUrl)]).then((responses) => {
35+
const data = responses[0].data
36+
const geoadminStyle = responses[1].data
37+
this.layerSource = {
38+
type: 'geojson',
39+
data: data,
40+
}
41+
this.layerStyle = {
42+
id: this.layerId,
43+
type: 'circle',
44+
paint: transformGeoadminGeoJSONStyleIntoMapboxStyle(geoadminStyle),
45+
}
46+
})
47+
},
48+
}
49+
</script>
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<script setup>
2+
import { computed } from 'vue'
3+
import { useStore } from 'vuex'
4+
5+
import LayerTypes from '@/api/layers/LayerTypes.enum'
6+
import MaplibreGeoJSONLayer from '@/modules/map/components/maplibre/MaplibreGeoJSONLayer.vue'
7+
import MaplibreWMSLayer from '@/modules/map/components/maplibre/MaplibreWMSLayer.vue'
8+
import MaplibreWMTSLayer from '@/modules/map/components/maplibre/MaplibreWMTSLayer.vue'
9+
10+
const { layerConfig, zIndex } = defineProps({
11+
layerConfig: {
12+
type: Object,
13+
default: null,
14+
},
15+
zIndex: {
16+
type: Number,
17+
default: -1,
18+
},
19+
})
20+
21+
const store = useStore()
22+
// To be able to manage aggregate layers, we need to know the current map resolution
23+
const resolution = computed(() => store.getters.resolution)
24+
25+
function shouldAggregateSubLayerBeVisible(subLayer) {
26+
// min and max resolution are set in the API file to the lowest/highest possible value if undefined, so we don't
27+
// have to worry about checking their validity
28+
return resolution.value >= subLayer.minResolution && resolution.value <= subLayer.maxResolution
29+
}
30+
</script>
31+
32+
<template>
33+
<div>
34+
<MaplibreWMTSLayer
35+
v-if="layerConfig.type === LayerTypes.WMTS"
36+
:wmts-layer-config="layerConfig"
37+
:z-index="zIndex"
38+
/>
39+
<MaplibreWMSLayer
40+
v-if="layerConfig.type === LayerTypes.WMS"
41+
:wms-layer-config="layerConfig"
42+
:z-index="zIndex"
43+
/>
44+
<MaplibreGeoJSONLayer
45+
v-if="layerConfig.type === LayerTypes.GEOJSON"
46+
:layer-id="layerConfig.id"
47+
:data-url="layerConfig.geoJsonUrl"
48+
:style-url="layerConfig.styleUrl"
49+
:z-index="zIndex"
50+
/><!--
51+
Aggregate layers are some kind of a edge case where two or more layers are joint together but only one of them
52+
is visible depending on the map resolution.
53+
We have to manage aggregate layers straight here otherwise we won't be able to make a recursive call to this
54+
component in another child (that would be OpenLayersAggregateLayer.vue component, that doesn't work).
55+
See https://vuejs.org/v2/guide/components-edge-cases.html#Recursive-Components for more info
56+
-->
57+
<div v-if="layerConfig.type === LayerTypes.AGGREGATE">
58+
<!-- we can't v-for and v-if at the same time, so we need to wrap all sub-layers in a <div> -->
59+
<div
60+
v-for="aggregateSubLayer in layerConfig.subLayers"
61+
:key="aggregateSubLayer.subLayerId"
62+
>
63+
<maplibre-internal-layer
64+
v-if="shouldAggregateSubLayerBeVisible(aggregateSubLayer)"
65+
:layer-config="aggregateSubLayer.layer"
66+
:z-index="zIndex"
67+
/>
68+
</div>
69+
</div>
70+
<!-- <OpenLayersKMLLayer-->
71+
<!-- v-if="layerConfig.type === LayerTypes.KML"-->
72+
<!-- :layer-id="layerConfig.id"-->
73+
<!-- :opacity="layerConfig.opacity"-->
74+
<!-- :url="layerConfig.getURL()"-->
75+
<!-- :z-index="zIndex"-->
76+
<!-- />-->
77+
<slot />
78+
</div>
79+
</template>
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
<script lang="ts">
2+
import { Map } from 'maplibre-gl'
3+
4+
export type MapLibreProvider = () => Map
5+
</script>
6+
<script lang="ts" setup>
7+
import { type SingleCoordinate, WEBMERCATOR, WGS84 } from '@geoadmin/coordinates'
8+
import { round } from '@geoadmin/numbers'
9+
import { Map } from 'maplibre-gl'
10+
import proj4 from 'proj4'
11+
import { computed, onMounted, provide, ref, watch } from 'vue'
12+
import { useStore } from 'vuex'
13+
14+
import type AbstractLayer from '@/api/layers/AbstractLayer.class'
15+
16+
import { getVectorTilesBaseUrl } from '@/config/baseUrl.config'
17+
import { VECTOR_LIGHT_BASE_MAP_STYLE_ID } from '@/config/vectortiles.config'
18+
import MaplibreInternalLayer from '@/modules/map/components/maplibre/MaplibreInternalLayer.vue'
19+
20+
import 'maplibre-gl/dist/maplibre-gl.css'
21+
22+
const dispatcher = {
23+
dispatcher: 'MapLibreMap.vue',
24+
}
25+
26+
const centerChangeTriggeredByMe = ref<boolean>()
27+
28+
const store = useStore()
29+
30+
const zoom = computed<number>(() => store.state.position.zoom - 1)
31+
const centerEpsg4326 = computed<SingleCoordinate>(() => store.getters.centerEpsg4326)
32+
const visibleLayers = computed<AbstractLayer[]>(() => store.getters.visibleLayers)
33+
const currentBackgroundLayer = computed<AbstractLayer>(() => store.getters.currentBackgroundLayer)
34+
35+
let mapLibreMap: Map | undefined
36+
37+
onMounted(() => {
38+
mapLibreMap = new Map({
39+
container: 'maplibre-map',
40+
style: `${getVectorTilesBaseUrl()}styles/${VECTOR_LIGHT_BASE_MAP_STYLE_ID}/testing/poc-terrain/style.json`,
41+
center: centerEpsg4326.value,
42+
zoom: zoom.value,
43+
maxPitch: 75,
44+
})
45+
46+
// Click management
47+
let clickStartTimestamp: number = 0
48+
let lastClickDuration: number = 0
49+
mapLibreMap.on('mousedown', () => {
50+
clickStartTimestamp = performance.now()
51+
})
52+
mapLibreMap.on('mouseup', () => {
53+
lastClickDuration = performance.now() - clickStartTimestamp
54+
clickStartTimestamp = 0
55+
})
56+
mapLibreMap.on('click', (e) => {
57+
const clickLocation: SingleCoordinate = proj4(WGS84.epsg, WEBMERCATOR.epsg, [
58+
round(e.lngLat.lng, 6),
59+
round(e.lngLat.lat, 6),
60+
])
61+
store.dispatch('click', {
62+
coordinate: clickLocation,
63+
millisecondsSpentMouseDown: lastClickDuration,
64+
...dispatcher,
65+
})
66+
})
67+
68+
// position management
69+
mapLibreMap.on('moveend', () => {
70+
const mapboxCenter = mapLibreMap.getCenter()
71+
centerChangeTriggeredByMe.value = true
72+
store.dispatch('setCenter', {
73+
center: proj4(WGS84.epsg, WEBMERCATOR.epsg, [
74+
round(mapboxCenter.lng, 6),
75+
round(mapboxCenter.lat, 6),
76+
]),
77+
...dispatcher,
78+
})
79+
const newZoom = round(mapLibreMap.getZoom(), 3)
80+
if (newZoom && newZoom !== zoom.value) {
81+
store.dispatch('setZoom', {
82+
zoom: newZoom + 1,
83+
...dispatcher,
84+
})
85+
}
86+
})
87+
})
88+
89+
provide<MapLibreProvider>('getMapLibreMap', () => mapLibreMap)
90+
provide('getStyle', () => style.value)
91+
92+
watch(centerEpsg4326, (newCenter) => {
93+
if (mapLibreMap) {
94+
if (centerChangeTriggeredByMe.value) {
95+
centerChangeTriggeredByMe.value = false
96+
} else {
97+
mapLibreMap.flyTo({
98+
center: newCenter,
99+
zoom: zoom.value,
100+
})
101+
}
102+
}
103+
})
104+
watch(zoom, (newZoom) => {
105+
mapLibreMap?.flyTo({
106+
center: centerEpsg4326.value,
107+
zoom: newZoom,
108+
})
109+
})
110+
</script>
111+
112+
<template>
113+
<div id="maplibre-map">
114+
<MaplibreInternalLayer
115+
v-for="(layer, index) in visibleLayers"
116+
:key="layer.id"
117+
:layer-config="layer"
118+
:z-index="index + (currentBackgroundLayer ? 1 : 0)"
119+
/>
120+
<slot />
121+
</div>
122+
</template>
123+
124+
<style>
125+
#maplibre-map {
126+
width: 100%;
127+
height: 100%;
128+
}
129+
</style>

0 commit comments

Comments
 (0)