332 lines
8.9 KiB
JavaScript
332 lines
8.9 KiB
JavaScript
import Spiderfy from 'https://cdn.jsdelivr.net/npm/@nazka/map-gl-js-spiderfy@1.2.10/dist/index.modern.js';
|
|
|
|
const MAP_SELECTOR = '[data-eventmanager-map="1"]';
|
|
|
|
const escapeHtml = (value) => {
|
|
const stringValue = String(value ?? '');
|
|
|
|
return stringValue
|
|
.replace(/&/g, '&')
|
|
.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,
|
|
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 popupHtmlFor = (item) => {
|
|
if (item.type === 'event') {
|
|
const locationTitle = escapeHtml(item.extra.locationTitle || '');
|
|
const startDate = escapeHtml(item.extra.startDate || '');
|
|
|
|
return [
|
|
`<strong>${escapeHtml(item.title)}</strong>`,
|
|
locationTitle ? `<div>${locationTitle}</div>` : '',
|
|
startDate ? `<div>${startDate}</div>` : '',
|
|
].join('');
|
|
}
|
|
|
|
return `<strong>${escapeHtml(item.title)}</strong>`;
|
|
};
|
|
|
|
const toFeature = (item) => ({
|
|
type: 'Feature',
|
|
geometry: {
|
|
type: 'Point',
|
|
coordinates: [item.longitude, item.latitude],
|
|
},
|
|
properties: {
|
|
id: item.id,
|
|
type: item.type,
|
|
title: item.title,
|
|
popupHtml: popupHtmlFor(item),
|
|
},
|
|
});
|
|
|
|
const addSimpleMarker = (map, item) => {
|
|
const popup = new maplibregl.Popup({ offset: 20 }).setHTML(popupHtmlFor(item));
|
|
|
|
new maplibregl.Marker()
|
|
.setLngLat([item.longitude, item.latitude])
|
|
.setPopup(popup)
|
|
.addTo(map);
|
|
};
|
|
|
|
const initEventLayers = (map, eventItems) => {
|
|
const sourceId = 'eventmanager-events-source';
|
|
const clusterLayerId = 'eventmanager-events-clusters';
|
|
const clusterCountLayerId = 'eventmanager-events-cluster-count';
|
|
const unclusteredLayerId = 'eventmanager-events-unclustered';
|
|
|
|
const features = eventItems.map(toFeature);
|
|
|
|
map.addSource(sourceId, {
|
|
type: 'geojson',
|
|
data: {
|
|
type: 'FeatureCollection',
|
|
features,
|
|
},
|
|
cluster: true,
|
|
clusterMaxZoom: 22,
|
|
clusterRadius: 22,
|
|
});
|
|
|
|
map.addLayer({
|
|
id: clusterLayerId,
|
|
type: 'circle',
|
|
source: sourceId,
|
|
filter: ['has', 'point_count'],
|
|
paint: {
|
|
'circle-radius': 18,
|
|
'circle-opacity': 0.8,
|
|
},
|
|
});
|
|
|
|
map.addLayer({
|
|
id: clusterCountLayerId,
|
|
type: 'symbol',
|
|
source: sourceId,
|
|
filter: ['has', 'point_count'],
|
|
layout: {
|
|
'text-field': ['get', 'point_count'],
|
|
'text-size': 12,
|
|
},
|
|
});
|
|
|
|
map.addLayer({
|
|
id: unclusteredLayerId,
|
|
type: 'circle',
|
|
source: sourceId,
|
|
filter: ['!', ['has', 'point_count']],
|
|
paint: {
|
|
'circle-radius': 9,
|
|
'circle-stroke-width': 2,
|
|
},
|
|
});
|
|
|
|
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 = '';
|
|
});
|
|
|
|
if (typeof Spiderfy === 'function') {
|
|
const spiderfy = new Spiderfy(map, {
|
|
closeOnLeafClick: true,
|
|
onLeafClick: (feature, spiderEvent) => {
|
|
const clickCoordinates = spiderEvent?.lngLat
|
|
? [spiderEvent.lngLat.lng, spiderEvent.lngLat.lat]
|
|
: null;
|
|
|
|
openEventPopup(feature, clickCoordinates);
|
|
},
|
|
});
|
|
|
|
spiderfy.applyTo(clusterLayerId);
|
|
} else {
|
|
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,
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
map.on('mouseenter', clusterLayerId, () => {
|
|
map.getCanvas().style.cursor = 'pointer';
|
|
});
|
|
|
|
map.on('mouseleave', clusterLayerId, () => {
|
|
map.getCanvas().style.cursor = '';
|
|
});
|
|
|
|
};
|
|
|
|
const initSingleMap = (container) => {
|
|
if (container.dataset.mapInitialized === '1') {
|
|
return;
|
|
}
|
|
|
|
const items = parseItems(container);
|
|
|
|
if (!items.length || typeof maplibregl === 'undefined') {
|
|
return;
|
|
}
|
|
|
|
container.dataset.mapInitialized = '1';
|
|
|
|
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: 12,
|
|
pitch: 0,
|
|
bearing: 0,
|
|
center: [items[0].longitude, items[0].latitude],
|
|
});
|
|
|
|
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', () => {
|
|
const nonEventItems = items.filter((item) => item.type !== 'event');
|
|
const eventItems = items.filter((item) => item.type === 'event');
|
|
const allCoordinates = items.map((item) => [item.longitude, item.latitude]);
|
|
|
|
nonEventItems.forEach((item) => {
|
|
addSimpleMarker(map, item);
|
|
});
|
|
|
|
if (eventItems.length) {
|
|
initEventLayers(map, eventItems);
|
|
}
|
|
|
|
fitBounds(allCoordinates);
|
|
});
|
|
};
|
|
|
|
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();
|
|
}
|