diff --git a/contao/templates/frontend/event_map.html.twig b/contao/templates/frontend/event_map.html.twig
index 7b68ecf..28c8a64 100644
--- a/contao/templates/frontend/event_map.html.twig
+++ b/contao/templates/frontend/event_map.html.twig
@@ -85,4 +85,4 @@
>
-
+
diff --git a/public/assets/map-module.js b/public/assets/map-module.js
index 4d99043..9ec8c99 100644
--- a/public/assets/map-module.js
+++ b/public/assets/map-module.js
@@ -12,16 +12,26 @@ const DEFAULT_EVENT_COLOR = '#BC5067';
const EVENT_FADE_DURATION_MS = 200;
const EVENT_CLUSTER_LAYER_ID = 'eventmanager-events-clusters';
const EVENT_UNCLUSTERED_LAYER_ID = 'eventmanager-events-unclustered';
-const SATELLITE_SOURCE_ID = 'eventmanager-satellite-source';
-const SATELLITE_LAYER_ID = 'eventmanager-satellite-layer';
-const SATELLITE_TILE_URL = 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}';
-const SATELLITE_LABELS_SOURCE_ID = 'eventmanager-satellite-labels-source';
-const SATELLITE_LABELS_LAYER_ID = 'eventmanager-satellite-labels-layer';
-const SATELLITE_LABELS_TILE_URL = 'https://services.arcgisonline.com/ArcGIS/rest/services/Reference/World_Boundaries_and_Places/MapServer/tile/{z}/{y}/{x}';
-const SATELLITE_ROADS_SOURCE_ID = 'eventmanager-satellite-roads-source';
-const SATELLITE_ROADS_LAYER_ID = 'eventmanager-satellite-roads-layer';
-const SATELLITE_ROADS_TILE_URL = 'https://services.arcgisonline.com/ArcGIS/rest/services/Reference/World_Transportation/MapServer/tile/{z}/{y}/{x}';
-const SATELLITE_ATTRIBUTION = '© Esri, Maxar';
+// Luftbild: amtliche Digitale Orthophotos des GeoSN (20 cm) über den Cache-Proxy auf maps.mummert.media.
+// Besucher rufen nur maps.mummert.media ab, keine Verbindung zu geodienste.sachsen.de.
+const SATELLITE_SOURCE_ID = 'eventmanager-luftbild-sn';
+const SATELLITE_LAYER_ID = 'eventmanager-luftbild-sn';
+const SATELLITE_TILE_URL = 'https://maps.mummert.media/dienste/dop-sn/{z}/{x}/{y}.webp';
+const SATELLITE_TILE_SIZE = 512;
+const SATELLITE_MIN_ZOOM = 7; // darunter lehnt der Proxy ab
+const SATELLITE_MAX_ZOOM = 18; // native Auflösung, darüber skaliert MapLibre
+const SATELLITE_BOUNDS = [11.7889, 50.1506, 15.0869, 51.7209]; // Sachsen
+const SATELLITE_ATTRIBUTION = '© GeoSN, dl-de/by-2-0';
+// Beschriftung im Luftbild lesbar machen (weiße Schrift, dunkler Rand), Unruhe ausblenden
+const AERIAL_HIDE_LAYER_IDS = ['pois', 'address_label', 'roads_oneway'];
+const AERIAL_KEEP_LAYER_IDS = ['roads_shields'];
+const AERIAL_TEXT_PAINT = {
+ 'text-color': '#ffffff',
+ 'text-halo-color': 'rgba(0, 0, 0, .78)',
+ 'text-halo-width': 1.6,
+ 'text-halo-blur': .6,
+};
+const AERIAL_WATER_TEXT_PAINT = { ...AERIAL_TEXT_PAINT, 'text-color': '#cfe0ff' };
const PROTOMAPS_SOURCE_ID = 'protomaps';
const DEFAULT_BASE_STYLE_URL = 'https://maps.mummert.media/metadaten/world-light.json';
const PROTOMAPS_PM_TILES_URL = 'pmtiles://https://maps.mummert.media/tiles/world.pmtiles';
@@ -392,6 +402,9 @@ const applyDefaultProjection = (map) => {
}
};
+const isBaseSymbolLayer = (layer) => 'symbol' === layer?.type
+ && layer.source === PROTOMAPS_SOURCE_ID;
+
const ensureSatelliteLayer = (map) => {
if (!map || map.getSource(SATELLITE_SOURCE_ID)) {
return;
@@ -400,15 +413,17 @@ const ensureSatelliteLayer = (map) => {
map.addSource(SATELLITE_SOURCE_ID, {
type: 'raster',
tiles: [SATELLITE_TILE_URL],
- tileSize: 256,
+ tileSize: SATELLITE_TILE_SIZE,
+ minzoom: SATELLITE_MIN_ZOOM,
+ maxzoom: SATELLITE_MAX_ZOOM,
+ bounds: SATELLITE_BOUNDS,
attribution: SATELLITE_ATTRIBUTION,
});
- const markerLayerCandidates = [
- EVENT_CLUSTER_LAYER_ID,
- EVENT_UNCLUSTERED_LAYER_ID,
- ];
- const beforeId = markerLayerCandidates.find((layerId) => map.getLayer(layerId));
+ // Unter die erste Beschriftungsebene der Grundkarte: Orts- und Straßennamen
+ // sowie die Veranstaltungs-Marker bleiben über dem Luftbild.
+ const firstSymbolLayer = (map.getStyle()?.layers || []).find(isBaseSymbolLayer)
+ || (map.getStyle()?.layers || []).find((layer) => 'symbol' === layer.type);
map.addLayer({
id: SATELLITE_LAYER_ID,
@@ -418,51 +433,57 @@ const ensureSatelliteLayer = (map) => {
visibility: 'none',
},
paint: {
- 'raster-opacity': 1,
- 'raster-brightness-min': 0,
- 'raster-brightness-max': 1,
- 'raster-contrast': 0,
- 'raster-saturation': 0,
+ 'raster-opacity': 0,
+ 'raster-opacity-transition': { duration: EVENT_FADE_DURATION_MS, delay: 0 },
},
- }, beforeId);
+ }, firstSymbolLayer?.id);
+};
- map.addSource(SATELLITE_ROADS_SOURCE_ID, {
- type: 'raster',
- tiles: [SATELLITE_ROADS_TILE_URL],
- tileSize: 256,
- attribution: SATELLITE_ATTRIBUTION,
+const aerialLabelBackups = new WeakMap();
+
+const setAerialLabels = (map, enabled) => {
+ const layers = (map.getStyle()?.layers || []).filter(isBaseSymbolLayer);
+
+ if (!aerialLabelBackups.has(map)) {
+ aerialLabelBackups.set(map, new Map());
+ }
+
+ const backups = aerialLabelBackups.get(map);
+
+ layers.forEach((layer) => {
+ const layerId = layer.id;
+
+ if (!map.getLayer(layerId) || AERIAL_KEEP_LAYER_IDS.includes(layerId)) {
+ return;
+ }
+
+ if (!backups.has(layerId)) {
+ const paint = {};
+
+ Object.keys(AERIAL_TEXT_PAINT).forEach((property) => {
+ paint[property] = map.getPaintProperty(layerId, property);
+ });
+
+ backups.set(layerId, {
+ visibility: map.getLayoutProperty(layerId, 'visibility') || 'visible',
+ paint,
+ });
+ }
+
+ const backup = backups.get(layerId);
+
+ if (AERIAL_HIDE_LAYER_IDS.includes(layerId)) {
+ map.setLayoutProperty(layerId, 'visibility', enabled ? 'none' : backup.visibility);
+
+ return;
+ }
+
+ const aerialPaint = layerId.startsWith('water') ? AERIAL_WATER_TEXT_PAINT : AERIAL_TEXT_PAINT;
+
+ Object.keys(aerialPaint).forEach((property) => {
+ map.setPaintProperty(layerId, property, enabled ? aerialPaint[property] : backup.paint[property]);
+ });
});
-
- map.addLayer({
- id: SATELLITE_ROADS_LAYER_ID,
- type: 'raster',
- source: SATELLITE_ROADS_SOURCE_ID,
- layout: {
- visibility: 'none',
- },
- paint: {
- 'raster-opacity': 1,
- },
- }, beforeId);
-
- map.addSource(SATELLITE_LABELS_SOURCE_ID, {
- type: 'raster',
- tiles: [SATELLITE_LABELS_TILE_URL],
- tileSize: 256,
- attribution: SATELLITE_ATTRIBUTION,
- });
-
- map.addLayer({
- id: SATELLITE_LABELS_LAYER_ID,
- type: 'raster',
- source: SATELLITE_LABELS_SOURCE_ID,
- layout: {
- visibility: 'none',
- },
- paint: {
- 'raster-opacity': 1,
- },
- }, beforeId);
};
const setSatelliteMode = (map, enabled) => {
@@ -476,30 +497,20 @@ const setSatelliteMode = (map, enabled) => {
return;
}
- const hybridOverlayLayerIds = [SATELLITE_ROADS_LAYER_ID, SATELLITE_LABELS_LAYER_ID];
-
- map.setLayoutProperty(SATELLITE_LAYER_ID, 'visibility', enabled ? 'visible' : 'none');
- hybridOverlayLayerIds.forEach((layerId) => {
- if (map.getLayer(layerId)) {
- map.setLayoutProperty(layerId, 'visibility', enabled ? 'visible' : 'none');
- map.setPaintProperty(layerId, 'raster-opacity-transition', { duration: EVENT_FADE_DURATION_MS, delay: 0 });
- map.setPaintProperty(layerId, 'raster-opacity', enabled ? 1 : 0);
- }
- });
- map.setPaintProperty(SATELLITE_LAYER_ID, 'raster-opacity-transition', { duration: EVENT_FADE_DURATION_MS, delay: 0 });
- map.setPaintProperty(SATELLITE_LAYER_ID, 'raster-brightness-min-transition', { duration: EVENT_FADE_DURATION_MS, delay: 0 });
- map.setPaintProperty(SATELLITE_LAYER_ID, 'raster-brightness-max-transition', { duration: EVENT_FADE_DURATION_MS, delay: 0 });
- map.setPaintProperty(SATELLITE_LAYER_ID, 'raster-contrast-transition', { duration: EVENT_FADE_DURATION_MS, delay: 0 });
- map.setPaintProperty(SATELLITE_LAYER_ID, 'raster-saturation-transition', { duration: EVENT_FADE_DURATION_MS, delay: 0 });
-
- if (!enabled) {
- return;
+ if (enabled) {
+ map.setLayoutProperty(SATELLITE_LAYER_ID, 'visibility', 'visible');
}
- 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);
+ map.setPaintProperty(SATELLITE_LAYER_ID, 'raster-opacity', enabled ? 1 : 0);
+ setAerialLabels(map, enabled);
+
+ if (!enabled) {
+ window.setTimeout(() => {
+ if (map.getLayer(SATELLITE_LAYER_ID) && 0 === map.getPaintProperty(SATELLITE_LAYER_ID, 'raster-opacity')) {
+ map.setLayoutProperty(SATELLITE_LAYER_ID, 'visibility', 'none');
+ }
+ }, EVENT_FADE_DURATION_MS);
+ }
};
const normalizeTagIds = (value) => {