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