218 lines
7.6 KiB
JavaScript
218 lines
7.6 KiB
JavaScript
// 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 `
|
|
<svg viewBox="0 0 ${width} ${height}" class="activity-route-svg" aria-hidden="true">
|
|
<rect x="0" y="0" width="${width}" height="${height}" rx="16" class="activity-route-bg"></rect>
|
|
<polyline points="${pointsAttribute}" class="activity-route-line-shadow"></polyline>
|
|
<polyline points="${pointsAttribute}" class="activity-route-line"></polyline>
|
|
<circle cx="${first.x.toFixed(1)}" cy="${first.y.toFixed(1)}" r="4.2" class="activity-route-start"></circle>
|
|
<circle cx="${last.x.toFixed(1)}" cy="${last.y.toFixed(1)}" r="4.2" class="activity-route-end"></circle>
|
|
</svg>`;
|
|
}
|
|
|
|
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 = `<div class="activity-route-placeholder"><span>${escapeHtml(item?.sport || item?.format || "Attività")}</span></div>`;
|
|
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 = `<div class="activity-route-placeholder"><span>${escapeHtml(item?.sport || item?.format || "Attività")}</span></div>`;
|
|
}
|
|
}
|
|
|
|
window.ActivityThumbs = Object.freeze({
|
|
renderRouteThumb,
|
|
getRoutePoints,
|
|
parseTcxPoints,
|
|
parseGpxPoints,
|
|
createRouteSvg
|
|
});
|
|
|
|
window.renderActivityRouteThumb = renderRouteThumb;
|
|
window.getActivityRoutePoints = getRoutePoints;
|
|
window.parseTcxPoints = parseTcxPoints;
|
|
window.parseGpxPoints = parseGpxPoints;
|
|
window.createRouteSvg = createRouteSvg;
|
|
})();
|