Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/geoadmin-coordinates/src/proj/CoordinateSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import CoordinateSystemBounds from '@/proj/CoordinateSystemBounds'
* could lead to initialization errors (even when initializing the constants before importing the
* class). Thus we declare them here, at the root class of the coordinates systems.
*/
export const STANDARD_ZOOM_LEVEL_1_25000_MAP: number = 15.5
export const STANDARD_ZOOM_LEVEL_1_25000_MAP: number = 8
export const SWISS_ZOOM_LEVEL_1_25000_MAP: number = 8

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/mapviewer/.env.development
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ VITE_API_SERVICES_BASE_URL=https://sys-map.dev.bgdi.ch/api/
VITE_API_SERVICE_KML_BASE_URL=https://sys-public.dev.bgdi.ch/
VITE_APP_API_SERVICE_SHORTLINK_BASE_URL=https://sys-s.dev.bgdi.ch/
VITE_APP_3D_TILES_BASE_URL=https://sys-3d.dev.bgdi.ch/
VITE_APP_VECTORTILES_BASE_URL=https://sys-verctortiles.dev.bgdi.ch/
VITE_APP_VECTORTILES_BASE_URL=https://sys-vectortiles.dev.bgdi.ch/
VITE_APP_SERVICE_PROXY_BASE_URL=https://sys-proxy.dev.bgdi.ch/
2 changes: 1 addition & 1 deletion packages/mapviewer/.env.integration
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ VITE_API_SERVICES_BASE_URL=https://sys-map.int.bgdi.ch/api/
VITE_API_SERVICE_KML_BASE_URL=https://sys-public.int.bgdi.ch/
VITE_APP_API_SERVICE_SHORTLINK_BASE_URL=https://sys-s.int.bgdi.ch/
VITE_APP_3D_TILES_BASE_URL=https://sys-3d.int.bgdi.ch/
VITE_APP_VECTORTILES_BASE_URL=https://sys-verctortiles.int.bgdi.ch/
VITE_APP_VECTORTILES_BASE_URL=https://sys-vectortiles.int.bgdi.ch/
VITE_APP_SERVICE_PROXY_BASE_URL=https://sys-proxy.int.bgdi.ch/
17 changes: 16 additions & 1 deletion packages/mapviewer/src/api/layers/layers.api.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import GeoAdminWMSLayer from '@/api/layers/GeoAdminWMSLayer.class'
import GeoAdminWMTSLayer from '@/api/layers/GeoAdminWMTSLayer.class'
import LayerTimeConfig from '@/api/layers/LayerTimeConfig.class'
import LayerTimeConfigEntry from '@/api/layers/LayerTimeConfigEntry.class'
import { getApi3BaseUrl } from '@/config/baseUrl.config'
import { getApi3BaseUrl, getVectorTilesBaseUrl } from '@/config/baseUrl.config'
import { DEFAULT_GEOADMIN_MAX_WMTS_RESOLUTION } from '@/config/map.config'
import { VECTOR_LIGHT_BASE_MAP_STYLE_ID } from '@/config/vectortiles.config.js'

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

Expand Down Expand Up @@ -260,3 +261,17 @@ export const loadLayersConfigFromBackend = (lang) => {
}
})
}

export function loadVectorTileStyle() {
return new Promise((resolve, reject) => {
axios
.get(`${getVectorTilesBaseUrl()}styles/${VECTOR_LIGHT_BASE_MAP_STYLE_ID}/style.json`)
.then((response) => {
resolve(response.data)
})
.catch((err) => {
log.error('Unable to load vector tile style', err)
reject(err)
})
})
}
4 changes: 2 additions & 2 deletions packages/mapviewer/src/config/map.config.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { LV95 } from '@geoadmin/coordinates'
import { WEBMERCATOR } from '@geoadmin/coordinates'

/**
* Default projection to be used throughout the application
*
* @type {CoordinateSystem}
*/
export const DEFAULT_PROJECTION = LV95
export const DEFAULT_PROJECTION = WEBMERCATOR

/**
* Default tile size to use when requesting WMS tiles with our internal WMSs (512px)
Expand Down
4 changes: 2 additions & 2 deletions packages/mapviewer/src/config/vectortiles.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*
* @type {string}
*/
export const VECTOR_LIGHT_BASE_MAP_STYLE_ID = 'ch.swisstopo.leichte-basiskarte_world.vt'
export const VECTOR_LIGHT_BASE_MAP_STYLE_ID = 'ch.swisstopo.basemap.vt'

