// public/js/map/terrain3d.js // Switch Leaflet <-> Mapbox GL Terrain 3D (() => { "use strict"; let mapboxMap = null; let enabled = false; let savedLeafletView = null; let savedLeafletMap = null; let terrainMarkerRefreshTimer = null; let terrainMarkerRefreshToken = 0; let terrainEventsMap = null; let destroying = false; function getToken() { return window.AppConfig?.mapboxToken || ""; } function getContainer() { return document.getElementById("terrainMap"); } function getLeafletMap() { if (window.MapEngine?.type === "mapbox") { return savedLeafletMap; } return ( window.GlobalMap?.getMap?.() || window.MapEngine?.map || window.leafletMap || null ); } function createMapbox3D() { const container = getContainer(); if (!container) { console.error("[Terrain3D] #terrainMap non trovato"); return; } const token = getToken(); if (!token) { console.error("[Terrain3D] Token Mapbox mancante"); return; } if (!window.mapboxgl) { console.error("[Terrain3D] Mapbox GL JS non caricato"); return; } // Impedisce la creazione // di una seconda istanza. if (mapboxMap) { console.warn("[Terrain3D] Mapbox già inizializzato"); return; } // ================================= // MOSTRA IL CONTENITORE MAPBOX // ================================= container.classList.add("open"); container.style.display = "block"; container.style.visibility = "visible"; container.style.pointerEvents = "auto"; // ================================= // RECUPERA LA MAPPA LEAFLET // ================================= if (!savedLeafletMap) { savedLeafletMap = getLeafletMap(); } const leafletMap = savedLeafletMap; console.log("[Terrain3D] Leaflet trovato:", leafletMap); // ================================= // VISTA INIZIALE // ================================= let center = [12.4964, 41.9028]; let zoom = 6; if (leafletMap) { const leafletCenter = leafletMap.getCenter(); center = [leafletCenter.lng, leafletCenter.lat]; zoom = leafletMap.getZoom(); savedLeafletView = { lat: leafletCenter.lat, lng: leafletCenter.lng, zoom: leafletMap.getZoom(), }; console.log("[Terrain3D] Vista Leaflet salvata", savedLeafletView); } // ================================= // NASCONDE LEAFLET // ================================= if (leafletMap) { const leafletMapContainer = leafletMap.getContainer(); leafletMapContainer.style.visibility = "hidden"; leafletMapContainer.style.pointerEvents = "none"; console.log("[Terrain3D] Leaflet nascosto"); } // ================================= // CREA MAPBOX // ================================= mapboxgl.accessToken = token; mapboxMap = new mapboxgl.Map({ container, style: "mapbox://styles/mapbox/outdoors-v12", center, zoom, pitch: 60, bearing: -20, antialias: true, }); // ================================= // CARICAMENTO MAPBOX // ================================= mapboxMap.on("load", () => { if (!mapboxMap) { return; } // ------------------------------- // SOURCE DEL TERRENO // ------------------------------- if (!mapboxMap.getSource("mapbox-dem")) { mapboxMap.addSource("mapbox-dem", { type: "raster-dem", url: "mapbox://mapbox.mapbox-terrain-dem-v1", tileSize: 512, maxzoom: 14, }); } // ------------------------------- // TERRENO 3D // ------------------------------- mapboxMap.setTerrain({ source: "mapbox-dem", exaggeration: 1.5, }); // ------------------------------- // CONTROLLI MAPBOX // ------------------------------- mapboxMap.addControl( new mapboxgl.NavigationControl(), "top-right", ); // ------------------------------- // COLLEGA MAPENGINE // ------------------------------- if (window.MapEngine) { window.MapEngine.setMapbox(mapboxMap); } // ------------------------------- // COLLEGA MARKER CONTROLLER // ------------------------------- window.MapboxMarkers?.setMap?.(mapboxMap); window.MapBaseLayers?.mountTerrainStyleControl?.(container); // ------------------------------- // ATTIVA MAPBOX 3D // ------------------------------- /* * enabled deve diventare true prima * di registrare e utilizzare il refresh. */ enabled = true; const activitiesMode = window.state?.mode === "activities"; if (activitiesMode) { window.showActivityRoutesToggle?.(); } else { window.hideActivityRoutesToggle?.( true, ); } // ------------------------------- // COLLEGA MOVEEND // ------------------------------- /* * Registra il listener nominato * handleTerrainMoveEnd. * * Non devono esserci altri listener * anonimi moveend in questa funzione. */ setupTerrainMapEvents(); // ------------------------------- // PRIMO REFRESH // ------------------------------- /* * Aspetta il completamento del primo * rendering prima di caricare marker, * cluster e route. */ mapboxMap.once("idle", () => { scheduleTerrainMarkerRefresh("initial-idle"); }); console.log("[Terrain3D] Mapbox 3D attivo"); // ------------------------------- // AGGIORNA LE DIMENSIONI // ------------------------------- requestAnimationFrame(() => { mapboxMap?.resize?.(); }); }); // ================================= // ERRORI MAPBOX // ================================= mapboxMap.on("error", (event) => { console.error("[Terrain3D] errore Mapbox", event?.error || event); }); } function destroyMapbox3D() { if (!mapboxMap) { return; } destroying = true; console.log("[Terrain3D] Chiusura MapboxGL"); const mapboxCenter = mapboxMap.getCenter?.(); const mapboxZoom = mapboxMap.getZoom?.(); if ( mapboxCenter && Number.isFinite(Number(mapboxCenter.lat)) && Number.isFinite(Number(mapboxCenter.lng)) && Number.isFinite(Number(mapboxZoom)) ) { savedLeafletView = { lat: Number(mapboxCenter.lat), lng: Number(mapboxCenter.lng), zoom: Number(mapboxZoom), }; } removeTerrainMapEvents(); /* * Pulisce marker e cluster mentre * la vecchia mappa è ancora valida. */ window.MapboxMarkers?.clearMarkers?.(); window.MapBaseLayers?.removeTerrainStyleControl?.(); /* * Soltanto dopo la pulizia viene * distrutta l'istanza Mapbox. */ mapboxMap.remove(); mapboxMap = null; enabled = false; destroying = false; const terrainContainer = document.getElementById("terrainMap"); if (terrainContainer) { terrainContainer.classList.remove("open"); terrainContainer.style.display = "none"; } const globalMapContainer = document.getElementById("globalMap"); if (globalMapContainer) { globalMapContainer.style.visibility = "visible"; globalMapContainer.classList.add("open"); } const leafletMap = savedLeafletMap; if (leafletMap) { const leafletMapContainer = leafletMap.getContainer(); leafletMapContainer.style.display = "block"; leafletMapContainer.style.visibility = "visible"; leafletMapContainer.style.pointerEvents = "auto"; leafletMap.invalidateSize(); if (savedLeafletView) { leafletMap.setView( [savedLeafletView.lat, savedLeafletView.lng], savedLeafletView.zoom, ); } console.log("[Terrain3D] Leaflet ripristinato"); } if ( savedLeafletMap && window.MapEngine ) { window.MapEngine.type = "leaflet"; window.MapEngine.map = savedLeafletMap; window.MapEngine.markerLayer = window.globalMarkers; } requestAnimationFrame( async () => { if (!savedLeafletMap) { return; } savedLeafletMap.invalidateSize?.(); if ( window.globalMarkers && !savedLeafletMap.hasLayer( window.globalMarkers, ) ) { savedLeafletMap.addLayer( window.globalMarkers, ); } window.globalMarkers ?.clearLayers?.(); if ( window.state?.mode === "activities" ) { await window .renderActivityMapLayer?.(); } else { await window .renderPhotoMapLayer?.(); } savedLeafletMap.invalidateSize?.(); console.log( "[Terrain3D] Leaflet ridisegnato", { mode: window.state?.mode, markerLayers: window.globalMarkers ?.getLayers?.() ?.length ?? null, clusterOnMap: !!( window.globalMarkers && savedLeafletMap.hasLayer( window.globalMarkers, ) ), }, ); }, ); console.log( "[Terrain3D] ritorno Leaflet", ); } function waitForMapboxReady(timeoutMs = 2500) { return new Promise((resolve) => { if (!mapboxMap) { resolve(false); return; } if (mapboxMap.isStyleLoaded?.() && !mapboxMap.isMoving?.()) { resolve(true); return; } let completed = false; const finish = () => { if (completed) { return; } completed = true; clearTimeout(timeout); mapboxMap?.off?.("idle", handleReady); resolve(!!mapboxMap && !!mapboxMap.isStyleLoaded?.()); }; const handleReady = () => { if (!mapboxMap) { finish(); return; } if (mapboxMap.isStyleLoaded?.() && !mapboxMap.isMoving?.()) { finish(); } }; const timeout = setTimeout(finish, timeoutMs); mapboxMap.on("idle", handleReady); }); } function scheduleTerrainMarkerRefresh(reason = "moveend") { if (!mapboxMap) { return; } if (!enabled) { return; } if (destroying) { return; } if (terrainMarkerRefreshTimer) { clearTimeout(terrainMarkerRefreshTimer); } const token = ++terrainMarkerRefreshToken; terrainMarkerRefreshTimer = setTimeout(async () => { terrainMarkerRefreshTimer = null; if (!mapboxMap || !enabled) { return; } if (token !== terrainMarkerRefreshToken) { return; } /* * Dopo moveend Mapbox può ancora * avere isStyleLoaded() false. * Aspettiamo il successivo idle. */ const ready = await waitForMapboxReady(); if (!ready || !mapboxMap || !enabled) { console.warn("[Terrain3D] refresh annullato, Mapbox non pronto", { reason, token, }); return; } if (token !== terrainMarkerRefreshToken) { return; } if (typeof window.refreshTerrainMarkers !== "function") { return; } console.log("[Terrain3D] aggiorno marker", { reason, token, styleLoaded: mapboxMap.isStyleLoaded?.(), moving: mapboxMap.isMoving?.(), zoom: mapboxMap.getZoom?.(), bounds: mapboxMap.getBounds?.()?.toArray?.(), }); await window.refreshTerrainMarkers(); }, 180); } function handleTerrainMoveEnd() { scheduleTerrainMarkerRefresh("moveend"); } function setupTerrainMapEvents() { if (!mapboxMap) { return; } if (terrainEventsMap === mapboxMap) { return; } removeTerrainMapEvents(); terrainEventsMap = mapboxMap; terrainEventsMap.on("moveend", handleTerrainMoveEnd); console.log("[Terrain3D] listener moveend collegato"); } function removeTerrainMapEvents() { if (terrainMarkerRefreshTimer) { clearTimeout(terrainMarkerRefreshTimer); terrainMarkerRefreshTimer = null; } terrainMarkerRefreshToken++; if (!terrainEventsMap) { return; } terrainEventsMap.off("moveend", handleTerrainMoveEnd); terrainEventsMap = null; console.log("[Terrain3D] listener moveend rimosso"); } async function enable() { // Mapbox è già attivo. if (enabled && mapboxMap) { mapboxMap.resize?.(); return true; } // Rimuove un'istanza rimasta // parzialmente inizializzata. if (mapboxMap && !enabled) { console.warn("[Terrain3D] rimuovo istanza Mapbox incompleta"); try { removeTerrainMapEvents(); window.MapBaseLayers?.removeTerrainStyleControl?.(); window.MapboxMarkers?.clearMarkers?.(); mapboxMap.remove(); } catch (error) { console.warn("[Terrain3D] errore pulizia istanza incompleta", error); } mapboxMap = null; } // Avvia una nuova istanza Mapbox. createMapbox3D(); const success = await new Promise((resolve) => { let completed = false; let check = null; let timeout = null; const finish = (result) => { if (completed) { return; } completed = true; if (check !== null) { clearInterval(check); check = null; } if (timeout !== null) { clearTimeout(timeout); timeout = null; } resolve(result); }; check = setInterval(() => { if (!mapboxMap) { return; } if (!enabled) { return; } if (!mapboxMap.isStyleLoaded?.()) { return; } mapboxMap.resize?.(); finish(true); }, 50); timeout = setTimeout(() => { finish(false); }, 10000); }); window.updateTerrainButton?.(enabled); if (!success) { console.warn("[Terrain3D] attivazione non completata entro 10 secondi"); // Pulisce l'istanza che non ha // completato correttamente il load. if (mapboxMap) { try { removeTerrainMapEvents(); window.MapBaseLayers?.removeTerrainStyleControl?.(); window.MapboxMarkers?.clearMarkers?.(); mapboxMap.remove(); } catch (error) { console.warn("[Terrain3D] errore pulizia dopo timeout", error); } } mapboxMap = null; enabled = false; window.updateTerrainButton?.(false); return false; } console.log("[Terrain3D] attivazione completata"); return true; } function disable() { if (!mapboxMap) { enabled = false; window.updateTerrainButton?.(false); return; } destroyMapbox3D(); enabled = false; window.updateTerrainButton?.(false); } async function toggle() { if (enabled) { disable(); } else { await enable(); } } function isEnabled() { return enabled; } function isActivityItem(item) { if (!item) { return false; } const format = String( item.format || item.file_extension || item.mime_type || item.file_name || item.name || "", ).toLowerCase(); return ( format.includes("fit") || format.includes("gpx") || format.includes("tcx") || item.distance_meters != null || item.duration_millis != null || item.track_preview != null ); } window.refreshTerrainMarkers = async function () { if (!mapboxMap) { return; } if (!enabled) { return; } if (destroying) { return; } const items = typeof window.getLocalItems === "function" ? window.getLocalItems() : []; const isActivitiesMode = window.state?.mode === "activities"; let photos = []; let activities = []; let routes = []; if (isActivitiesMode) { window.showActivityRoutesToggle?.(); if ( window.MapboxPhotoSpiderfy ?.isOpen?.() ) { window.MapboxPhotoSpiderfy ?.clear?.(); } window.MapboxPhotoMarkers ?.clear?.(); window.MapboxPhotoClusters ?.clear?.(); window.MapboxMarkers ?.setPhotos?.([]); activities = typeof window .getActivityMapGeoJSON === "function" ? window .getActivityMapGeoJSON() : []; routes = typeof window .getActivityRoutesGeoJSON === "function" ? window .getActivityRoutesGeoJSON() : []; if ( activities.length === 0 && typeof window .renderActivityMapLayer === "function" ) { console.log( "[Terrain3D] caricamento iniziale attività", { localItems: items.length, }, ); await window .renderActivityMapLayer(); activities = typeof window .getActivityMapGeoJSON === "function" ? window .getActivityMapGeoJSON() : []; routes = typeof window .getActivityRoutesGeoJSON === "function" ? window .getActivityRoutesGeoJSON() : []; } window.MapboxMarkers ?.setActivities?.( activities, ); const showRoutes = window .areActivityRoutesVisible?.() ?? false; window.MapboxMarkers ?.setRoutes?.( showRoutes ? routes : [], ); } else { window.hideActivityRoutesToggle?.( true, ); if ( window.MapboxActivitySpiderfy ?.isOpen?.() ) { window.MapboxActivitySpiderfy ?.clear?.(); } window.MapboxActivityMarkers ?.clear?.(); window.MapboxActivityClusters ?.clear?.(); window.MapboxMarkers ?.setActivities?.([]); window.MapboxMarkers ?.setRoutes?.([]); photos = typeof window .getPhotoMapGeoJSON === "function" ? window .getPhotoMapGeoJSON() : []; window.MapboxMarkers ?.setPhotos?.( photos, ); } console.log("[Terrain3D] marker aggiornati", { mode: isActivitiesMode ? "activities" : "photos", localItems: items.length, photos: photos.length, activities: activities.length, routesAvailable: routes.length, routesVisible: isActivitiesMode ? (window.areActivityRoutesVisible?.() ?? false) : false, styleLoaded: !!mapboxMap?.isStyleLoaded?.(), moving: !!mapboxMap?.isMoving?.(), }); }; window.MapTerrain3D = Object.freeze({ enable, disable, toggle, enabled: isEnabled, }); })();