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

537 lines
14 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
}
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 = `
<button class="info-close-btn" type="button" aria-label="Chiudi informazioni">×</button>
<h3>Informazioni</h3>
<div class="info-row"><b>Nome:</b> ${escapeInfoHtml(photo.name ?? "-")}</div>
<div class="info-row"><b>Data:</b> ${escapeInfoHtml(photo.taken_at ?? "-")}</div>
<div class="info-row"><b>Latitudine:</b> ${escapeInfoHtml(gps.lat ?? "-")}</div>
<div class="info-row"><b>Longitudine:</b> ${escapeInfoHtml(gps.lng ?? "-")}</div>
<div class="info-row"><b>Altitudine:</b> ${escapeInfoHtml(gps.alt ?? "-")} m</div>
<div class="info-row"><b>Dimensioni:</b> ${escapeInfoHtml(photo.width ?? "-")} × ${escapeInfoHtml(photo.height ?? "-")}</div>
<div class="info-row"><b>Peso:</b> ${
photo.size_bytes
? `${(Number(photo.size_bytes) / 1024 / 1024).toFixed(2)} MB`
: "-"
}</div>
<div class="info-row"><b>Tipo:</b> ${escapeInfoHtml(photo.mime_type ?? "-")}</div>
<div class="info-row"><b>Cartella:</b> ${escapeInfoHtml(folder)}</div>
<div class="info-spacer"></div>
<h3>Mappa</h3>
${
gps.lat !== "-" && gps.lng !== "-"
? '<div id="infoMap" class="info-map"></div>'
: '<div class="info-row">Coordinate non disponibili</div>'
}
<div class="info-spacer"></div>
<h3>Location</h3>
${loc.continent ? `<div class="info-row"><b>Continente:</b> ${escapeInfoHtml(loc.continent)}</div>` : ""}
${loc.country ? `<div class="info-row"><b>Nazione:</b> ${escapeInfoHtml(loc.country)}</div>` : ""}
${loc.region ? `<div class="info-row"><b>Regione:</b> ${escapeInfoHtml(loc.region)}</div>` : ""}
${loc.city ? `<div class="info-row"><b>Città:</b> ${escapeInfoHtml(loc.city)}</div>` : ""}
${loc.address ? `<div class="info-row"><b>Indirizzo:</b> ${escapeInfoHtml(loc.address)}</div>` : ""}
${loc.postcode ? `<div class="info-row"><b>CAP:</b> ${escapeInfoHtml(loc.postcode)}</div>` : ""}
${loc.county_code ? `<div class="info-row"><b>Provincia:</b> ${escapeInfoHtml(loc.county_code)}</div>` : ""}
${loc.timezone ? `<div class="info-row"><b>Timezone:</b> ${escapeInfoHtml(loc.timezone)}</div>` : ""}
${loc.time ? `<div class="info-row"><b>Offset:</b> ${escapeInfoHtml(loc.time)}</div>` : ""}
`;
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 = `
<button class="info-close-btn" type="button" aria-label="Chiudi informazioni">×</button>
<h3>Attività</h3>
<div class="info-row"><b>Nome:</b> ${escapeInfoHtml(name)}</div>
<div class="info-row"><b>Sport:</b> ${escapeInfoHtml(a.sport ?? "-")}</div>
<div class="info-row"><b>Tipo attività:</b> ${escapeInfoHtml(a.activity_type ?? "-")}</div>
<div class="info-row"><b>Formato:</b> ${escapeInfoHtml(a.format || a.file_extension || "-")}</div>
<div class="info-spacer"></div>
<h3>Prestazione</h3>
<div class="info-row"><b>Distanza:</b> ${fmtKm(a.distance_meters)}</div>
<div class="info-row"><b>Durata:</b> ${fmtDuration(a.duration_millis)}</div>
<div class="info-row"><b>Dislivello positivo:</b> ${fmtMeters(a.elevation_gain_meters)}</div>
<div class="info-row"><b>Dislivello negativo:</b> ${fmtMeters(a.elevation_loss_meters)}</div>
<div class="info-row"><b>Quota minima:</b> ${fmtMeters(a.min_elevation_meters)}</div>
<div class="info-row"><b>Quota massima:</b> ${fmtMeters(a.max_elevation_meters)}</div>
<div class="info-row"><b>Velocità media:</b> ${fmtSpeedMps(a.average_speed_mps)}</div>
<div class="info-row"><b>Velocità massima:</b> ${fmtSpeedMps(a.max_speed_mps)}</div>
<div class="info-row"><b>Passo medio:</b> ${fmtPace(a.average_pace_sec_per_km)}</div>
<div class="info-spacer"></div>
<h3>Frequenza, cadenza, potenza</h3>
<div class="info-row"><b>FC media:</b> ${fmtInt(a.average_heart_rate)} bpm</div>
<div class="info-row"><b>FC massima:</b> ${fmtInt(a.max_heart_rate)} bpm</div>
<div class="info-row"><b>Cadenza media:</b> ${fmtInt(a.average_cadence)}</div>
<div class="info-row"><b>Cadenza massima:</b> ${fmtInt(a.max_cadence)}</div>
<div class="info-row"><b>Calorie:</b> ${fmtInt(a.calories)} kcal</div>
<div class="info-row"><b>Potenza media:</b> ${fmtInt(a.power_avg)} W</div>
<div class="info-row"><b>Potenza massima:</b> ${fmtInt(a.power_max)} W</div>
<div class="info-spacer"></div>
<h3>Tempo</h3>
<div class="info-row"><b>Inizio:</b> ${fmtDateFromMillis(a.start_time_millis)}</div>
<div class="info-row"><b>Fine:</b> ${fmtDateFromMillis(a.end_time_millis)}</div>
<div class="info-row"><b>Ultima modifica file:</b> ${fmtDateFromMillis(a.last_modified_millis)}</div>
<div class="info-spacer"></div>
<h3>Mappa</h3>
${
gps.lat !== "-" && gps.lng !== "-"
? '<div id="infoMap" class="info-map"></div>'
: '<div class="info-row">Coordinate non disponibili</div>'
}
<div class="info-spacer"></div>
<h3>File e sorgente</h3>
<div class="info-row"><b>File:</b> ${escapeInfoHtml(a.file_name ?? "-")}</div>
<div class="info-row"><b>Cartella:</b> ${escapeInfoHtml(a.cartella ?? "-")}</div>
<div class="info-row"><b>Percorso:</b> ${escapeInfoHtml(a.path ?? "-")}</div>
<div class="info-row"><b>Dimensione:</b> ${
a.size_bytes
? `${(Number(a.size_bytes) / 1024 / 1024).toFixed(2)} MB`
: "-"
}</div>
<div class="info-row"><b>Dispositivo:</b> ${
escapeInfoHtml(
[a.device_manufacturer, a.device_model]
.filter(Boolean)
.join(" ") || "-"
)
}</div>
<div class="info-row"><b>App sorgente:</b> ${escapeInfoHtml(a.source_app ?? "-")}</div>
<div class="info-spacer"></div>
<h3>Traccia</h3>
<div class="info-row"><b>Punti:</b> ${fmtInt(a.point_count)}</div>
<div class="info-row"><b>Segmenti:</b> ${fmtInt(a.segment_count)}</div>
<div class="info-row"><b>Lap:</b> ${fmtInt(a.lap_count)}</div>
<div class="info-row"><b>Centro:</b> ${
gps.lat !== "-" && gps.lng !== "-"
? `${gps.lat}, ${gps.lng}`
: "-"
}</div>
`;
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?.();
}
});