// public/js/mapBaseLayers.js // Basemap 2D Leaflet e selettore livelli. (() => { "use strict"; const STORAGE_KEY = "selectedMapStyle"; const DEFAULT_STYLE = "osm"; let currentLayer = null; let layers = {}; let layerControl = null; let initializedMap = null; let terrainPlaceholderLayer = null; let terrainStyleControl = null; let styleChangeInProgress = false; function getMapboxToken() { return window.AppConfig?.mapboxToken || ""; } function createOsmLayer() { return L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { subdomains: "abc", maxZoom: 19, attribution: "© OpenStreetMap contributors", }); } function createOpenTopoLayer() { return L.tileLayer("https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png", { subdomains: "abc", maxZoom: 17, attribution: "Map data: © OpenStreetMap contributors, SRTM | " + "Map style: © OpenTopoMap", }); } function createMapboxLayer(styleId, token) { if (!token) { return null; } const encodedToken = encodeURIComponent(token); const tileUrl = `https://api.mapbox.com/styles/v1/mapbox/${styleId}` + `/tiles/512/{z}/{x}/{y}@2x` + `?access_token=${encodedToken}`; return L.tileLayer(tileUrl, { tileSize: 512, zoomOffset: -1, maxZoom: 22, attribution: "© Mapbox © OpenStreetMap contributors", }); } function createLayers() { const token = getMapboxToken(); const result = { osm: { label: "OpenStreetMap", layer: createOsmLayer(), engine: "leaflet", }, opentopo: { label: "Topografica", layer: createOpenTopoLayer(), engine: "leaflet", }, }; if (!token) { console.warn( "[mapBaseLayers] Token Mapbox non configurato. " + "Saranno disponibili solamente OpenStreetMap " + "e OpenTopoMap.", ); return result; } const mapboxDefinitions = { mapboxStreets: { label: "Mapbox Streets", styleId: "streets-v12", }, mapboxOutdoors: { label: "Mapbox Outdoors", styleId: "outdoors-v12", }, mapboxSatellite: { label: "Mapbox Satellite", styleId: "satellite-v9", }, mapboxSatelliteStreets: { label: "Mapbox Satellite + strade", styleId: "satellite-streets-v12", }, }; for (const [id, definition] of Object.entries(mapboxDefinitions)) { const layer = createMapboxLayer(definition.styleId, token); if (!layer) { continue; } result[id] = { label: definition.label, layer, }; } terrainPlaceholderLayer = L.layerGroup(); result.terrain3d = { label: "Terrain 3D", layer: terrainPlaceholderLayer, engine: "mapbox", }; return result; } function getSavedStyle() { const savedStyle = localStorage.getItem(STORAGE_KEY); if (savedStyle && layers[savedStyle]) { return savedStyle; } return DEFAULT_STYLE; } function findStyleIdByLayer(selectedLayer) { for (const [styleId, definition] of Object.entries(layers)) { if (definition.layer === selectedLayer) { return styleId; } } return null; } function dispatchStyleChange(styleId, label) { const definition = layers[styleId]; window.dispatchEvent( new CustomEvent("mapstylechange", { detail: { engine: definition?.engine || "leaflet", styleId, label, }, }), ); } async function setStyle(map, styleId, save = true) { if (!map) { console.error("[mapBaseLayers] Istanza Leaflet mancante"); return false; } if (styleChangeInProgress) { return false; } const selectedStyle = layers[styleId] ? styleId : DEFAULT_STYLE; const definition = layers[selectedStyle]; if (!definition) { console.error("[mapBaseLayers] Basemap non trovata:", styleId); return false; } styleChangeInProgress = true; try { // =============================== // TERRAIN 3D // =============================== if (selectedStyle === "terrain3d") { const success = await window.MapTerrain3D?.enable?.(); if (!success) { console.warn("[mapBaseLayers] Terrain 3D non attivato"); return false; } currentLayer = terrainPlaceholderLayer; if (save) { localStorage.setItem(STORAGE_KEY, selectedStyle); } syncTerrainStyleControl(selectedStyle); dispatchStyleChange(selectedStyle, definition.label); return true; } // =============================== // MAPPA LEAFLET // =============================== if (window.MapTerrain3D?.enabled?.()) { window.MapTerrain3D?.disable?.(); } if ( currentLayer && currentLayer !== terrainPlaceholderLayer && map.hasLayer(currentLayer) ) { map.removeLayer(currentLayer); } if (terrainPlaceholderLayer && map.hasLayer(terrainPlaceholderLayer)) { map.removeLayer(terrainPlaceholderLayer); } currentLayer = definition.layer; if (currentLayer && !map.hasLayer(currentLayer)) { currentLayer.addTo(map); } if (save) { localStorage.setItem(STORAGE_KEY, selectedStyle); } syncTerrainStyleControl(selectedStyle); dispatchStyleChange(selectedStyle, definition.label); return true; } finally { styleChangeInProgress = false; } } function createControl(map) { if (layerControl) { map.removeControl(layerControl); layerControl = null; } const baseLayers = {}; for (const definition of Object.values(layers)) { baseLayers[definition.label] = definition.layer; } layerControl = L.control.layers(baseLayers, null, { position: "topright", collapsed: true, }); layerControl.addTo(map); map.on("baselayerchange", handleBaseLayerChange); } async function handleBaseLayerChange(event) { const styleId = findStyleIdByLayer(event.layer); if (!styleId) { return; } // =============================== // TERRAIN 3D // =============================== if (styleId === "terrain3d") { const success = await setStyle(initializedMap, "terrain3d", true); if (!success) { await setStyle(initializedMap, DEFAULT_STYLE, true); } return; } // =============================== // LEAFLET // =============================== currentLayer = event.layer; if (window.MapTerrain3D?.enabled?.()) { window.MapTerrain3D?.disable?.(); } localStorage.setItem(STORAGE_KEY, styleId); syncTerrainStyleControl(styleId); dispatchStyleChange(styleId, event.name); } async function waitForConfiguration() { if (window.AppConfigPromise) { try { await window.AppConfigPromise; } catch (error) { console.error("[mapBaseLayers] Errore configurazione:", error); } } return window.AppConfig || {}; } function getSelectedStyle() { if (window.MapTerrain3D?.enabled?.()) { return "terrain3d"; } return findStyleIdByLayer(currentLayer) || DEFAULT_STYLE; } function syncTerrainStyleControl(styleId) { const select = terrainStyleControl?.querySelector?.("select"); if (!select) { return; } if (layers[styleId]) { select.value = styleId; } } function removeTerrainStyleControl() { if (!terrainStyleControl) { return; } terrainStyleControl.remove(); terrainStyleControl = null; } function mountTerrainStyleControl(targetContainer) { if (!targetContainer) { return; } removeTerrainStyleControl(); const wrapper = document.createElement("div"); wrapper.className = "terrain-style-control"; const select = document.createElement("select"); select.className = "terrain-style-select"; select.setAttribute("aria-label", "Tipo di mappa"); for (const [id, definition] of Object.entries(layers)) { const option = document.createElement("option"); option.value = id; option.textContent = definition.label; select.appendChild(option); } select.value = getSelectedStyle(); select.addEventListener("change", async () => { const selectedStyle = select.value; const success = await setStyle(initializedMap, selectedStyle, true); if (!success) { select.value = getSelectedStyle(); } }); wrapper.appendChild(select); targetContainer.appendChild(wrapper); terrainStyleControl = wrapper; } async function initialize(map) { if (!map) { console.error("[mapBaseLayers] Istanza mappa non disponibile"); return false; } if (typeof window.L === "undefined") { console.error("[mapBaseLayers] Leaflet non disponibile"); return false; } await waitForConfiguration(); /* * Evita una doppia inizializzazione sulla stessa * istanza Leaflet. */ if (initializedMap === map && Object.keys(layers).length > 0) { return true; } /* * Se il modulo viene inizializzato su una nuova mappa, * rimuove il listener dalla precedente. */ if (initializedMap && initializedMap !== map) { initializedMap.off("baselayerchange", handleBaseLayerChange); } initializedMap = map; layers = createLayers(); const initialStyle = getSavedStyle(); const safeInitialStyle = initialStyle === "terrain3d" ? DEFAULT_STYLE : initialStyle; const styleApplied = await setStyle(map, safeInitialStyle, false); createControl(map); console.log("[mapBaseLayers] Basemap inizializzate", { currentStyle: getCurrentStyle(), availableStyles: getAvailableStyles().map((style) => style.id), mapboxConfigured: Boolean(getMapboxToken()), }); return styleApplied; } function getCurrentStyle() { if (window.MapTerrain3D?.enabled?.()) { return "terrain3d"; } return findStyleIdByLayer(currentLayer) || DEFAULT_STYLE; } function getAvailableStyles() { return Object.entries(layers).map(([id, definition]) => ({ id, label: definition.label, })); } function createLeafletLayer( styleId, ) { const token = getMapboxToken(); switch (styleId) { case "osm": return createOsmLayer(); case "opentopo": return createOpenTopoLayer(); case "mapboxStreets": return createMapboxLayer( "streets-v12", token, ); case "mapboxOutdoors": return createMapboxLayer( "outdoors-v12", token, ); case "mapboxSatellite": return createMapboxLayer( "satellite-v9", token, ); case "mapboxSatelliteStreets": return createMapboxLayer( "satellite-streets-v12", token, ); default: return createOsmLayer(); } } function getMapboxStyleUrl( styleId, ) { switch (styleId) { case "mapboxStreets": return "mapbox://styles/mapbox/streets-v12"; case "mapboxSatellite": return "mapbox://styles/mapbox/satellite-v9"; case "mapboxSatelliteStreets": return "mapbox://styles/mapbox/satellite-streets-v12"; case "terrain3d": case "mapboxOutdoors": default: return "mapbox://styles/mapbox/outdoors-v12"; } } window.MapBaseLayers = Object.freeze({ initialize, setStyle, getCurrentStyle, getAvailableStyles, createLeafletLayer, getMapboxStyleUrl, mountTerrainStyleControl, removeTerrainStyleControl, }); })();