diff --git a/contao/templates/frontend/event_map.html.twig b/contao/templates/frontend/event_map.html.twig
index 9e2adb0..2e17f7f 100644
--- a/contao/templates/frontend/event_map.html.twig
+++ b/contao/templates/frontend/event_map.html.twig
@@ -112,4 +112,4 @@
-
+
diff --git a/public/assets/map-module.js b/public/assets/map-module.js
index c81a3cf..3a0f5f8 100644
--- a/public/assets/map-module.js
+++ b/public/assets/map-module.js
@@ -21,8 +21,60 @@ const SATELLITE_ROADS_SOURCE_ID = 'eventmanager-satellite-roads-source';
const SATELLITE_ROADS_LAYER_ID = 'eventmanager-satellite-roads-layer';
const SATELLITE_ROADS_TILE_URL = 'https://services.arcgisonline.com/ArcGIS/rest/services/Reference/World_Transportation/MapServer/tile/{z}/{y}/{x}';
const SATELLITE_ATTRIBUTION = '© Esri, Maxar';
+const PROTOMAPS_SOURCE_ID = 'protomaps';
+const DEFAULT_BASE_STYLE_URL = 'https://maps.mummert.media/metadaten/world-light.json';
+const PROTOMAPS_PM_TILES_URL = 'pmtiles://https://maps.mummert.media/tiles/world.pmtiles';
+const PROTOMAPS_ATTRIBUTION = 'Protomaps © OpenStreetMap';
+const PROTOMAPS_GLYPHS_URL = 'https://maps.mummert.media/fonts/{fontstack}/{range}.pbf';
+const PROTOMAPS_SPRITE_URL = 'https://maps.mummert.media/sprites/v4/light';
+const PROTOMAPS_FLAVOR_URL = 'https://maps.mummert.media/metadaten/flavors/bio.json';
+const PROTOMAPS_BASEMAPS_ESM_URLS = [
+ 'https://cdn.jsdelivr.net/npm/@protomaps/basemaps@5/+esm',
+ 'https://esm.sh/@protomaps/basemaps@5',
+];
+const PROTOMAPS_BASEMAPS_UMD_URL = 'https://unpkg.com/@protomaps/basemaps@5/dist/basemaps.js';
let dependenciesPromise = null;
+let basemapsApiPromise = null;
+const protomapsStylePromises = new Map();
+
+const getBasemapsApi = async () => {
+ if (basemapsApiPromise) {
+ return basemapsApiPromise;
+ }
+
+ basemapsApiPromise = (async () => {
+ for (const url of PROTOMAPS_BASEMAPS_ESM_URLS) {
+ try {
+ const module = await import(url);
+ const moduleApi = module && 'object' === typeof module
+ ? (module.layers ? module : (module.default && module.default.layers ? module.default : null))
+ : null;
+
+ if (moduleApi && 'function' === typeof moduleApi.layers) {
+ return moduleApi;
+ }
+ } catch (error) {
+ continue;
+ }
+ }
+
+ await loadScript(PROTOMAPS_BASEMAPS_UMD_URL);
+
+ if (window.basemaps && 'function' === typeof window.basemaps.layers) {
+ return window.basemaps;
+ }
+
+ throw new Error('Failed to load Protomaps basemaps API');
+ })();
+
+ basemapsApiPromise = basemapsApiPromise.catch((error) => {
+ basemapsApiPromise = null;
+ throw error;
+ });
+
+ return basemapsApiPromise;
+};
const ensureStylesheet = (href) => {
if (!href) {
@@ -94,6 +146,69 @@ const ensureDependencies = async () => {
return dependenciesPromise;
};
+const getProtomapsStyle = async (baseStyleUrl = DEFAULT_BASE_STYLE_URL) => {
+ const normalizedBaseStyleUrl = String(baseStyleUrl || DEFAULT_BASE_STYLE_URL).trim() || DEFAULT_BASE_STYLE_URL;
+
+ if (protomapsStylePromises.has(normalizedBaseStyleUrl)) {
+ return protomapsStylePromises.get(normalizedBaseStyleUrl);
+ }
+
+ const stylePromise = (async () => {
+ const baseStyleResponse = await fetch(normalizedBaseStyleUrl, { credentials: 'omit' });
+
+ if (!baseStyleResponse.ok) {
+ throw new Error(`Failed to load base map style: ${baseStyleResponse.status}`);
+ }
+
+ const baseStyle = await baseStyleResponse.json();
+ const baseSources = baseStyle?.sources && 'object' === typeof baseStyle.sources
+ ? baseStyle.sources
+ : {};
+ const protomapsSource = baseSources[PROTOMAPS_SOURCE_ID] && 'object' === typeof baseSources[PROTOMAPS_SOURCE_ID]
+ ? baseSources[PROTOMAPS_SOURCE_ID]
+ : {
+ type: 'vector',
+ attribution: PROTOMAPS_ATTRIBUTION,
+ url: PROTOMAPS_PM_TILES_URL,
+ };
+
+ try {
+ const basemapsApi = await getBasemapsApi();
+ const flavorResponse = await fetch(PROTOMAPS_FLAVOR_URL, { credentials: 'omit' });
+
+ if (!flavorResponse.ok) {
+ throw new Error(`Failed to load Protomaps flavor: ${flavorResponse.status}`);
+ }
+
+ const flavor = await flavorResponse.json();
+ const generatedLayers = basemapsApi.layers(PROTOMAPS_SOURCE_ID, flavor, { lang: 'de' });
+
+ return {
+ ...baseStyle,
+ version: 8,
+ glyphs: baseStyle?.glyphs || PROTOMAPS_GLYPHS_URL,
+ sprite: baseStyle?.sprite || PROTOMAPS_SPRITE_URL,
+ sources: {
+ ...baseSources,
+ [PROTOMAPS_SOURCE_ID]: protomapsSource,
+ },
+ layers: generatedLayers,
+ };
+ } catch (error) {
+ return baseStyle;
+ }
+ })();
+
+ const cachedStylePromise = stylePromise.catch((error) => {
+ protomapsStylePromises.delete(normalizedBaseStyleUrl);
+ throw error;
+ });
+
+ protomapsStylePromises.set(normalizedBaseStyleUrl, cachedStylePromise);
+
+ return cachedStylePromise;
+};
+
const rewriteGlyphUrl = (url) => {
if (!url) {
return url;
@@ -1152,7 +1267,7 @@ const initSingleMap = (container) => {
container.dataset.mapInitialized = '1';
ensureDependencies()
- .then(() => {
+ .then(async () => {
if (typeof maplibregl === 'undefined') {
return;
}
@@ -1163,7 +1278,8 @@ const initSingleMap = (container) => {
window.__eventmanagerPmtilesRegistered = true;
}
- const style = container.dataset.mapStyle || 'https://maps.mummert.media/metadaten/world-light.json';
+ const baseStyleUrl = container.dataset.mapStyle || DEFAULT_BASE_STYLE_URL;
+ const style = await getProtomapsStyle(baseStyleUrl);
const map = new maplibregl.Map({
container,
style,