// public/js/activityModal.js let activityMap = null; let activityMapbox = null; let activityMapLayer = null; let activityMapRenderToken = 0; let activityModalLoadToken = 0; let activityRouteLayer = null; let activityStartMarker = null; let activityEndMarker = null; let activityCurrentLatLngs = []; let activityCurrentItem = null; let activityMediaVisible = false; let activityMediaItems = []; let activityMediaLeafletLayer = null; let activityMediaMapboxMarkers = []; const savedActivityTerrainExaggerationRaw = localStorage.getItem( "activityTerrainExaggeration", ); const savedActivityTerrainExaggeration = savedActivityTerrainExaggerationRaw !== null ? Number( savedActivityTerrainExaggerationRaw, ) : NaN; let activityTerrainExaggeration = Number.isFinite( savedActivityTerrainExaggeration, ) ? Math.min( 3, Math.max( 0, savedActivityTerrainExaggeration, ), ) : 1.5; function clearActivityMediaMarkers() { if (activityMediaLeafletLayer) { try { activityMediaLeafletLayer.remove(); } catch {} activityMediaLeafletLayer = null; } for ( const marker of activityMediaMapboxMarkers ) { try { marker.remove(); } catch {} } activityMediaMapboxMarkers = []; } function destroyActivityMap() { activityMapRenderToken++; clearActivityMediaMarkers(); if (activityMapLayer) { try { activityMapLayer.remove?.(); } catch {} activityMapLayer = null; } if (activityMap) { try { activityMap.remove(); } catch {} activityMap = null; } if (activityMapbox) { try { activityMapbox.remove(); } catch {} activityMapbox = null; } const mapElement = document.getElementById( "activityMap", ); if (mapElement) { mapElement.replaceChildren(); mapElement.classList.remove( "mapboxgl-map", ); } activityRouteLayer = null; activityStartMarker = null; activityEndMarker = null; activityCurrentLatLngs = []; activityCurrentItem = null; } function closeActivityModal() { activityModalLoadToken++; activityMediaVisible = false; activityMediaItems = []; clearActivityMediaMarkers(); const modal = document.getElementById( "activityModal", ); const container = document.getElementById( "activityModalContent", ); const focusedElement = document.activeElement; if ( modal && focusedElement instanceof HTMLElement && modal.contains( focusedElement, ) ) { focusedElement.blur(); } destroyActivityMap(); if (container) { container.innerHTML = ""; } if (modal) { modal.classList.remove( "open", ); modal.setAttribute( "aria-hidden", "true", ); modal.inert = true; } window.currentActivity = null; if ( typeof window.closeInfoPanel === "function" ) { window.closeInfoPanel(); } document.body.style.overflow = ""; document .getElementById( "modeToggle", ) ?.focus?.({ preventScroll: true, }); } function findMediaForActivity( activity, ) { const ONE_HOUR_MS = 60 * 60 * 1000; const activityStart = Number( activity?.start_time_millis, ); const activityEnd = Number( activity?.end_time_millis, ); const minLatitude = Number( activity?.min_latitude, ); const maxLatitude = Number( activity?.max_latitude, ); const minLongitude = Number( activity?.min_longitude, ); const maxLongitude = Number( activity?.max_longitude, ); if ( !Number.isFinite( activityStart, ) || !Number.isFinite( activityEnd, ) || !Number.isFinite( minLatitude, ) || !Number.isFinite( maxLatitude, ) || !Number.isFinite( minLongitude, ) || !Number.isFinite( maxLongitude, ) ) { console.warn( "[activity media] Dati attività incompleti", { activityStart, activityEnd, minLatitude, maxLatitude, minLongitude, maxLongitude, }, ); return []; } const rangeStart = activityStart - ONE_HOUR_MS; const rangeEnd = activityEnd + ONE_HOUR_MS; const mediaItems = window .getLocalItemsForMode?.( "photos", ) || []; const results = mediaItems.filter( (media) => { const isImage = media.mime_type ?.startsWith( "image/", ); const isVideo = media.mime_type ?.startsWith( "video/", ); if ( !isImage && !isVideo ) { return false; } if (media.deleted_at) { return false; } const latitude = Number( media.gps?.lat, ); const longitude = Number( media.gps?.lng, ); const mediaTime = media.taken_at ? new Date( media.taken_at, ).getTime() : NaN; if ( !Number.isFinite( latitude, ) || !Number.isFinite( longitude, ) || !Number.isFinite( mediaTime, ) ) { return false; } const insideTime = mediaTime >= rangeStart && mediaTime <= rangeEnd; const insideBounds = latitude >= minLatitude && latitude <= maxLatitude && longitude >= minLongitude && longitude <= maxLongitude; return ( insideTime && insideBounds ); }, ); results.sort( (first, second) => new Date( first.taken_at, ).getTime() - new Date( second.taken_at, ).getTime(), ); console.log( "[activity media] risultati", { activityId: activity.id, totalMedia: mediaItems.length, found: results.length, rangeStart: new Date( rangeStart, ).toISOString(), rangeEnd: new Date( rangeEnd, ).toISOString(), bounds: { minLatitude, maxLatitude, minLongitude, maxLongitude, }, }, ); return results; } window.openActivityModal = async function ( item, list = [], index = 0, ) { window.closeBottomSheet?.(); const modal = document.getElementById( "activityModal", ); const container = document.getElementById( "activityModalContent", ); if ( !modal || !container ) { console.error( "[activity modal] Mancano activityModal o activityModalContent", ); return; } if (!item) { return; } const loadToken = ++activityModalLoadToken; destroyActivityMap(); activityMediaVisible = false; activityMediaItems = []; window.currentActivity = item; window.activityModalList = Array.isArray(list) ? list : []; window.activityModalIndex = Number.isInteger(index) ? index : 0; activityCurrentItem = item; const title = item.display_name || item.route_name || item.title || item.file_name || item.name || "Attività"; const distance = Number( item.distance_meters, ); const duration = Number( item.duration_millis, ); const elevation = Number( item.elevation_gain_meters, ); const previewPoints = Array.isArray( item.track_preview, ) ? item.track_preview : []; const previewLatLngs = getActivityLatLngs( previewPoints, ); activityCurrentLatLngs = previewLatLngs; container.innerHTML = `
Caricamento traccia completa...
${escapeActivityHtml( title, )}
${ item.sport ? `
${escapeActivityHtml( item.sport, )}
` : "" }
${ Number.isFinite( distance, ) ? `${( distance / 1000 ).toFixed( 1, )} km` : "Distanza non disponibile" } · ${formatActivityModalDuration( duration, )}
${ Number.isFinite( elevation, ) && elevation > 0 ? `
⛰ ${Math.round( elevation, )} m D+
` : "" }
`; modal.inert = false; modal.classList.add( "open", ); modal.setAttribute( "aria-hidden", "false", ); document.body.style.overflow = "hidden"; const activityInfoBtn = document.getElementById( "activityInfoBtn", ); const activityMediaBtn = document.getElementById( "activityMediaBtn", ); initializeActivityTerrainControl(); if (activityMediaBtn) { activityMediaBtn.setAttribute( "aria-pressed", "false", ); activityMediaBtn.onclick = (event) => { event.preventDefault(); event.stopPropagation(); event.stopImmediatePropagation(); /* * Primo click: esegue la ricerca. * I click successivi riutilizzano * i risultati già trovati. */ if ( activityMediaItems.length === 0 ) { activityMediaItems = findMediaForActivity( item, ); } if ( activityMediaItems.length === 0 ) { activityMediaVisible = false; activityMediaBtn.classList.remove( "active", ); activityMediaBtn.setAttribute( "aria-pressed", "false", ); activityMediaBtn.title = "Nessuna foto o video trovati"; console.log( "[activity media] nessun risultato", { activityId: item.id, }, ); return; } activityMediaVisible = !activityMediaVisible; activityMediaBtn.classList.toggle( "active", activityMediaVisible, ); activityMediaBtn.setAttribute( "aria-pressed", String( activityMediaVisible, ), ); activityMediaBtn.title = activityMediaVisible ? "Nascondi foto e video" : "Mostra foto e video"; renderActivityMediaMarkers(); console.log( "[activity media] visibilità", { activityId: item.id, visible: activityMediaVisible, media: activityMediaItems.length, engine: activityMapbox ? "mapbox" : activityMap ? "leaflet" : null, }, ); }; } if (activityInfoBtn) { activityInfoBtn.onclick = (event) => { event.preventDefault(); event.stopPropagation(); event.stopImmediatePropagation(); window.currentActivity = item; if ( typeof window .toggleInfoPanel === "function" ) { window.toggleInfoPanel( item, ); } else if ( typeof window .openInfoPanel === "function" ) { window.openInfoPanel( item, ); } else { console.error( "[activity info] Nessuna funzione info panel disponibile", ); } }; } /* * Attende che il modal sia realmente * visibile e dimensionato. */ await new Promise( (resolve) => { requestAnimationFrame( () => { requestAnimationFrame( resolve, ); }, ); }, ); /* * Interrompe l'apertura se nel frattempo * il modal è stato chiuso o è stata aperta * un'altra attività. */ if ( loadToken !== activityModalLoadToken || !modal.classList.contains( "open", ) || window.currentActivity !== item ) { return; } const mapElement = document.getElementById( "activityMap", ); if (!mapElement) { return; } /* * Inizializza subito la basemap usando * track_preview per la traccia iniziale * e per il calcolo dei bounds. */ initializeActivityMapSelector( item, previewPoints, ); console.log( "[activity modal] preview iniziale", { id: item.id, previewPoints: previewLatLngs.length, expected: Number( item.point_count, ) || null, }, ); /* * Recupera in background la traccia * completa. La basemap è già visibile. */ let fullPoints = []; try { if ( typeof window .getActivityRoutePoints !== "function" ) { throw new Error( "Funzione getActivityRoutePoints non disponibile", ); } fullPoints = await window .getActivityRoutePoints( item, { full: true, }, ); } catch (error) { console.error( "[activity modal] Errore caricamento traccia completa:", error, ); if ( loadToken !== activityModalLoadToken || !modal.classList.contains( "open", ) || window.currentActivity !== item ) { return; } const loadingIndicator = document.getElementById( "activityRouteLoading", ); if (loadingIndicator) { loadingIndicator.classList.add( "is-error", ); loadingIndicator.replaceChildren(); const errorText = document.createElement( "span", ); errorText.textContent = "Traccia completa non disponibile"; loadingIndicator.appendChild( errorText, ); setTimeout( () => { if ( loadToken === activityModalLoadToken && window.currentActivity === item ) { loadingIndicator.remove(); } }, 3000, ); } mapElement.setAttribute( "aria-busy", "false", ); /* * La preview resta visibile anche * quando il caricamento completo fallisce. */ return; } /* * Ignora una risposta tardiva se il modal * è stato chiuso oppure se è stata aperta * un'altra attività. */ if ( loadToken !== activityModalLoadToken || !modal.classList.contains( "open", ) || window.currentActivity !== item ) { return; } if ( !Array.isArray( fullPoints, ) ) { fullPoints = []; } console.log( "[activity modal] traccia completa", { id: item.id, expected: Number( item.point_count, ) || null, loaded: fullPoints.length, preview: previewPoints.length, format: item.format || item.file_extension || null, }, ); if ( fullPoints.length >= 2 ) { const fullLatLngs = getActivityLatLngs( fullPoints, ); activityCurrentItem = item; activityCurrentLatLngs = fullLatLngs; /* * Aggiorna solamente la geometria. * * Leaflet aggiorna polyline e marker. * Mapbox aggiorna la source GeoJSON. * La basemap non viene ricreata. */ const updated = updateActivityModalRoute( item, fullPoints, false, ); /* * Se Mapbox sta ancora caricando lo stile, * updateActivityModalRoute restituisce false. * I punti completi restano comunque salvati * in activityCurrentLatLngs e saranno usati * dal callback "load" di Mapbox. */ if (!updated) { console.log( "[activity modal] aggiornamento traccia differito", { id: item.id, points: fullLatLngs.length, mapboxLoading: Boolean( activityMapbox && !activityMapbox .isStyleLoaded?.(), ), }, ); } } else { console.warn( "[activity modal] Traccia completa senza punti GPS sufficienti", { id: item.id, loaded: fullPoints.length, preview: previewPoints.length, }, ); } mapElement.setAttribute( "aria-busy", "false", ); const loadingIndicator = document.getElementById( "activityRouteLoading", ); loadingIndicator?.remove(); }; function getActivityLatLngs( points, ) { if (!Array.isArray(points)) { return []; } return points .map( (point) => [ Number(point?.lat), Number(point?.lng), ], ) .filter( ([lat, lng]) => Number.isFinite(lat) && Number.isFinite(lng), ); } function getActivityMapCenter( item, latlngs, ) { if (latlngs.length > 0) { return latlngs[0]; } const latitude = Number( item?.center_latitude, ); const longitude = Number( item?.center_longitude, ); if ( Number.isFinite(latitude) && Number.isFinite(longitude) ) { return [ latitude, longitude, ]; } return [42.5, 12.5]; } function updateActivityLeafletRoute( item, latlngs, fitBounds = true, ) { if (!activityMap) { return; } if (activityRouteLayer) { activityRouteLayer.remove(); activityRouteLayer = null; } if (activityStartMarker) { activityStartMarker.remove(); activityStartMarker = null; } if (activityEndMarker) { activityEndMarker.remove(); activityEndMarker = null; } if (latlngs.length >= 2) { activityRouteLayer = L.polyline( latlngs, { color: "#2b8cff", weight: 5, opacity: 1, lineCap: "round", lineJoin: "round", }, ).addTo(activityMap); activityStartMarker = L.circleMarker( latlngs[0], { radius: 6, color: "#087f23", fillColor: "#18b83f", fillOpacity: 1, }, ) .bindTooltip("Partenza") .addTo(activityMap); activityEndMarker = L.circleMarker( latlngs[ latlngs.length - 1 ], { radius: 6, color: "#b00020", fillColor: "#e32636", fillOpacity: 1, }, ) .bindTooltip("Arrivo") .addTo(activityMap); if (fitBounds) { activityMap.fitBounds( activityRouteLayer.getBounds(), { paddingTopLeft: [30, 70], paddingBottomRight: [30, 120], maxZoom: 16, }, ); } return; } activityMap.setView( getActivityMapCenter( item, latlngs, ), 12, ); } function renderActivityMediaLeaflet() { if ( !activityMap || !activityMediaVisible || activityMediaItems.length === 0 ) { return; } if (activityMediaLeafletLayer) { try { activityMediaLeafletLayer.remove(); } catch {} } activityMediaLeafletLayer = L.layerGroup(); for ( const media of activityMediaItems ) { const latitude = Number( media?.gps?.lat, ); const longitude = Number( media?.gps?.lng, ); if ( !Number.isFinite(latitude) || !Number.isFinite(longitude) ) { continue; } let marker = window.PhotoMarkerFactory ?.createIndependent?.( media, ); /* * Fallback nel caso createIndependent() * non sia ancora disponibile. */ if (!marker) { const thumb = window.toAbsoluteUrl?.( media.thub1 || media.thub2 || media.path, media.name, "thumbs", media.cartella, ) || ""; const icon = L.icon({ iconUrl: thumb, iconSize: [46, 46], iconAnchor: [23, 23], className: "photo-marker activity-media-marker", }); marker = L.marker( [ latitude, longitude, ], { icon, title: media.display_name || media.name || "", }, ); marker.on( "click", () => { window.openPhotoMapItem?.( media, ); }, ); } marker.addTo( activityMediaLeafletLayer, ); } activityMediaLeafletLayer.addTo( activityMap, ); console.log( "[activity media] marker Leaflet", { media: activityMediaItems.length, markers: activityMediaLeafletLayer .getLayers?.() ?.length ?? null, }, ); } function renderActivityLeafletMap( mapElement, styleId, item, latlngs, ) { destroyActivityMap(); activityCurrentItem = item; activityCurrentLatLngs = latlngs; activityMap = L.map( mapElement, { zoomControl: true, attributionControl: true, }, ); activityMapLayer = window.MapBaseLayers ?.createLeafletLayer?.( styleId, ); if (!activityMapLayer) { activityMapLayer = window.MapBaseLayers ?.createLeafletLayer?.( "osm", ); } activityMapLayer?.addTo?.( activityMap, ); updateActivityLeafletRoute( item, latlngs, true, ); renderActivityMediaMarkers(); requestAnimationFrame( () => { activityMap ?.invalidateSize?.( true, ); }, ); } function updateActivityMapboxRoute( item, latlngs, fitBounds = true, ) { if ( !activityMapbox || !activityMapbox.isStyleLoaded?.() ) { return false; } if ( !Array.isArray(latlngs) || latlngs.length < 2 ) { return false; } const coordinates = latlngs .map( ([lat, lng]) => [ Number(lng), Number(lat), ], ) .filter( ([lng, lat]) => Number.isFinite(lng) && Number.isFinite(lat), ); if (coordinates.length < 2) { return false; } const data = { type: "FeatureCollection", features: [ { type: "Feature", geometry: { type: "LineString", coordinates, }, properties: {}, }, ], }; const source = activityMapbox.getSource( "activity-modal-route", ); if (source) { source.setData( data, ); } else { activityMapbox.addSource( "activity-modal-route", { type: "geojson", data, }, ); } if ( !activityMapbox.getLayer( "activity-modal-route-line", ) ) { activityMapbox.addLayer({ id: "activity-modal-route-line", type: "line", source: "activity-modal-route", layout: { "line-cap": "round", "line-join": "round", }, paint: { "line-color": "#2b8cff", "line-width": 5, "line-opacity": 1, }, }); } /* * Quando arriva la traccia FIT completa, * aggiorna soltanto la source GeoJSON. * Non modifica la camera impostata * dall'utente. */ if (!fitBounds) { console.log( "[activity modal] traccia Mapbox aggiornata senza modificare la camera", { points: coordinates.length, zoom: activityMapbox.getZoom?.(), pitch: activityMapbox.getPitch?.(), bearing: activityMapbox.getBearing?.(), center: activityMapbox .getCenter?.(), }, ); return true; } /* * I bounds vengono applicati soltanto * durante il rendering iniziale, * normalmente usando la preview. */ const bounds = new mapboxgl.LngLatBounds(); for ( const coordinate of coordinates ) { bounds.extend( coordinate, ); } const isTerrain3D = activityMapbox .getTerrain?.() != null; activityMapbox.fitBounds( bounds, { padding: { top: 70, right: 30, bottom: 120, left: 30, }, maxZoom: 16, duration: 0, pitch: isTerrain3D ? 60 : 0, bearing: isTerrain3D ? -20 : 0, }, ); console.log( "[activity modal] traccia Mapbox aggiornata con bounds", { points: coordinates.length, terrain: isTerrain3D, }, ); return true; } function renderActivityMediaMapbox() { if ( !activityMapbox || !activityMediaVisible || activityMediaItems.length === 0 ) { return; } for ( const marker of activityMediaMapboxMarkers ) { try { marker.remove(); } catch {} } activityMediaMapboxMarkers = []; for ( const media of activityMediaItems ) { const latitude = Number( media?.gps?.lat, ); const longitude = Number( media?.gps?.lng, ); if ( !Number.isFinite(latitude) || !Number.isFinite(longitude) ) { continue; } const thumb = window.toAbsoluteUrl?.( media.thub1 || media.thub2 || media.path, media.name, "thumbs", media.cartella, ) || ""; const element = document.createElement( "button", ); element.type = "button"; element.className = "activity-media-mapbox-marker"; element.setAttribute( "aria-label", media.name || "Apri contenuto", ); const image = document.createElement( "img", ); image.src = thumb; image.alt = media.name || ""; image.width = 46; image.height = 46; element.appendChild( image, ); if ( media.mime_type ?.startsWith( "video/", ) ) { const play = document.createElement( "span", ); play.className = "activity-media-video-icon"; play.textContent = "▶"; play.setAttribute( "aria-hidden", "true", ); element.appendChild( play, ); } element.addEventListener( "pointerdown", (event) => { event.stopPropagation(); }, ); element.addEventListener( "click", (event) => { event.preventDefault(); event.stopPropagation(); window.openPhotoMapItem?.( media, ); }, ); const marker = new mapboxgl.Marker({ element, anchor: "center", }) .setLngLat([ longitude, latitude, ]) .addTo( activityMapbox, ); marker.__item = media; activityMediaMapboxMarkers.push( marker, ); } console.log( "[activity media] marker Mapbox", { media: activityMediaItems.length, markers: activityMediaMapboxMarkers.length, }, ); } function renderActivityMediaMarkers() { clearActivityMediaMarkers(); if ( !activityMediaVisible || activityMediaItems.length === 0 ) { return; } if (activityMap) { renderActivityMediaLeaflet(); return; } if (activityMapbox) { renderActivityMediaMapbox(); } } function updateActivityTerrainControlVisibility( styleId, ) { const control = document.getElementById( "activityTerrainControl", ); if (!control) { return; } control.hidden = styleId !== "terrain3d"; } function applyActivityTerrainExaggeration( exaggeration, ) { const numericValue = Number( exaggeration, ); const normalizedValue = Number.isFinite( numericValue, ) ? Math.min( 3, Math.max( 0, numericValue, ), ) : 1.5; activityTerrainExaggeration = normalizedValue; localStorage.setItem( "activityTerrainExaggeration", String( normalizedValue, ), ); const output = document.getElementById( "activityTerrainExaggerationValue", ); if (output) { output.textContent = `${normalizedValue.toFixed(1)}×`; } if ( !activityMapbox || !activityMapbox.isStyleLoaded?.() || !activityMapbox.getSource( "activity-modal-dem", ) ) { return; } activityMapbox.setTerrain({ source: "activity-modal-dem", exaggeration: normalizedValue, }); console.log( "[activity modal] altezza Terrain 3D", { exaggeration: normalizedValue, zoom: activityMapbox.getZoom?.(), pitch: activityMapbox.getPitch?.(), bearing: activityMapbox.getBearing?.(), }, ); } function initializeActivityTerrainControl() { const range = document.getElementById( "activityTerrainExaggeration", ); const output = document.getElementById( "activityTerrainExaggerationValue", ); if (!range) { return; } range.value = String( activityTerrainExaggeration, ); if (output) { output.textContent = `${activityTerrainExaggeration.toFixed(1)}×`; } range.addEventListener( "input", () => { applyActivityTerrainExaggeration( range.value, ); }, ); } function renderActivityMapboxMap( mapElement, styleId, item, latlngs, ) { destroyActivityMap(); const token = window.AppConfig ?.mapboxToken || ""; if ( !token || !window.mapboxgl ) { renderActivityLeafletMap( mapElement, "osm", item, latlngs, ); return; } const renderToken = ++activityMapRenderToken; const isTerrain3D = styleId === "terrain3d"; mapboxgl.accessToken = token; activityCurrentItem = item; activityCurrentLatLngs = Array.isArray(latlngs) ? latlngs : []; const centerLatLng = getActivityMapCenter( item, activityCurrentLatLngs, ); activityMapbox = new mapboxgl.Map({ container: mapElement, style: window.MapBaseLayers ?.getMapboxStyleUrl?.( styleId, ) || "mapbox://styles/mapbox/outdoors-v12", center: [ centerLatLng[1], centerLatLng[0], ], zoom: 10, pitch: isTerrain3D ? 60 : 0, bearing: isTerrain3D ? -20 : 0, antialias: true, }); activityMapbox.addControl( new mapboxgl.NavigationControl({ showCompass: true, showZoom: true, visualizePitch: true, }), "top-left", ); activityMapbox.on( "load", () => { if ( !activityMapbox || renderToken !== activityMapRenderToken ) { return; } /* * Attiva il DEM e l'inclinazione * soltanto per Terrain 3D. */ if ( isTerrain3D && !activityMapbox.getSource( "activity-modal-dem", ) ) { activityMapbox.addSource( "activity-modal-dem", { type: "raster-dem", url: "mapbox://mapbox.mapbox-terrain-dem-v1", tileSize: 512, maxzoom: 14, }, ); activityMapbox.setTerrain({ source: "activity-modal-dem", exaggeration: activityTerrainExaggeration, }); } const terrainOutput = document.getElementById( "activityTerrainExaggerationValue", ); if (terrainOutput) { terrainOutput.textContent = `${activityTerrainExaggeration.toFixed(1)}×`; } /* * Usa i punti correnti. * * Normalmente, al primo caricamento, * questi punti appartengono alla preview. * * Se nel frattempo è arrivata la traccia * completa, activityCurrentLatLngs contiene * già tutti i punti FIT. */ const currentLatLngs = Array.isArray( activityCurrentLatLngs, ) ? activityCurrentLatLngs : []; if ( currentLatLngs.length < 2 ) { const currentCenter = getActivityMapCenter( activityCurrentItem || item, currentLatLngs, ); activityMapbox.jumpTo({ center: [ currentCenter[1], currentCenter[0], ], zoom: 12, pitch: isTerrain3D ? 60 : 0, bearing: isTerrain3D ? -20 : 0, }); renderActivityMediaMarkers(); return; } const coordinates = currentLatLngs .map( ([lat, lng]) => [ Number(lng), Number(lat), ], ) .filter( ([lng, lat]) => Number.isFinite(lng) && Number.isFinite(lat), ); if ( coordinates.length < 2 ) { renderActivityMediaMarkers(); return; } const routeData = { type: "FeatureCollection", features: [ { type: "Feature", geometry: { type: "LineString", coordinates, }, properties: {}, }, ], }; /* * La source normalmente non esiste * al primo caricamento. * * Il controllo rende però la funzione * sicura anche se il callback viene * eseguito dopo un aggiornamento. */ const existingSource = activityMapbox.getSource( "activity-modal-route", ); if (existingSource) { existingSource.setData( routeData, ); } else { activityMapbox.addSource( "activity-modal-route", { type: "geojson", data: routeData, }, ); } if ( !activityMapbox.getLayer( "activity-modal-route-line", ) ) { activityMapbox.addLayer({ id: "activity-modal-route-line", type: "line", source: "activity-modal-route", layout: { "line-cap": "round", "line-join": "round", }, paint: { "line-color": "#2b8cff", "line-width": 5, "line-opacity": 1, }, }); } const bounds = new mapboxgl.LngLatBounds(); for ( const coordinate of coordinates ) { bounds.extend( coordinate, ); } activityMapbox.fitBounds( bounds, { padding: { top: 70, right: 30, bottom: 120, left: 30, }, maxZoom: 16, duration: 0, pitch: isTerrain3D ? 60 : 0, bearing: isTerrain3D ? -20 : 0, }, ); renderActivityMediaMarkers(); console.log( "[activity modal] Mapbox inizializzata", { styleId, terrain: isTerrain3D, points: coordinates.length, previewPoints: Array.isArray( item?.track_preview, ) ? item.track_preview .length : 0, }, ); }, ); activityMapbox.on( "error", (event) => { if ( renderToken !== activityMapRenderToken ) { return; } console.error( "[activity modal] Errore Mapbox:", event?.error || event, ); }, ); } function updateActivityModalRoute( item, points, fitBounds = false, ) { const latlngs = getActivityLatLngs( points, ); if ( latlngs.length < 2 ) { return false; } activityCurrentItem = item; activityCurrentLatLngs = latlngs; if (activityMap) { updateActivityLeafletRoute( item, latlngs, fitBounds, ); return true; } if (activityMapbox) { if ( activityMapbox .isStyleLoaded?.() ) { return ( updateActivityMapboxRoute( item, latlngs, fitBounds, ) !== false ); } return false; } return false; } function initializeActivityMapSelector( item, points, ) { const mapElement = document.getElementById( "activityMap", ); const select = document.getElementById( "activityMapStyle", ); if ( !mapElement || !select ) { console.error( "[activity modal] Mappa o selettore non disponibile", ); return; } select.disabled = false; const latlngs = getActivityLatLngs( points, ); const availableStyles = window.MapBaseLayers ?.getAvailableStyles?.() || []; const hasMapboxToken = Boolean( window.AppConfig ?.mapboxToken, ); const fallbackStyles = [ { id: "osm", label: "OpenStreetMap", }, { id: "opentopo", label: "Topografica", }, ]; if (hasMapboxToken) { fallbackStyles.push( { id: "mapboxStreets", label: "Mapbox Streets", }, { id: "mapboxOutdoors", label: "Mapbox Outdoors", }, { id: "mapboxSatellite", label: "Mapbox Satellite", }, { id: "mapboxSatelliteStreets", label: "Mapbox Satellite + strade", }, { id: "terrain3d", label: "Terrain 3D", }, ); } const styles = availableStyles.length > 0 ? availableStyles : fallbackStyles; select.replaceChildren(); for (const style of styles) { const option = document.createElement( "option", ); option.value = style.id; option.textContent = style.label; select.appendChild( option, ); } const savedStyle = localStorage.getItem( "selectedMapStyle", ); const savedStyleAvailable = styles.some( (style) => style.id === savedStyle, ); const initialStyle = savedStyleAvailable ? savedStyle : "osm"; select.value = initialStyle; function renderSelectedStyle( styleId, ) { const selectedStyle = styles.some( (style) => style.id === styleId, ) ? styleId : "osm"; updateActivityTerrainControlVisibility( selectedStyle, ); const currentLatLngs = activityCurrentLatLngs .length >= 2 ? activityCurrentLatLngs : latlngs; if ( selectedStyle === "terrain3d" ) { renderActivityMapboxMap( mapElement, selectedStyle, activityCurrentItem || item, currentLatLngs, ); return; } renderActivityLeafletMap( mapElement, selectedStyle, activityCurrentItem || item, currentLatLngs, ); } select.addEventListener( "change", () => { const selectedStyle = select.value; localStorage.setItem( "selectedMapStyle", selectedStyle, ); renderSelectedStyle( selectedStyle, ); }, ); renderSelectedStyle( initialStyle, ); } function formatActivityModalDuration(milliseconds) { if (!Number.isFinite(milliseconds)) { return "Durata non disponibile"; } const totalSeconds = Math.round(milliseconds / 1000); const hours = Math.floor(totalSeconds / 3600); const minutes = Math.floor((totalSeconds % 3600) / 60); return hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`; } function escapeActivityHtml(value) { return String(value ?? "") .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); } window.closeActivityModal = closeActivityModal; document.addEventListener("DOMContentLoaded", () => { const modal = document.getElementById("activityModal"); const closeButton = document.getElementById("activityModalClose"); closeButton?.addEventListener("click", event => { event.stopPropagation(); closeActivityModal(); }); modal?.addEventListener("click", event => { if (event.target === modal) { closeActivityModal(); } }); document.addEventListener("keydown", event => { if ( event.key === "Escape" && modal?.classList.contains("open") ) { closeActivityModal(); } }); });