// public/js/map/photos/photoSpiderfy.js // =================================== // MAPBOX PHOTO SPIDERFY // =================================== (() => { "use strict"; let mapboxMap = null; const spiderMarkers = []; let spiderfyOpen = false; const SOURCE_ID = "photos"; // =================================== // SET MAP // =================================== function setMap(map) { mapboxMap = map; console.log("[PhotoSpiderfy] setMap"); // --------------------------------- // ZOOM // --------------------------------- mapboxMap.on("zoomstart", () => { if (!spiderfyOpen) { return; } console.log( "[PhotoSpiderfy] zoom iniziato → chiudo", ); clear(); }); // --------------------------------- // MOVEEND // --------------------------------- mapboxMap.on("moveend", () => { if (spiderfyOpen) { return; } if ( window.MapboxPhotoClusters ) { window.MapboxPhotoClusters.update(); } if ( window.MapboxPhotoMarkers ) { const photos = getAllPhotos(); window.MapboxPhotoMarkers.update( photos, ); } }); // --------------------------------- // CLICK FUORI // --------------------------------- mapboxMap.on("click", (event) => { if (!spiderfyOpen) { return; } const hits = mapboxMap.queryRenderedFeatures( event.point, { layers: [ "photos-clusters", ], }, ); if (hits.length) { return; } clear(); }); } // =================================== // GET TUTTE LE FOTO // =================================== function getAllPhotos() { if (!mapboxMap) { return []; } if ( !mapboxMap.getSource( SOURCE_ID, ) ) { return []; } return mapboxMap.querySourceFeatures( SOURCE_ID, { filter: [ "!", ["has", "point_count"], ], }, ); } // =================================== // DISTANZA // =================================== function distanceMeters(a, b) { const R = 6371000; const lat1 = (a[1] * Math.PI) / 180; const lat2 = (b[1] * Math.PI) / 180; const dLat = lat2 - lat1; const dLng = ((b[0] - a[0]) * Math.PI) / 180; const h = Math.sin(dLat / 2) ** 2 + Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLng / 2) ** 2; return ( 2 * R * Math.atan2( Math.sqrt(h), Math.sqrt(1 - h), ) ); } // =================================== // FOTO VICINE // =================================== function getNearbyPhotos( feature, maxDistance = 3, ) { if (!feature) { return []; } const center = feature.geometry.coordinates; /* * Preferiamo i dati completi * mantenuti dal source/orchestratore. */ let photos = []; if ( window.MapboxMarkers ?.getPhotos ) { photos = window.MapboxMarkers.getPhotos(); } /* * Fallback: * leggiamo direttamente * le feature dalla source. */ if (!photos.length) { photos = getAllPhotos(); } return photos.filter( (other) => distanceMeters( center, other.geometry.coordinates, ) <= maxDistance, ); } // =================================== // HIDE CLUSTER HTML // =================================== function hideClusterMarkers() { if ( !window.MapboxPhotoClusters ) { return; } window.MapboxPhotoClusters.hide(); } // =================================== // SHOW CLUSTER HTML // =================================== function showClusterMarkers() { if ( !window.MapboxPhotoClusters ) { return; } window.MapboxPhotoClusters.show(); } // =================================== // CLEAR SPIDERFY // =================================== function clear() { console.log( "[PhotoSpiderfy] chiusura", ); // --------------------------------- // RIMUOVI MARKER SPIDER // --------------------------------- for ( const marker of spiderMarkers ) { marker.remove(); const id = marker.__photoId; if ( id !== undefined && window.MapboxPhotoMarkers ) { /* * Rimuoviamo solo il marker * spider dal modulo marker. */ window.MapboxPhotoMarkers.remove( id, ); } } spiderMarkers.length = 0; spiderfyOpen = false; // --------------------------------- // REPAINT // --------------------------------- if (mapboxMap) { mapboxMap.triggerRepaint(); } showClusterMarkers(); // --------------------------------- // RICOSTRUISCI CLUSTER // --------------------------------- if ( window.MapboxPhotoClusters ) { window.MapboxPhotoClusters.update(); } // --------------------------------- // RICOSTRUISCI FOTO // --------------------------------- if ( window.MapboxPhotoMarkers ) { window.MapboxPhotoMarkers.update( getAllPhotos(), ); } console.log( "[PhotoSpiderfy] chiusa", ); } // =================================== // CREA MARKER SPIDER // =================================== function createSpiderMarker( feature, id, ) { const img = document.createElement("img"); img.src = feature.properties?.thumb || ""; img.width = 46; img.height = 46; img.style.borderRadius = "7px"; img.style.objectFit = "cover"; img.style.border = "2px solid white"; img.style.cursor = "pointer"; // --------------------------------- // CLICK // --------------------------------- img.addEventListener( "click", (event) => { event.stopPropagation(); const items = typeof window.getLocalItems === "function" ? window.getLocalItems() : []; const item = items.find( (photo) => String(photo.id) === id, ); if (item) { window.openPhotoMapItem?.( item, ); } clear(); }, ); // --------------------------------- // MARKER // --------------------------------- const marker = new mapboxgl.Marker({ element: img, }) .setLngLat( feature.geometry.coordinates, ) .addTo(mapboxMap); marker.__originalLngLat = feature.geometry.coordinates; marker.__spider = true; marker.__photoId = id; return marker; } // =================================== // SPIDERFY FOTO // =================================== function spiderfyPhotos( features, ) { if ( !features?.length || !mapboxMap ) { return; } const center = features[0].geometry.coordinates; // --------------------------------- // RAGGIERA DINAMICA // --------------------------------- const count = features.length; const radiusPx = Math.min( 69, Math.max( 20, 20 + count * 5, ), ); console.log( "[PhotoSpiderfy] raggiera", { foto: count, radiusPx, }, ); // --------------------------------- // CENTRO PIXEL // --------------------------------- const centerPoint = mapboxMap.project(center); hideClusterMarkers(); // --------------------------------- // CREA MARKER // --------------------------------- features.forEach( (feature, index) => { const id = String( feature.properties?.id, ); /* * Se esiste già un marker normale, * lo rimuoviamo temporaneamente. */ if ( window.MapboxPhotoMarkers ) { const existing = window.MapboxPhotoMarkers.get( id, ); if (existing) { existing.remove(); } } // -------------------------------- // CREA MARKER SPIDER // -------------------------------- const marker = createSpiderMarker( feature, id, ); // -------------------------------- // POSIZIONE // -------------------------------- const angle = (Math.PI * 2 * index) / features.length; const point = { x: centerPoint.x + Math.cos(angle) * radiusPx, y: centerPoint.y + Math.sin(angle) * radiusPx, }; const lngLat = mapboxMap.unproject( point, ); marker.setLngLat( lngLat, ); marker.__originalLngLat = feature.geometry.coordinates; spiderMarkers.push( marker, ); }, ); spiderfyOpen = true; console.log( "[PhotoSpiderfy] aperto:", features.length, ); } // =================================== // SMART ZOOM // =================================== function smartZoomCluster( clusterId, center, ) { if (!mapboxMap) { return; } const source = mapboxMap.getSource( SOURCE_ID, ); if (!source) { return; } const MAX_STEPS = 5; let steps = 0; function zoomStep() { if ( steps >= MAX_STEPS ) { console.log( "[PhotoSpiderfy] smart zoom limite", ); return; } steps++; source.getClusterExpansionZoom( Number(clusterId), (err, expansionZoom) => { if (err) { console.error( "[PhotoSpiderfy] smart zoom error", err, ); return; } const currentZoom = mapboxMap.getZoom(); const targetZoom = Math.max( expansionZoom, currentZoom + 1, ); console.log( "[PhotoSpiderfy] smart zoom step", { clusterId, step: steps, currentZoom, expansionZoom, targetZoom, }, ); mapboxMap.once( "moveend", () => { const hits = mapboxMap.querySourceFeatures( SOURCE_ID, { filter: [ "has", "point_count", ], }, ); const sameCluster = hits.some( (feature) => Number( feature .properties ?.cluster_id, ) === Number( clusterId, ), ); console.log( "[PhotoSpiderfy] smart zoom check", { clusterId, sameCluster, zoom: mapboxMap.getZoom(), }, ); if (sameCluster) { zoomStep(); } else { console.log( "[PhotoSpiderfy] cluster espanso", ); if ( window .MapboxPhotoClusters ) { window .MapboxPhotoClusters .update(); } } }, ); mapboxMap.easeTo({ center, zoom: targetZoom, duration: 700, }); }, ); } zoomStep(); } // =================================== // CLICK CLUSTER // =================================== function handleClusterClick( clusterId, center, ) { if (!mapboxMap) { return; } const source = mapboxMap.getSource( SOURCE_ID, ); if (!source) { return; } console.log( "[PhotoSpiderfy] cluster click", { clusterId, center, }, ); source.getClusterLeaves( Number(clusterId), 100, 0, (err, leaves) => { if (err) { console.error( "[PhotoSpiderfy] cluster leaves error", err, ); return; } if (!leaves.length) { return; } console.log( "[PhotoSpiderfy] cluster leaves:", leaves.length, ); // ================================= // CONTROLLA SE SONO RAGGRUPPATE // ================================= const first = leaves[0].geometry.coordinates; const firstLng = first[0]; const firstLat = first[1]; const radiusMeters = 30; const latDelta = radiusMeters / 111320; const lngDelta = radiusMeters / ( 111320 * Math.cos( (firstLat * Math.PI) / 180, ) ); const minLat = firstLat - latDelta; const maxLat = firstLat + latDelta; const minLng = firstLng - lngDelta; const maxLng = firstLng + lngDelta; let isSpider = true; for ( let i = 1; i < leaves.length; i++ ) { const coords = leaves[i].geometry.coordinates; const lng = coords[0]; const lat = coords[1]; if ( lat < minLat || lat > maxLat || lng < minLng || lng > maxLng ) { isSpider = false; break; } } console.log( "[PhotoSpiderfy] cluster spider:", isSpider, ); // ================================= // SPIDERFY // ================================= if (isSpider) { clear(); spiderfyPhotos( leaves, ); return; } // ================================= // SMART ZOOM // ================================= smartZoomCluster( clusterId, center, ); }, ); } // =================================== // IS OPEN // =================================== function isOpen() { return spiderfyOpen; } // =================================== // SIZE // =================================== function size() { return spiderMarkers.length; } // =================================== // DEBUG // =================================== function debug() { console.log( "[PhotoSpiderfy] debug", { open: spiderfyOpen, markers: spiderMarkers.length, }, ); } // =================================== // API PUBBLICA // =================================== window.MapboxPhotoSpiderfy = { setMap, clear, spiderfyPhotos, getNearbyPhotos, handleClusterClick, smartZoomCluster, isOpen, size, debug, }; })();