diff --git a/contao/dca/tl_module.php b/contao/dca/tl_module.php
index 0e5296a..3aaa269 100644
--- a/contao/dca/tl_module.php
+++ b/contao/dca/tl_module.php
@@ -9,7 +9,7 @@ use Contao\StringUtil;
$GLOBALS['TL_DCA']['tl_module']['palettes']['member_organizations'] = '{title_legend},name,headline,type;{eventmanager_legend},editPage;{protected_legend:hide},protected;{expert_legend:hide},guests,cssID';
$GLOBALS['TL_DCA']['tl_module']['palettes']['member_events'] = '{title_legend},name,headline,type;{eventmanager_legend},editPage;{protected_legend:hide},protected;{expert_legend:hide},guests,cssID';
$GLOBALS['TL_DCA']['tl_module']['palettes']['event_filter'] = '{title_legend},name,headline,type;{eventmanager_legend},cal_calendar,eventListDomId;{protected_legend:hide},protected;{expert_legend:hide},guests,cssID';
-$GLOBALS['TL_DCA']['tl_module']['palettes']['eventmanager_map'] = '{title_legend},name,headline,type;{protected_legend:hide},protected;{expert_legend:hide},guests,cssID';
+$GLOBALS['TL_DCA']['tl_module']['palettes']['eventmanager_map'] = '{title_legend},name,headline,type;{eventmanager_legend},mapShowOrganizations,mapShowExternalOrganizations,mapShowEvents,mapEventColor,mapOrganizationColorScheme,mapCenterMode;{protected_legend:hide},protected;{expert_legend:hide},guests,cssID';
$GLOBALS['TL_DCA']['tl_module']['palettes']['organization_edit'] = '{title_legend},name,headline,type;{eventmanager_legend},listPage,logoFolder,organizationTypeTags;{protected_legend:hide},protected;{expert_legend:hide},guests,cssID';
$GLOBALS['TL_DCA']['tl_module']['palettes']['event_edit'] = '{title_legend},name,headline,type;{eventmanager_legend},listPage,eventFolder,termsPage,frontendAuthorId,frontendArchiveId,eventTypeTags;{protected_legend:hide},protected;{expert_legend:hide},guests,cssID';
@@ -160,6 +160,83 @@ $GLOBALS['TL_DCA']['tl_module']['fields']['eventListDomId'] = [
'sql' => ['type' => 'string', 'length' => 128, 'default' => ''],
];
+$GLOBALS['TL_DCA']['tl_module']['palettes']['__selector__'][] = 'mapCenterMode';
+$GLOBALS['TL_DCA']['tl_module']['subpalettes']['mapCenterMode_custom'] = 'mapCenterLat,mapCenterLng,mapCenterZoom';
+
+$GLOBALS['TL_DCA']['tl_module']['fields']['mapShowOrganizations'] = [
+ 'label' => &$GLOBALS['TL_LANG']['tl_module']['mapShowOrganizations'],
+ 'exclude' => true,
+ 'inputType' => 'checkbox',
+ 'eval' => ['tl_class' => 'w50 m12'],
+ 'sql' => ['type' => 'string', 'length' => 1, 'fixed' => true, 'default' => ''],
+];
+
+$GLOBALS['TL_DCA']['tl_module']['fields']['mapShowExternalOrganizations'] = [
+ 'label' => &$GLOBALS['TL_LANG']['tl_module']['mapShowExternalOrganizations'],
+ 'exclude' => true,
+ 'inputType' => 'checkbox',
+ 'eval' => ['tl_class' => 'w50 m12'],
+ 'sql' => ['type' => 'string', 'length' => 1, 'fixed' => true, 'default' => ''],
+];
+
+$GLOBALS['TL_DCA']['tl_module']['fields']['mapShowEvents'] = [
+ 'label' => &$GLOBALS['TL_LANG']['tl_module']['mapShowEvents'],
+ 'exclude' => true,
+ 'inputType' => 'checkbox',
+ 'eval' => ['tl_class' => 'w50 m12'],
+ 'sql' => ['type' => 'string', 'length' => 1, 'fixed' => true, 'default' => ''],
+];
+
+$GLOBALS['TL_DCA']['tl_module']['fields']['mapEventColor'] = [
+ 'label' => &$GLOBALS['TL_LANG']['tl_module']['mapEventColor'],
+ 'exclude' => true,
+ 'inputType' => 'text',
+ 'eval' => ['maxlength' => 7, 'rgxp' => 'hexcolor', 'colorpicker' => true, 'tl_class' => 'w50'],
+ 'sql' => ['type' => 'string', 'length' => 7, 'default' => '#BC5067'],
+];
+
+$GLOBALS['TL_DCA']['tl_module']['fields']['mapOrganizationColorScheme'] = [
+ 'label' => &$GLOBALS['TL_LANG']['tl_module']['mapOrganizationColorScheme'],
+ 'exclude' => true,
+ 'inputType' => 'text',
+ 'eval' => ['maxlength' => 1024, 'tl_class' => 'clr long'],
+ 'sql' => ['type' => 'string', 'length' => 1024, 'default' => ''],
+];
+
+$GLOBALS['TL_DCA']['tl_module']['fields']['mapCenterMode'] = [
+ 'label' => &$GLOBALS['TL_LANG']['tl_module']['mapCenterMode'],
+ 'exclude' => true,
+ 'inputType' => 'select',
+ 'options' => ['markers', 'custom'],
+ 'reference' => &$GLOBALS['TL_LANG']['tl_module']['mapCenterMode_options'],
+ 'eval' => ['submitOnChange' => true, 'mandatory' => true, 'tl_class' => 'clr w50', 'includeBlankOption' => false],
+ 'sql' => ['type' => 'string', 'length' => 16, 'default' => 'markers'],
+];
+
+$GLOBALS['TL_DCA']['tl_module']['fields']['mapCenterLat'] = [
+ 'label' => &$GLOBALS['TL_LANG']['tl_module']['mapCenterLat'],
+ 'exclude' => true,
+ 'inputType' => 'text',
+ 'eval' => ['maxlength' => 32, 'tl_class' => 'w50'],
+ 'sql' => ['type' => 'string', 'length' => 32, 'default' => ''],
+];
+
+$GLOBALS['TL_DCA']['tl_module']['fields']['mapCenterLng'] = [
+ 'label' => &$GLOBALS['TL_LANG']['tl_module']['mapCenterLng'],
+ 'exclude' => true,
+ 'inputType' => 'text',
+ 'eval' => ['maxlength' => 32, 'tl_class' => 'w50'],
+ 'sql' => ['type' => 'string', 'length' => 32, 'default' => ''],
+];
+
+$GLOBALS['TL_DCA']['tl_module']['fields']['mapCenterZoom'] = [
+ 'label' => &$GLOBALS['TL_LANG']['tl_module']['mapCenterZoom'],
+ 'exclude' => true,
+ 'inputType' => 'text',
+ 'eval' => ['rgxp' => 'digit', 'maxlength' => 2, 'tl_class' => 'w50'],
+ 'sql' => ['type' => 'smallint', 'unsigned' => true, 'default' => 12],
+];
+
if (isset($GLOBALS['TL_DCA']['tl_module']['fields']['list_layout'])) {
$GLOBALS['TL_DCA']['tl_module']['fields']['list_layout']['options_callback'] = static function (): array {
$options = Controller::getTemplateGroup('list_');
diff --git a/contao/languages/de/tl_module.php b/contao/languages/de/tl_module.php
index 05ea239..6d9dedd 100644
--- a/contao/languages/de/tl_module.php
+++ b/contao/languages/de/tl_module.php
@@ -13,3 +13,16 @@ $GLOBALS['TL_LANG']['tl_module']['frontendArchiveId'] = ['ID des Newsarchivs', '
$GLOBALS['TL_LANG']['tl_module']['eventListDomId'] = ['Eventlisten-ID (DOM)', 'Wählen Sie die ID der Eventliste, die durch das Event-Filter-Modul gefiltert werden soll.'];
$GLOBALS['TL_LANG']['tl_module']['organizationTypeTags'] = ['Anzuzeigende Organisationstypen', 'Optional: Begrenzen Sie die im Frontend anzeigbaren Organisationstyp-Tags. Leer = alle.'];
$GLOBALS['TL_LANG']['tl_module']['eventTypeTags'] = ['Anzuzeigende Veranstaltungstypen', 'Optional: Begrenzen Sie die im Frontend anzeigbaren Veranstaltungstyp-Tags. Leer = alle.'];
+$GLOBALS['TL_LANG']['tl_module']['mapShowOrganizations'] = ['Organisationen anzeigen', 'Wenn aktiviert, werden Organisations-Marker auf der Karte dargestellt.'];
+$GLOBALS['TL_LANG']['tl_module']['mapShowExternalOrganizations'] = ['Externe Organisationen anzeigen', 'Wenn aktiviert, werden externe Organisationen (isExternal=1) zusätzlich auf der Karte dargestellt. Standard: nein.'];
+$GLOBALS['TL_LANG']['tl_module']['mapShowEvents'] = ['Veranstaltungen anzeigen', 'Wenn aktiviert, werden Event- (inkl. Orts-) Marker auf der Karte dargestellt.'];
+$GLOBALS['TL_LANG']['tl_module']['mapEventColor'] = ['Event-Farbe (Kreise/Linien)', 'Farbe für Event-Cluster, Event-Punkte und Spiderfy-Verbindungslinien (Hex, z. B. #BC5067).'];
+$GLOBALS['TL_LANG']['tl_module']['mapOrganizationColorScheme'] = ['Farbschema (Organisationstypen)', 'Kommagetrennte Farben für Organisationstypen/Tags, z. B. ff6600,77dd33,ff99bb. Markerfarbe richtet sich nach dem ersten Tag.'];
+$GLOBALS['TL_LANG']['tl_module']['mapCenterMode'] = ['Karten-Zentrierung', 'Wählen Sie, ob die Karte anhand der Marker oder mit festen Koordinaten zentriert werden soll.'];
+$GLOBALS['TL_LANG']['tl_module']['mapCenterMode_options'] = [
+ 'markers' => 'Anhand der Marker (alle Marker sichtbar)',
+ 'custom' => 'Feste Geodaten + Zoom-Level',
+];
+$GLOBALS['TL_LANG']['tl_module']['mapCenterLat'] = ['Breitengrad (Center)', 'Breitengrad für die feste Kartenzentrierung, z. B. 51.0538'];
+$GLOBALS['TL_LANG']['tl_module']['mapCenterLng'] = ['Längengrad (Center)', 'Längengrad für die feste Kartenzentrierung, z. B. 13.3080'];
+$GLOBALS['TL_LANG']['tl_module']['mapCenterZoom'] = ['Zoom-Level (Center)', 'Zoom-Level für die feste Kartenzentrierung (z. B. 12).'];
diff --git a/contao/languages/en/tl_module.php b/contao/languages/en/tl_module.php
index e70d55f..4a69d86 100644
--- a/contao/languages/en/tl_module.php
+++ b/contao/languages/en/tl_module.php
@@ -13,3 +13,16 @@ $GLOBALS['TL_LANG']['tl_module']['frontendArchiveId'] = ['News archive ID', 'Arc
$GLOBALS['TL_LANG']['tl_module']['eventListDomId'] = ['Event list ID (DOM)', 'Select the event list ID that should be filtered by the event filter module.'];
$GLOBALS['TL_LANG']['tl_module']['organizationTypeTags'] = ['Displayed organization types', 'Optional: Limit which organization type tags can be selected in the frontend. Empty = all.'];
$GLOBALS['TL_LANG']['tl_module']['eventTypeTags'] = ['Displayed event types', 'Optional: Limit which event type tags can be selected in the frontend. Empty = all.'];
+$GLOBALS['TL_LANG']['tl_module']['mapShowOrganizations'] = ['Show organizations', 'If enabled, organization markers are rendered on the map.'];
+$GLOBALS['TL_LANG']['tl_module']['mapShowExternalOrganizations'] = ['Show external organizations', 'If enabled, external organizations (isExternal=1) are additionally rendered on the map. Default: no.'];
+$GLOBALS['TL_LANG']['tl_module']['mapShowEvents'] = ['Show events', 'If enabled, event markers (including related locations) are rendered on the map.'];
+$GLOBALS['TL_LANG']['tl_module']['mapEventColor'] = ['Event color (circles/lines)', 'Color for event clusters, event points and spiderfy connector lines (hex, e.g. #BC5067).'];
+$GLOBALS['TL_LANG']['tl_module']['mapOrganizationColorScheme'] = ['Color scheme (organization types)', 'Comma-separated colors for organization type tags, e.g. ff6600,77dd33,ff99bb. Marker color follows the first tag.'];
+$GLOBALS['TL_LANG']['tl_module']['mapCenterMode'] = ['Map centering', 'Choose whether the map should center by markers or fixed coordinates.'];
+$GLOBALS['TL_LANG']['tl_module']['mapCenterMode_options'] = [
+ 'markers' => 'By markers (fit all visible markers)',
+ 'custom' => 'Fixed coordinates + zoom level',
+];
+$GLOBALS['TL_LANG']['tl_module']['mapCenterLat'] = ['Center latitude', 'Latitude for fixed map centering, e.g. 51.0538'];
+$GLOBALS['TL_LANG']['tl_module']['mapCenterLng'] = ['Center longitude', 'Longitude for fixed map centering, e.g. 13.3080'];
+$GLOBALS['TL_LANG']['tl_module']['mapCenterZoom'] = ['Center zoom level', 'Zoom level for fixed map centering (e.g. 12).'];
diff --git a/contao/templates/frontend/event_map.html.twig b/contao/templates/frontend/event_map.html.twig
index 2fceea8..5749aa7 100644
--- a/contao/templates/frontend/event_map.html.twig
+++ b/contao/templates/frontend/event_map.html.twig
@@ -1,12 +1,87 @@
+{% set tags = mapOrganizationTags|default([]) %}
+
+
+
+
+
+ {% if tags is iterable and tags|length > 0 %}
+ {% for tag in tags %}
+
+ {% endfor %}
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
+
-
+
diff --git a/public/assets/map-module.js b/public/assets/map-module.js
index fff752e..f51c1c2 100644
--- a/public/assets/map-module.js
+++ b/public/assets/map-module.js
@@ -1,6 +1,111 @@
-import Spiderfy from 'https://cdn.jsdelivr.net/npm/@nazka/map-gl-js-spiderfy@1.2.10/dist/index.modern.js';
+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 ?? '');
@@ -50,6 +155,7 @@ const parseItems = (container) => {
unique.set(`${item.type}:${id}`, {
type: item.type,
+ markerType: String(item.markerType || item.type || ''),
id,
title: String(item.title || ''),
latitude,
@@ -64,6 +170,245 @@ const parseItems = (container) => {
}
};
+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 || '');
@@ -88,28 +433,346 @@ const toFeature = (item) => ({
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 addSimpleMarker = (map, item) => {
- const popup = new maplibregl.Popup({ offset: 20 }).setHTML(popupHtmlFor(item));
+const initOrganizationMarkers = (map, organizationItems, organizationTagColorMap) => {
+ if (!organizationItems.length) {
+ return {
+ setActiveTagIds: () => {},
+ setAllVisible: () => {},
+ };
+ }
- new maplibregl.Marker()
- .setLngLat([item.longitude, item.latitude])
- .setPopup(popup)
- .addTo(map);
+ 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 initEventLayers = (map, eventItems) => {
+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 = 'eventmanager-events-clusters';
+ const clusterLayerId = EVENT_CLUSTER_LAYER_ID;
+ const clusterSpiderfyLayerId = EVENT_CLUSTER_SPIDERFY_LAYER_ID;
const clusterCountLayerId = 'eventmanager-events-cluster-count';
- const unclusteredLayerId = 'eventmanager-events-unclustered';
+ 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: {
@@ -117,7 +780,7 @@ const initEventLayers = (map, eventItems) => {
features,
},
cluster: true,
- clusterMaxZoom: 22,
+ clusterMaxZoom: 17,
clusterRadius: 22,
});
@@ -128,7 +791,9 @@ const initEventLayers = (map, eventItems) => {
filter: ['has', 'point_count'],
paint: {
'circle-radius': 18,
- 'circle-opacity': 0.8,
+ 'circle-opacity': 1,
+ 'circle-color': eventColor,
+ 'circle-stroke-width': 0,
},
});
@@ -139,7 +804,29 @@ const initEventLayers = (map, eventItems) => {
filter: ['has', 'point_count'],
layout: {
'text-field': ['get', 'point_count'],
- 'text-size': 12,
+ '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,
},
});
@@ -149,8 +836,10 @@ const initEventLayers = (map, eventItems) => {
source: sourceId,
filter: ['!', ['has', 'point_count']],
paint: {
- 'circle-radius': 9,
- 'circle-stroke-width': 2,
+ 'circle-radius': 11,
+ 'circle-color': eventColor,
+ 'circle-stroke-width': 0,
+ 'circle-opacity': 1,
},
});
@@ -192,20 +881,7 @@ const initEventLayers = (map, eventItems) => {
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 {
+ const bindClusterZoomFallback = () => {
map.on('click', clusterLayerId, (event) => {
const feature = event.features && event.features[0] ? event.features[0] : null;
@@ -236,6 +912,60 @@ const initEventLayers = (map, eventItems) => {
});
});
});
+ };
+
+ 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, () => {
@@ -246,6 +976,56 @@ const initEventLayers = (map, eventItems) => {
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) => {
@@ -254,69 +1034,140 @@ const initSingleMap = (container) => {
}
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;
- if (!items.length || typeof maplibregl === 'undefined') {
- return;
+ 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';
- if (typeof pmtiles !== 'undefined' && !window.__eventmanagerPmtilesRegistered) {
- const protocol = new pmtiles.Protocol();
- maplibregl.addProtocol('pmtiles', protocol.tile);
- window.__eventmanagerPmtilesRegistered = true;
- }
+ ensureDependencies()
+ .then(() => {
+ if (typeof maplibregl === 'undefined') {
+ return;
+ }
- 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],
- });
+ if (typeof pmtiles !== 'undefined' && !window.__eventmanagerPmtilesRegistered) {
+ const protocol = new pmtiles.Protocol();
+ maplibregl.addProtocol('pmtiles', protocol.tile);
+ window.__eventmanagerPmtilesRegistered = true;
+ }
- map.addControl(new maplibregl.NavigationControl({ visualizePitch: 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),
+ };
+ }
- const fitBounds = (coordinates) => {
- const points = coordinates.filter((coord) => Array.isArray(coord) && coord.length === 2);
+ return {
+ url,
+ };
+ },
+ });
- if (!points.length) {
- return;
- }
+ map.on('style.load', () => {
+ applyDefaultProjection(map);
+ });
- if (1 === points.length) {
- map.setCenter(points[0]);
- map.setZoom(14);
+ map.on('styleimagemissing', (event) => {
+ const missingImageId = event?.id || '';
- return;
- }
+ if (!missingImageId || map.hasImage(missingImageId)) {
+ return;
+ }
- const bounds = new maplibregl.LngLatBounds();
- points.forEach((coord) => bounds.extend(coord));
+ map.addImage(missingImageId, {
+ width: 1,
+ height: 1,
+ data: new Uint8Array([0, 0, 0, 0]),
+ });
+ });
- map.fitBounds(bounds, {
- padding: 60,
- maxZoom: 15,
+ 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';
});
- };
-
- 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 = () => {
diff --git a/public/assets/vendor/map-gl-js-spiderfy.js b/public/assets/vendor/map-gl-js-spiderfy.js
new file mode 100644
index 0000000..12b1124
--- /dev/null
+++ b/public/assets/vendor/map-gl-js-spiderfy.js
@@ -0,0 +1 @@
+var e={207:(e,t,r)=>{e.exports=r(452)},659:(e,t,r)=>{const n=r(156),o={};for(const e of Object.keys(n))o[n[e]]=e;const a={rgb:{channels:3,labels:"rgb"},hsl:{channels:3,labels:"hsl"},hsv:{channels:3,labels:"hsv"},hwb:{channels:3,labels:"hwb"},cmyk:{channels:4,labels:"cmyk"},xyz:{channels:3,labels:"xyz"},lab:{channels:3,labels:"lab"},lch:{channels:3,labels:"lch"},hex:{channels:1,labels:["hex"]},keyword:{channels:1,labels:["keyword"]},ansi16:{channels:1,labels:["ansi16"]},ansi256:{channels:1,labels:["ansi256"]},hcg:{channels:3,labels:["h","c","g"]},apple:{channels:3,labels:["r16","g16","b16"]},gray:{channels:1,labels:["gray"]}};e.exports=a;for(const e of Object.keys(a)){if(!("channels"in a[e]))throw new Error("missing channels property: "+e);if(!("labels"in a[e]))throw new Error("missing channel labels property: "+e);if(a[e].labels.length!==a[e].channels)throw new Error("channel and label counts mismatch: "+e);const{channels:t,labels:r}=a[e];delete a[e].channels,delete a[e].labels,Object.defineProperty(a[e],"channels",{value:t}),Object.defineProperty(a[e],"labels",{value:r})}a.rgb.hsl=function(e){const t=e[0]/255,r=e[1]/255,n=e[2]/255,o=Math.min(t,r,n),a=Math.max(t,r,n),i=a-o;let s,l;a===o?s=0:t===a?s=(r-n)/i:r===a?s=2+(n-t)/i:n===a&&(s=4+(t-r)/i),s=Math.min(60*s,360),s<0&&(s+=360);const c=(o+a)/2;return l=a===o?0:c<=.5?i/(a+o):i/(2-a-o),[s,100*l,100*c]},a.rgb.hsv=function(e){let t,r,n,o,a;const i=e[0]/255,s=e[1]/255,l=e[2]/255,c=Math.max(i,s,l),u=c-Math.min(i,s,l),h=function(e){return(c-e)/6/u+.5};return 0===u?(o=0,a=0):(a=u/c,t=h(i),r=h(s),n=h(l),i===c?o=n-r:s===c?o=1/3+t-n:l===c&&(o=2/3+r-t),o<0?o+=1:o>1&&(o-=1)),[360*o,100*a,100*c]},a.rgb.hwb=function(e){const t=e[0],r=e[1];let n=e[2];const o=a.rgb.hsl(e)[0],i=1/255*Math.min(t,Math.min(r,n));return n=1-1/255*Math.max(t,Math.max(r,n)),[o,100*i,100*n]},a.rgb.cmyk=function(e){const t=e[0]/255,r=e[1]/255,n=e[2]/255,o=Math.min(1-t,1-r,1-n);return[100*((1-t-o)/(1-o)||0),100*((1-r-o)/(1-o)||0),100*((1-n-o)/(1-o)||0),100*o]},a.rgb.keyword=function(e){const t=o[e];if(t)return t;let r,a=1/0;for(const t of Object.keys(n)){const o=(s=n[t],((i=e)[0]-s[0])**2+(i[1]-s[1])**2+(i[2]-s[2])**2);o.04045?((t+.055)/1.055)**2.4:t/12.92,r=r>.04045?((r+.055)/1.055)**2.4:r/12.92,n=n>.04045?((n+.055)/1.055)**2.4:n/12.92,[100*(.4124*t+.3576*r+.1805*n),100*(.2126*t+.7152*r+.0722*n),100*(.0193*t+.1192*r+.9505*n)]},a.rgb.lab=function(e){const t=a.rgb.xyz(e);let r=t[0],n=t[1],o=t[2];return r/=95.047,n/=100,o/=108.883,r=r>.008856?r**(1/3):7.787*r+16/116,n=n>.008856?n**(1/3):7.787*n+16/116,o=o>.008856?o**(1/3):7.787*o+16/116,[116*n-16,500*(r-n),200*(n-o)]},a.hsl.rgb=function(e){const t=e[0]/360,r=e[1]/100,n=e[2]/100;let o,a,i;if(0===r)return i=255*n,[i,i,i];o=n<.5?n*(1+r):n+r-n*r;const s=2*n-o,l=[0,0,0];for(let e=0;e<3;e++)a=t+1/3*-(e-1),a<0&&a++,a>1&&a--,i=6*a<1?s+6*(o-s)*a:2*a<1?o:3*a<2?s+(o-s)*(2/3-a)*6:s,l[e]=255*i;return l},a.hsl.hsv=function(e){const t=e[0];let r=e[1]/100,n=e[2]/100,o=r;const a=Math.max(n,.01);return n*=2,r*=n<=1?n:2-n,o*=a<=1?a:2-a,[t,100*(0===n?2*o/(a+o):2*r/(n+r)),(n+r)/2*100]},a.hsv.rgb=function(e){const t=e[0]/60,r=e[1]/100;let n=e[2]/100;const o=Math.floor(t)%6,a=t-Math.floor(t),i=255*n*(1-r),s=255*n*(1-r*a),l=255*n*(1-r*(1-a));switch(n*=255,o){case 0:return[n,l,i];case 1:return[s,n,i];case 2:return[i,n,l];case 3:return[i,s,n];case 4:return[l,i,n];case 5:return[n,i,s]}},a.hsv.hsl=function(e){const t=e[0],r=e[1]/100,n=e[2]/100,o=Math.max(n,.01);let a,i;i=(2-r)*n;const s=(2-r)*o;return a=r*o,a/=s<=1?s:2-s,a=a||0,i/=2,[t,100*a,100*i]},a.hwb.rgb=function(e){const t=e[0]/360;let r=e[1]/100,n=e[2]/100;const o=r+n;let a;o>1&&(r/=o,n/=o);const i=Math.floor(6*t),s=1-n;a=6*t-i,1&i&&(a=1-a);const l=r+a*(s-r);let c,u,h;switch(i){default:case 6:case 0:c=s,u=l,h=r;break;case 1:c=l,u=s,h=r;break;case 2:c=r,u=s,h=l;break;case 3:c=r,u=l,h=s;break;case 4:c=l,u=r,h=s;break;case 5:c=s,u=r,h=l}return[255*c,255*u,255*h]},a.cmyk.rgb=function(e){const t=e[0]/100,r=e[1]/100,n=e[2]/100,o=e[3]/100;return[255*(1-Math.min(1,t*(1-o)+o)),255*(1-Math.min(1,r*(1-o)+o)),255*(1-Math.min(1,n*(1-o)+o))]},a.xyz.rgb=function(e){const t=e[0]/100,r=e[1]/100,n=e[2]/100;let o,a,i;return o=3.2406*t+-1.5372*r+-.4986*n,a=-.9689*t+1.8758*r+.0415*n,i=.0557*t+-.204*r+1.057*n,o=o>.0031308?1.055*o**(1/2.4)-.055:12.92*o,a=a>.0031308?1.055*a**(1/2.4)-.055:12.92*a,i=i>.0031308?1.055*i**(1/2.4)-.055:12.92*i,o=Math.min(Math.max(0,o),1),a=Math.min(Math.max(0,a),1),i=Math.min(Math.max(0,i),1),[255*o,255*a,255*i]},a.xyz.lab=function(e){let t=e[0],r=e[1],n=e[2];return t/=95.047,r/=100,n/=108.883,t=t>.008856?t**(1/3):7.787*t+16/116,r=r>.008856?r**(1/3):7.787*r+16/116,n=n>.008856?n**(1/3):7.787*n+16/116,[116*r-16,500*(t-r),200*(r-n)]},a.lab.xyz=function(e){let t,r,n;r=(e[0]+16)/116,t=e[1]/500+r,n=r-e[2]/200;const o=r**3,a=t**3,i=n**3;return r=o>.008856?o:(r-16/116)/7.787,t=a>.008856?a:(t-16/116)/7.787,n=i>.008856?i:(n-16/116)/7.787,t*=95.047,r*=100,n*=108.883,[t,r,n]},a.lab.lch=function(e){const t=e[0],r=e[1],n=e[2];let o;return o=360*Math.atan2(n,r)/2/Math.PI,o<0&&(o+=360),[t,Math.sqrt(r*r+n*n),o]},a.lch.lab=function(e){const t=e[0],r=e[1],n=e[2]/360*2*Math.PI;return[t,r*Math.cos(n),r*Math.sin(n)]},a.rgb.ansi16=function(e,t=null){const[r,n,o]=e;let i=null===t?a.rgb.hsv(e)[2]:t;if(i=Math.round(i/50),0===i)return 30;let s=30+(Math.round(o/255)<<2|Math.round(n/255)<<1|Math.round(r/255));return 2===i&&(s+=60),s},a.hsv.ansi16=function(e){return a.rgb.ansi16(a.hsv.rgb(e),e[2])},a.rgb.ansi256=function(e){const t=e[0],r=e[1],n=e[2];return t===r&&r===n?t<8?16:t>248?231:Math.round((t-8)/247*24)+232:16+36*Math.round(t/255*5)+6*Math.round(r/255*5)+Math.round(n/255*5)},a.ansi16.rgb=function(e){let t=e%10;if(0===t||7===t)return e>50&&(t+=3.5),t=t/10.5*255,[t,t,t];const r=.5*(1+~~(e>50));return[(1&t)*r*255,(t>>1&1)*r*255,(t>>2&1)*r*255]},a.ansi256.rgb=function(e){if(e>=232){const t=10*(e-232)+8;return[t,t,t]}let t;return e-=16,[Math.floor(e/36)/5*255,Math.floor((t=e%36)/6)/5*255,t%6/5*255]},a.rgb.hex=function(e){const t=(((255&Math.round(e[0]))<<16)+((255&Math.round(e[1]))<<8)+(255&Math.round(e[2]))).toString(16).toUpperCase();return"000000".substring(t.length)+t},a.hex.rgb=function(e){const t=e.toString(16).match(/[a-f0-9]{6}|[a-f0-9]{3}/i);if(!t)return[0,0,0];let r=t[0];3===t[0].length&&(r=r.split("").map((e=>e+e)).join(""));const n=parseInt(r,16);return[n>>16&255,n>>8&255,255&n]},a.rgb.hcg=function(e){const t=e[0]/255,r=e[1]/255,n=e[2]/255,o=Math.max(Math.max(t,r),n),a=Math.min(Math.min(t,r),n),i=o-a;let s,l;return s=i<1?a/(1-i):0,l=i<=0?0:o===t?(r-n)/i%6:o===r?2+(n-t)/i:4+(t-r)/i,l/=6,l%=1,[360*l,100*i,100*s]},a.hsl.hcg=function(e){const t=e[1]/100,r=e[2]/100,n=r<.5?2*t*r:2*t*(1-r);let o=0;return n<1&&(o=(r-.5*n)/(1-n)),[e[0],100*n,100*o]},a.hsv.hcg=function(e){const t=e[1]/100,r=e[2]/100,n=t*r;let o=0;return n<1&&(o=(r-n)/(1-n)),[e[0],100*n,100*o]},a.hcg.rgb=function(e){const t=e[0]/360,r=e[1]/100,n=e[2]/100;if(0===r)return[255*n,255*n,255*n];const o=[0,0,0],a=t%1*6,i=a%1,s=1-i;let l=0;switch(Math.floor(a)){case 0:o[0]=1,o[1]=i,o[2]=0;break;case 1:o[0]=s,o[1]=1,o[2]=0;break;case 2:o[0]=0,o[1]=1,o[2]=i;break;case 3:o[0]=0,o[1]=s,o[2]=1;break;case 4:o[0]=i,o[1]=0,o[2]=1;break;default:o[0]=1,o[1]=0,o[2]=s}return l=(1-r)*n,[255*(r*o[0]+l),255*(r*o[1]+l),255*(r*o[2]+l)]},a.hcg.hsv=function(e){const t=e[1]/100,r=t+e[2]/100*(1-t);let n=0;return r>0&&(n=t/r),[e[0],100*n,100*r]},a.hcg.hsl=function(e){const t=e[1]/100,r=e[2]/100*(1-t)+.5*t;let n=0;return r>0&&r<.5?n=t/(2*r):r>=.5&&r<1&&(n=t/(2*(1-r))),[e[0],100*n,100*r]},a.hcg.hwb=function(e){const t=e[1]/100,r=t+e[2]/100*(1-t);return[e[0],100*(r-t),100*(1-r)]},a.hwb.hcg=function(e){const t=e[1]/100,r=1-e[2]/100,n=r-t;let o=0;return n<1&&(o=(r-n)/(1-n)),[e[0],100*n,100*o]},a.apple.rgb=function(e){return[e[0]/65535*255,e[1]/65535*255,e[2]/65535*255]},a.rgb.apple=function(e){return[e[0]/255*65535,e[1]/255*65535,e[2]/255*65535]},a.gray.rgb=function(e){return[e[0]/100*255,e[0]/100*255,e[0]/100*255]},a.gray.hsl=function(e){return[0,0,e[0]]},a.gray.hsv=a.gray.hsl,a.gray.hwb=function(e){return[0,100,e[0]]},a.gray.cmyk=function(e){return[0,0,0,e[0]]},a.gray.lab=function(e){return[e[0],0,0]},a.gray.hex=function(e){const t=255&Math.round(e[0]/100*255),r=((t<<16)+(t<<8)+t).toString(16).toUpperCase();return"000000".substring(r.length)+r},a.rgb.gray=function(e){return[(e[0]+e[1]+e[2])/3/255*100]}},734:(e,t,r)=>{const n=r(659),o=r(507),a={};Object.keys(n).forEach((e=>{a[e]={},Object.defineProperty(a[e],"channels",{value:n[e].channels}),Object.defineProperty(a[e],"labels",{value:n[e].labels});const t=o(e);Object.keys(t).forEach((r=>{const n=t[r];a[e][r]=function(e){const t=function(...t){const r=t[0];if(null==r)return r;r.length>1&&(t=r);const n=e(t);if("object"==typeof n)for(let e=n.length,t=0;t1&&(t=r),e(t))};return"conversion"in e&&(t.conversion=e.conversion),t}(n)}))})),e.exports=a},507:(e,t,r)=>{const n=r(659);function o(e,t){return function(r){return t(e(r))}}function a(e,t){const r=[t[e].parent,e];let a=n[t[e].parent][e],i=t[e].parent;for(;t[i].parent;)r.unshift(t[i].parent),a=o(n[t[i].parent][i],a),i=t[i].parent;return a.conversion=r,a}e.exports=function(e){const t=function(e){const t=function(){const e={},t=Object.keys(n);for(let r=t.length,n=0;n{e.exports={aliceblue:[240,248,255],antiquewhite:[250,235,215],aqua:[0,255,255],aquamarine:[127,255,212],azure:[240,255,255],beige:[245,245,220],bisque:[255,228,196],black:[0,0,0],blanchedalmond:[255,235,205],blue:[0,0,255],blueviolet:[138,43,226],brown:[165,42,42],burlywood:[222,184,135],cadetblue:[95,158,160],chartreuse:[127,255,0],chocolate:[210,105,30],coral:[255,127,80],cornflowerblue:[100,149,237],cornsilk:[255,248,220],crimson:[220,20,60],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgoldenrod:[184,134,11],darkgray:[169,169,169],darkgreen:[0,100,0],darkgrey:[169,169,169],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkseagreen:[143,188,143],darkslateblue:[72,61,139],darkslategray:[47,79,79],darkslategrey:[47,79,79],darkturquoise:[0,206,209],darkviolet:[148,0,211],deeppink:[255,20,147],deepskyblue:[0,191,255],dimgray:[105,105,105],dimgrey:[105,105,105],dodgerblue:[30,144,255],firebrick:[178,34,34],floralwhite:[255,250,240],forestgreen:[34,139,34],fuchsia:[255,0,255],gainsboro:[220,220,220],ghostwhite:[248,248,255],gold:[255,215,0],goldenrod:[218,165,32],gray:[128,128,128],green:[0,128,0],greenyellow:[173,255,47],grey:[128,128,128],honeydew:[240,255,240],hotpink:[255,105,180],indianred:[205,92,92],indigo:[75,0,130],ivory:[255,255,240],khaki:[240,230,140],lavender:[230,230,250],lavenderblush:[255,240,245],lawngreen:[124,252,0],lemonchiffon:[255,250,205],lightblue:[173,216,230],lightcoral:[240,128,128],lightcyan:[224,255,255],lightgoldenrodyellow:[250,250,210],lightgray:[211,211,211],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightsalmon:[255,160,122],lightseagreen:[32,178,170],lightskyblue:[135,206,250],lightslategray:[119,136,153],lightslategrey:[119,136,153],lightsteelblue:[176,196,222],lightyellow:[255,255,224],lime:[0,255,0],limegreen:[50,205,50],linen:[250,240,230],magenta:[255,0,255],maroon:[128,0,0],mediumaquamarine:[102,205,170],mediumblue:[0,0,205],mediumorchid:[186,85,211],mediumpurple:[147,112,219],mediumseagreen:[60,179,113],mediumslateblue:[123,104,238],mediumspringgreen:[0,250,154],mediumturquoise:[72,209,204],mediumvioletred:[199,21,133],midnightblue:[25,25,112],mintcream:[245,255,250],mistyrose:[255,228,225],moccasin:[255,228,181],navajowhite:[255,222,173],navy:[0,0,128],oldlace:[253,245,230],olive:[128,128,0],olivedrab:[107,142,35],orange:[255,165,0],orangered:[255,69,0],orchid:[218,112,214],palegoldenrod:[238,232,170],palegreen:[152,251,152],paleturquoise:[175,238,238],palevioletred:[219,112,147],papayawhip:[255,239,213],peachpuff:[255,218,185],peru:[205,133,63],pink:[255,192,203],plum:[221,160,221],powderblue:[176,224,230],purple:[128,0,128],rebeccapurple:[102,51,153],red:[255,0,0],rosybrown:[188,143,143],royalblue:[65,105,225],saddlebrown:[139,69,19],salmon:[250,128,114],sandybrown:[244,164,96],seagreen:[46,139,87],seashell:[255,245,238],sienna:[160,82,45],silver:[192,192,192],skyblue:[135,206,235],slateblue:[106,90,205],slategray:[112,128,144],slategrey:[112,128,144],snow:[255,250,250],springgreen:[0,255,127],steelblue:[70,130,180],tan:[210,180,140],teal:[0,128,128],thistle:[216,191,216],tomato:[255,99,71],turquoise:[64,224,208],violet:[238,130,238],wheat:[245,222,179],white:[255,255,255],whitesmoke:[245,245,245],yellow:[255,255,0],yellowgreen:[154,205,50]}},854:(e,t,r)=>{var n=r(156),o=r(872),a=Object.hasOwnProperty,i={};for(var s in n)a.call(n,s)&&(i[n[s]]=s);var l=e.exports={to:{},get:{}};function c(e,t,r){return Math.min(Math.max(t,e),r)}function u(e){var t=Math.round(e).toString(16).toUpperCase();return t.length<2?"0"+t:t}l.get=function(e){var t,r;switch(e.substring(0,3).toLowerCase()){case"hsl":t=l.get.hsl(e),r="hsl";break;case"hwb":t=l.get.hwb(e),r="hwb";break;default:t=l.get.rgb(e),r="rgb"}return t?{model:r,value:t}:null},l.get.rgb=function(e){if(!e)return null;var t,r,o,i=[0,0,0,1];if(t=e.match(/^#([a-f0-9]{6})([a-f0-9]{2})?$/i)){for(o=t[2],t=t[1],r=0;r<3;r++){var s=2*r;i[r]=parseInt(t.slice(s,s+2),16)}o&&(i[3]=parseInt(o,16)/255)}else if(t=e.match(/^#([a-f0-9]{3,4})$/i)){for(o=(t=t[1])[3],r=0;r<3;r++)i[r]=parseInt(t[r]+t[r],16);o&&(i[3]=parseInt(o+o,16)/255)}else if(t=e.match(/^rgba?\(\s*([+-]?\d+)(?=[\s,])\s*(?:,\s*)?([+-]?\d+)(?=[\s,])\s*(?:,\s*)?([+-]?\d+)\s*(?:[,|\/]\s*([+-]?[\d\.]+)(%?)\s*)?\)$/)){for(r=0;r<3;r++)i[r]=parseInt(t[r+1],0);t[4]&&(t[5]?i[3]=.01*parseFloat(t[4]):i[3]=parseFloat(t[4]))}else{if(!(t=e.match(/^rgba?\(\s*([+-]?[\d\.]+)\%\s*,?\s*([+-]?[\d\.]+)\%\s*,?\s*([+-]?[\d\.]+)\%\s*(?:[,|\/]\s*([+-]?[\d\.]+)(%?)\s*)?\)$/)))return(t=e.match(/^(\w+)$/))?"transparent"===t[1]?[0,0,0,0]:a.call(n,t[1])?((i=n[t[1]])[3]=1,i):null:null;for(r=0;r<3;r++)i[r]=Math.round(2.55*parseFloat(t[r+1]));t[4]&&(t[5]?i[3]=.01*parseFloat(t[4]):i[3]=parseFloat(t[4]))}for(r=0;r<3;r++)i[r]=c(i[r],0,255);return i[3]=c(i[3],0,1),i},l.get.hsl=function(e){if(!e)return null;var t=e.match(/^hsla?\(\s*([+-]?(?:\d{0,3}\.)?\d+)(?:deg)?\s*,?\s*([+-]?[\d\.]+)%\s*,?\s*([+-]?[\d\.]+)%\s*(?:[,|\/]\s*([+-]?(?=\.\d|\d)(?:0|[1-9]\d*)?(?:\.\d*)?(?:[eE][+-]?\d+)?)\s*)?\)$/);if(t){var r=parseFloat(t[4]);return[(parseFloat(t[1])%360+360)%360,c(parseFloat(t[2]),0,100),c(parseFloat(t[3]),0,100),c(isNaN(r)?1:r,0,1)]}return null},l.get.hwb=function(e){if(!e)return null;var t=e.match(/^hwb\(\s*([+-]?\d{0,3}(?:\.\d+)?)(?:deg)?\s*,\s*([+-]?[\d\.]+)%\s*,\s*([+-]?[\d\.]+)%\s*(?:,\s*([+-]?(?=\.\d|\d)(?:0|[1-9]\d*)?(?:\.\d*)?(?:[eE][+-]?\d+)?)\s*)?\)$/);if(t){var r=parseFloat(t[4]);return[(parseFloat(t[1])%360+360)%360,c(parseFloat(t[2]),0,100),c(parseFloat(t[3]),0,100),c(isNaN(r)?1:r,0,1)]}return null},l.to.hex=function(){var e=o(arguments);return"#"+u(e[0])+u(e[1])+u(e[2])+(e[3]<1?u(Math.round(255*e[3])):"")},l.to.rgb=function(){var e=o(arguments);return e.length<4||1===e[3]?"rgb("+Math.round(e[0])+", "+Math.round(e[1])+", "+Math.round(e[2])+")":"rgba("+Math.round(e[0])+", "+Math.round(e[1])+", "+Math.round(e[2])+", "+e[3]+")"},l.to.rgb.percent=function(){var e=o(arguments),t=Math.round(e[0]/255*100),r=Math.round(e[1]/255*100),n=Math.round(e[2]/255*100);return e.length<4||1===e[3]?"rgb("+t+"%, "+r+"%, "+n+"%)":"rgba("+t+"%, "+r+"%, "+n+"%, "+e[3]+")"},l.to.hsl=function(){var e=o(arguments);return e.length<4||1===e[3]?"hsl("+e[0]+", "+e[1]+"%, "+e[2]+"%)":"hsla("+e[0]+", "+e[1]+"%, "+e[2]+"%, "+e[3]+")"},l.to.hwb=function(){var e=o(arguments),t="";return e.length>=4&&1!==e[3]&&(t=", "+e[3]),"hwb("+e[0]+", "+e[1]+"%, "+e[2]+"%"+t+")"},l.to.keyword=function(e){return i[e.slice(0,3)]}},520:(e,t,r)=>{const n=r(854),o=r(734),a=[].slice,i=["keyword","gray","hex"],s={};for(const e of Object.keys(o))s[a.call(o[e].labels).sort().join("")]=e;const l={};function c(e,t){if(!(this instanceof c))return new c(e,t);if(t&&t in i&&(t=null),t&&!(t in o))throw new Error("Unknown model: "+t);let r,u;if(null==e)this.model="rgb",this.color=[0,0,0],this.valpha=1;else if(e instanceof c)this.model=e.model,this.color=e.color.slice(),this.valpha=e.valpha;else if("string"==typeof e){const t=n.get(e);if(null===t)throw new Error("Unable to parse color from string: "+e);this.model=t.model,u=o[this.model].channels,this.color=t.value.slice(0,u),this.valpha="number"==typeof t.value[u]?t.value[u]:1}else if(e.length>0){this.model=t||"rgb",u=o[this.model].channels;const r=a.call(e,0,u);this.color=p(r,u),this.valpha="number"==typeof e[u]?e[u]:1}else if("number"==typeof e)this.model="rgb",this.color=[e>>16&255,e>>8&255,255&e],this.valpha=1;else{this.valpha=1;const t=Object.keys(e);"alpha"in e&&(t.splice(t.indexOf("alpha"),1),this.valpha="number"==typeof e.alpha?e.alpha:0);const n=t.sort().join("");if(!(n in s))throw new Error("Unable to parse color from object: "+JSON.stringify(e));this.model=s[n];const a=o[this.model].labels,i=[];for(r=0;r0?new c(this.color.concat(Math.max(0,Math.min(1,e))),this.model):this.valpha},red:u("rgb",0,h(255)),green:u("rgb",1,h(255)),blue:u("rgb",2,h(255)),hue:u(["hsl","hsv","hsl","hwb","hcg"],0,(e=>(e%360+360)%360)),saturationl:u("hsl",1,h(100)),lightness:u("hsl",2,h(100)),saturationv:u("hsv",1,h(100)),value:u("hsv",2,h(100)),chroma:u("hcg",1,h(100)),gray:u("hcg",2,h(100)),white:u("hwb",1,h(100)),wblack:u("hwb",2,h(100)),cyan:u("cmyk",0,h(100)),magenta:u("cmyk",1,h(100)),yellow:u("cmyk",2,h(100)),black:u("cmyk",3,h(100)),x:u("xyz",0,h(100)),y:u("xyz",1,h(100)),z:u("xyz",2,h(100)),l:u("lab",0,h(100)),a:u("lab",1),b:u("lab",2),keyword(e){return arguments.length>0?new c(e):o[this.model].keyword(this.color)},hex(e){return arguments.length>0?new c(e):n.to.hex(this.rgb().round().color)},rgbNumber(){const e=this.rgb().color;return(255&e[0])<<16|(255&e[1])<<8|255&e[2]},luminosity(){const e=this.rgb().color,t=[];for(const[r,n]of e.entries()){const e=n/255;t[r]=e<=.03928?e/12.92:((e+.055)/1.055)**2.4}return.2126*t[0]+.7152*t[1]+.0722*t[2]},contrast(e){const t=this.luminosity(),r=e.luminosity();return t>r?(t+.05)/(r+.05):(r+.05)/(t+.05)},level(e){const t=this.contrast(e);return t>=7.1?"AAA":t>=4.5?"AA":""},isDark(){const e=this.rgb().color;return(299*e[0]+587*e[1]+114*e[2])/1e3<128},isLight(){return!this.isDark()},negate(){const e=this.rgb();for(let t=0;t<3;t++)e.color[t]=255-e.color[t];return e},lighten(e){const t=this.hsl();return t.color[2]+=t.color[2]*e,t},darken(e){const t=this.hsl();return t.color[2]-=t.color[2]*e,t},saturate(e){const t=this.hsl();return t.color[1]+=t.color[1]*e,t},desaturate(e){const t=this.hsl();return t.color[1]-=t.color[1]*e,t},whiten(e){const t=this.hwb();return t.color[1]+=t.color[1]*e,t},blacken(e){const t=this.hwb();return t.color[2]+=t.color[2]*e,t},grayscale(){const e=this.rgb().color,t=.3*e[0]+.59*e[1]+.11*e[2];return c.rgb(t,t,t)},fade(e){return this.alpha(this.valpha-this.valpha*e)},opaquer(e){return this.alpha(this.valpha+this.valpha*e)},rotate(e){const t=this.hsl();let r=t.color[0];return r=(r+e)%360,r=r<0?360+r:r,t.color[0]=r,t},mix(e,t){if(!e||!e.rgb)throw new Error('Argument to "mix" was not a Color instance, but rather an instance of '+typeof e);const r=e.rgb(),n=this.rgb(),o=void 0===t?.5:t,a=2*o-1,i=r.alpha()-n.alpha(),s=((a*i==-1?a:(a+i)/(1+a*i))+1)/2,l=1-s;return c.rgb(s*r.red()+l*n.red(),s*r.green()+l*n.green(),s*r.blue()+l*n.blue(),r.alpha()*o+n.alpha()*(1-o))}};for(const e of Object.keys(o)){if(i.includes(e))continue;const t=o[e].channels;c.prototype[e]=function(){if(this.model===e)return new c(this);if(arguments.length>0)return new c(arguments,e);const r="number"==typeof arguments[t]?t:this.valpha;return new c((n=o[this.model][e].raw(this.color),Array.isArray(n)?n:[n]).concat(r),e);var n},c[e]=function(r){return"number"==typeof r&&(r=p(a.call(arguments),t)),new c(r,e)}}function u(e,t,r){e=Array.isArray(e)?e:[e];for(const n of e)(l[n]||(l[n]=[]))[t]=r;return e=e[0],function(n){let o;return arguments.length>0?(r&&(n=r(n)),o=this[e](),o.color[t]=n,o):(o=this[e]().color[t],r&&(o=r(o)),o)}}function h(e){return function(t){return Math.max(0,Math.min(e,t))}}function p(e,t){for(let r=0;r{e.exports=function(e){return!(!e||"string"==typeof e)&&(e instanceof Array||Array.isArray(e)||e.length>=0&&(e.splice instanceof Function||Object.getOwnPropertyDescriptor(e,e.length-1)&&"String"!==e.constructor.name))}},452:e=>{var t=function(e){var t,r=Object.prototype,n=r.hasOwnProperty,o="function"==typeof Symbol?Symbol:{},a=o.iterator||"@@iterator",i=o.asyncIterator||"@@asyncIterator",s=o.toStringTag||"@@toStringTag";function l(e,t,r){return Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}),e[t]}try{l({},"")}catch(e){l=function(e,t,r){return e[t]=r}}function c(e,t,r,n){var o=t&&t.prototype instanceof v?t:v,a=Object.create(o.prototype),i=new P(n||[]);return a._invoke=function(e,t,r){var n=h;return function(o,a){if(n===f)throw new Error("Generator is already running");if(n===d){if("throw"===o)throw a;return C()}for(r.method=o,r.arg=a;;){var i=r.delegate;if(i){var s=j(i,r);if(s){if(s===y)continue;return s}}if("next"===r.method)r.sent=r._sent=r.arg;else if("throw"===r.method){if(n===h)throw n=d,r.arg;r.dispatchException(r.arg)}else"return"===r.method&&r.abrupt("return",r.arg);n=f;var l=u(e,t,r);if("normal"===l.type){if(n=r.done?d:p,l.arg===y)continue;return{value:l.arg,done:r.done}}"throw"===l.type&&(n=d,r.method="throw",r.arg=l.arg)}}}(e,r,i),a}function u(e,t,r){try{return{type:"normal",arg:e.call(t,r)}}catch(e){return{type:"throw",arg:e}}}e.wrap=c;var h="suspendedStart",p="suspendedYield",f="executing",d="completed",y={};function v(){}function g(){}function m(){}var b={};l(b,a,(function(){return this}));var w=Object.getPrototypeOf,O=w&&w(w(_([])));O&&O!==r&&n.call(O,a)&&(b=O);var k=m.prototype=v.prototype=Object.create(b);function L(e){["next","throw","return"].forEach((function(t){l(e,t,(function(e){return this._invoke(t,e)}))}))}function x(e,t){function r(o,a,i,s){var l=u(e[o],e,a);if("throw"!==l.type){var c=l.arg,h=c.value;return h&&"object"==typeof h&&n.call(h,"__await")?t.resolve(h.__await).then((function(e){r("next",e,i,s)}),(function(e){r("throw",e,i,s)})):t.resolve(h).then((function(e){c.value=e,i(c)}),(function(e){return r("throw",e,i,s)}))}s(l.arg)}var o;this._invoke=function(e,n){function a(){return new t((function(t,o){r(e,n,t,o)}))}return o=o?o.then(a,a):a()}}function j(e,r){var n=e.iterator[r.method];if(n===t){if(r.delegate=null,"throw"===r.method){if(e.iterator.return&&(r.method="return",r.arg=t,j(e,r),"throw"===r.method))return y;r.method="throw",r.arg=new TypeError("The iterator does not provide a 'throw' method")}return y}var o=u(n,e.iterator,r.arg);if("throw"===o.type)return r.method="throw",r.arg=o.arg,r.delegate=null,y;var a=o.arg;return a?a.done?(r[e.resultName]=a.value,r.next=e.nextLoc,"return"!==r.method&&(r.method="next",r.arg=t),r.delegate=null,y):a:(r.method="throw",r.arg=new TypeError("iterator result is not an object"),r.delegate=null,y)}function M(e){var t={tryLoc:e[0]};1 in e&&(t.catchLoc=e[1]),2 in e&&(t.finallyLoc=e[2],t.afterLoc=e[3]),this.tryEntries.push(t)}function S(e){var t=e.completion||{};t.type="normal",delete t.arg,e.completion=t}function P(e){this.tryEntries=[{tryLoc:"root"}],e.forEach(M,this),this.reset(!0)}function _(e){if(e){var r=e[a];if(r)return r.call(e);if("function"==typeof e.next)return e;if(!isNaN(e.length)){var o=-1,i=function r(){for(;++o=0;--a){var i=this.tryEntries[a],s=i.completion;if("root"===i.tryLoc)return o("end");if(i.tryLoc<=this.prev){var l=n.call(i,"catchLoc"),c=n.call(i,"finallyLoc");if(l&&c){if(this.prev=0;--r){var o=this.tryEntries[r];if(o.tryLoc<=this.prev&&n.call(o,"finallyLoc")&&this.prev=0;--t){var r=this.tryEntries[t];if(r.finallyLoc===e)return this.complete(r.completion,r.afterLoc),S(r),y}},catch:function(e){for(var t=this.tryEntries.length-1;t>=0;--t){var r=this.tryEntries[t];if(r.tryLoc===e){var n=r.completion;if("throw"===n.type){var o=n.arg;S(r)}return o}}throw new Error("illegal catch attempt")},delegateYield:function(e,r,n){return this.delegate={iterator:_(e),resultName:r,nextLoc:n},"next"===this.method&&(this.arg=t),y}},e}(e.exports);try{regeneratorRuntime=t}catch(e){"object"==typeof globalThis?globalThis.regeneratorRuntime=t:Function("r","regeneratorRuntime = r")(t)}},872:(e,t,r)=>{var n=r(195),o=Array.prototype.concat,a=Array.prototype.slice,i=e.exports=function(e){for(var t=[],r=0,i=e.length;r{for(var n in t)r.o(t,n)&&!r.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},r.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t);var n={};function o(e,t,r){return t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}function a(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function i(e,t){for(var r=0;re.length)&&(t=e.length);for(var r=0,n=new Array(t);rF});var v=r(207);const g={maxLeaves:255,minZoomLevel:0,zoomIncrement:2,closeOnLeafClick:!0,circleSpiralSwitchover:10,circleOptions:{leavesSeparation:50,leavesOffset:[0,0]},spiralOptions:{legLengthStart:25,legLengthFactor:2.2,leavesSeparation:30,leavesOffset:[0,0]},spiderLegsAreHidden:!1,spiderLegsWidth:1,spiderLegsColor:"rgba(100, 100, 100, .7)",spiderLeavesLayout:null,spiderLeavesPaint:null};var m=r(520);function b(e,t){return w.apply(this,arguments)}function w(){return(w=y(v.mark((function e(t,r){var n,o,a;return v.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return n=t.map(r),e.next=3,Promise.all(n);case 3:return o=e.sent,a=o.findIndex((function(e){return e})),e.abrupt("return",t[a]);case 6:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function O(e,t,r){return k.apply(this,arguments)}function k(){return(k=y(v.mark((function e(t,r,n){return v.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",new Promise((function(e,o){t.getClusterLeaves.toString().includes("sendAsync")?t.getClusterLeaves(r,n,0).then((function(t){e(t)})):t.getClusterLeaves(r,n,0,(function(t,r){t?o(t):e(r)}))})));case 1:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function L(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),r.push.apply(r,n)}return r}function x(e){for(var t=1;tid ?? 0));
$dataElementId = sprintf('%s-data', $containerId);
+ $filterWrapperId = sprintf('%s-filter', $containerId);
+ $filterGroupId = sprintf('%s-filter-group', $containerId);
+ $showOrganizations = '1' === (string) ($model->mapShowOrganizations ?? '');
+ $showExternalOrganizations = '1' === (string) ($model->mapShowExternalOrganizations ?? '');
+ $showEvents = '1' === (string) ($model->mapShowEvents ?? '');
+ $centerMode = (string) ($model->mapCenterMode ?? self::DEFAULT_CENTER_MODE);
+ $eventColor = $this->normalizeHexColor((string) ($model->mapEventColor ?? self::DEFAULT_EVENT_COLOR));
+ $organizationColorScheme = trim((string) ($model->mapOrganizationColorScheme ?? ''));
+
+ if (!in_array($centerMode, ['markers', 'custom'], true)) {
+ $centerMode = self::DEFAULT_CENTER_MODE;
+ }
$template->set('mapContainerId', $containerId);
$template->set('mapDataElementId', $dataElementId);
+ $template->set('mapFilterWrapperId', $filterWrapperId);
+ $template->set('mapFilterGroupId', $filterGroupId);
$template->set('mapStyleUrl', self::MAP_STYLE_URL);
+ $template->set('mapCenterMode', $centerMode);
+ $template->set('mapEventColor', $eventColor);
+ $template->set('mapOrganizationColorScheme', $organizationColorScheme);
+ $template->set('mapCenterLat', trim((string) ($model->mapCenterLat ?? '')));
+ $template->set('mapCenterLng', trim((string) ($model->mapCenterLng ?? '')));
+ $template->set('mapCenterZoom', (int) ($model->mapCenterZoom ?? 12));
$template->set('mapItemsJson', json_encode(
- $this->mapModuleDataProvider->getMapItems(),
+ $this->mapModuleDataProvider->getMapItems($showOrganizations, $showEvents, $showExternalOrganizations),
\JSON_HEX_TAG | \JSON_HEX_AMP | \JSON_HEX_APOS | \JSON_HEX_QUOT | \JSON_UNESCAPED_UNICODE | \JSON_THROW_ON_ERROR,
));
+ $template->set('mapOrganizationTags', $this->mapModuleDataProvider->getOrganizationTags());
return $template->getResponse();
}
+
+ private function normalizeHexColor(string $value): string
+ {
+ $normalized = strtoupper(trim($value));
+
+ if (preg_match('/^#[0-9A-F]{6}$/', $normalized)) {
+ return $normalized;
+ }
+
+ if (preg_match('/^[0-9A-F]{6}$/', $normalized)) {
+ return '#'.$normalized;
+ }
+
+ if (preg_match('/^#[0-9A-F]{3}$/', $normalized)) {
+ return sprintf(
+ '#%1$s%1$s%2$s%2$s%3$s%3$s',
+ $normalized[1],
+ $normalized[2],
+ $normalized[3],
+ );
+ }
+
+ if (preg_match('/^[0-9A-F]{3}$/', $normalized)) {
+ return sprintf(
+ '#%1$s%1$s%2$s%2$s%3$s%3$s',
+ $normalized[0],
+ $normalized[1],
+ $normalized[2],
+ );
+ }
+
+ return self::DEFAULT_EVENT_COLOR;
+ }
}
diff --git a/src/Service/MapModuleDataProvider.php b/src/Service/MapModuleDataProvider.php
index 9896fac..a764dbb 100644
--- a/src/Service/MapModuleDataProvider.php
+++ b/src/Service/MapModuleDataProvider.php
@@ -6,7 +6,9 @@ namespace MummertMedia\EventManagerBundle\Service;
use Contao\Config;
use Contao\Date;
+use Doctrine\DBAL\ArrayParameterType;
use Doctrine\DBAL\Connection;
+use Doctrine\DBAL\ParameterType;
use Doctrine\DBAL\Query\QueryBuilder;
class MapModuleDataProvider
@@ -22,113 +24,295 @@ class MapModuleDataProvider
}
/**
- * @return list}>
+ * @return list}>
*/
- public function getMapItems(): array
+ public function getMapItems(bool $includeOrganizations = true, bool $includeEvents = true, bool $includeExternalOrganizations = false): array
{
+ if (!$includeOrganizations && !$includeEvents) {
+ return [];
+ }
+
$locationTable = $this->resolveExistingTable(['tl_location']);
$organizationTable = $this->resolveExistingTable(['tl_organization', 'tl_organisation']);
+ $locationGeoColumns = null !== $locationTable ? $this->resolveGeoColumns($locationTable) : null;
- if (null === $locationTable) {
+ if (null === $locationTable || null === $locationGeoColumns) {
return [];
}
$items = [];
$seen = [];
+ $organizationRows = $includeOrganizations
+ ? $this->fetchOrganizationRows($organizationTable, $locationTable, $locationGeoColumns, $includeExternalOrganizations)
+ : [];
+ $organizationTagMap = $this->fetchOrganizationTagMap(array_values(array_unique(array_map(
+ static fn (array $row): int => (int) ($row['id'] ?? 0),
+ $organizationRows,
+ ))));
- foreach ($this->fetchOrganizationRows($organizationTable) as $row) {
- $id = (int) ($row['id'] ?? 0);
+ if ($includeOrganizations) {
+ foreach ($organizationRows as $row) {
+ $id = (int) ($row['id'] ?? 0);
- if ($id <= 0 || isset($seen['organisation'][$id])) {
- continue;
+ if ($id <= 0 || isset($seen['organisation'][$id])) {
+ continue;
+ }
+
+ $locationId = (int) ($row['location_id'] ?? 0);
+
+ if ($locationId > 0) {
+ $coords = $this->extractCoordinates($row['location_latitude'] ?? null, $row['location_longitude'] ?? null);
+
+ if (null === $coords) {
+ continue;
+ }
+ } else {
+ $coords = $this->extractCoordinates($row['latitude'] ?? null, $row['longitude'] ?? null);
+ }
+
+ if (null === $coords) {
+ continue;
+ }
+
+ $seen['organisation'][$id] = true;
+ $tagData = $organizationTagMap[$id] ?? ['ids' => [], 'labels' => []];
+ $items[] = [
+ 'type' => 'organisation',
+ 'markerType' => $this->buildOrganizationMarkerType($tagData['ids']),
+ 'id' => $id,
+ 'title' => trim((string) ($row['title'] ?? '')),
+ 'latitude' => $coords['latitude'],
+ 'longitude' => $coords['longitude'],
+ 'extra' => [
+ 'organizationTagIds' => $tagData['ids'],
+ 'organizationTagLabels' => $tagData['labels'],
+ ],
+ ];
}
-
- $coords = $this->extractCoordinates($row['latitude'] ?? null, $row['longitude'] ?? null);
-
- if (null === $coords) {
- continue;
- }
-
- $seen['organisation'][$id] = true;
- $items[] = [
- 'type' => 'organisation',
- 'id' => $id,
- 'title' => trim((string) ($row['title'] ?? '')),
- 'latitude' => $coords['latitude'],
- 'longitude' => $coords['longitude'],
- 'extra' => [],
- ];
}
- foreach ($this->fetchLocationRows($locationTable) as $row) {
- $id = (int) ($row['id'] ?? 0);
+ if ($includeEvents) {
+ foreach ($this->fetchLocationRows($locationTable, $locationGeoColumns) as $row) {
+ $id = (int) ($row['id'] ?? 0);
- if ($id <= 0 || isset($seen['location'][$id])) {
- continue;
+ if ($id <= 0 || isset($seen['location'][$id])) {
+ continue;
+ }
+
+ $coords = $this->extractCoordinates($row['latitude'] ?? null, $row['longitude'] ?? null);
+
+ if (null === $coords) {
+ continue;
+ }
+
+ $seen['location'][$id] = true;
+ $items[] = [
+ 'type' => 'location',
+ 'markerType' => 'location',
+ 'id' => $id,
+ 'title' => trim((string) ($row['title'] ?? '')),
+ 'latitude' => $coords['latitude'],
+ 'longitude' => $coords['longitude'],
+ 'extra' => [],
+ ];
}
- $coords = $this->extractCoordinates($row['latitude'] ?? null, $row['longitude'] ?? null);
+ foreach ($this->fetchEventRows($locationTable, $locationGeoColumns) as $row) {
+ $id = (int) ($row['event_id'] ?? 0);
- if (null === $coords) {
- continue;
+ if ($id <= 0 || isset($seen['event'][$id])) {
+ continue;
+ }
+
+ $coords = $this->extractCoordinates($row['latitude'] ?? null, $row['longitude'] ?? null);
+
+ if (null === $coords) {
+ continue;
+ }
+
+ $seen['event'][$id] = true;
+ $items[] = [
+ 'type' => 'event',
+ 'markerType' => 'event',
+ 'id' => $id,
+ 'title' => trim((string) ($row['event_title'] ?? '')),
+ 'latitude' => $coords['latitude'],
+ 'longitude' => $coords['longitude'],
+ 'extra' => [
+ 'locationTitle' => trim((string) ($row['location_title'] ?? '')),
+ 'startDate' => $this->formatStartDate((int) ($row['startDate'] ?? 0)),
+ ],
+ ];
}
-
- $seen['location'][$id] = true;
- $items[] = [
- 'type' => 'location',
- 'id' => $id,
- 'title' => trim((string) ($row['title'] ?? '')),
- 'latitude' => $coords['latitude'],
- 'longitude' => $coords['longitude'],
- 'extra' => [],
- ];
- }
-
- foreach ($this->fetchEventRows($locationTable) as $row) {
- $id = (int) ($row['event_id'] ?? 0);
-
- if ($id <= 0 || isset($seen['event'][$id])) {
- continue;
- }
-
- $coords = $this->extractCoordinates($row['latitude'] ?? null, $row['longitude'] ?? null);
-
- if (null === $coords) {
- continue;
- }
-
- $seen['event'][$id] = true;
- $items[] = [
- 'type' => 'event',
- 'id' => $id,
- 'title' => trim((string) ($row['event_title'] ?? '')),
- 'latitude' => $coords['latitude'],
- 'longitude' => $coords['longitude'],
- 'extra' => [
- 'locationTitle' => trim((string) ($row['location_title'] ?? '')),
- 'startDate' => $this->formatStartDate((int) ($row['startDate'] ?? 0)),
- ],
- ];
}
return $items;
}
/**
- * @return list>
+ * @return list
*/
- private function fetchOrganizationRows(?string $table): array
+ public function getOrganizationTags(): array
{
- if (null === $table) {
+ if (!$this->tableExists('tl_tags')) {
+ return [];
+ }
+
+ $columns = $this->getColumnMap('tl_tags');
+ $labelColumn = isset($columns['title']) ? 'title' : (isset($columns['tag']) ? 'tag' : null);
+
+ if (null === $labelColumn) {
return [];
}
$qb = $this->connection->createQueryBuilder();
$qb
- ->select('o.id', 'o.title', 'o.latitude', 'o.longitude')
+ ->select('t.id', sprintf('t.%s AS label', $labelColumn))
+ ->from('tl_tags', 't')
+ ->orderBy(sprintf('t.%s', $labelColumn), 'ASC');
+
+ if (isset($columns['invisible'])) {
+ $qb->andWhere("(t.invisible IS NULL OR t.invisible = '' OR t.invisible = '0')");
+ }
+
+ $rows = $qb->executeQuery()->fetchAllAssociative();
+ $tags = [];
+ $seen = [];
+
+ foreach ($rows as $row) {
+ $id = (int) ($row['id'] ?? 0);
+ $label = trim((string) ($row['label'] ?? ''));
+
+ if ($id <= 0 || '' === $label || isset($seen[$id])) {
+ continue;
+ }
+
+ $seen[$id] = true;
+ $tags[] = [
+ 'id' => $id,
+ 'label' => $label,
+ ];
+ }
+
+ return $tags;
+ }
+
+ /**
+ * @param list $organizationIds
+ * @return array, labels: list}>
+ */
+ private function fetchOrganizationTagMap(array $organizationIds): array
+ {
+ $organizationIds = array_values(array_unique(array_filter($organizationIds, static fn (int $id): bool => $id > 0)));
+
+ if ([] === $organizationIds || !$this->tableExists('tl_tags_rel') || !$this->tableExists('tl_tags')) {
+ return [];
+ }
+
+ $rows = $this->connection->executeQuery(
+ 'SELECT r.pid AS organization_id, r.tag_id, t.tag AS label
+ FROM tl_tags_rel r
+ INNER JOIN tl_tags t ON t.id = r.tag_id
+ WHERE r.ptable = ? AND r.field = ? AND r.pid IN (?)
+ ORDER BY r.pid ASC, r.tag_id ASC',
+ ['tl_organization', 'tags', $organizationIds],
+ [ParameterType::STRING, ParameterType::STRING, ArrayParameterType::INTEGER],
+ )->fetchAllAssociative();
+
+ if ([] === $rows) {
+ $rows = $this->connection->executeQuery(
+ 'SELECT r.pid AS organization_id, r.tag_id, t.tag AS label
+ FROM tl_tags_rel r
+ INNER JOIN tl_tags t ON t.id = r.tag_id
+ WHERE r.ptable = ? AND r.pid IN (?)
+ ORDER BY r.pid ASC, r.tag_id ASC',
+ ['tl_organization', $organizationIds],
+ [ParameterType::STRING, ArrayParameterType::INTEGER],
+ )->fetchAllAssociative();
+ }
+
+ $map = [];
+ $seen = [];
+
+ foreach ($rows as $row) {
+ $organizationId = (int) ($row['organization_id'] ?? 0);
+ $tagId = (int) ($row['tag_id'] ?? 0);
+ $label = trim((string) ($row['label'] ?? ''));
+
+ if ($organizationId <= 0 || $tagId <= 0 || '' === $label || isset($seen[$organizationId][$tagId])) {
+ continue;
+ }
+
+ $seen[$organizationId][$tagId] = true;
+ $map[$organizationId]['ids'][] = (string) $tagId;
+ $map[$organizationId]['labels'][] = $label;
+ }
+
+ foreach ($map as $organizationId => $tagData) {
+ $map[$organizationId]['ids'] = array_values(array_unique($tagData['ids'] ?? []));
+ $map[$organizationId]['labels'] = array_values(array_unique($tagData['labels'] ?? []));
+ }
+
+ return $map;
+ }
+
+ /**
+ * @param list $tagIds
+ */
+ private function buildOrganizationMarkerType(array $tagIds): string
+ {
+ $firstTagId = (string) ($tagIds[0] ?? '');
+
+ if ('' !== $firstTagId && ctype_digit($firstTagId) && (int) $firstTagId > 0) {
+ return sprintf('organisation-tag-%d', (int) $firstTagId);
+ }
+
+ return 'organisation';
+ }
+
+ /**
+ * @return list>
+ */
+ private function fetchOrganizationRows(?string $table, string $locationTable, array $locationGeoColumns, bool $includeExternalOrganizations): array
+ {
+ if (null === $table) {
+ return [];
+ }
+
+ $organizationGeoColumns = $this->resolveGeoColumns($table);
+ $organizationColumns = $this->getColumnMap($table);
+ $hasLocationReference = isset($organizationColumns['location_id']);
+
+ if (null === $organizationGeoColumns && !$hasLocationReference) {
+ return [];
+ }
+
+ $qb = $this->connection->createQueryBuilder();
+ $qb
+ ->select('o.id', 'o.title')
->from($table, 'o')
->orderBy('o.id', 'ASC');
+ if (null !== $organizationGeoColumns) {
+ $qb
+ ->addSelect(sprintf('o.%s AS latitude', $organizationGeoColumns['latitude']))
+ ->addSelect(sprintf('o.%s AS longitude', $organizationGeoColumns['longitude']));
+ }
+
+ if ($hasLocationReference) {
+ $qb
+ ->addSelect('o.location_id')
+ ->leftJoin('o', $locationTable, 'ol', 'ol.id = o.location_id')
+ ->addSelect(sprintf('ol.%s AS location_latitude', $locationGeoColumns['latitude']))
+ ->addSelect(sprintf('ol.%s AS location_longitude', $locationGeoColumns['longitude']));
+
+ $this->applyPublicationConstraints($qb, 'ol', $locationTable);
+ }
+
+ if (!$includeExternalOrganizations && isset($organizationColumns['isexternal'])) {
+ $qb->andWhere("(o.isExternal IS NULL OR o.isExternal = '' OR o.isExternal = '0')");
+ }
+
$this->applyPublicationConstraints($qb, 'o', $table);
return $qb->executeQuery()->fetchAllAssociative();
@@ -137,11 +321,13 @@ class MapModuleDataProvider
/**
* @return list>
*/
- private function fetchLocationRows(string $locationTable): array
+ private function fetchLocationRows(string $locationTable, array $locationGeoColumns): array
{
$qb = $this->connection->createQueryBuilder();
$qb
- ->select('l.id', 'l.title', 'l.latitude', 'l.longitude')
+ ->select('l.id', 'l.title')
+ ->addSelect(sprintf('l.%s AS latitude', $locationGeoColumns['latitude']))
+ ->addSelect(sprintf('l.%s AS longitude', $locationGeoColumns['longitude']))
->from($locationTable, 'l')
->orderBy('l.id', 'ASC');
@@ -153,12 +339,15 @@ class MapModuleDataProvider
/**
* @return list>
*/
- private function fetchEventRows(string $locationTable): array
+ private function fetchEventRows(string $locationTable, array $locationGeoColumns): array
{
if (!$this->tableExists('tl_calendar_events')) {
return [];
}
+ $eventColumns = $this->getColumnMap('tl_calendar_events');
+ $today = strtotime('today');
+
$qb = $this->connection->createQueryBuilder();
$qb
->select(
@@ -166,14 +355,20 @@ class MapModuleDataProvider
'e.title AS event_title',
'e.startDate',
'l.title AS location_title',
- 'l.latitude',
- 'l.longitude'
+ sprintf('l.%s AS latitude', $locationGeoColumns['latitude']),
+ sprintf('l.%s AS longitude', $locationGeoColumns['longitude'])
)
->from('tl_calendar_events', 'e')
->innerJoin('e', $locationTable, 'l', 'l.id = e.location_id')
->andWhere('e.location_id > 0')
->orderBy('e.id', 'ASC');
+ if (isset($eventColumns['startdate'])) {
+ $qb
+ ->andWhere('e.startDate >= :event_start_date_from')
+ ->setParameter('event_start_date_from', $today);
+ }
+
$this->applyPublicationConstraints($qb, 'e', 'tl_calendar_events');
$this->applyPublicationConstraints($qb, 'l', $locationTable);
@@ -220,9 +415,20 @@ class MapModuleDataProvider
return null;
}
+ $latitudeFloat = (float) $lat;
+ $longitudeFloat = (float) $lng;
+
+ if (
+ ($latitudeFloat < -90.0 || $latitudeFloat > 90.0)
+ || ($longitudeFloat < -180.0 || $longitudeFloat > 180.0)
+ || (0.0 === $latitudeFloat && 0.0 === $longitudeFloat)
+ ) {
+ return null;
+ }
+
return [
- 'latitude' => (float) $lat,
- 'longitude' => (float) $lng,
+ 'latitude' => $latitudeFloat,
+ 'longitude' => $longitudeFloat,
];
}
@@ -251,6 +457,30 @@ class MapModuleDataProvider
return null;
}
+ /**
+ * @return array{latitude:string,longitude:string}|null
+ */
+ private function resolveGeoColumns(string $table): ?array
+ {
+ $columns = $this->getColumnMap($table);
+
+ if (isset($columns['latitude'], $columns['longitude'])) {
+ return [
+ 'latitude' => 'latitude',
+ 'longitude' => 'longitude',
+ ];
+ }
+
+ if (isset($columns['lat'], $columns['lng'])) {
+ return [
+ 'latitude' => 'lat',
+ 'longitude' => 'lng',
+ ];
+ }
+
+ return null;
+ }
+
private function tableExists(string $table): bool
{
try {