Map filters: conditional visibility + initial display handling

This commit is contained in:
Jürgen Mummert
2026-02-26 20:31:45 +01:00
parent fb58c50f18
commit dedf8868b5
6 changed files with 265 additions and 98 deletions
+167 -72
View File
@@ -345,6 +345,65 @@ const normalizeTagIds = (value) => {
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
? '<g transform="translate(8.0, 8.0)"><circle fill="#000000" opacity="0.25" cx="5.5" cy="5.5" r="5.4999962"></circle><circle fill="#FFFFFF" cx="5.5" cy="5.5" r="5.4999962"></circle></g>'
: '';
const svg = `<svg xmlns="http://www.w3.org/2000/svg" display="block" width="27" height="41" viewBox="0 0 27 41"><g fill-rule="nonzero"><g transform="translate(3.0, 29.0)" fill="#000000"><ellipse opacity="0.04" cx="10.5" cy="5.80029008" rx="10.5" ry="5.25002273"></ellipse><ellipse opacity="0.04" cx="10.5" cy="5.80029008" rx="10.5" ry="5.25002273"></ellipse><ellipse opacity="0.04" cx="10.5" cy="5.80029008" rx="9.5" ry="4.77275007"></ellipse><ellipse opacity="0.04" cx="10.5" cy="5.80029008" rx="8.5" ry="4.29549936"></ellipse><ellipse opacity="0.04" cx="10.5" cy="5.80029008" rx="7.5" ry="3.81822308"></ellipse><ellipse opacity="0.04" cx="10.5" cy="5.80029008" rx="6.5" ry="3.34094679"></ellipse><ellipse opacity="0.04" cx="10.5" cy="5.80029008" rx="5.5" ry="2.86367051"></ellipse><ellipse opacity="0.04" cx="10.5" cy="5.80029008" rx="4.5" ry="2.38636864"></ellipse></g><g fill="${color}"><path d="M27,13.5 C27,19.074644 20.250001,27.000002 14.75,34.500002 C14.016665,35.500004 12.983335,35.500004 12.25,34.500002 C6.7499993,27.000002 0,19.222562 0,13.5 C0,6.0441559 6.0441559,0 13.5,0 C20.955844,0 27,6.0441559 27,13.5 Z"></path></g><g opacity="0.25" fill="#000000"><path d="M13.5,0 C6.0441559,0 0,6.0441559 0,13.5 C0,19.222562 6.7499993,27 12.25,34.5 C13,35.522727 14.016664,35.500004 14.75,34.5 C20.250001,27 27,19.074644 27,13.5 C27,6.0441559 20.955844,0 13.5,0 Z M13.5,1 C20.415404,1 26,6.584596 26,13.5 C26,15.898657 24.495584,19.181431 22.220703,22.738281 C19.945823,26.295132 16.705119,30.142167 13.943359,33.908203 C13.743445,34.180814 13.612715,34.322738 13.5,34.441406 C13.387285,34.322738 13.256555,34.180814 13.056641,33.908203 C10.284481,30.127985 7.4148684,26.314159 5.015625,22.773438 C2.6163816,19.232715 1,15.953538 1,13.5 C1,6.584596 6.584596,1 13.5,1 Z"></path></g>${innerDotMarkup}</g></svg>`;
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 locationTitle = escapeHtml(item.extra.locationTitle || '');
@@ -462,6 +521,8 @@ const bindExternalTagFilters = (container, map, organizationMarkerManager, event
}
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');
@@ -469,7 +530,8 @@ const bindExternalTagFilters = (container, map, organizationMarkerManager, event
};
const eventToggleButton = wrapper.querySelector('[data-map-event-toggle="1"]');
const actionAll = wrapper.querySelector('[data-map-filter-action="all"]');
const canFilterByTag = tagButtons.length > 0;
const canFilterByEvents = !!eventToggleButton;
const setEventButtonState = (isActive) => {
if (!eventToggleButton) {
@@ -480,14 +542,9 @@ const bindExternalTagFilters = (container, map, organizationMarkerManager, event
eventToggleButton.classList.toggle('is-active', isActive);
};
const setAllButtonState = (isActive) => {
if (!actionAll) {
return;
}
actionAll.setAttribute('aria-pressed', isActive ? 'true' : 'false');
actionAll.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;
@@ -495,7 +552,6 @@ const bindExternalTagFilters = (container, map, organizationMarkerManager, event
const applyFilter = () => {
if (eventsOnly) {
organizationMarkerManager.setVisible(false);
setAllButtonState(false);
if (eventLayerManager) {
eventLayerManager.setVisible(true);
@@ -508,7 +564,6 @@ const bindExternalTagFilters = (container, map, organizationMarkerManager, event
if (activeTagId) {
organizationMarkerManager.setOnlyTagId(activeTagId);
setAllButtonState(false);
if (eventLayerManager) {
eventLayerManager.setVisible(false);
@@ -518,7 +573,6 @@ const bindExternalTagFilters = (container, map, organizationMarkerManager, event
}
organizationMarkerManager.setAllVisible();
setAllButtonState(true);
if (eventLayerManager) {
eventLayerManager.setVisible(true);
@@ -547,16 +601,6 @@ const bindExternalTagFilters = (container, map, organizationMarkerManager, event
});
});
if (actionAll) {
actionAll.addEventListener('click', () => {
activeTagId = null;
eventsOnly = false;
tagButtons.forEach((button) => setButtonState(button, false));
setEventButtonState(false);
applyFilter();
});
}
const toggleButton = wrapper.querySelector('[data-map-filter-toggle="1"]');
const filterGroup = wrapper.querySelector('.eventmanager-map-filter__group');
@@ -631,11 +675,50 @@ const bindExternalTagFilters = (container, map, organizationMarkerManager, event
});
}
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);
};
applyInitialDisplayMode();
applyFilter();
applyMapStyleMode();
};
const initLocationLayers = (map, locationItems, eventColor) => {
const initLocationLayers = (map, locationItems, markerImageId) => {
if (!locationItems.length) {
return {
setVisible: () => {},
@@ -644,7 +727,6 @@ const initLocationLayers = (map, locationItems, eventColor) => {
const sourceId = 'eventmanager-non-events-source';
const layerId = 'eventmanager-non-events';
map.addSource(sourceId, {
type: 'geojson',
data: {
@@ -655,13 +737,17 @@ const initLocationLayers = (map, locationItems, eventColor) => {
map.addLayer({
id: layerId,
type: 'circle',
type: 'symbol',
source: sourceId,
layout: {
'icon-image': markerImageId,
'icon-size': 1,
'icon-anchor': 'bottom',
'icon-allow-overlap': true,
'icon-ignore-placement': true,
},
paint: {
'circle-radius': 8,
'circle-stroke-width': 0,
'circle-color': eventColor,
'circle-opacity': 1,
'icon-opacity': 1,
},
});
@@ -720,8 +806,8 @@ const initLocationLayers = (map, locationItems, eventColor) => {
return;
}
map.setPaintProperty(layerId, 'circle-opacity-transition', { duration: EVENT_FADE_DURATION_MS, delay: 0 });
map.setPaintProperty(layerId, 'circle-opacity', value);
map.setPaintProperty(layerId, 'icon-opacity-transition', { duration: EVENT_FADE_DURATION_MS, delay: 0 });
map.setPaintProperty(layerId, 'icon-opacity', value);
};
return {
@@ -742,14 +828,12 @@ const initLocationLayers = (map, locationItems, eventColor) => {
};
};
const initEventLayers = (map, eventItems, eventColor) => {
const initEventLayers = (map, eventItems, eventColor, markerImageId, clusterMarkerImageId) => {
const sourceId = 'eventmanager-events-source';
const clusterLayerId = EVENT_CLUSTER_LAYER_ID;
const clusterSpiderfyLayerId = EVENT_CLUSTER_SPIDERFY_LAYER_ID;
const clusterCountLayerId = 'eventmanager-events-cluster-count';
const unclusteredLayerId = EVENT_UNCLUSTERED_LAYER_ID;
const spiderfyHitAreaImageId = 'eventmanager-spiderfy-hit-area';
const features = eventItems.map(toFeature);
if (!map.hasImage(spiderfyHitAreaImageId)) {
@@ -773,31 +857,25 @@ const initEventLayers = (map, eventItems, eventColor) => {
map.addLayer({
id: clusterLayerId,
type: 'circle',
source: sourceId,
filter: ['has', 'point_count'],
paint: {
'circle-radius': 18,
'circle-opacity': 1,
'circle-color': eventColor,
'circle-stroke-width': 0,
},
});
map.addLayer({
id: clusterCountLayerId,
type: 'symbol',
source: sourceId,
filter: ['has', 'point_count'],
layout: {
'icon-image': clusterMarkerImageId,
'icon-size': 1,
'icon-anchor': 'bottom',
'icon-allow-overlap': true,
'icon-ignore-placement': true,
'text-field': ['get', 'point_count'],
'text-size': 16,
'text-size': 18,
'text-font': [GLYPH_FONTSTACK],
'text-allow-overlap': true,
'text-ignore-placement': true,
'text-offset': [0, -1.85],
},
paint: {
'text-color': '#ffffff',
'icon-opacity': 1,
'text-color': '#FFFFFF',
'text-opacity': 1,
},
});
@@ -810,6 +888,8 @@ const initEventLayers = (map, eventItems, eventColor) => {
layout: {
'icon-image': spiderfyHitAreaImageId,
'icon-size': 36,
'icon-anchor': 'bottom',
'icon-offset': [0, -1.0],
'icon-allow-overlap': true,
},
paint: {
@@ -819,14 +899,18 @@ const initEventLayers = (map, eventItems, eventColor) => {
map.addLayer({
id: unclusteredLayerId,
type: 'circle',
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,
},
paint: {
'circle-radius': 11,
'circle-color': eventColor,
'circle-stroke-width': 0,
'circle-opacity': 1,
'icon-opacity': 1,
},
});
@@ -925,13 +1009,14 @@ const initEventLayers = (map, eventItems, eventColor) => {
spiderLegsWidth: 2,
spiderLegsColor: eventColor,
spiderLeavesLayout: {
'text-field': '●',
'text-size': 18,
'text-allow-overlap': true,
'icon-image': markerImageId,
'icon-size': 0.9,
'icon-anchor': 'bottom',
'icon-allow-overlap': true,
},
spiderLeavesPaint: {
'text-color': eventColor,
'text-opacity': 1,
'icon-opacity': 1,
'icon-translate': [0, 8],
},
onLeafClick: (feature, spiderEvent) => {
const clickCoordinates = spiderEvent?.lngLat
@@ -942,11 +1027,11 @@ const initEventLayers = (map, eventItems, eventColor) => {
},
});
spiderfy.applyTo(clusterSpiderfyLayerId);
spiderfy.applyTo(clusterLayerId);
spiderfyInstance = spiderfy;
if (map.getLayer(clusterCountLayerId)) {
map.moveLayer(clusterCountLayerId);
if (map.getLayer(clusterLayerId)) {
map.moveLayer(clusterLayerId);
}
} catch (error) {
bindClusterZoomFallback();
@@ -982,7 +1067,6 @@ const initEventLayers = (map, eventItems, eventColor) => {
const eventLayerIds = [
clusterLayerId,
clusterCountLayerId,
clusterSpiderfyLayerId,
unclusteredLayerId,
];
@@ -992,9 +1076,9 @@ const initEventLayers = (map, eventItems, eventColor) => {
if (isVisible) {
eventLayerIds.forEach((layerId) => setLayerLayoutVisibility(layerId, true));
fadeLayerOpacity(clusterLayerId, 'circle-opacity', 1);
fadeLayerOpacity(unclusteredLayerId, 'circle-opacity', 1);
fadeLayerOpacity(clusterCountLayerId, 'text-opacity', 1);
fadeLayerOpacity(clusterLayerId, 'icon-opacity', 1);
fadeLayerOpacity(clusterLayerId, 'text-opacity', 1);
fadeLayerOpacity(unclusteredLayerId, 'icon-opacity', 1);
return;
}
@@ -1003,9 +1087,9 @@ const initEventLayers = (map, eventItems, eventColor) => {
spiderfyInstance.unspiderfyAll();
}
fadeLayerOpacity(clusterLayerId, 'circle-opacity', 0);
fadeLayerOpacity(unclusteredLayerId, 'circle-opacity', 0);
fadeLayerOpacity(clusterCountLayerId, 'text-opacity', 0);
fadeLayerOpacity(clusterLayerId, 'icon-opacity', 0);
fadeLayerOpacity(clusterLayerId, 'text-opacity', 0);
fadeLayerOpacity(unclusteredLayerId, 'icon-opacity', 0);
window.setTimeout(() => {
eventLayerIds.forEach((layerId) => setLayerLayoutVisibility(layerId, false));
@@ -1117,7 +1201,7 @@ const initSingleMap = (container) => {
});
};
map.on('load', () => {
map.on('load', async () => {
applyDefaultProjection(map);
const organizationItems = items.filter((item) => item.type === 'organisation');
@@ -1128,13 +1212,24 @@ const initSingleMap = (container) => {
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);
const locationLayerManager = initLocationLayers(map, locationItems, eventColor);
const locationLayerManager = initLocationLayers(map, locationItems, eventMarkerImageId);
let eventLayerManager = null;
if (eventItems.length) {
eventLayerManager = initEventLayers(map, eventItems, eventColor);
eventLayerManager = initEventLayers(map, eventItems, eventColor, eventMarkerImageId, clusterMarkerImageId);
}
const combinedEventLayerManager = {