224 lines
No EOL
8.3 KiB
JavaScript
224 lines
No EOL
8.3 KiB
JavaScript
// ===============================
|
||
// MAPPA GLOBALE — stile Google Photos Web
|
||
// - Cluster numerici che si spezzano con lo zoom
|
||
// - Click su cluster → zoom progressivo e, quando "pochi", strip in basso
|
||
// - Click su marker singolo → MODAL immediato (niente bottom)
|
||
// - Raggruppamento dinamico basato su raggio in pixel → metri
|
||
// ===============================
|
||
|
||
window.globalMap = null;
|
||
window.globalMarkers = null; // qui sarà un MarkerClusterGroup
|
||
|
||
document.addEventListener("DOMContentLoaded", () => {
|
||
const openBtn = document.getElementById("openMapBtn");
|
||
if (!openBtn) {
|
||
console.error("openMapBtn non trovato nel DOM");
|
||
return;
|
||
}
|
||
openBtn.addEventListener("click", openGlobalMap);
|
||
|
||
// —— Parametri "alla GP" regolabili ————————————————————————————————
|
||
const RADIUS_PX = 50; // raggio "visivo" sullo schermo per raggruppare vicini
|
||
const DISABLE_CLUSTER_AT_ZOOM = 18; // oltre questo zoom i cluster si spaccano
|
||
const OPEN_STRIP_CHILDREN_MAX = 20; // se un cluster ha <= N marker → apri strip invece di continuare a zoomare
|
||
// ————————————————————————————————————————————————————————————————
|
||
|
||
async function openGlobalMap() {
|
||
const mapDiv = document.getElementById("globalMap");
|
||
const gallery = document.getElementById("gallery");
|
||
if (!mapDiv) {
|
||
console.error("globalMap DIV non trovato");
|
||
return;
|
||
}
|
||
|
||
const isOpen = mapDiv.classList.contains("open");
|
||
|
||
// Chiudi mappa
|
||
if (isOpen) {
|
||
mapDiv.classList.remove("open");
|
||
gallery?.classList.remove("hidden");
|
||
window.closeBottomSheet?.();
|
||
return;
|
||
}
|
||
|
||
// Apri mappa e nascondi galleria
|
||
mapDiv.classList.add("open");
|
||
gallery?.classList.add("hidden");
|
||
|
||
|
||
// 👇 NUOVO: se la mappa è già stata creata in passato, riallinea l’alias
|
||
if (window.globalMap) {
|
||
window.leafletMapInstance = window.globalMap;
|
||
}
|
||
|
||
|
||
// Attendi dimensioni reali (evita init a height:0)
|
||
await new Promise(r => requestAnimationFrame(r));
|
||
let tries = 0;
|
||
while (mapDiv.getBoundingClientRect().height < 50 && tries < 10) {
|
||
await new Promise(r => setTimeout(r, 30));
|
||
tries++;
|
||
}
|
||
|
||
// Inizializza solo la prima volta
|
||
if (window.globalMap === null) {
|
||
console.log("Inizializzo mappa Leaflet + MarkerCluster…");
|
||
|
||
window.globalMap = L.map("globalMap", {
|
||
zoomControl: true,
|
||
attributionControl: true
|
||
}).setView([42.5, 12.5], 6);
|
||
|
||
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||
maxZoom: 19
|
||
}).addTo(window.globalMap);
|
||
|
||
// ✅ CLUSTER come Google Photos
|
||
window.globalMarkers = L.markerClusterGroup({
|
||
showCoverageOnHover: false,
|
||
spiderfyOnMaxZoom: true,
|
||
disableClusteringAtZoom: DISABLE_CLUSTER_AT_ZOOM
|
||
});
|
||
|
||
// Listener "clusterclick": zooma oppure, quando pochi, apri strip
|
||
window.globalMarkers.on("clusterclick", (a) => {
|
||
const childMarkers = a.layer.getAllChildMarkers();
|
||
const count = childMarkers.length;
|
||
|
||
if (count <= OPEN_STRIP_CHILDREN_MAX || window.globalMap.getZoom() >= DISABLE_CLUSTER_AT_ZOOM - 1) {
|
||
// Converti i child markers in lista foto e apri la strip
|
||
const photos = childMarkers
|
||
.map(m => m.__photo)
|
||
.filter(Boolean);
|
||
|
||
if (photos.length > 1) {
|
||
window.openBottomSheet?.(photos);
|
||
} else if (photos.length === 1) {
|
||
// Singola → modal diretto
|
||
openPhotoModal(photos[0]);
|
||
}
|
||
} else {
|
||
// Continua a zoomare sui bounds del cluster
|
||
window.globalMap.fitBounds(a.layer.getBounds(), { padding: [60, 60], maxZoom: DISABLE_CLUSTER_AT_ZOOM, animate: true });
|
||
}
|
||
});
|
||
|
||
window.globalMap.addLayer(window.globalMarkers);
|
||
|
||
// Disegna i marker
|
||
redrawPhotoMarkers();
|
||
}
|
||
|
||
// Fix dimensioni dopo apertura
|
||
setTimeout(() => window.globalMap?.invalidateSize?.(), 120);
|
||
}
|
||
|
||
// —— Icona thumbnail per i marker ———————————————————————————————
|
||
function createPhotoIcon(photo) {
|
||
const thumb = (typeof toAbsoluteUrl === "function")
|
||
? toAbsoluteUrl(photo?.thub2 || photo?.thub1)
|
||
: (photo?.thub2 || photo?.thub1);
|
||
|
||
return L.icon({
|
||
iconUrl: thumb || "",
|
||
iconSize: [56, 56],
|
||
iconAnchor: [28, 28],
|
||
className: "photo-marker"
|
||
});
|
||
}
|
||
|
||
// —— Modal diretto per singola foto ————————————————————————————
|
||
function openPhotoModal(photo) {
|
||
const thumb = (typeof toAbsoluteUrl === "function")
|
||
? toAbsoluteUrl(photo?.thub2 || photo?.thub1 || photo?.path)
|
||
: (photo?.thub2 || photo?.thub1 || photo?.path);
|
||
|
||
const original = (typeof toAbsoluteUrl === "function")
|
||
? toAbsoluteUrl(photo?.path)
|
||
: photo?.path;
|
||
|
||
window.closeBottomSheet?.();
|
||
window.openModal?.(original, thumb, photo);
|
||
}
|
||
|
||
// —— Raggio in metri a partire da N pixel all’zoom attuale ——————————
|
||
function radiusMetersAtZoom(latlng, px) {
|
||
if (!window.globalMap) return 0;
|
||
const p = window.globalMap.latLngToContainerPoint(latlng);
|
||
const p2 = L.point(p.x + px, p.y);
|
||
const ll2 = window.globalMap.containerPointToLatLng(p2);
|
||
return window.globalMap.distance(latlng, ll2);
|
||
}
|
||
|
||
// —— Distanza (metri) ————————————————————————————————————————
|
||
function distanceMeters(lat1, lng1, lat2, lng2) {
|
||
const toRad = d => d * Math.PI / 180;
|
||
const R = 6371000;
|
||
const φ1 = toRad(lat1), φ2 = toRad(lat2);
|
||
const dφ = toRad(lat2 - lat1);
|
||
const dλ = toRad(lng2 - lng1);
|
||
const a = Math.sin(dφ/2) ** 2 +
|
||
Math.cos(φ1) * Math.cos(φ2) *
|
||
Math.sin(dλ/2) ** 2;
|
||
return 2 * R * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||
}
|
||
|
||
// —— Raggruppa foto entro raggio (metri) ——————————————————————
|
||
function buildGroupByRadius(lat, lng, data, radiusM) {
|
||
return data.filter(p => {
|
||
const plat = +p?.gps?.lat;
|
||
const plng = +p?.gps?.lng;
|
||
if (!plat || !plng) return false;
|
||
return distanceMeters(lat, lng, plat, plng) <= radiusM;
|
||
});
|
||
}
|
||
|
||
// —— Disegno/refresh marker ————————————————————————————————
|
||
function redrawPhotoMarkers() {
|
||
if (!window.globalMarkers || !window.globalMap) return;
|
||
|
||
window.globalMarkers.clearLayers();
|
||
const data = Array.isArray(window.photosData) ? window.photosData : [];
|
||
|
||
data.forEach(photo => {
|
||
const lat = +photo?.gps?.lat;
|
||
const lng = +photo?.gps?.lng;
|
||
if (!lat || !lng) return;
|
||
|
||
const marker = L.marker([lat, lng], {
|
||
icon: createPhotoIcon(photo),
|
||
title: photo?.name || ""
|
||
});
|
||
|
||
// Alleghiamo la foto al marker per recuperarla in eventi cluster
|
||
marker.__photo = photo;
|
||
|
||
// Click su marker: calcola raggio dinamico e apri strip o modal
|
||
marker.on("click", () => {
|
||
const here = L.latLng(lat, lng);
|
||
const radiusM = radiusMetersAtZoom(here, RADIUS_PX);
|
||
const dataAll = Array.isArray(window.photosData) ? window.photosData : [];
|
||
const group = buildGroupByRadius(lat, lng, dataAll, radiusM);
|
||
|
||
if (group.length > 1) {
|
||
window.openBottomSheet?.(group);
|
||
} else if (group.length === 1) {
|
||
openPhotoModal(group[0]);
|
||
} else {
|
||
openPhotoModal(photo);
|
||
}
|
||
});
|
||
|
||
window.globalMarkers.addLayer(marker);
|
||
});
|
||
}
|
||
|
||
// Se la gallery si aggiorna (loadPhotos), ridisegna marker
|
||
const originalRefresh = window.refreshGallery;
|
||
window.refreshGallery = function wrappedRefreshGallery(...args) {
|
||
try { originalRefresh?.apply(this, args); } catch (_) {}
|
||
if (window.globalMap && window.globalMarkers) {
|
||
redrawPhotoMarkers();
|
||
}
|
||
};
|
||
}); |