Skip to content

Commit 230a8e7

Browse files
committed
WIP maplibre component
1 parent 8708b04 commit 230a8e7

24 files changed

+594
-16
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: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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, previousLayerId } = defineProps({
11+
layerConfig: {
12+
type: Object,
13+
default: null,
14+
},
15+
previousLayerId: {
16+
type: String,
17+
default: null,
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+
<MaplibreWMTSLayer
34+
v-if="layerConfig.type === LayerTypes.WMTS"
35+
:wmts-layer-config="layerConfig"
36+
:previous-layer-id="previousLayerId"
37+
/>
38+
<MaplibreWMSLayer
39+
v-if="layerConfig.type === LayerTypes.WMS"
40+
:wms-layer-config="layerConfig"
41+
:previous-layer-id="previousLayerId"
42+
/>
43+
<MaplibreGeoJSONLayer
44+
v-if="layerConfig.type === LayerTypes.GEOJSON"
45+
:layer-id="layerConfig.id"
46+
:data-url="layerConfig.geoJsonUrl"
47+
:style-url="layerConfig.styleUrl"
48+
/><!--
49+
Aggregate layers are some kind of a edge case where two or more layers are joint together but only one of them
50+
is visible depending on the map resolution.
51+
We have to manage aggregate layers straight here otherwise we won't be able to make a recursive call to this
52+
component in another child (that would be OpenLayersAggregateLayer.vue component, that doesn't work).
53+
See https://vuejs.org/v2/guide/components-edge-cases.html#Recursive-Components for more info
54+
-->
55+
<template v-if="layerConfig.type === LayerTypes.AGGREGATE">
56+
<!-- we can't v-for and v-if at the same time, so we need to wrap all sub-layers in a <div> -->
57+
<template
58+
v-for="aggregateSubLayer in layerConfig.subLayers"
59+
:key="aggregateSubLayer.subLayerId"
60+
>
61+
<maplibre-internal-layer
62+
v-if="shouldAggregateSubLayerBeVisible(aggregateSubLayer)"
63+
:layer-config="aggregateSubLayer.layer"
64+
:previous-layer-id="previousLayerId"
65+
/>
66+
</template>
67+
</template>
68+
<!-- <OpenLayersKMLLayer-->
69+
<!-- v-if="layerConfig.type === LayerTypes.KML"-->
70+
<!-- :layer-id="layerConfig.id"-->
71+
<!-- :opacity="layerConfig.opacity"-->
72+
<!-- :url="layerConfig.getURL()"-->
73+
<!-- :z-index="zIndex"-->
74+
<!-- />-->
75+
<slot />
76+
</template>
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
<script setup>
2+
import { WEBMERCATOR, WGS84 } from '@geoadmin/coordinates'
3+
import { round } from '@geoadmin/numbers'
4+
import { Map } from 'maplibre-gl'
5+
import proj4 from 'proj4'
6+
import { computed, onMounted, provide, ref, useTemplateRef, watch } from 'vue'
7+
import { useStore } from 'vuex'
8+
9+
import { getVectorTilesBaseUrl } from '@/config/baseUrl.config'
10+
import { VECTOR_LIGHT_BASE_MAP_STYLE_ID } from '@/config/vectortiles.config'
11+
import MaplibreInternalLayer from '@/modules/map/components/maplibre/MaplibreInternalLayer.vue'
12+
13+
const dispatcher = {
14+
dispatcher: 'MapLibreMap.vue',
15+
}
16+
17+
const centerChangeTriggeredByMe = ref(false)
18+
const mapLibreMapElement = useTemplateRef('mapLibreContainer')
19+
20+
const store = useStore()
21+
22+
const zoom = computed(() => store.state.position.zoom - 1)
23+
const centerEpsg4326 = computed(() => store.getters.centerEpsg4326)
24+
const visibleLayers = computed(() => store.getters.visibleLayers)
25+
26+
let mapLibreMap
27+
28+
onMounted(() => {
29+
mapLibreMap = new Map({
30+
container: mapLibreMapElement.value,
31+
style: `${getVectorTilesBaseUrl()}styles/${VECTOR_LIGHT_BASE_MAP_STYLE_ID}/testing/poc-terrain/style.json`,
32+
center: centerEpsg4326.value,
33+
zoom: zoom.value,
34+
maxPitch: 75,
35+
})
36+
37+
mapLibreMap.once('load', () => {
38+
store.dispatch('mapModuleReady', dispatcher)
39+
})
40+
41+
// Click management
42+
let clickStartTimestamp = 0
43+
let lastClickDuration = 0
44+
mapLibreMap.on('mousedown', () => {
45+
clickStartTimestamp = performance.now()
46+
})
47+
mapLibreMap.on('mouseup', () => {
48+
lastClickDuration = performance.now() - clickStartTimestamp
49+
clickStartTimestamp = 0
50+
})
51+
mapLibreMap.on('click', (e) => {
52+
const clickLocation = proj4(WGS84.epsg, WEBMERCATOR.epsg, [
53+
round(e.lngLat.lng, 6),
54+
round(e.lngLat.lat, 6),
55+
])
56+
store.dispatch('click', {
57+
coordinate: clickLocation,
58+
millisecondsSpentMouseDown: lastClickDuration,
59+
...dispatcher,
60+
})
61+
})
62+
63+
// position management
64+
mapLibreMap.on('moveend', () => {
65+
if (!mapLibreMap) {
66+
return
67+
}
68+
const mapboxCenter = mapLibreMap.getCenter()
69+
centerChangeTriggeredByMe.value = true
70+
store.dispatch('setCenter', {
71+
center: proj4(WGS84.epsg, WEBMERCATOR.epsg, [
72+
round(mapboxCenter.lng, 6),
73+
round(mapboxCenter.lat, 6),
74+
]),
75+
...dispatcher,
76+
})
77+
const newZoom = round(mapLibreMap.getZoom(), 3)
78+
if (newZoom && newZoom !== zoom.value) {
79+
store.dispatch('setZoom', {
80+
zoom: newZoom + 1,
81+
...dispatcher,
82+
})
83+
}
84+
})
85+
})
86+
87+
provide('getMapLibreMap', () => mapLibreMap)
88+
89+
watch(centerEpsg4326, (newCenter) => {
90+
if (mapLibreMap) {
91+
if (centerChangeTriggeredByMe.value) {
92+
centerChangeTriggeredByMe.value = false
93+
} else {
94+
mapLibreMap.flyTo({
95+
center: newCenter,
96+
zoom: zoom.value,
97+
})
98+
}
99+
}
100+
})
101+
watch(zoom, (newZoom) => {
102+
mapLibreMap?.flyTo({
103+
center: centerEpsg4326.value,
104+
zoom: newZoom,
105+
})
106+
})
107+
</script>
108+
109+
<template>
110+
<div
111+
ref="mapLibreContainer"
112+
class="maplibre-map"
113+
>
114+
<MaplibreInternalLayer
115+
v-for="(layer, index) in visibleLayers.toReversed()"
116+
:key="layer.id"
117+
:layer-config="layer"
118+
:previous-layer-id="index === 0 ? null : visibleLayers[index - 1].id"
119+
/>
120+
<slot />
121+
</div>
122+
</template>
123+
124+
<style lang="scss" scoped>
125+
@import '@/scss/webmapviewer-bootstrap-theme';
126+
127+
.maplibre-map {
128+
top: 0;
129+
left: 0;
130+
width: 100%;
131+
height: 100%;
132+
position: absolute; // Element must be positioned to set a z-index
133+
z-index: $zindex-map;
134+
}
135+
</style>

0 commit comments

Comments
 (0)