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_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_LABELS_SOURCE_ID = 'eventmanager-satellite-labels-source'; const SATELLITE_LABELS_LAYER_ID = 'eventmanager-satellite-labels-layer'; const SATELLITE_LABELS_TILE_URL = 'https://services.arcgisonline.com/ArcGIS/rest/services/Reference/World_Boundaries_and_Places/MapServer/tile/{z}/{y}/{x}'; 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) { 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 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; } 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 decodeHtmlEntities = (value) => { const stringValue = String(value ?? ''); if ('undefined' === typeof document || !document.createElement) { return stringValue; } const textarea = document.createElement('textarea'); textarea.innerHTML = stringValue; return textarea.value; }; const sanitizeDisplayText = (value) => escapeHtml(decodeHtmlEntities(value)); 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', '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 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_UNCLUSTERED_LAYER_ID, ]; 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); map.addSource(SATELLITE_ROADS_SOURCE_ID, { type: 'raster', tiles: [SATELLITE_ROADS_TILE_URL], tileSize: 256, attribution: SATELLITE_ATTRIBUTION, }); map.addLayer({ id: SATELLITE_ROADS_LAYER_ID, type: 'raster', source: SATELLITE_ROADS_SOURCE_ID, layout: { visibility: 'none', }, paint: { 'raster-opacity': 1, }, }, beforeId); map.addSource(SATELLITE_LABELS_SOURCE_ID, { type: 'raster', tiles: [SATELLITE_LABELS_TILE_URL], tileSize: 256, attribution: SATELLITE_ATTRIBUTION, }); map.addLayer({ id: SATELLITE_LABELS_LAYER_ID, type: 'raster', source: SATELLITE_LABELS_SOURCE_ID, layout: { visibility: 'none', }, paint: { 'raster-opacity': 1, }, }, beforeId); }; const setSatelliteMode = (map, enabled) => { if (!map) { return; } ensureSatelliteLayer(map); if (!map.getLayer(SATELLITE_LAYER_ID)) { return; } const hybridOverlayLayerIds = [SATELLITE_ROADS_LAYER_ID, SATELLITE_LABELS_LAYER_ID]; map.setLayoutProperty(SATELLITE_LAYER_ID, 'visibility', enabled ? 'visible' : 'none'); hybridOverlayLayerIds.forEach((layerId) => { if (map.getLayer(layerId)) { map.setLayoutProperty(layerId, 'visibility', enabled ? 'visible' : 'none'); map.setPaintProperty(layerId, 'raster-opacity-transition', { duration: EVENT_FADE_DURATION_MS, delay: 0 }); map.setPaintProperty(layerId, 'raster-opacity', enabled ? 1 : 0); } }); 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 getMarkerImageId = (prefix, color) => `${prefix}-${String(color || '').replace('#', '').toLowerCase() || 'default'}`; const SVG_MARKER_WIDTH = 68; const SVG_MARKER_HEIGHT = 104; const createPinSvgDataUrl = (color, options = {}) => { const withInnerDot = options.withInnerDot !== false; const innerDotMarkup = withInnerDot ? '' : ''; const svg = `${innerDotMarkup}`; return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`; }; const loadSvgImageData = (dataUrl) => new Promise((resolve, reject) => { const image = new Image(); image.onload = () => { const canvas = document.createElement('canvas'); canvas.width = SVG_MARKER_WIDTH; canvas.height = SVG_MARKER_HEIGHT; const context = canvas.getContext('2d'); if (!context) { reject(new Error('Failed to get canvas context for SVG marker')); return; } context.clearRect(0, 0, SVG_MARKER_WIDTH, SVG_MARKER_HEIGHT); context.drawImage(image, 0, 0, SVG_MARKER_WIDTH, SVG_MARKER_HEIGHT); resolve(context.getImageData(0, 0, SVG_MARKER_WIDTH, SVG_MARKER_HEIGHT)); }; image.onerror = () => reject(new Error('Failed to load SVG marker image')); image.src = dataUrl; }); const ensureMarkerImage = (map, color, prefix = 'eventmanager-marker', options = {}) => { const imageId = getMarkerImageId(prefix, color); if (map.hasImage(imageId)) { return Promise.resolve(imageId); } const dataUrl = createPinSvgDataUrl(color, options); return loadSvgImageData(dataUrl) .then((imageData) => { if (!map.hasImage(imageId)) { map.addImage(imageId, imageData, { pixelRatio: 2 }); } return imageId; }); }; const popupHtmlFor = (item) => { if (item.type === 'event') { const title = sanitizeDisplayText(item.title || ''); const locationTitle = sanitizeDisplayText(item.extra.locationTitle || ''); const startDate = sanitizeDisplayText(item.extra.startDate || ''); const detailUrl = String(item.extra.detailUrl || '').trim(); const hasDetailUrl = /^(https?:\/\/|\/)/i.test(detailUrl); const detailLink = hasDetailUrl ? `
mehr Infos
` : ''; return [ `${title}`, locationTitle ? `
${locationTitle}
` : '', startDate ? `
${startDate}
` : '', detailLink, ].join(''); } if (item.type === 'organisation') { const title = sanitizeDisplayText(item.title || ''); const detailUrl = String(item.extra?.detailUrl || '').trim(); const hasDetailUrl = /^(https?:\/\/|\/)/i.test(detailUrl); const detailLink = hasDetailUrl ? `
mehr Infos
` : ''; return [ `${title}`, detailLink, ].join(''); } return `${sanitizeDisplayText(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, latitude: item.latitude, popupHtml: popupHtmlFor(item), }, }); const initOrganizationMarkers = (map, organizationItems, organizationColor, openPopup) => { if (!organizationItems.length) { return { setOnlyTagId: () => {}, setAllVisible: () => {}, setVisible: () => {}, }; } const sortedOrganizationItems = [...organizationItems].sort((firstItem, secondItem) => secondItem.latitude - firstItem.latitude); const markerEntries = sortedOrganizationItems.map((item) => { const popupHtml = popupHtmlFor(item); const marker = new maplibregl.Marker(organizationColor ? { color: organizationColor } : undefined) .setLngLat([item.longitude, item.latitude]); marker.addTo(map); const markerElement = marker.getElement(); if (markerElement) { markerElement.style.zIndex = String(Math.round((90 - item.latitude) * 1000)); markerElement.style.cursor = 'pointer'; if (popupHtml && typeof openPopup === 'function') { markerElement.addEventListener('click', (event) => { event.preventDefault(); event.stopPropagation(); openPopup([item.longitude, item.latitude], String(popupHtml)); }); } } return { marker, tagIds: normalizeTagIds(item.extra?.organizationTagIds), }; }); return { setOnlyTagId: (activeTagId) => { const normalizedTagId = String(activeTagId ?? '').trim(); const hasActiveTag = /^\d+$/.test(normalizedTagId); markerEntries.forEach((entry) => { const markerElement = entry.marker.getElement(); if (!markerElement) { return; } const hasTags = entry.tagIds.length > 0; const isVisible = !hasActiveTag || !hasTags || entry.tagIds.includes(normalizedTagId); markerElement.style.display = isVisible ? '' : 'none'; }); }, setAllVisible: () => { markerEntries.forEach((entry) => { const markerElement = entry.marker.getElement(); if (!markerElement) { return; } markerElement.style.display = ''; }); }, setVisible: (isVisible) => { markerEntries.forEach((entry) => { const markerElement = entry.marker.getElement(); if (!markerElement) { return; } markerElement.style.display = isVisible ? '' : 'none'; }); }, }; }; const bindExternalTagFilters = (container, map, organizationMarkerManager, eventLayerManager, closePopup) => { 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 initialDisplayModeRaw = String(container.dataset.mapInitialDisplay || '').trim(); const initialTagIdRaw = String(container.dataset.mapInitialTagId || '').trim(); 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 canFilterByTag = tagButtons.length > 0; const canFilterByEvents = !!eventToggleButton; const canToggleEvents = !!(eventToggleButton && eventLayerManager); const setEventButtonState = (isActive) => { if (!eventToggleButton) { return; } eventToggleButton.setAttribute('aria-pressed', isActive ? 'true' : 'false'); eventToggleButton.classList.toggle('is-active', isActive); }; const availableTagIds = tagButtons .map((button) => String(button.dataset.mapTagFilter || '').trim()) .filter((tagId) => /^\d+$/.test(tagId)); let activeTagId = null; let eventsOnly = false; const updateExclusiveButtonAccessibility = () => { tagButtons.forEach((button) => { const isActive = button.getAttribute('aria-pressed') === 'true'; button.setAttribute('aria-disabled', isActive ? 'true' : 'false'); }); if (!eventToggleButton) { return; } if (!canToggleEvents) { eventToggleButton.setAttribute('aria-disabled', 'true'); return; } const isEventActive = eventToggleButton.getAttribute('aria-pressed') === 'true'; eventToggleButton.setAttribute('aria-disabled', isEventActive ? 'true' : 'false'); }; const applyFilter = () => { if (eventsOnly) { organizationMarkerManager.setVisible(false); if (eventLayerManager) { eventLayerManager.setVisible(true); } return; } organizationMarkerManager.setVisible(true); if (activeTagId) { organizationMarkerManager.setOnlyTagId(activeTagId); if (eventLayerManager) { eventLayerManager.setVisible(false); } return; } organizationMarkerManager.setAllVisible(); if (eventLayerManager) { eventLayerManager.setVisible(true); } }; tagButtons.forEach((button) => { button.addEventListener('click', () => { if (button.getAttribute('aria-disabled') === 'true') { return; } const clickedTagId = String(button.dataset.mapTagFilter || '').trim(); if (!/^\d+$/.test(clickedTagId)) { return; } activeTagId = clickedTagId; eventsOnly = false; tagButtons.forEach((otherButton) => { const otherTagId = String(otherButton.dataset.mapTagFilter || '').trim(); setButtonState(otherButton, !!activeTagId && otherTagId === activeTagId); }); setEventButtonState(false); updateExclusiveButtonAccessibility(); if (typeof closePopup === 'function') { closePopup(); } applyFilter(); }); }); 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.classList.toggle('is-expanded', nextExpanded); toggleButton.classList.toggle('is-collapsed', !nextExpanded); wrapper.classList.toggle('is-filter-expanded', nextExpanded); wrapper.classList.toggle('is-filter-collapsed', !nextExpanded); }); const initiallyExpanded = toggleButton.getAttribute('aria-expanded') !== 'false'; filterGroup.hidden = !initiallyExpanded; toggleButton.classList.toggle('is-expanded', initiallyExpanded); toggleButton.classList.toggle('is-collapsed', !initiallyExpanded); wrapper.classList.toggle('is-filter-expanded', initiallyExpanded); wrapper.classList.toggle('is-filter-collapsed', !initiallyExpanded); } if (eventToggleButton && eventLayerManager) { eventToggleButton.addEventListener('click', () => { if (eventToggleButton.getAttribute('aria-disabled') === 'true') { return; } eventsOnly = true; activeTagId = null; tagButtons.forEach((button) => setButtonState(button, false)); setEventButtonState(eventsOnly); updateExclusiveButtonAccessibility(); if (typeof closePopup === 'function') { closePopup(); } applyFilter(); }); } 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(); }); } const applyInitialDisplayMode = () => { const validModes = new Set(['random', 'events', 'organization_tag']); const mode = validModes.has(initialDisplayModeRaw) ? initialDisplayModeRaw : 'random'; const randomTagId = canFilterByTag && availableTagIds.length ? availableTagIds[Math.floor(Math.random() * availableTagIds.length)] : null; const configuredTagId = canFilterByTag && /^\d+$/.test(initialTagIdRaw) && availableTagIds.includes(initialTagIdRaw) ? initialTagIdRaw : null; activeTagId = null; eventsOnly = false; if ('events' === mode && canFilterByEvents) { eventsOnly = true; } else if ('organization_tag' === mode && canFilterByTag) { activeTagId = configuredTagId || randomTagId; } else if ('random' === mode) { if (canFilterByTag && canFilterByEvents) { if (randomTagId && Math.random() >= 0.5) { activeTagId = randomTagId; } else { eventsOnly = true; } } else if (canFilterByTag) { activeTagId = randomTagId; } else if (canFilterByEvents) { eventsOnly = true; } } tagButtons.forEach((button) => { const tagId = String(button.dataset.mapTagFilter || '').trim(); setButtonState(button, null !== activeTagId && tagId === activeTagId); }); setEventButtonState(eventsOnly); updateExclusiveButtonAccessibility(); }; applyInitialDisplayMode(); updateExclusiveButtonAccessibility(); applyFilter(); applyMapStyleMode(); }; const initEventLayers = (map, eventItems, eventColor, markerImageId, clusterMarkerImageId, openPopup, closePopup) => { const sourceId = 'eventmanager-events-source'; const clusterLayerId = EVENT_CLUSTER_LAYER_ID; const unclusteredLayerId = EVENT_UNCLUSTERED_LAYER_ID; const features = eventItems.map(toFeature); map.addSource(sourceId, { type: 'geojson', data: { type: 'FeatureCollection', features, }, cluster: true, clusterMaxZoom: 17, clusterRadius: 22, }); map.addLayer({ id: clusterLayerId, type: 'symbol', source: sourceId, filter: ['has', 'point_count'], layout: { 'icon-image': clusterMarkerImageId, 'icon-size': 1, 'icon-anchor': 'bottom', 'icon-allow-overlap': true, 'icon-ignore-placement': true, 'text-field': ['get', 'point_count'], 'text-size': 18, 'text-font': [GLYPH_FONTSTACK], 'text-allow-overlap': true, 'text-ignore-placement': true, 'text-offset': [0, -1.85], }, paint: { 'icon-opacity': 1, 'icon-translate': [0, 4], 'text-color': '#FFFFFF', 'text-opacity': 1, 'text-translate': [0, 4], }, }); map.addLayer({ id: unclusteredLayerId, type: 'symbol', source: sourceId, filter: ['!', ['has', 'point_count']], layout: { 'icon-image': markerImageId, 'icon-size': 1, 'icon-anchor': 'bottom', 'icon-allow-overlap': true, 'icon-ignore-placement': true, 'symbol-sort-key': ['*', -1, ['to-number', ['get', 'latitude'], 0]], }, paint: { 'icon-opacity': 1, 'icon-translate': [0, 4], }, }); 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; } if (typeof openPopup === 'function') { openPopup([lng, lat], String(popupHtml)); } }; 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: { 'icon-image': markerImageId, 'icon-size': 0.9, 'icon-anchor': 'bottom', 'icon-allow-overlap': true, }, spiderLeavesPaint: { 'icon-opacity': 1, 'icon-translate': [-3, 9], }, onLeafClick: (feature, spiderEvent) => { const clickCoordinates = spiderEvent?.lngLat ? [spiderEvent.lngLat.lng, spiderEvent.lngLat.lat] : null; openEventPopup(feature, clickCoordinates); }, }); spiderfy.applyTo(clusterLayerId); spiderfyInstance = spiderfy; if (map.getLayer(clusterLayerId)) { map.moveLayer(clusterLayerId); } } catch (error) { bindClusterZoomFallback(); } } else { bindClusterZoomFallback(); } map.on('mouseenter', clusterLayerId, () => { map.getCanvas().style.cursor = 'pointer'; }); map.on('mouseleave', clusterLayerId, () => { map.getCanvas().style.cursor = ''; }); 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, unclusteredLayerId, ]; const clusterFilter = ['has', 'point_count']; const unclusteredFilter = ['!', ['has', 'point_count']]; const hiddenFilter = ['has', '__eventmanager_hidden__']; let hideLayersTimeoutId = null; const setLayerFilter = (layerId, filter) => { if (!map.getLayer(layerId)) { return; } map.setFilter(layerId, filter); }; return { setVisible: (isVisible) => { if (isVisible) { if (null !== hideLayersTimeoutId) { window.clearTimeout(hideLayersTimeoutId); hideLayersTimeoutId = null; } setLayerFilter(clusterLayerId, clusterFilter); setLayerFilter(unclusteredLayerId, unclusteredFilter); fadeLayerOpacity(clusterLayerId, 'icon-opacity', 1); fadeLayerOpacity(clusterLayerId, 'text-opacity', 1); fadeLayerOpacity(unclusteredLayerId, 'icon-opacity', 1); return; } if (spiderfyInstance) { if (typeof spiderfyInstance._clearSpiderifiedCluster === 'function') { spiderfyInstance._clearSpiderifiedCluster(); } else if (typeof spiderfyInstance.unspiderfyAll === 'function') { spiderfyInstance.unspiderfyAll(); } } if (typeof closePopup === 'function') { closePopup(); } fadeLayerOpacity(clusterLayerId, 'icon-opacity', 0); fadeLayerOpacity(clusterLayerId, 'text-opacity', 0); fadeLayerOpacity(unclusteredLayerId, 'icon-opacity', 0); hideLayersTimeoutId = window.setTimeout(() => { setLayerFilter(clusterLayerId, hiddenFilter); setLayerFilter(unclusteredLayerId, hiddenFilter); hideLayersTimeoutId = null; }, 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 customPitch = Number(container.dataset.mapPitch); const eventColor = normalizeHexColor(container.dataset.mapEventColor, DEFAULT_EVENT_COLOR); const hasCustomCenter = null !== customLat && null !== customLng; const initialPitch = Number.isFinite(customPitch) ? Math.max(0, Math.min(85, customPitch)) : 0; 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(async () => { 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 baseStyleUrl = container.dataset.mapStyle || DEFAULT_BASE_STYLE_URL; const style = await getProtomapsStyle(baseStyleUrl); const map = new maplibregl.Map({ container, style, zoom: initialZoom, pitch: initialPitch, bearing: 0, center: initialCenter, transformRequest: (url, resourceType) => { if ('Glyphs' === resourceType) { return { url: rewriteGlyphUrl(url), }; } return { url, }; }, }); let activePopup = null; const closePopup = () => { if (!activePopup) { return; } activePopup.remove(); activePopup = null; }; const openPopup = (coordinates, popupHtml) => { if (!Array.isArray(coordinates) || coordinates.length !== 2 || !popupHtml) { return; } const [lng, lat] = coordinates.map((value) => Number(value)); if (!Number.isFinite(lng) || !Number.isFinite(lat)) { return; } closePopup(); const popup = new maplibregl.Popup({ offset: 15 }) .setLngLat([lng, lat]) .setHTML(String(popupHtml)) .addTo(map); popup.on('close', () => { if (activePopup === popup) { activePopup = null; } }); activePopup = popup; }; 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', async () => { applyDefaultProjection(map); const organizationItems = items .filter((item) => item.type === 'organisation') .sort((firstItem, secondItem) => secondItem.latitude - firstItem.latitude); const eventItems = items .filter((item) => item.type === 'event') .sort((firstItem, secondItem) => secondItem.latitude - firstItem.latitude); const allCoordinates = items.map((item) => [item.longitude, item.latitude]); const organizationColor = normalizeHexColor( container.dataset.mapOrganizationColor, eventColor, ); let eventMarkerImageId = null; let clusterMarkerImageId = null; try { [eventMarkerImageId, clusterMarkerImageId] = await Promise.all([ ensureMarkerImage(map, eventColor, 'eventmanager-event-marker', { withInnerDot: true }), ensureMarkerImage(map, eventColor, 'eventmanager-event-cluster-marker', { withInnerDot: false }), ]); } catch (error) { return; } const organizationMarkerManager = initOrganizationMarkers(map, organizationItems, organizationColor, openPopup); let eventLayerManager = null; if (eventItems.length) { eventLayerManager = initEventLayers(map, eventItems, eventColor, eventMarkerImageId, clusterMarkerImageId, openPopup, closePopup); } bindExternalTagFilters(container, map, organizationMarkerManager, eventLayerManager, closePopup); 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(); }