1087 lines
24 KiB
JavaScript
1087 lines
24 KiB
JavaScript
// public/js/map/activitiesLayer.js
|
|
// ===============================
|
|
// LAYER MAPPA ATTIVITÀ
|
|
// ===============================
|
|
|
|
(function () {
|
|
const ACTIVITY_ROUTE_COLORS = [
|
|
"#e6194b",
|
|
"#3cb44b",
|
|
"#4363d8",
|
|
"#f58231",
|
|
"#911eb4",
|
|
"#46f0f0",
|
|
"#f032e6",
|
|
"#bcf60c",
|
|
"#008080",
|
|
"#9a6324",
|
|
"#800000",
|
|
"#000075",
|
|
"#ff6b00",
|
|
"#00a8e8",
|
|
"#8d6e63",
|
|
"#2e7d32",
|
|
];
|
|
|
|
const MAX_GLOBAL_ACTIVITY_ROUTES = 80;
|
|
|
|
let activityRoutesLayer = null;
|
|
let selectedActivityLayer = null;
|
|
let selectedActivityEndMarker = null;
|
|
let showAllActivityRoutes = false;
|
|
let cachedRouteData = [];
|
|
let lastRenderToken = 0;
|
|
let activityStartMarkers = [];
|
|
|
|
// ===============================
|
|
// CLUSTER ATTIVITÀ
|
|
// ===============================
|
|
|
|
|
|
window.createActivityClusterIcon = function (cluster) {
|
|
const count = cluster.getChildCount();
|
|
|
|
const cls =
|
|
count > 200
|
|
? "cluster-xl"
|
|
: count > 50
|
|
? "cluster-lg"
|
|
: count > 10
|
|
? "cluster-md"
|
|
: "cluster-sm";
|
|
|
|
return L.divIcon({
|
|
html: `
|
|
<div class="leaflet-activity-cluster ${cls}">
|
|
<span>${count}</span>
|
|
</div>
|
|
`,
|
|
|
|
className: "activity-leaflet-cluster",
|
|
|
|
iconSize: L.point(48, 48),
|
|
|
|
iconAnchor: L.point(24, 24),
|
|
});
|
|
};
|
|
|
|
// ===============================
|
|
// PULIZIA LAYER ATTIVITÀ
|
|
// ===============================
|
|
|
|
window.clearActivityMapLayers = function () {
|
|
clearActivityStartMarkers();
|
|
|
|
if (activityRoutesLayer) {
|
|
activityRoutesLayer.clearLayers();
|
|
}
|
|
|
|
if (selectedActivityLayer) {
|
|
selectedActivityLayer.remove();
|
|
selectedActivityLayer = null;
|
|
}
|
|
|
|
if (selectedActivityEndMarker) {
|
|
selectedActivityEndMarker.remove();
|
|
selectedActivityEndMarker = null;
|
|
}
|
|
};
|
|
|
|
// ===============================
|
|
// PULIZIA MARKERS
|
|
// ===============================
|
|
|
|
function clearActivityStartMarkers() {
|
|
const markersToRemove = [...activityStartMarkers];
|
|
|
|
activityStartMarkers.length = 0;
|
|
|
|
for (const marker of markersToRemove) {
|
|
try {
|
|
window.globalMarkers?.removeLayer?.(marker);
|
|
|
|
/*
|
|
* Rimuove direttamente il marker
|
|
* dalla mappa Leaflet, se presente.
|
|
*/
|
|
if (window.globalMap?.hasLayer?.(marker)) {
|
|
window.globalMap.removeLayer(marker);
|
|
}
|
|
|
|
marker.remove?.();
|
|
} catch (error) {
|
|
console.warn(
|
|
"[activitiesLayer] errore rimozione marker Leaflet",
|
|
error,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ===============================
|
|
// TOGGLE TRACCE
|
|
// ===============================
|
|
function ensureActivityRoutesToggle() {
|
|
let btn = document.getElementById("toggleActivityRoutesBtn");
|
|
|
|
if (!btn) {
|
|
btn = document.createElement("button");
|
|
|
|
btn.id = "toggleActivityRoutesBtn";
|
|
|
|
btn.type = "button";
|
|
|
|
btn.className = "map-toggle-routes";
|
|
|
|
btn.addEventListener("click", async (event) => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
|
|
showAllActivityRoutes = !showAllActivityRoutes;
|
|
|
|
updateActivityRoutesToggle();
|
|
|
|
const isMapbox3D = !!window.MapTerrain3D?.enabled?.();
|
|
|
|
if (isMapbox3D) {
|
|
updateMapboxActivityRoutes();
|
|
} else {
|
|
await renderAllActivityRoutes();
|
|
}
|
|
});
|
|
}
|
|
|
|
moveActivityRoutesToggle(btn);
|
|
|
|
updateActivityRoutesToggle();
|
|
|
|
return btn;
|
|
}
|
|
|
|
function moveActivityRoutesToggle(btn) {
|
|
if (!btn) {
|
|
return;
|
|
}
|
|
|
|
const isMapbox3D = !!window.MapTerrain3D?.enabled?.();
|
|
|
|
const targetContainer = isMapbox3D
|
|
? document.getElementById("terrainMap")
|
|
: document.getElementById("globalMap");
|
|
|
|
if (!targetContainer) {
|
|
return;
|
|
}
|
|
|
|
if (btn.parentElement !== targetContainer) {
|
|
targetContainer.appendChild(btn);
|
|
}
|
|
}
|
|
|
|
function updateActivityRoutesToggle() {
|
|
const btn = document.getElementById("toggleActivityRoutesBtn");
|
|
|
|
if (!btn) {
|
|
return;
|
|
}
|
|
|
|
btn.classList.toggle("active", showAllActivityRoutes);
|
|
|
|
btn.textContent = showAllActivityRoutes
|
|
? "Nascondi tracce"
|
|
: "Mostra tracce";
|
|
|
|
btn.setAttribute("aria-pressed", showAllActivityRoutes ? "true" : "false");
|
|
|
|
btn.setAttribute(
|
|
"aria-label",
|
|
showAllActivityRoutes
|
|
? "Nascondi tutte le tracce"
|
|
: "Mostra tutte le tracce",
|
|
);
|
|
|
|
btn.title = showAllActivityRoutes
|
|
? "Nascondi tutte le tracce"
|
|
: "Mostra tutte le tracce";
|
|
}
|
|
|
|
window.hideActivityRoutesToggle =
|
|
function (resetState = false) {
|
|
const btn =
|
|
document.getElementById(
|
|
"toggleActivityRoutesBtn",
|
|
);
|
|
|
|
if (btn) {
|
|
btn.style.display =
|
|
"none";
|
|
|
|
btn.style.visibility =
|
|
"hidden";
|
|
|
|
btn.style.pointerEvents =
|
|
"none";
|
|
}
|
|
|
|
if (!resetState) {
|
|
return;
|
|
}
|
|
|
|
showAllActivityRoutes =
|
|
false;
|
|
|
|
updateActivityRoutesToggle();
|
|
|
|
if (
|
|
window.MapTerrain3D
|
|
?.enabled?.()
|
|
) {
|
|
window.MapboxMarkers
|
|
?.setRoutes?.([]);
|
|
}
|
|
|
|
activityRoutesLayer
|
|
?.clearLayers?.();
|
|
};
|
|
|
|
function showActivityRoutesToggle() {
|
|
const btn =
|
|
ensureActivityRoutesToggle();
|
|
|
|
if (!btn) {
|
|
return;
|
|
}
|
|
|
|
moveActivityRoutesToggle(
|
|
btn,
|
|
);
|
|
|
|
btn.style.display =
|
|
"inline-flex";
|
|
|
|
btn.style.visibility =
|
|
"visible";
|
|
|
|
btn.style.pointerEvents =
|
|
"auto";
|
|
|
|
updateActivityRoutesToggle();
|
|
}
|
|
|
|
window.showActivityRoutesToggle =
|
|
showActivityRoutesToggle;
|
|
|
|
// ===============================
|
|
// ICONA MARKER ATTIVITÀ
|
|
// ===============================
|
|
function createActivityStartIcon(item) {
|
|
const sport = String(item?.sport || "")
|
|
.toLowerCase()
|
|
.trim();
|
|
|
|
let emoji = "●";
|
|
let cls = "activity-marker-generic";
|
|
|
|
if (sport.includes("run") || sport.includes("corsa")) {
|
|
emoji = "🏃";
|
|
cls = "activity-marker-run";
|
|
} else if (
|
|
sport.includes("bike") ||
|
|
sport.includes("bici") ||
|
|
sport.includes("cycle") ||
|
|
sport.includes("cycling") ||
|
|
sport.includes("ride") ||
|
|
sport.includes("cicl")
|
|
) {
|
|
emoji = "🚴";
|
|
cls = "activity-marker-bike";
|
|
} else if (sport.includes("walk") || sport.includes("cammin")) {
|
|
emoji = "🚶";
|
|
cls = "activity-marker-walk";
|
|
} else if (
|
|
sport.includes("hike") ||
|
|
sport.includes("hiking") ||
|
|
sport.includes("trek")
|
|
) {
|
|
emoji = "🥾";
|
|
cls = "activity-marker-hiking";
|
|
} else if (sport.includes("swim") || sport.includes("nuoto")) {
|
|
emoji = "🏊";
|
|
cls = "activity-marker-swimming";
|
|
}
|
|
|
|
return L.divIcon({
|
|
html: `
|
|
<div class="activity-start-marker ${cls}">
|
|
<span class="activity-start-marker-icon">
|
|
${emoji}
|
|
</span>
|
|
</div>
|
|
`,
|
|
|
|
className: "activity-leaflet-marker",
|
|
|
|
iconSize: L.point(42, 42),
|
|
|
|
iconAnchor: L.point(21, 21),
|
|
});
|
|
}
|
|
|
|
// ===============================
|
|
// RENDER ATTIVITÀ
|
|
// ===============================
|
|
|
|
window.renderActivityMapLayer =
|
|
async function () {
|
|
if (
|
|
window.state?.mode !==
|
|
"activities"
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const renderMode =
|
|
window.state.mode;
|
|
|
|
const isMapbox3D =
|
|
!!window.MapTerrain3D
|
|
?.enabled?.();
|
|
|
|
if (
|
|
!isMapbox3D &&
|
|
(
|
|
!window.globalMap ||
|
|
!window.globalMarkers
|
|
)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const token =
|
|
++lastRenderToken;
|
|
|
|
clearActivityStartMarkers();
|
|
showActivityRoutesToggle();
|
|
|
|
/*
|
|
* Il pulsante e i layer Leaflet vengono
|
|
* gestiti soltanto in modalità standard.
|
|
*/
|
|
if (!isMapbox3D) {
|
|
if (!activityRoutesLayer) {
|
|
activityRoutesLayer = L.layerGroup().addTo(window.globalMap);
|
|
}
|
|
|
|
activityRoutesLayer.clearLayers();
|
|
|
|
clearSelectedActivityOnly();
|
|
} else {
|
|
/*
|
|
* Durante la transizione al 3D pulisce
|
|
* eventuali residui senza richiedere
|
|
* che Leaflet sia ancora disponibile.
|
|
*/
|
|
activityRoutesLayer?.clearLayers?.();
|
|
|
|
clearSelectedActivityOnly();
|
|
}
|
|
|
|
// =================================
|
|
// RECUPERA LE ATTIVITÀ
|
|
// =================================
|
|
|
|
const items =
|
|
typeof window.getLocalItems === "function" ? window.getLocalItems() : [];
|
|
|
|
const activityItems = items.filter(isProbablyActivity);
|
|
|
|
cachedRouteData = [];
|
|
|
|
// =================================
|
|
// CARICA I PUNTI DEI PERCORSI
|
|
// =================================
|
|
|
|
const routeDataList = await Promise.all(
|
|
activityItems.map(async (item, index) => {
|
|
const points = await getRoutePointsSafe(item);
|
|
|
|
if (!Array.isArray(points) || points.length < 2) {
|
|
return null;
|
|
}
|
|
|
|
const latlngs = pointsToLatLngs(points);
|
|
|
|
if (!Array.isArray(latlngs) || latlngs.length < 2) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
key: getActivityKey(item, index),
|
|
|
|
index,
|
|
item,
|
|
points,
|
|
latlngs,
|
|
|
|
start: latlngs[0],
|
|
|
|
end: latlngs[latlngs.length - 1],
|
|
|
|
cells: getRouteCells(points),
|
|
};
|
|
}),
|
|
);
|
|
|
|
/*
|
|
* Se nel frattempo è iniziato un altro
|
|
* rendering, questo risultato è obsoleto.
|
|
*/
|
|
if (
|
|
token !== lastRenderToken ||
|
|
window.state?.mode !==
|
|
renderMode
|
|
) {
|
|
return;
|
|
}
|
|
|
|
cachedRouteData = routeDataList.filter(Boolean);
|
|
|
|
console.log("[activitiesLayer] route caricate", {
|
|
inputItems: items.length,
|
|
|
|
activityItems: activityItems.length,
|
|
|
|
routeResults: routeDataList.length,
|
|
|
|
validRoutes: cachedRouteData.length,
|
|
|
|
isMapbox3D,
|
|
});
|
|
|
|
// =================================
|
|
// MODALITÀ MAPBOX 3D
|
|
// =================================
|
|
|
|
if (isMapbox3D) {
|
|
activityRoutesLayer?.clearLayers?.();
|
|
clearSelectedActivityOnly();
|
|
|
|
const activityFeatures =
|
|
typeof window.getActivityMapGeoJSON === "function"
|
|
? window.getActivityMapGeoJSON()
|
|
: [];
|
|
|
|
const routeFeatures =
|
|
typeof window.getActivityRoutesGeoJSON === "function"
|
|
? window.getActivityRoutesGeoJSON()
|
|
: [];
|
|
|
|
console.log("[activitiesLayer] invio dati a Mapbox", {
|
|
cachedActivities: cachedRouteData.length,
|
|
|
|
activityFeatures: activityFeatures.length,
|
|
|
|
routeFeatures: routeFeatures.length,
|
|
});
|
|
|
|
if (typeof window.MapboxMarkers?.setActivities === "function") {
|
|
window.MapboxMarkers.setActivities(activityFeatures);
|
|
} else {
|
|
console.warn(
|
|
"[activitiesLayer] MapboxMarkers.setActivities non disponibile",
|
|
);
|
|
}
|
|
|
|
if (typeof window.MapboxMarkers?.setRoutes === "function") {
|
|
window.MapboxMarkers.setRoutes(
|
|
showAllActivityRoutes ? routeFeatures : [],
|
|
);
|
|
}
|
|
} else {
|
|
// =================================
|
|
// MODALITÀ LEAFLET
|
|
// =================================
|
|
|
|
console.log("[activitiesLayer] rendering marker Leaflet", {
|
|
routes: cachedRouteData.length,
|
|
|
|
globalMap: !!window.globalMap,
|
|
|
|
globalMarkers: !!window.globalMarkers,
|
|
|
|
mapEngineType: window.MapEngine?.type,
|
|
|
|
mapEngineMapIsLeaflet: window.MapEngine?.map === window.globalMap,
|
|
|
|
markerLayerIsGlobalMarkers:
|
|
window.MapEngine?.markerLayer === window.globalMarkers,
|
|
});
|
|
|
|
if (
|
|
window.globalMap &&
|
|
window.globalMarkers &&
|
|
!window.globalMap.hasLayer(window.globalMarkers)
|
|
) {
|
|
console.warn(
|
|
"[activitiesLayer] cluster group non presente, lo ripristino",
|
|
);
|
|
|
|
window.globalMarkers.addTo(window.globalMap);
|
|
}
|
|
|
|
for (const route of cachedRouteData) {
|
|
const marker = L.marker(route.start, {
|
|
icon: createActivityStartIcon(route.item),
|
|
|
|
title:
|
|
route.item.display_name ||
|
|
route.item.route_name ||
|
|
route.item.name ||
|
|
route.item.file_name ||
|
|
"",
|
|
});
|
|
|
|
marker.__item = route.item;
|
|
|
|
marker.__routeData = route;
|
|
|
|
marker.on(
|
|
"click",
|
|
(event) => {
|
|
event?.originalEvent
|
|
?.preventDefault?.();
|
|
|
|
event?.originalEvent
|
|
?.stopPropagation?.();
|
|
|
|
const activityList =
|
|
cachedRouteData.map(
|
|
(routeData) =>
|
|
routeData.item,
|
|
);
|
|
|
|
const activityIndex =
|
|
cachedRouteData.findIndex(
|
|
(routeData) =>
|
|
routeData.key ===
|
|
route.key,
|
|
);
|
|
|
|
window.openActivityModal?.(
|
|
route.item,
|
|
activityList,
|
|
activityIndex >= 0
|
|
? activityIndex
|
|
: 0,
|
|
);
|
|
},
|
|
);
|
|
|
|
if (window.globalMarkers?.addLayer) {
|
|
window.globalMarkers.addLayer(marker);
|
|
} else {
|
|
console.warn("[activitiesLayer] globalMarkers non disponibile", {
|
|
globalMarkers: window.globalMarkers,
|
|
|
|
mapEngineType: window.MapEngine?.type,
|
|
|
|
markerLayer: window.MapEngine?.markerLayer,
|
|
});
|
|
}
|
|
|
|
activityStartMarkers.push(marker);
|
|
}
|
|
|
|
|
|
console.log("[activitiesLayer] marker Leaflet aggiunti", {
|
|
trackedMarkers: activityStartMarkers.length,
|
|
|
|
clusterLayers: window.globalMarkers?.getLayers?.()?.length ?? null,
|
|
|
|
clusterOnMap: !!(
|
|
window.globalMap &&
|
|
window.globalMarkers &&
|
|
window.globalMap.hasLayer(window.globalMarkers)
|
|
),
|
|
|
|
mode: window.state?.mode,
|
|
});
|
|
|
|
}
|
|
|
|
// =================================
|
|
// ADATTA LA MAPPA ALLE ATTIVITÀ
|
|
// =================================
|
|
|
|
if (
|
|
token !== lastRenderToken ||
|
|
window.state?.mode !==
|
|
renderMode
|
|
) {
|
|
return;
|
|
}
|
|
|
|
/*
|
|
* fitBounds automatico soltanto nella
|
|
* modalità standard.
|
|
*
|
|
* In Mapbox 3D può provocare un ulteriore
|
|
* cambio zoom proprio mentre cluster e
|
|
* marker vengono aggiornati.
|
|
*/
|
|
if (!isMapbox3D && window.globalMap && cachedRouteData.length > 0) {
|
|
const bounds = L.latLngBounds(
|
|
cachedRouteData.map((route) => route.start),
|
|
);
|
|
|
|
if (bounds.isValid()) {
|
|
window.globalMap.fitBounds(bounds, {
|
|
padding: [60, 60],
|
|
maxZoom: 12,
|
|
});
|
|
}
|
|
}
|
|
|
|
// =================================
|
|
// MOSTRA TUTTE LE TRACCE
|
|
// =================================
|
|
|
|
/*
|
|
* Le tracce Leaflet devono essere
|
|
* renderizzate soltanto nella modalità
|
|
* standard. In 3D vengono gestite dalla
|
|
* source routes di Mapbox.
|
|
*/
|
|
if (showAllActivityRoutes && !isMapbox3D) {
|
|
await renderAllActivityRoutes();
|
|
}
|
|
|
|
if (
|
|
token !== lastRenderToken ||
|
|
window.state?.mode !==
|
|
renderMode
|
|
) {
|
|
return;
|
|
}
|
|
|
|
|
|
console.log("[activitiesLayer] render completato", {
|
|
mode: isMapbox3D ? "mapbox-3d" : "standard",
|
|
|
|
activities: cachedRouteData.length,
|
|
|
|
token,
|
|
});
|
|
};
|
|
|
|
// ===============================
|
|
// TRACCIA SELEZIONATA
|
|
// ===============================
|
|
function showSelectedActivityOnGlobalMap(route) {
|
|
clearSelectedActivityOnly();
|
|
|
|
selectedActivityLayer = L.polyline(route.latlngs, {
|
|
color: "#ff6b00",
|
|
weight: 5,
|
|
opacity: 1,
|
|
lineCap: "round",
|
|
lineJoin: "round",
|
|
}).addTo(window.globalMap);
|
|
|
|
selectedActivityLayer.on("click", () => {
|
|
window.openActivityModal?.(route.item);
|
|
});
|
|
|
|
selectedActivityEndMarker = L.circleMarker(route.end, {
|
|
radius: 7,
|
|
color: "#b00020",
|
|
weight: 2,
|
|
fillColor: "#e32636",
|
|
fillOpacity: 1,
|
|
})
|
|
.bindTooltip("Arrivo")
|
|
.addTo(window.globalMap);
|
|
|
|
selectedActivityEndMarker.on("click", () => {
|
|
window.openActivityModal?.(route.item);
|
|
});
|
|
|
|
const popupHtml = createActivityPopupHtml(route.item);
|
|
|
|
selectedActivityLayer.bindPopup(popupHtml).openPopup();
|
|
|
|
window.globalMap.fitBounds(selectedActivityLayer.getBounds(), {
|
|
padding: [50, 50],
|
|
maxZoom: 15,
|
|
});
|
|
|
|
setTimeout(() => {
|
|
const btn = document.querySelector(
|
|
`[data-open-activity-key="${route.key}"]`,
|
|
);
|
|
|
|
btn?.addEventListener("click", (event) => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
|
|
window.openActivityModal?.(route.item);
|
|
});
|
|
}, 0);
|
|
}
|
|
|
|
function clearSelectedActivityOnly() {
|
|
if (selectedActivityLayer) {
|
|
selectedActivityLayer.remove();
|
|
selectedActivityLayer = null;
|
|
}
|
|
|
|
if (selectedActivityEndMarker) {
|
|
selectedActivityEndMarker.remove();
|
|
selectedActivityEndMarker = null;
|
|
}
|
|
}
|
|
|
|
function updateMapboxActivityRoutes() {
|
|
if (!window.MapTerrain3D?.enabled?.()) {
|
|
return;
|
|
}
|
|
|
|
if (typeof window.MapboxMarkers?.setRoutes !== "function") {
|
|
console.warn("[activitiesLayer] MapboxMarkers.setRoutes non disponibile");
|
|
|
|
return;
|
|
}
|
|
|
|
if (!showAllActivityRoutes) {
|
|
window.MapboxMarkers.setRoutes([]);
|
|
|
|
console.log("[activitiesLayer] tracce Mapbox nascoste");
|
|
|
|
return;
|
|
}
|
|
|
|
const routeFeatures =
|
|
typeof window.getActivityRoutesGeoJSON === "function"
|
|
? window.getActivityRoutesGeoJSON()
|
|
: [];
|
|
|
|
window.MapboxMarkers.setRoutes(routeFeatures);
|
|
|
|
console.log("[activitiesLayer] tracce Mapbox visibili", {
|
|
routes: routeFeatures.length,
|
|
});
|
|
}
|
|
|
|
// ===============================
|
|
// TOGGLE TUTTE LE TRACCE
|
|
// ===============================
|
|
async function renderAllActivityRoutes() {
|
|
if (!activityRoutesLayer) {
|
|
activityRoutesLayer = L.layerGroup().addTo(window.globalMap);
|
|
}
|
|
|
|
activityRoutesLayer.clearLayers();
|
|
|
|
if (!showAllActivityRoutes) return;
|
|
|
|
let routesToDraw = cachedRouteData;
|
|
|
|
if (routesToDraw.length > MAX_GLOBAL_ACTIVITY_ROUTES) {
|
|
const bounds = window.globalMap.getBounds();
|
|
|
|
routesToDraw = routesToDraw.filter((route) =>
|
|
bounds.contains(route.start),
|
|
);
|
|
}
|
|
|
|
if (routesToDraw.length > MAX_GLOBAL_ACTIVITY_ROUTES) {
|
|
console.warn(
|
|
`[activity map] Troppe tracce da disegnare: ${routesToDraw.length}. Zooma di più.`,
|
|
);
|
|
return;
|
|
}
|
|
|
|
const colors = assignSmartRouteColors(routesToDraw);
|
|
|
|
routesToDraw.forEach((route) => {
|
|
const color =
|
|
colors.get(route.key) ||
|
|
ACTIVITY_ROUTE_COLORS[route.index % ACTIVITY_ROUTE_COLORS.length];
|
|
|
|
const polyline = L.polyline(route.latlngs, {
|
|
color,
|
|
weight: 5,
|
|
opacity: 0.75,
|
|
lineCap: "round",
|
|
lineJoin: "round",
|
|
});
|
|
|
|
polyline.on("click", () => {
|
|
showSelectedActivityOnGlobalMap(route);
|
|
});
|
|
|
|
activityRoutesLayer.addLayer(polyline);
|
|
});
|
|
}
|
|
|
|
// ===============================
|
|
// COLORI INTELLIGENTI
|
|
// ===============================
|
|
function assignSmartRouteColors(routes) {
|
|
const cellToColors = new Map();
|
|
const result = new Map();
|
|
|
|
routes.forEach((route, index) => {
|
|
const usedNearbyColors = new Set();
|
|
|
|
route.cells.forEach((cell) => {
|
|
const colors = cellToColors.get(cell);
|
|
|
|
if (!colors) return;
|
|
|
|
colors.forEach((color) => usedNearbyColors.add(color));
|
|
});
|
|
|
|
const color =
|
|
ACTIVITY_ROUTE_COLORS.find((c) => !usedNearbyColors.has(c)) ||
|
|
ACTIVITY_ROUTE_COLORS[index % ACTIVITY_ROUTE_COLORS.length];
|
|
|
|
result.set(route.key, color);
|
|
|
|
route.cells.forEach((cell) => {
|
|
if (!cellToColors.has(cell)) {
|
|
cellToColors.set(cell, new Set());
|
|
}
|
|
|
|
cellToColors.get(cell).add(color);
|
|
});
|
|
});
|
|
|
|
return result;
|
|
}
|
|
|
|
function getRouteCells(points, precision = 3) {
|
|
const cells = new Set();
|
|
|
|
points.forEach((point) => {
|
|
const lat = Number(point.lat);
|
|
const lng = Number(point.lng);
|
|
|
|
if (!Number.isFinite(lat) || !Number.isFinite(lng)) return;
|
|
|
|
cells.add(`${lat.toFixed(precision)}:${lng.toFixed(precision)}`);
|
|
});
|
|
|
|
return cells;
|
|
}
|
|
|
|
// ===============================
|
|
// HELPERS
|
|
// ===============================
|
|
function isProbablyActivity(item) {
|
|
if (!item) return false;
|
|
|
|
const format = String(
|
|
item.format ||
|
|
item.file_extension ||
|
|
item.mime_type ||
|
|
item.file_name ||
|
|
item.name ||
|
|
"",
|
|
).toLowerCase();
|
|
|
|
return (
|
|
format.includes("fit") ||
|
|
format.includes("gpx") ||
|
|
format.includes("tcx") ||
|
|
item.distance_meters != null ||
|
|
item.duration_millis != null ||
|
|
item.track_preview != null
|
|
);
|
|
}
|
|
|
|
async function getRoutePointsSafe(item) {
|
|
try {
|
|
if (typeof window.getActivityRoutePoints === "function") {
|
|
return await window.getActivityRoutePoints(item);
|
|
}
|
|
|
|
if (typeof getActivityRoutePoints === "function") {
|
|
return await getActivityRoutePoints(item);
|
|
}
|
|
} catch (error) {
|
|
console.warn("[activity map] errore caricamento punti:", error);
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
function pointsToLatLngs(points) {
|
|
return points
|
|
.map((point) => [Number(point.lat), Number(point.lng)])
|
|
.filter(
|
|
(point) => Number.isFinite(point[0]) && Number.isFinite(point[1]),
|
|
);
|
|
}
|
|
|
|
function getActivityKey(item, index) {
|
|
const value =
|
|
item?.id ?? item?.path ?? item?.file_name ?? item?.name ?? index;
|
|
|
|
return String(value);
|
|
}
|
|
|
|
window.getActivityMapKey = getActivityKey;
|
|
|
|
function createActivityPopupHtml(item) {
|
|
const key =
|
|
escapeHtml(
|
|
getActivityKey(
|
|
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 distanceText = Number.isFinite(distance)
|
|
? `${(distance / 1000).toFixed(1)} km`
|
|
: "-";
|
|
|
|
const durationText =
|
|
Number.isFinite(duration) &&
|
|
typeof window.formatDurationShort === "function"
|
|
? window.formatDurationShort(duration)
|
|
: formatDurationFallback(duration);
|
|
|
|
return `
|
|
<div class="activity-map-popup">
|
|
<strong>${escapeHtml(title)}</strong>
|
|
|
|
${item.sport ? `<div>${escapeHtml(item.sport)}</div>` : ""}
|
|
|
|
<div>${distanceText} · ${durationText}</div>
|
|
|
|
<button
|
|
type="button"
|
|
class="activity-popup-open"
|
|
data-open-activity-key="${key}"
|
|
>
|
|
Apri percorso
|
|
</button>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function formatDurationFallback(milliseconds) {
|
|
const value = Number(milliseconds);
|
|
|
|
if (!Number.isFinite(value)) return "-";
|
|
|
|
const totalSeconds = Math.round(value / 1000);
|
|
const hours = Math.floor(totalSeconds / 3600);
|
|
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
|
|
return hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`;
|
|
}
|
|
|
|
function escapeHtml(value) {
|
|
return String(value ?? "")
|
|
.replaceAll("&", "&")
|
|
.replaceAll("<", "<")
|
|
.replaceAll(">", ">")
|
|
.replaceAll('"', """)
|
|
.replaceAll("'", "'");
|
|
}
|
|
|
|
// ===============================
|
|
// GEOJSON MAPBOX ATTIVITÀ
|
|
// ===============================
|
|
window.getActivityMapGeoJSON = function () {
|
|
const features = [];
|
|
|
|
for (const route of cachedRouteData) {
|
|
if (!route.start) {
|
|
continue;
|
|
}
|
|
|
|
features.push({
|
|
type: "Feature",
|
|
|
|
geometry: {
|
|
type: "Point",
|
|
|
|
// route.start = [lat, lng]
|
|
coordinates: [
|
|
route.start[1], // lng
|
|
route.start[0], // lat
|
|
],
|
|
},
|
|
|
|
properties: {
|
|
id: route.key,
|
|
type: "activity",
|
|
|
|
name:
|
|
route.item.display_name ||
|
|
route.item.route_name ||
|
|
route.item.name ||
|
|
"Attività",
|
|
|
|
sport: route.item.sport,
|
|
distance: route.item.distance_meters,
|
|
duration: route.item.duration_millis,
|
|
},
|
|
});
|
|
}
|
|
|
|
return features;
|
|
};
|
|
|
|
// ===============================
|
|
// LINEE PERCORSI MAPBOX
|
|
// ===============================
|
|
window.getActivityRoutesGeoJSON = function () {
|
|
const features = [];
|
|
|
|
for (const route of cachedRouteData) {
|
|
if (!route.latlngs || route.latlngs.length < 2) {
|
|
continue;
|
|
}
|
|
|
|
features.push({
|
|
type: "Feature",
|
|
|
|
geometry: {
|
|
type: "LineString",
|
|
|
|
// route.latlngs contiene [lat, lng]
|
|
coordinates: route.latlngs.map((p) => [
|
|
p[1], // lng
|
|
p[0], // lat
|
|
]),
|
|
},
|
|
|
|
properties: {
|
|
id: route.key,
|
|
type: "route",
|
|
},
|
|
});
|
|
}
|
|
|
|
return features;
|
|
};
|
|
|
|
window.areActivityRoutesVisible = function () {
|
|
return showAllActivityRoutes;
|
|
};
|
|
})();
|