// 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 normalizePoints(candidate) { if (!Array.isArray(candidate)) { return []; } return candidate .map(normalizePoint) .filter(Boolean); } function extractPreviewPoints(item) { if (!item) { return []; } const preview = normalizePoints( item.track_preview, ); if (preview.length >= 2) { return preview; } return extractFullDirectPoints( item, ); } function extractFullDirectPoints(item) { if (!item) { return []; } const candidates = [ item.points, item.track_points, item.coordinates, ]; for (const candidate of candidates) { const points = normalizePoints( candidate, ); 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; return normalizePoints( coordinates, ); } 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(); } async function fetchFullActivityPoints( item, ) { if (!item) { return []; } const activityId = item.id; if ( activityId === undefined || activityId === null || activityId === "" ) { return []; } const token = localStorage.getItem( "token", ) || sessionStorage.getItem( "token", ); const headers = {}; if (token) { headers.Authorization = `Bearer ${token}`; } const response = await fetch( `/activities/${encodeURIComponent( activityId, )}/route-points`, { method: "GET", headers, cache: "no-store", credentials: "same-origin", }, ); if (!response.ok) { let message = `HTTP ${response.status}`; try { const errorPayload = await response.json(); message = errorPayload.error || errorPayload.details || message; } catch {} throw new Error( message, ); } const payload = await response.json(); const rawPoints = Array.isArray( payload.points, ) ? payload.points : []; const points = rawPoints .map(normalizePoint) .filter(Boolean); console.log( "[activity route] punti completi ricevuti", { id: activityId, expected: payload .expected_point_count ?? null, received: points.length, format: payload.format ?? item.format ?? null, }, ); return points; } 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, options = {}, ) { if (!item) { return []; } const full = options.full === true; const baseCacheKey = item.id || item.path || item.name; const cacheKey = baseCacheKey ? `${full ? "full" : "preview"}:${baseCacheKey}` : null; if ( cacheKey && routeCache.has( cacheKey, ) ) { const cachedPoints = routeCache.get( cacheKey, ); console.log( "[activity route] punti dalla cache", { mode: full ? "full" : "preview", loaded: Array.isArray( cachedPoints, ) ? cachedPoints.length : 0, cacheKey, }, ); return Array.isArray( cachedPoints, ) ? cachedPoints : []; } const format = String( item.format || item.file_extension || item.mime_type || "", ) .toLowerCase() .trim(); const lowerPath = String( item.path || item.file_name || item.name || "", ) .toLowerCase() .trim(); const isFit = format.includes("fit") || lowerPath.endsWith( ".fit", ); const isTcx = format.includes("tcx") || lowerPath.endsWith( ".tcx", ); const isGpx = format.includes("gpx") || lowerPath.endsWith( ".gpx", ); let points = full ? extractFullDirectPoints( item, ) : extractPreviewPoints( item, ); /* * In modalità full, per i FIT non usa * track_preview: richiede tutti i punti * all'endpoint protetto. */ if ( full && points.length < 2 && isFit ) { points = await fetchFullActivityPoints( item, ); } /* * GPX e TCX possono essere letti * direttamente dal file originale. */ if ( full && points.length < 2 && item.path && ( isTcx || isGpx ) ) { const text = await fetchActivityText( item.path, ); points = isTcx ? parseTcxPoints( text, ) : parseGpxPoints( text, ); } /* * Solo le preview vengono ridotte. * La traccia full rimane completa. */ if (!full) { points = simplifyPoints( points, 350, ); } if (!Array.isArray(points)) { points = []; } if (cacheKey) { routeCache.set( cacheKey, points, ); } console.log( "[activity route] punti caricati", { id: item.id, mode: full ? "full" : "preview", format: item.format || item.file_extension || null, expected: Number( item.point_count, ) || null, loaded: points.length, cacheKey, }, ); 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 = `
${escapeHtml(item?.sport || item?.format || "Attività")}
`; return; } container.innerHTML = createRouteSvg(points); container.classList.remove("loading"); container.classList.add("ready"); } catch (error) { console.warn("[activity thumb] errore:", item?.name, error); container.classList.remove("loading"); container.classList.add("empty"); container.innerHTML = `
${escapeHtml(item?.sport || item?.format || "Attività")}
`; } } window.ActivityThumbs = Object.freeze({ renderRouteThumb, getRoutePoints, fetchFullActivityPoints, parseTcxPoints, parseGpxPoints, createRouteSvg, }); window.renderActivityRouteThumb = renderRouteThumb; window.fetchFullActivityPoints = fetchFullActivityPoints; window.getActivityRoutePoints = getRoutePoints; window.parseTcxPoints = parseTcxPoints; window.parseGpxPoints = parseGpxPoints; window.createRouteSvg = createRouteSvg; })();