2614 lines
44 KiB
JavaScript
2614 lines
44 KiB
JavaScript
// 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("&", "&")
|
||
.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();
|
||
}
|
||
});
|
||
});
|