server_photo_s85js/public/js/map/photos/photoSpiderfy.js
2026-08-11 08:45:04 +02:00

864 lines
16 KiB
JavaScript

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