// 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();
}
});
});