// public/js/infoPanel.js // =============================== // PANNELLO INFO - foto + attività + mappa // =============================== const infoPanel = document.getElementById("infoPanel"); let infoMapInstance = null; // ------------------------------- // Helpers UI / stato // ------------------------------- function isPanelOpen() { return ( infoPanel.classList.contains("open") || infoPanel.getAttribute("aria-hidden") === "false" || infoPanel.getAttribute("data-open") === "1" || infoPanel.style.display === "block" ); } function markButtonActive(active) { const photoBtn = document.getElementById("modalInfoBtn"); const activityBtn = document.getElementById("activityInfoBtn"); if (photoBtn) { photoBtn.classList.toggle("active", !!active); } if (activityBtn) { activityBtn.classList.toggle("active", !!active); } } function escapeInfoHtml(value) { return String(value ?? "") .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); } function fmtInt(value) { const n = Number(value); return Number.isFinite(n) ? String(Math.round(n)) : "-"; } function fmtKm(meters) { const n = Number(meters); return Number.isFinite(n) ? `${(n / 1000).toFixed(1)} km` : "-"; } function fmtMeters(value) { const n = Number(value); return Number.isFinite(n) ? `${Math.round(n)} m` : "-"; } function fmtSpeedMps(value) { const n = Number(value); return Number.isFinite(n) ? `${(n * 3.6).toFixed(1)} km/h` : "-"; } function fmtPace(secPerKm) { const n = Number(secPerKm); if (!Number.isFinite(n) || n <= 0) { return "-"; } const min = Math.floor(n / 60); const sec = Math.round(n % 60); return `${min}:${String(sec).padStart(2, "0")} /km`; } function fmtDuration(ms) { const n = Number(ms); if (!Number.isFinite(n) || n < 0) { return "-"; } const totalSec = Math.round(n / 1000); const h = Math.floor(totalSec / 3600); const m = Math.floor((totalSec % 3600) / 60); const s = totalSec % 60; if (h > 0) { return `${h}h ${m}m ${s}s`; } return `${m}m ${s}s`; } function fmtDateFromMillis(ms) { const n = Number(ms); if (!Number.isFinite(n)) { return "-"; } try { return new Date(n).toLocaleString("it-IT"); } catch { return "-"; } } function normalizeGpsFromItem(item) { if (!item) { return { lat: "-", lng: "-" }; } if (item.gps && item.gps.lat && item.gps.lng) { return item.gps; } const lat = item.center_latitude ?? item.latitude ?? item.lat ?? item.min_latitude; const lng = item.center_longitude ?? item.longitude ?? item.lng ?? item.min_longitude; if ( Number.isFinite(Number(lat)) && Number.isFinite(Number(lng)) ) { return { lat: Number(lat), lng: Number(lng) }; } return { lat: "-", lng: "-" }; } function isActivityItem(item) { if (!item) return false; const format = String( item.format || item.file_extension || item.mime_type || item.file_name || item.name || "" ).toLowerCase(); return ( window.state?.mode === "activities" || format.includes("fit") || format.includes("gpx") || format.includes("tcx") || item.distance_meters != null || item.duration_millis != null || item.track_preview != null ); } // ------------------------------- // RENDER FOTO // ------------------------------- function renderInfoPhoto(photo) { const gps = photo.gps || { lat: "-", lng: "-", alt: "-" }; const loc = photo.location || {}; const folder = photo.cartella || "-"; infoPanel.innerHTML = `

Informazioni

Nome: ${escapeInfoHtml(photo.name ?? "-")}
Data: ${escapeInfoHtml(photo.taken_at ?? "-")}
Latitudine: ${escapeInfoHtml(gps.lat ?? "-")}
Longitudine: ${escapeInfoHtml(gps.lng ?? "-")}
Altitudine: ${escapeInfoHtml(gps.alt ?? "-")} m
Dimensioni: ${escapeInfoHtml(photo.width ?? "-")} × ${escapeInfoHtml(photo.height ?? "-")}
Peso: ${ photo.size_bytes ? `${(Number(photo.size_bytes) / 1024 / 1024).toFixed(2)} MB` : "-" }
Tipo: ${escapeInfoHtml(photo.mime_type ?? "-")}
Cartella: ${escapeInfoHtml(folder)}

