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) => {