server_photo_s85js/public/js/activityThumbs.js
2026-08-11 08:45:04 +02:00

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;
})();