import Spiderfy from './vendor/map-gl-js-spiderfy.js'; const MAP_SELECTOR = '[data-eventmanager-map="1"]'; const MAPLIBRE_CSS_URL = 'https://maps.mummert.media/libraries/maplibre-gl.css'; const MAPLIBRE_JS_URL = 'https://maps.mummert.media/libraries/maplibre-gl.js'; const PMTILES_JS_URL = 'https://maps.mummert.media/libraries/pmtiles.js'; const GLYPH_FONTSTACK = 'Noto Sans Regular'; const DEFAULT_CENTER = [13.404954, 52.520008]; const DEFAULT_ZOOM = 6; const DEFAULT_EVENT_COLOR = '#BC5067'; const EVENT_FADE_DURATION_MS = 200; const EVENT_CLUSTER_LAYER_ID = 'eventmanager-events-clusters'; const EVENT_CLUSTER_SPIDERFY_LAYER_ID = 'eventmanager-events-clusters-spiderfy'; const EVENT_UNCLUSTERED_LAYER_ID = 'eventmanager-events-unclustered'; const SATELLITE_SOURCE_ID = 'eventmanager-satellite-source'; const SATELLITE_LAYER_ID = 'eventmanager-satellite-layer'; const SATELLITE_TILE_URL = 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}'; const SATELLITE_ATTRIBUTION = '© Esri, Maxar'; let dependenciesPromise = null; const ensureStylesheet = (href) => { if (!href) { return; } const existing = document.querySelector(`link[rel="stylesheet"][href="${href}"]`); if (existing) { return; } const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = href; document.head.appendChild(link); }; const loadScript = (src) => new Promise((resolve, reject) => { if (!src) { resolve(); return; } const existing = document.querySelector(`script[src="${src}"]`); if (existing) { if (existing.dataset.loaded === '1') { resolve(); return; } existing.addEventListener('load', () => resolve(), { once: true }); existing.addEventListener('error', () => reject(new Error(`Failed to load script: ${src}`)), { once: true }); return; } const script = document.createElement('script'); script.src = src; script.async = true; script.addEventListener('load', () => { script.dataset.loaded = '1'; resolve(); }, { once: true }); script.addEventListener('error', () => reject(new Error(`Failed to load script: ${src}`)), { once: true }); document.head.appendChild(script); }); const ensureDependencies = async () => { if (dependenciesPromise) { return dependenciesPromise; } dependenciesPromise = (async () => { ensureStylesheet(MAPLIBRE_CSS_URL); if (typeof maplibregl === 'undefined') { await loadScript(MAPLIBRE_JS_URL); } if (typeof pmtiles === 'undefined') { await loadScript(PMTILES_JS_URL); } })(); return dependenciesPromise; }; const rewriteGlyphUrl = (url) => { if (!url) { return url; } const match = url.match(/\/fonts\/[^/]+\/(\d+-\d+\.pbf)(\?.*)?$/); if (!match) { return url; } const range = match[1]; const query = match[2] || ''; const base = url.slice(0, match.index); return `${base}/fonts/${encodeURIComponent(GLYPH_FONTSTACK)}/${range}${query}`; }; const escapeHtml = (value) => { const stringValue = String(value ?? ''); return stringValue .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); }; const parseItems = (container) => { const dataElementId = container.dataset.mapDataId || ''; if (!dataElementId) { return []; } const payload = document.getElementById(dataElementId); if (!payload) { return []; } try { const parsed = JSON.parse(payload.textContent || '[]'); if (!Array.isArray(parsed)) { return []; } const unique = new Map(); parsed.forEach((item) => { if (!item || !['event', 'location', 'organisation'].includes(item.type)) { return; } const id = Number(item.id || 0); const latitude = Number(item.latitude); const longitude = Number(item.longitude); if (!Number.isFinite(id) || id <= 0 || !Number.isFinite(latitude) || !Number.isFinite(longitude)) { return; } unique.set(`${item.type}:${id}`, { type: item.type, markerType: String(item.markerType || item.type || ''), id, title: String(item.title || ''), latitude, longitude, extra: item.extra && typeof item.extra === 'object' ? item.extra : {}, }); }); return Array.from(unique.values()); } catch (error) { return []; } }; const parseCoordinate = (value) => { const normalized = String(value ?? '').trim().replace(',', '.'); const parsed = Number(normalized); return Number.isFinite(parsed) ? parsed : null; }; const normalizeHexColor = (value, fallback = DEFAULT_EVENT_COLOR) => { const normalized = String(value || '').trim().toUpperCase(); if (/^#[0-9A-F]{6}$/.test(normalized)) { return normalized; } if (/^[0-9A-F]{6}$/.test(normalized)) { return `#${normalized}`; } if (/^#[0-9A-F]{3}$/.test(normalized)) { return `#${normalized[1]}${normalized[1]}${normalized[2]}${normalized[2]}${normalized[3]}${normalized[3]}`; } if (/^[0-9A-F]{3}$/.test(normalized)) { return `#${normalized[0]}${normalized[0]}${normalized[1]}${normalized[1]}${normalized[2]}${normalized[2]}`; } return fallback; }; const normalizeHexColorOrNull = (value) => { const fallback = '__INVALID__'; const normalized = normalizeHexColor(value, fallback); return normalized === fallback ? null : normalized; }; const parseOrganizationColorScheme = (value) => { const raw = String(value || '').trim(); if ('' === raw) { return []; } return raw .split(',') .map((part) => normalizeHexColorOrNull(part)) .filter((part) => null !== part); }; const buildOrganizationTagColorMap = (container, colors, organizationItems) => { if (!colors.length) { return { byTagId: {}, fallbackColor: null, }; } const fallbackColor = colors[0]; const wrapperId = container.dataset.mapFilterWrapperId || ''; const wrapper = wrapperId ? document.getElementById(wrapperId) : null; let orderedTagIds = []; if (wrapper) { orderedTagIds = Array.from(wrapper.querySelectorAll('[data-map-tag-filter]')) .map((button) => String(button.dataset.mapTagFilter || '').trim()) .filter((value) => /^\d+$/.test(value)); } if (!orderedTagIds.length) { const seen = new Set(); organizationItems.forEach((item) => { const firstTagId = String(item?.extra?.organizationTagIds?.[0] || '').trim(); if (/^\d+$/.test(firstTagId) && !seen.has(firstTagId)) { seen.add(firstTagId); orderedTagIds.push(firstTagId); } }); } const byTagId = {}; orderedTagIds.forEach((tagId, index) => { byTagId[tagId] = colors[index] || fallbackColor; }); return { byTagId, fallbackColor, }; }; const applyDefaultProjection = (map) => { if (!map || typeof map.setProjection !== 'function') { return; } const hasGlobeProjection = () => { if (typeof map.getProjection !== 'function') { return false; } const projection = map.getProjection(); const projectionName = String(projection?.type || projection?.name || '').toLowerCase(); return 'globe' === projectionName; }; let globeApplied = false; try { map.setProjection({ type: 'globe' }); globeApplied = hasGlobeProjection(); } catch (error) { } if (!globeApplied) { try { map.setProjection('globe'); globeApplied = hasGlobeProjection(); } catch (error) { } } if (!globeApplied) { return; } if (typeof map.setFog === 'function') { try { map.setFog({ color: 'rgb(186, 210, 235)', 'high-color': 'rgb(36, 92, 223)', 'horizon-blend': 0.08, 'space-color': 'rgb(11, 11, 25)', 'star-intensity': 0.2, }); } catch (error) { } } if (typeof map.setSky === 'function') { try { map.setSky({ 'sky-color': '#87ceeb', 'horizon-color': '#ffffff', 'fog-color': '#dff6ff', 'fog-ground-blend': 0.2, 'sky-horizon-blend': 0.4, }); } catch (error) { } } }; const ensureSatelliteLayer = (map) => { if (!map || map.getSource(SATELLITE_SOURCE_ID)) { return; } map.addSource(SATELLITE_SOURCE_ID, { type: 'raster', tiles: [SATELLITE_TILE_URL], tileSize: 256, attribution: SATELLITE_ATTRIBUTION, }); const markerLayerCandidates = [ EVENT_CLUSTER_LAYER_ID, EVENT_CLUSTER_SPIDERFY_LAYER_ID, EVENT_UNCLUSTERED_LAYER_ID, 'eventmanager-non-events', ]; const beforeId = markerLayerCandidates.find((layerId) => map.getLayer(layerId)); map.addLayer({ id: SATELLITE_LAYER_ID, type: 'raster', source: SATELLITE_SOURCE_ID, layout: { visibility: 'none', }, paint: { 'raster-opacity': 1, 'raster-brightness-min': 0, 'raster-brightness-max': 1, 'raster-contrast': 0, 'raster-saturation': 0, }, }, beforeId); }; const setSatelliteMode = (map, enabled) => { if (!map) { return; } ensureSatelliteLayer(map); if (!map.getLayer(SATELLITE_LAYER_ID)) { return; } map.setLayoutProperty(SATELLITE_LAYER_ID, 'visibility', enabled ? 'visible' : 'none'); map.setPaintProperty(SATELLITE_LAYER_ID, 'raster-opacity-transition', { duration: EVENT_FADE_DURATION_MS, delay: 0 }); map.setPaintProperty(SATELLITE_LAYER_ID, 'raster-brightness-min-transition', { duration: EVENT_FADE_DURATION_MS, delay: 0 }); map.setPaintProperty(SATELLITE_LAYER_ID, 'raster-brightness-max-transition', { duration: EVENT_FADE_DURATION_MS, delay: 0 }); map.setPaintProperty(SATELLITE_LAYER_ID, 'raster-contrast-transition', { duration: EVENT_FADE_DURATION_MS, delay: 0 }); map.setPaintProperty(SATELLITE_LAYER_ID, 'raster-saturation-transition', { duration: EVENT_FADE_DURATION_MS, delay: 0 }); if (!enabled) { return; } map.setPaintProperty(SATELLITE_LAYER_ID, 'raster-brightness-min', 0); map.setPaintProperty(SATELLITE_LAYER_ID, 'raster-brightness-max', 1); map.setPaintProperty(SATELLITE_LAYER_ID, 'raster-contrast', 0); map.setPaintProperty(SATELLITE_LAYER_ID, 'raster-saturation', 0); }; const normalizeTagIds = (value) => { if (!Array.isArray(value)) { return []; } const unique = new Set(); value.forEach((rawValue) => { const normalized = String(rawValue ?? '').trim(); if (normalized !== '' && /^\d+$/.test(normalized)) { unique.add(normalized); } }); return Array.from(unique.values()); }; const popupHtmlFor = (item) => { if (item.type === 'event') { const locationTitle = escapeHtml(item.extra.locationTitle || ''); const startDate = escapeHtml(item.extra.startDate || ''); return [ `${escapeHtml(item.title)}`, locationTitle ? `
${locationTitle}
` : '', startDate ? `
${startDate}
` : '', ].join(''); } return `${escapeHtml(item.title)}`; }; const toFeature = (item) => ({ type: 'Feature', geometry: { type: 'Point', coordinates: [item.longitude, item.latitude], }, properties: { id: item.id, type: item.type, markerType: item.markerType, markerTagIds: Array.isArray(item.extra?.organizationTagIds) ? item.extra.organizationTagIds : [], markerTagLabels: Array.isArray(item.extra?.organizationTagLabels) ? item.extra.organizationTagLabels : [], title: item.title, popupHtml: popupHtmlFor(item), }, }); const initOrganizationMarkers = (map, organizationItems, organizationTagColorMap) => { if (!organizationItems.length) { return { setActiveTagIds: () => {}, setAllVisible: () => {}, }; } const markerEntries = organizationItems.map((item) => { const popupHtml = popupHtmlFor(item); const primaryTagId = String(item?.extra?.organizationTagIds?.[0] || '').trim(); const configuredColor = /^\d+$/.test(primaryTagId) ? (organizationTagColorMap?.byTagId?.[primaryTagId] || organizationTagColorMap?.fallbackColor || null) : null; const marker = new maplibregl.Marker(configuredColor ? { color: configuredColor } : undefined) .setLngLat([item.longitude, item.latitude]); if (popupHtml) { marker.setPopup(new maplibregl.Popup({ offset: 15 }).setHTML(String(popupHtml))); } marker.addTo(map); return { marker, tagIds: normalizeTagIds(item.extra?.organizationTagIds), }; }); return { setActiveTagIds: (activeTagIds) => { const activeSet = new Set(normalizeTagIds(activeTagIds)); markerEntries.forEach((entry) => { const markerElement = entry.marker.getElement(); if (!markerElement) { return; } const hasTags = entry.tagIds.length > 0; const isVisible = !hasTags || entry.tagIds.some((tagId) => activeSet.has(tagId)); markerElement.style.display = isVisible ? '' : 'none'; }); }, setAllVisible: () => { markerEntries.forEach((entry) => { const markerElement = entry.marker.getElement(); if (!markerElement) { return; } markerElement.style.display = ''; }); }, }; }; const bindExternalTagFilters = (container, map, organizationMarkerManager, eventLayerManager) => { const wrapperId = container.dataset.mapFilterWrapperId || ''; if (!wrapperId || !organizationMarkerManager) { return; } const wrapper = document.getElementById(wrapperId); if (!wrapper) { return; } const tagButtons = Array.from(wrapper.querySelectorAll('[data-map-tag-filter]')); const collectActiveTagIds = () => tagButtons .filter((button) => button.getAttribute('aria-pressed') === 'true') .map((button) => String(button.dataset.mapTagFilter || '').trim()) .filter((value) => /^\d+$/.test(value)); const setButtonState = (button, isActive) => { button.setAttribute('aria-pressed', isActive ? 'true' : 'false'); button.classList.toggle('is-active', isActive); }; const eventToggleButton = wrapper.querySelector('[data-map-event-toggle="1"]'); const setEventButtonState = (isActive) => { if (!eventToggleButton) { return; } eventToggleButton.setAttribute('aria-pressed', isActive ? 'true' : 'false'); eventToggleButton.classList.toggle('is-active', isActive); }; const applyFilter = () => { if (!tagButtons.length) { organizationMarkerManager.setAllVisible(); return; } organizationMarkerManager.setActiveTagIds(collectActiveTagIds()); }; tagButtons.forEach((button) => { button.addEventListener('click', () => { const isActive = button.getAttribute('aria-pressed') === 'true'; setButtonState(button, !isActive); applyFilter(); }); }); const actionAll = wrapper.querySelector('[data-map-filter-action="all"]'); const actionNone = wrapper.querySelector('[data-map-filter-action="none"]'); if (actionAll) { actionAll.addEventListener('click', () => { tagButtons.forEach((button) => setButtonState(button, true)); applyFilter(); if (eventLayerManager) { setEventButtonState(true); eventLayerManager.setVisible(true); } }); } if (actionNone) { actionNone.addEventListener('click', () => { tagButtons.forEach((button) => setButtonState(button, false)); applyFilter(); if (eventLayerManager) { setEventButtonState(false); eventLayerManager.setVisible(false); } }); } const toggleButton = wrapper.querySelector('[data-map-filter-toggle="1"]'); const filterGroup = wrapper.querySelector('.eventmanager-map-filter__group'); if (toggleButton && filterGroup) { toggleButton.addEventListener('click', () => { const expanded = toggleButton.getAttribute('aria-expanded') !== 'false'; const nextExpanded = !expanded; toggleButton.setAttribute('aria-expanded', nextExpanded ? 'true' : 'false'); filterGroup.hidden = !nextExpanded; toggleButton.textContent = nextExpanded ? 'Bereiche ausblenden' : 'Bereiche anzeigen'; }); } if (eventToggleButton && eventLayerManager) { eventToggleButton.addEventListener('click', () => { const isActive = eventToggleButton.getAttribute('aria-pressed') === 'true'; const nextActive = !isActive; setEventButtonState(nextActive); eventLayerManager.setVisible(nextActive); }); } else if (eventToggleButton) { eventToggleButton.disabled = true; eventToggleButton.setAttribute('aria-disabled', 'true'); } const streetStyleButton = wrapper.querySelector('[data-map-style-mode="street"]'); const satelliteStyleButton = wrapper.querySelector('[data-map-style-mode="satellite"]'); let currentStyleMode = 'street'; const applyStyleButtonState = () => { if (streetStyleButton) { streetStyleButton.setAttribute('aria-pressed', 'street' === currentStyleMode ? 'true' : 'false'); } if (satelliteStyleButton) { satelliteStyleButton.setAttribute('aria-pressed', 'satellite' === currentStyleMode ? 'true' : 'false'); } }; const applyMapStyleMode = () => { setSatelliteMode(map, 'satellite' === currentStyleMode); applyStyleButtonState(); }; if (streetStyleButton) { streetStyleButton.addEventListener('click', () => { currentStyleMode = 'street'; applyMapStyleMode(); }); } if (satelliteStyleButton) { satelliteStyleButton.addEventListener('click', () => { currentStyleMode = 'satellite'; applyMapStyleMode(); }); } applyFilter(); applyMapStyleMode(); }; const initLocationLayers = (map, locationItems, eventColor) => { if (!locationItems.length) { return { setVisible: () => {}, }; } const sourceId = 'eventmanager-non-events-source'; const layerId = 'eventmanager-non-events'; map.addSource(sourceId, { type: 'geojson', data: { type: 'FeatureCollection', features: locationItems.map(toFeature), }, }); map.addLayer({ id: layerId, type: 'circle', source: sourceId, paint: { 'circle-radius': 8, 'circle-stroke-width': 0, 'circle-color': eventColor, 'circle-opacity': 1, }, }); map.on('click', layerId, (event) => { const blockingLayers = [ EVENT_CLUSTER_LAYER_ID, EVENT_CLUSTER_SPIDERFY_LAYER_ID, EVENT_UNCLUSTERED_LAYER_ID, ].filter((id) => map.getLayer(id)); if (blockingLayers.length) { const topEventFeatures = map.queryRenderedFeatures(event.point, { layers: blockingLayers }); if (topEventFeatures.length) { return; } } const feature = event.features && event.features[0] ? event.features[0] : null; if (!feature) { return; } const popupHtml = feature?.properties?.popupHtml || ''; const coordinates = event.lngLat ? [event.lngLat.lng, event.lngLat.lat] : null; if (!coordinates) { return; } new maplibregl.Popup({ offset: 15 }) .setLngLat(coordinates) .setHTML(String(popupHtml)) .addTo(map); }); map.on('mouseenter', layerId, () => { map.getCanvas().style.cursor = 'pointer'; }); map.on('mouseleave', layerId, () => { map.getCanvas().style.cursor = ''; }); const setLayerLayoutVisibility = (isVisible) => { if (!map.getLayer(layerId)) { return; } map.setLayoutProperty(layerId, 'visibility', isVisible ? 'visible' : 'none'); }; const fadeLayerOpacity = (value) => { if (!map.getLayer(layerId)) { return; } map.setPaintProperty(layerId, 'circle-opacity-transition', { duration: EVENT_FADE_DURATION_MS, delay: 0 }); map.setPaintProperty(layerId, 'circle-opacity', value); }; return { setVisible: (isVisible) => { if (isVisible) { setLayerLayoutVisibility(true); fadeLayerOpacity(1); return; } fadeLayerOpacity(0); window.setTimeout(() => { setLayerLayoutVisibility(false); }, EVENT_FADE_DURATION_MS + 10); }, }; }; const initEventLayers = (map, eventItems, eventColor) => { const sourceId = 'eventmanager-events-source'; const clusterLayerId = EVENT_CLUSTER_LAYER_ID; const clusterSpiderfyLayerId = EVENT_CLUSTER_SPIDERFY_LAYER_ID; const clusterCountLayerId = 'eventmanager-events-cluster-count'; const unclusteredLayerId = EVENT_UNCLUSTERED_LAYER_ID; const spiderfyHitAreaImageId = 'eventmanager-spiderfy-hit-area'; const features = eventItems.map(toFeature); if (!map.hasImage(spiderfyHitAreaImageId)) { map.addImage(spiderfyHitAreaImageId, { width: 1, height: 1, data: new Uint8Array([0, 0, 0, 0]), }); } map.addSource(sourceId, { type: 'geojson', data: { type: 'FeatureCollection', features, }, cluster: true, clusterMaxZoom: 17, clusterRadius: 22, }); map.addLayer({ id: clusterLayerId, type: 'circle', source: sourceId, filter: ['has', 'point_count'], paint: { 'circle-radius': 18, 'circle-opacity': 1, 'circle-color': eventColor, 'circle-stroke-width': 0, }, }); map.addLayer({ id: clusterCountLayerId, type: 'symbol', source: sourceId, filter: ['has', 'point_count'], layout: { 'text-field': ['get', 'point_count'], 'text-size': 16, 'text-font': [GLYPH_FONTSTACK], 'text-allow-overlap': true, 'text-ignore-placement': true, }, paint: { 'text-color': '#ffffff', 'text-opacity': 1, }, }); map.addLayer({ id: clusterSpiderfyLayerId, type: 'symbol', source: sourceId, filter: ['has', 'point_count'], layout: { 'icon-image': spiderfyHitAreaImageId, 'icon-size': 36, 'icon-allow-overlap': true, }, paint: { 'icon-opacity': 0, }, }); map.addLayer({ id: unclusteredLayerId, type: 'circle', source: sourceId, filter: ['!', ['has', 'point_count']], paint: { 'circle-radius': 11, 'circle-color': eventColor, 'circle-stroke-width': 0, 'circle-opacity': 1, }, }); const openEventPopup = (feature, coordinatesOverride = null) => { const popupHtml = feature?.properties?.popupHtml || ''; const rawCoords = coordinatesOverride || feature?.geometry?.coordinates || null; if (!Array.isArray(rawCoords) || rawCoords.length !== 2) { return; } const [lng, lat] = rawCoords.map((coord) => Number(coord)); if (!Number.isFinite(lng) || !Number.isFinite(lat)) { return; } new maplibregl.Popup({ offset: 15 }) .setLngLat([lng, lat]) .setHTML(String(popupHtml)) .addTo(map); }; map.on('click', unclusteredLayerId, (event) => { const feature = event.features && event.features[0] ? event.features[0] : null; if (!feature) { return; } openEventPopup(feature, event.lngLat ? [event.lngLat.lng, event.lngLat.lat] : null); }); map.on('mouseenter', unclusteredLayerId, () => { map.getCanvas().style.cursor = 'pointer'; }); map.on('mouseleave', unclusteredLayerId, () => { map.getCanvas().style.cursor = ''; }); const bindClusterZoomFallback = () => { map.on('click', clusterLayerId, (event) => { const feature = event.features && event.features[0] ? event.features[0] : null; if (!feature) { return; } const clusterId = feature.properties?.cluster_id; if (undefined === clusterId || null === clusterId) { return; } const source = map.getSource(sourceId); if (!source || typeof source.getClusterExpansionZoom !== 'function') { return; } source.getClusterExpansionZoom(clusterId, (error, zoom) => { if (error) { return; } map.easeTo({ center: event.lngLat, zoom, }); }); }); }; let spiderfyInstance = null; if (typeof Spiderfy === 'function') { try { const spiderfy = new Spiderfy(map, { renderMethod: '3D', minZoomLevel: 0, zoomIncrement: 0, closeOnLeafClick: false, circleSpiralSwitchover: 10, circleOptions: { leavesSeparation: 70, leavesOffset: [0, 0], }, spiralOptions: { legLengthStart: 45, legLengthFactor: 5, leavesSeparation: 45, leavesOffset: [0, 0], }, spiderLegsAreHidden: false, spiderLegsWidth: 2, spiderLegsColor: eventColor, spiderLeavesLayout: { 'text-field': '●', 'text-size': 18, 'text-allow-overlap': true, }, spiderLeavesPaint: { 'text-color': eventColor, 'text-opacity': 1, }, onLeafClick: (feature, spiderEvent) => { const clickCoordinates = spiderEvent?.lngLat ? [spiderEvent.lngLat.lng, spiderEvent.lngLat.lat] : null; openEventPopup(feature, clickCoordinates); }, }); spiderfy.applyTo(clusterSpiderfyLayerId); spiderfyInstance = spiderfy; if (map.getLayer(clusterCountLayerId)) { map.moveLayer(clusterCountLayerId); } } catch (error) { bindClusterZoomFallback(); } } else { bindClusterZoomFallback(); } map.on('mouseenter', clusterLayerId, () => { map.getCanvas().style.cursor = 'pointer'; }); map.on('mouseleave', clusterLayerId, () => { map.getCanvas().style.cursor = ''; }); const setLayerLayoutVisibility = (layerId, isVisible) => { if (!map.getLayer(layerId)) { return; } map.setLayoutProperty(layerId, 'visibility', isVisible ? 'visible' : 'none'); }; const fadeLayerOpacity = (layerId, property, value, duration = EVENT_FADE_DURATION_MS) => { if (!map.getLayer(layerId)) { return; } map.setPaintProperty(layerId, `${property}-transition`, { duration, delay: 0 }); map.setPaintProperty(layerId, property, value); }; const eventLayerIds = [ clusterLayerId, clusterCountLayerId, clusterSpiderfyLayerId, unclusteredLayerId, ]; return { setVisible: (isVisible) => { if (isVisible) { eventLayerIds.forEach((layerId) => setLayerLayoutVisibility(layerId, true)); fadeLayerOpacity(clusterLayerId, 'circle-opacity', 1); fadeLayerOpacity(unclusteredLayerId, 'circle-opacity', 1); fadeLayerOpacity(clusterCountLayerId, 'text-opacity', 1); return; } if (spiderfyInstance && typeof spiderfyInstance.unspiderfyAll === 'function') { spiderfyInstance.unspiderfyAll(); } fadeLayerOpacity(clusterLayerId, 'circle-opacity', 0); fadeLayerOpacity(unclusteredLayerId, 'circle-opacity', 0); fadeLayerOpacity(clusterCountLayerId, 'text-opacity', 0); window.setTimeout(() => { eventLayerIds.forEach((layerId) => setLayerLayoutVisibility(layerId, false)); }, EVENT_FADE_DURATION_MS + 10); }, }; }; const initSingleMap = (container) => { if (container.dataset.mapInitialized === '1') { return; } const items = parseItems(container); const centerMode = container.dataset.mapCenterMode === 'custom' ? 'custom' : 'markers'; const customLat = parseCoordinate(container.dataset.mapCenterLat); const customLng = parseCoordinate(container.dataset.mapCenterLng); const customZoom = Number(container.dataset.mapCenterZoom); const eventColor = normalizeHexColor(container.dataset.mapEventColor, DEFAULT_EVENT_COLOR); const hasCustomCenter = null !== customLat && null !== customLng; let initialCenter = DEFAULT_CENTER; let initialZoom = DEFAULT_ZOOM; if ('custom' === centerMode && hasCustomCenter) { initialCenter = [customLng, customLat]; initialZoom = Number.isFinite(customZoom) ? customZoom : 12; } else if (items.length) { initialCenter = [items[0].longitude, items[0].latitude]; initialZoom = 12; } container.dataset.mapInitialized = '1'; ensureDependencies() .then(() => { if (typeof maplibregl === 'undefined') { return; } if (typeof pmtiles !== 'undefined' && !window.__eventmanagerPmtilesRegistered) { const protocol = new pmtiles.Protocol(); maplibregl.addProtocol('pmtiles', protocol.tile); window.__eventmanagerPmtilesRegistered = true; } const style = container.dataset.mapStyle || 'https://maps.mummert.media/metadaten/world-light.json'; const map = new maplibregl.Map({ container, style, zoom: initialZoom, pitch: 0, bearing: 0, center: initialCenter, transformRequest: (url, resourceType) => { if ('Glyphs' === resourceType) { return { url: rewriteGlyphUrl(url), }; } return { url, }; }, }); map.on('style.load', () => { applyDefaultProjection(map); }); map.on('styleimagemissing', (event) => { const missingImageId = event?.id || ''; if (!missingImageId || map.hasImage(missingImageId)) { return; } map.addImage(missingImageId, { width: 1, height: 1, data: new Uint8Array([0, 0, 0, 0]), }); }); map.addControl(new maplibregl.NavigationControl({ visualizePitch: true })); const fitBounds = (coordinates) => { const points = coordinates.filter((coord) => Array.isArray(coord) && coord.length === 2); if (!points.length) { return; } if (1 === points.length) { map.setCenter(points[0]); map.setZoom(14); return; } const bounds = new maplibregl.LngLatBounds(); points.forEach((coord) => bounds.extend(coord)); map.fitBounds(bounds, { padding: 60, maxZoom: 15, }); }; map.on('load', () => { applyDefaultProjection(map); const organizationItems = items.filter((item) => item.type === 'organisation'); const locationItems = items.filter((item) => item.type === 'location'); const eventItems = items.filter((item) => item.type === 'event'); const allCoordinates = items.map((item) => [item.longitude, item.latitude]); const organizationColors = parseOrganizationColorScheme(container.dataset.mapOrganizationColors); const organizationTagColorMap = buildOrganizationTagColorMap(container, organizationColors, organizationItems); const organizationMarkerManager = initOrganizationMarkers(map, organizationItems, organizationTagColorMap); const locationLayerManager = initLocationLayers(map, locationItems, eventColor); let eventLayerManager = null; if (eventItems.length) { eventLayerManager = initEventLayers(map, eventItems, eventColor); } const combinedEventLayerManager = { setVisible: (isVisible) => { locationLayerManager.setVisible(isVisible); if (eventLayerManager) { eventLayerManager.setVisible(isVisible); } }, }; bindExternalTagFilters(container, map, organizationMarkerManager, combinedEventLayerManager); if ('markers' === centerMode) { fitBounds(allCoordinates); } }); }) .catch(() => { container.dataset.mapInitialized = '0'; }); }; const bootstrapMapModules = () => { const modules = document.querySelectorAll(MAP_SELECTOR); modules.forEach((container) => initSingleMap(container)); }; if ('loading' === document.readyState) { document.addEventListener('DOMContentLoaded', bootstrapMapModules, { once: true }); } else { bootstrapMapModules(); }