/**
* Imagery base map style ID
Expand All @@ -14,4 +14,4 @@ export const VECTOR_LIGHT_BASE_MAP_STYLE_ID = 'ch.swisstopo.leichte-basiskarte_w
*
* @type {string}
*/
export const VECTOR_TILES_IMAGERY_STYLE_ID = 'ch.swisstopo.leichte-basiskarte-imagery_world.vt'
export const VECTOR_TILES_IMAGERY_STYLE_ID = 'ch.swisstopo.imagerybasemap.vt'
8 changes: 8 additions & 0 deletions packages/mapviewer/src/modules/map/MapModule.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@ import { useStore } from 'vuex'
import CompareSlider from './components/CompareSlider.vue'
import LocationPopup from './components/LocationPopup.vue'
import WarningRibbon from './components/WarningRibbon.vue'

const CesiumMap = defineAsyncComponent(() => import('./components/cesium/CesiumMap.vue'))
const MapLibreMap = defineAsyncComponent(() => import('./components/maplibre/MaplibreMap.vue'))
const OpenLayersMap = defineAsyncComponent(
() => import('./components/openlayers/OpenLayersMap.vue')
)

const store = useStore()

const is3DActive = computed(() => store.state.cesium.active)
const showMapLibre = computed(() => store.state.debug.showMapLibre)

