galleria_photo_face_rknn_op.../public/js/activityModal.js
2026-10-03 12:16:58 +02:00

2614 lines
44 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 = `
<div class="activity-modal-wrapper">
<div
class="activity-map-style-control leaflet-control-layers-toggle"
title="Tipo di mappa"
>
<select
id="activityMapStyle"
class="activity-map-style-select"
aria-label="Tipo di mappa"
title="Tipo di mappa"
></select>
</div>
<div
id="activityTerrainControl"
class="activity-terrain-control"
hidden
>
<label for="activityTerrainExaggeration">
Altezza
</label>
<input
id="activityTerrainExaggeration"
class="activity-terrain-range"
type="range"
min="0"
max="3"
step="0.1"
value="${activityTerrainExaggeration}"
aria-label="Esagerazione delle altezze del terreno"
/>
<output
id="activityTerrainExaggerationValue"
for="activityTerrainExaggeration"
>
${activityTerrainExaggeration.toFixed(1)}×
</output>
</div>
<div
id="activityMap"
aria-busy="true"
></div>
<div
id="activityRouteLoading"
class="activity-route-loading"
role="status"
aria-live="polite"
>
<span
class="activity-route-loading-spinner"
aria-hidden="true"
></span>
<span>
Caricamento traccia completa...
</span>
</div>
<button
id="activityInfoBtn"
class="modal-info-btn activity-info-btn"
type="button"
aria-label="Informazioni attività"
>
ℹ️
</button>
<button
id="activityMediaBtn"
class="activity-media-btn"
type="button"
aria-label="Mostra foto e video dell'attività"
title="Mostra foto e video dell'attività"
>
Foto
</button>
<div class="activity-modal-info">
<div class="activity-modal-title">
${escapeActivityHtml(
title,
)}
</div>
${
item.sport
? `
<div class="activity-modal-meta">
${escapeActivityHtml(
item.sport,
)}
</div>
`
: ""
}
<div class="activity-modal-meta">
${
Number.isFinite(
distance,
)
? `${(
distance /
1000
).toFixed(
1,
)} km`
: "Distanza non disponibile"
}
·
${formatActivityModalDuration(
duration,
)}
</div>
${
Number.isFinite(
elevation,
) &&
elevation > 0
? `
<div class="activity-modal-meta">
⛰ ${Math.round(
elevation,
)} m D+
</div>
`
: ""
}
</div>
</div>
`;
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("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
}
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();
}
});
});