// public/js/mapActivitiesLayer.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; // =============================== // CLUSTER ATTIVITÀ // =============================== window.createActivityClusterIcon = function (cluster) { const count = cluster.getChildCount(); return L.divIcon({ html: `
${count}
`, className: "marker-cluster-wrapper", iconSize: L.point(48, 48), }); }; // =============================== // PULIZIA LAYER ATTIVITÀ // =============================== window.clearActivityMapLayers = function () { if (activityRoutesLayer) { activityRoutesLayer.clearLayers(); } if (selectedActivityLayer) { selectedActivityLayer.remove(); selectedActivityLayer = null; } if (selectedActivityEndMarker) { selectedActivityEndMarker.remove(); selectedActivityEndMarker = null; } }; // =============================== // TOGGLE TRACCE // =============================== function ensureActivityRoutesToggle() { let btn = document.getElementById("toggleActivityRoutesBtn"); if (btn) return btn; btn = document.createElement("button"); btn.id = "toggleActivityRoutesBtn"; btn.type = "button"; btn.className = "map-toggle-routes"; btn.textContent = "Mostra tracce"; btn.addEventListener("click", async (event) => { event.preventDefault(); event.stopPropagation(); showAllActivityRoutes = !showAllActivityRoutes; btn.classList.toggle("active", showAllActivityRoutes); btn.textContent = showAllActivityRoutes ? "Nascondi tracce" : "Mostra tracce"; await renderAllActivityRoutes(); }); document.getElementById("globalMap")?.appendChild(btn); return btn; } window.hideActivityRoutesToggle = function () { const btn = document.getElementById("toggleActivityRoutesBtn"); if (btn) { btn.style.display = "none"; } showAllActivityRoutes = false; }; function showActivityRoutesToggle() { const btn = ensureActivityRoutesToggle(); btn.style.display = "inline-flex"; btn.classList.toggle("active", showAllActivityRoutes); btn.textContent = showAllActivityRoutes ? "Nascondi tracce" : "Mostra tracce"; } // =============================== // ICONA MARKER ATTIVITÀ // =============================== function createActivityStartIcon(item) { const sport = String(item.sport || "").toLowerCase(); 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") ) { emoji = "🚴"; cls = "activity-marker-bike"; } else if (sport.includes("walk") || sport.includes("cammin")) { emoji = "🚶"; cls = "activity-marker-walk"; } return L.divIcon({ html: `
${emoji}
`, className: "marker-cluster-wrapper", iconSize: L.point(42, 42), iconAnchor: L.point(21, 21), }); } // =============================== // RENDER ATTIVITÀ // =============================== window.renderActivityMapLayer = async function () { if (!window.globalMap || !window.globalMarkers) return; const token = ++lastRenderToken; showActivityRoutesToggle(); if (!activityRoutesLayer) { activityRoutesLayer = L.layerGroup().addTo(window.globalMap); } activityRoutesLayer.clearLayers(); clearSelectedActivityOnly(); const items = getLocalItems(); const activityItems = items.filter(isProbablyActivity); cachedRouteData = []; const routeDataList = await Promise.all( activityItems.map(async (item, index) => { const points = await getRoutePointsSafe(item); if (!points || points.length < 2) { return null; } const latlngs = pointsToLatLngs(points); if (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), }; }), ); if (token !== lastRenderToken) return; cachedRouteData = routeDataList.filter(Boolean); cachedRouteData.forEach((route) => { 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", () => { showSelectedActivityOnGlobalMap(route); }); window.MapEngine?.addMarker(marker); }); if (cachedRouteData.length > 0) { const bounds = L.latLngBounds( cachedRouteData.map((route) => route.start), ); if (bounds.isValid()) { window.globalMap.fitBounds(bounds, { padding: [60, 60], maxZoom: 12, }); } } if (showAllActivityRoutes) { await renderAllActivityRoutes(); } }; // =============================== // 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; } } // =============================== // 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) { return String(item.id || item.path || item.file_name || item.name || index); } function createActivityPopupHtml(item) { const key = escapeHtml( String(item.id || item.path || item.file_name || item.name || ""), ); 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 `
${escapeHtml(title)} ${item.sport ? `
${escapeHtml(item.sport)}
` : ""}
${distanceText} · ${durationText}
`; } 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", coordinates: [route.start.lng, route.start.lat], }, properties: { id: route.key, type: "activity", name: route.item.display_name || route.item.route_name || route.item.name || "Attività", }, }); } 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", coordinates: route.latlngs.map((p) => [p.lng, p.lat]), }, properties: { id: route.key, type: "route", }, }); } return features; }; })();