// public/js/activityThumbs.js // Caricamento, parsing e rendering delle anteprime dei percorsi. (() => { "use strict"; const routeCache = window.activityRouteCache || new Map(); window.activityRouteCache = routeCache; function normalizePoint(point) { if (!point) return null; if (Number.isFinite(Number(point.lat)) && Number.isFinite(Number(point.lng))) { return { lat: Number(point.lat), lng: Number(point.lng) }; } if (Number.isFinite(Number(point.latitude)) && Number.isFinite(Number(point.longitude))) { return { lat: Number(point.latitude), lng: Number(point.longitude) }; } if (Number.isFinite(Number(point.lat)) && Number.isFinite(Number(point.lon))) { return { lat: Number(point.lat), lng: Number(point.lon) }; } if (Array.isArray(point) && point.length >= 2) { const lng = Number(point[0]); const lat = Number(point[1]); if (Number.isFinite(lat) && Number.isFinite(lng)) return { lat, lng }; } return null; } function extractDirectPoints(item) { const candidates = [ item.track_preview, item.points, item.track_points, item.coordinates ]; for (const candidate of candidates) { if (!Array.isArray(candidate)) continue; const points = candidate.map(normalizePoint).filter(Boolean); if (points.length >= 2) return points; } const geojson = item.geojson; const coordinates = geojson?.type === "LineString" ? geojson.coordinates : geojson?.type === "Feature" && geojson.geometry?.type === "LineString" ? geojson.geometry.coordinates : null; if (!Array.isArray(coordinates)) return []; return coordinates.map(normalizePoint).filter(Boolean); } function simplifyPoints(points, maxPoints = 350) { if (!Array.isArray(points)) return []; if (points.length <= maxPoints) return points; const step = Math.ceil(points.length / maxPoints); const output = []; for (let index = 0; index < points.length; index += step) { output.push(points[index]); } const last = points.at(-1); const outputLast = output.at(-1); if (!outputLast || outputLast.lat !== last.lat || outputLast.lng !== last.lng) { output.push(last); } return output; } async function fetchActivityText(filePath) { const headers = {}; const token = localStorage.getItem("token"); if (token) headers.Authorization = `Bearer ${token}`; const response = await fetch(filePath, { headers }); if (!response.ok) { throw new Error(`HTTP ${response.status} caricando ${filePath}`); } return response.text(); } function parseTcxPoints(text) { const xml = new DOMParser().parseFromString(text, "application/xml"); const parseError = xml.getElementsByTagName("parsererror")[0]; if (parseError) { console.warn("[parseTcxPoints] XML parser error", parseError.textContent); return []; } return Array.from(xml.getElementsByTagName("Trackpoint")) .map(trackpoint => { const lat = Number(trackpoint.getElementsByTagName("LatitudeDegrees")[0]?.textContent); const lng = Number(trackpoint.getElementsByTagName("LongitudeDegrees")[0]?.textContent); return Number.isFinite(lat) && Number.isFinite(lng) ? { lat, lng } : null; }) .filter(Boolean); } function parseGpxPoints(text) { const xml = new DOMParser().parseFromString(text, "application/xml"); const parseError = xml.getElementsByTagName("parsererror")[0]; if (parseError) { console.warn("[parseGpxPoints] XML parser error", parseError.textContent); return []; } return Array.from(xml.getElementsByTagName("trkpt")) .map(point => { const lat = Number(point.getAttribute("lat")); const lng = Number(point.getAttribute("lon")); return Number.isFinite(lat) && Number.isFinite(lng) ? { lat, lng } : null; }) .filter(Boolean); } async function getRoutePoints(item) { if (!item) return []; const cacheKey = item.id || item.path || item.name; if (cacheKey && routeCache.has(cacheKey)) return routeCache.get(cacheKey); let points = extractDirectPoints(item); if (points.length < 2 && item.path) { const format = String(item.format || item.file_extension || item.mime_type || "").toLowerCase(); const lowerPath = String(item.path).toLowerCase(); if (format === "tcx" || lowerPath.endsWith(".tcx")) { points = parseTcxPoints(await fetchActivityText(item.path)); } else if (format === "gpx" || lowerPath.endsWith(".gpx")) { points = parseGpxPoints(await fetchActivityText(item.path)); } } points = simplifyPoints(points, 350); if (cacheKey) routeCache.set(cacheKey, points); return points; } function createRouteSvg(points) { const width = 240; const height = 160; const padding = 16; const lats = points.map(point => point.lat); const lngs = points.map(point => point.lng); const minLat = Math.min(...lats); const maxLat = Math.max(...lats); const minLng = Math.min(...lngs); const maxLng = Math.max(...lngs); const latRange = maxLat - minLat || 0.000001; const lngRange = maxLng - minLng || 0.000001; const usableWidth = width - padding * 2; const usableHeight = height - padding * 2; const svgPoints = points.map(point => { const x = padding + ((point.lng - minLng) / lngRange) * usableWidth; const y = padding + ((maxLat - point.lat) / latRange) * usableHeight; return { x, y, text: `${x.toFixed(1)},${y.toFixed(1)}` }; }); const pointsAttribute = svgPoints.map(point => point.text).join(" "); const first = svgPoints[0]; const last = svgPoints.at(-1); return ` `; } async function renderRouteThumb(container, item) { const escapeHtml = window.GalleryUtils?.escapeHtml || (value => String(value ?? "")); try { const points = await getRoutePoints(item); if (points.length < 2) { container.classList.remove("loading"); container.classList.add("empty"); container.innerHTML = `