Mappa

${ gps.lat !== "-" && gps.lng !== "-" ? '
' : '
Coordinate non disponibili
' }

Location

${loc.continent ? `
Continente: ${escapeInfoHtml(loc.continent)}
` : ""} ${loc.country ? `
Nazione: ${escapeInfoHtml(loc.country)}
` : ""} ${loc.region ? `
Regione: ${escapeInfoHtml(loc.region)}
` : ""} ${loc.city ? `
Città: ${escapeInfoHtml(loc.city)}
` : ""} ${loc.address ? `
Indirizzo: ${escapeInfoHtml(loc.address)}
` : ""} ${loc.postcode ? `
CAP: ${escapeInfoHtml(loc.postcode)}
` : ""} ${loc.county_code ? `
Provincia: ${escapeInfoHtml(loc.county_code)}
` : ""} ${loc.timezone ? `
Timezone: ${escapeInfoHtml(loc.timezone)}
` : ""} ${loc.time ? `
Offset: ${escapeInfoHtml(loc.time)}
` : ""} `; renderMap(gps); } // ------------------------------- // RENDER ATTIVITÀ // ------------------------------- function renderInfoActivity(a) { const gps = normalizeGpsFromItem(a); const name = a.display_name || a.route_name || a.title || a.file_name || a.name || "-"; infoPanel.innerHTML = `

Attività

Nome: ${escapeInfoHtml(name)}
Sport: ${escapeInfoHtml(a.sport ?? "-")}
Tipo attività: ${escapeInfoHtml(a.activity_type ?? "-")}
Formato: ${escapeInfoHtml(a.format || a.file_extension || "-")}

Prestazione

Distanza: ${fmtKm(a.distance_meters)}
Durata: ${fmtDuration(a.duration_millis)}
Dislivello positivo: ${fmtMeters(a.elevation_gain_meters)}
Dislivello negativo: ${fmtMeters(a.elevation_loss_meters)}
Quota minima: ${fmtMeters(a.min_elevation_meters)}
Quota massima: ${fmtMeters(a.max_elevation_meters)}
Velocità media: ${fmtSpeedMps(a.average_speed_mps)}
Velocità massima: ${fmtSpeedMps(a.max_speed_mps)}
Passo medio: ${fmtPace(a.average_pace_sec_per_km)}

Frequenza, cadenza, potenza

FC media: ${fmtInt(a.average_heart_rate)} bpm
FC massima: ${fmtInt(a.max_heart_rate)} bpm
Cadenza media: ${fmtInt(a.average_cadence)}
Cadenza massima: ${fmtInt(a.max_cadence)}
Calorie: ${fmtInt(a.calories)} kcal
Potenza media: ${fmtInt(a.power_avg)} W
Potenza massima: ${fmtInt(a.power_max)} W

Tempo

Inizio: ${fmtDateFromMillis(a.start_time_millis)}
Fine: ${fmtDateFromMillis(a.end_time_millis)}
Ultima modifica file: ${fmtDateFromMillis(a.last_modified_millis)}

Mappa

${ gps.lat !== "-" && gps.lng !== "-" ? '
' : '
Coordinate non disponibili
' }

File e sorgente

File: ${escapeInfoHtml(a.file_name ?? "-")}
Cartella: ${escapeInfoHtml(a.cartella ?? "-")}
Percorso: ${escapeInfoHtml(a.path ?? "-")}
Dimensione: ${ a.size_bytes ? `${(Number(a.size_bytes) / 1024 / 1024).toFixed(2)} MB` : "-" }
Dispositivo: ${ escapeInfoHtml( [a.device_manufacturer, a.device_model] .filter(Boolean) .join(" ") || "-" ) }
App sorgente: ${escapeInfoHtml(a.source_app ?? "-")}

Traccia

Punti: ${fmtInt(a.point_count)}
Segmenti: ${fmtInt(a.segment_count)}
Lap: ${fmtInt(a.lap_count)}
Centro: ${ gps.lat !== "-" && gps.lng !== "-" ? `${gps.lat}, ${gps.lng}` : "-" }
`; renderMap(gps); } // ------------------------------- // MAPPA INFO PANEL // ------------------------------- function renderMap(gps) { try { infoMapInstance?.remove(); } catch {} infoMapInstance = null; if (gps.lat === "-" || gps.lng === "-") return; setTimeout(() => { try { const lat = Number(gps.lat); const lng = Number(gps.lng); if (!Number.isFinite(lat) || !Number.isFinite(lng)) return; infoMapInstance = L.map("infoMap", { zoomControl: false, attributionControl: false }).setView([lat, lng], 13); L.tileLayer( "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { maxZoom: 19 } ).addTo(infoMapInstance); L.marker([lat, lng]).addTo(infoMapInstance); setTimeout(() => { infoMapInstance?.invalidateSize?.(); }, 100); } catch (err) { console.warn("Errore creazione mappa info:", err); } }, 80); } // ------------------------------- // RENDER DISPATCHER // ------------------------------- function renderInfo(item) { if (!item) return; if (isActivityItem(item)) { renderInfoActivity(item); } else { renderInfoPhoto(item); } } // ------------------------------- // API pubbliche // ------------------------------- window.openInfoPanel = function(item) { renderInfo(item || window.currentPhoto || window.currentActivity); infoPanel.classList.add("open"); infoPanel.setAttribute("aria-hidden", "false"); infoPanel.setAttribute("data-open", "1"); markButtonActive(true); }; window.closeInfoPanel = function() { infoPanel.classList.remove("open"); infoPanel.setAttribute("aria-hidden", "true"); infoPanel.setAttribute("data-open", "0"); markButtonActive(false); try { infoMapInstance?.remove(); } catch {} infoMapInstance = null; }; window.toggleInfoPanel = function(item) { if (isPanelOpen()) { window.closeInfoPanel(); } else { window.openInfoPanel(item || window.currentPhoto || window.currentActivity); } }; // ------------------------------- // Chiudi pannello cliccando fuori // ------------------------------- /*document.addEventListener("click", event => { if (!isPanelOpen()) return; const insidePanel = infoPanel.contains(event.target); const isPhotoInfoButton = event.target.id === "modalInfoBtn" || event.target.closest?.("#modalInfoBtn"); const isActivityInfoButton = event.target.id === "activityInfoBtn" || event.target.closest?.("#activityInfoBtn"); if (!insidePanel && !isPhotoInfoButton && !isActivityInfoButton) { window.closeInfoPanel(); } }); */ document.addEventListener("click", event => { if (!isPanelOpen()) return; const insidePanel = infoPanel.contains(event.target); const isPhotoInfoButton = event.target.closest?.("#modalInfoBtn"); const isActivityInfoButton = event.target.closest?.("#activityInfoBtn"); const insideMedia = event.target.closest?.("#modalMediaContainer"); if (insidePanel || isPhotoInfoButton || isActivityInfoButton) { return; } if (insideMedia) { window.closeInfoPanel(); return; } window.closeInfoPanel(); }); // Chiudi info panel cliccando sul contenuto media del modal document.getElementById("modalMediaContainer") ?.addEventListener("click", event => { if (!isPanelOpen()) return; const isVideo = event.target.closest("video"); const isImage = event.target.closest("img"); if (isVideo || isImage) { window.closeInfoPanel(); } }); // ------------------------------- // Auto-refresh su cambio media nel modal foto // ------------------------------- (() => { const mediaContainer = document.getElementById("modalMediaContainer"); if (!mediaContainer) return; const refreshIfOpen = () => { if (!isPanelOpen()) return; const item = window.currentPhoto || window.currentActivity; if (item) { renderInfo(item); } }; const mo = new MutationObserver(() => { setTimeout(refreshIfOpen, 0); }); mo.observe(mediaContainer, { childList: true }); document.getElementById("modalPrev")?.addEventListener("click", () => setTimeout(refreshIfOpen, 0) ); document.getElementById("modalNext")?.addEventListener("click", () => setTimeout(refreshIfOpen, 0) ); document.addEventListener("keydown", event => { if ( event.key === "ArrowLeft" || event.key === "ArrowRight" ) { setTimeout(refreshIfOpen, 0); } }); })(); document .getElementById("modalMediaContainer") ?.addEventListener("click", () => { if (isPanelOpen()) { window.closeInfoPanel(); } }, true); document.addEventListener("click", event => { const btn = event.target.closest(".info-close-btn"); if (btn) { window.closeInfoPanel?.(); } });