const displayLocationPopup = computed(
() => store.state.map.locationPopupCoordinates && !store.state.ui.embed
Expand All @@ -33,6 +36,11 @@ const isCompareSliderActive = computed(() => {
<LocationPopup v-if="displayLocationPopup" />
<slot name="footer" />
</CesiumMap>
<MapLibreMap v-else-if="showMapLibre">
<slot />
<LocationPopup v-if="displayLocationPopup" />
<slot name="footer" />
</MapLibreMap>
<OpenLayersMap v-else>
<!-- So that external modules can have access to the map instance through the provided 'getMap' -->
<slot />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<template>
<div>
<slot />
</div>
</template>
<script>
import axios from 'axios'

import transformGeoadminGeoJSONStyleIntoMapboxStyle from '@/modules/map/components/maplibre/utils/transformGeoadminGeoJSONStyleIntoMapboxStyle.js'

import addLayerToMaplibreMixin from './utils/addLayerToMaplibre-mixins.js'

export default {
mixins: [addLayerToMaplibreMixin],
props: {
layerId: {
type: String,
required: true,
},
styleUrl: {
type: String,
required: true,
},
dataUrl: {
type: String,
required: true,
},
zIndex: {
type: Number,
default: -1,
},
},
created() {
axios.all([axios.get(this.dataUrl), axios.get(this.styleUrl)]).then((responses) => {
const data = responses[0].data
const geoadminStyle = responses[1].data
this.layerSource = {
type: 'geojson',
data: data,
}
this.layerStyle = {
id: this.layerId,
type: 'circle',
paint: transformGeoadminGeoJSONStyleIntoMapboxStyle(geoadminStyle),
}
})
},
}
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<script setup>
import { computed } from 'vue'
import { useStore } from 'vuex'

import LayerTypes from '@/api/layers/LayerTypes.enum'
import MaplibreGeoJSONLayer from '@/modules/map/components/maplibre/MaplibreGeoJSONLayer.vue'
import MaplibreWMSLayer from '@/modules/map/components/maplibre/MaplibreWMSLayer.vue'
import MaplibreWMTSLayer from '@/modules/map/components/maplibre/MaplibreWMTSLayer.vue'

const { layerConfig, previousLayerId } = defineProps({
layerConfig: {
type: Object,
default: null,
},
previousLayerId: {
type: String,
default: null,
},
})

const store = useStore()
// To be able to manage aggregate layers, we need to know the current map resolution
const resolution = computed(() => store.getters.resolution)

function shouldAggregateSubLayerBeVisible(subLayer) {
// min and max resolution are set in the API file to the lowest/highest possible value if undefined, so we don't
// have to worry about checking their validity
return resolution.value >= subLayer.minResolution && resolution.value <= subLayer.maxResolution
}
</script>

<template>
<MaplibreWMTSLayer
v-if="layerConfig.type === LayerTypes.WMTS"
:wmts-layer-config="layerConfig"
:previous-layer-id="previousLayerId"
/>
<MaplibreWMSLayer
v-if="layerConfig.type === LayerTypes.WMS"
:wms-layer-config="layerConfig"
:previous-layer-id="previousLayerId"
/>
<MaplibreGeoJSONLayer
v-if="layerConfig.type === LayerTypes.GEOJSON"
:layer-id="layerConfig.id"
:data-url="layerConfig.geoJsonUrl"
:style-url="layerConfig.styleUrl"
/><!--
Aggregate layers are some kind of a edge case where two or more layers are joint together but only one of them
is visible depending on the map resolution.
We have to manage aggregate layers straight here otherwise we won't be able to make a recursive call to this
component in another child (that would be OpenLayersAggregateLayer.vue component, that doesn't work).
See https://vuejs.org/v2/guide/components-edge-cases.html#Recursive-Components for more info
-->
<template v-if="layerConfig.type === LayerTypes.AGGREGATE">
<!-- we can't v-for and v-if at the same time, so we need to wrap all sub-layers in a <div> -->
<template
v-for="aggregateSubLayer in layerConfig.subLayers"
:key="aggregateSubLayer.subLayerId"
>
<maplibre-internal-layer
v-if="shouldAggregateSubLayerBeVisible(aggregateSubLayer)"
:layer-config="aggregateSubLayer.layer"
:previous-layer-id="previousLayerId"
/>
</template>
</template>
<!-- <OpenLayersKMLLayer-->
<!-- v-if="layerConfig.type === LayerTypes.KML"-->
<!-- :layer-id="layerConfig.id"-->
<!-- :opacity="layerConfig.opacity"-->
<!-- :url="layerConfig.getURL()"-->
<!-- :z-index="zIndex"-->
<!-- />-->
<slot />
</template>
135 changes: 135 additions & 0 deletions packages/mapviewer/src/modules/map/components/maplibre/MaplibreMap.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
<script setup>
import { WEBMERCATOR, WGS84 } from '@geoadmin/coordinates'
import { round } from '@geoadmin/numbers'
import { Map } from 'maplibre-gl'
import proj4 from 'proj4'
import { computed, onMounted, provide, ref, useTemplateRef, watch } from 'vue'
import { useStore } from 'vuex'

import { getVectorTilesBaseUrl } from '@/config/baseUrl.config'
import { VECTOR_LIGHT_BASE_MAP_STYLE_ID } from '@/config/vectortiles.config'
import MaplibreInternalLayer from '@/modules/map/components/maplibre/MaplibreInternalLayer.vue'

const dispatcher = {
dispatcher: 'MapLibreMap.vue',
}

const centerChangeTriggeredByMe = ref(false)
const mapLibreMapElement = useTemplateRef('mapLibreContainer')

const store = useStore()

const zoom = computed(() => store.state.position.zoom - 1)
const centerEpsg4326 = computed(() => store.getters.centerEpsg4326)
const visibleLayers = computed(() => store.getters.visibleLayers)

let mapLibreMap

onMounted(() => {
mapLibreMap = new Map({
container: mapLibreMapElement.value,
style: `${getVectorTilesBaseUrl()}styles/${VECTOR_LIGHT_BASE_MAP_STYLE_ID}/testing/poc-terrain/style.json`,
center: centerEpsg4326.value,
zoom: zoom.value,
maxPitch: 75,
})

mapLibreMap.once('load', () => {
store.dispatch('mapModuleReady', dispatcher)
})

// Click management
let clickStartTimestamp = 0
let lastClickDuration = 0
mapLibreMap.on('mousedown', () => {
clickStartTimestamp = performance.now()
})
mapLibreMap.on('mouseup', () => {
lastClickDuration = performance.now() - clickStartTimestamp
clickStartTimestamp = 0
})
mapLibreMap.on('click', (e) => {
const clickLocation = proj4(WGS84.epsg, WEBMERCATOR.epsg, [
round(e.lngLat.lng, 6),
round(e.lngLat.lat, 6),
])
store.dispatch('click', {
coordinate: clickLocation,
millisecondsSpentMouseDown: lastClickDuration,
...dispatcher,
})
})

// position management
mapLibreMap.on('moveend', () => {
if (!mapLibreMap) {
return
}
const mapboxCenter = mapLibreMap.getCenter()
centerChangeTriggeredByMe.value = true
store.dispatch('setCenter', {
center: proj4(WGS84.epsg, WEBMERCATOR.epsg, [
round(mapboxCenter.lng, 6),
round(mapboxCenter.lat, 6),
]),
...dispatcher,
})
const newZoom = round(mapLibreMap.getZoom(), 3)
if (newZoom && newZoom !== zoom.value) {
store.dispatch('setZoom', {
zoom: newZoom + 1,
...dispatcher,
})
}
})
})

provide('getMapLibreMap', () => mapLibreMap)

watch(centerEpsg4326, (newCenter) => {
if (mapLibreMap) {
if (centerChangeTriggeredByMe.value) {
centerChangeTriggeredByMe.value = false
} else {
mapLibreMap.flyTo({
center: newCenter,
zoom: zoom.value,
})
}
}
})
watch(zoom, (newZoom) => {
mapLibreMap?.flyTo({
center: centerEpsg4326.value,
zoom: newZoom,
})
})
</script>

<template>
<div
ref="mapLibreContainer"
class="maplibre-map"
>
<MaplibreInternalLayer
v-for="(layer, index) in visibleLayers.toReversed()"
:key="layer.id"
:layer-config="layer"
:previous-layer-id="index === 0 ? null : visibleLayers[index - 1].id"
/>
<slot />
</div>
</template>

<style lang="scss" scoped>
@import '@/scss/webmapviewer-bootstrap-theme';

.maplibre-map {
top: 0;
left: 0;
width: 100%;
height: 100%;
position: absolute; // Element must be positioned to set a z-index
z-index: $zindex-map;
}
</style>
Loading
Loading