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

663 lines
12 KiB
Text

// public/js/map/photos/photoClusters.js
// ===================================
// MAPBOX PHOTO CLUSTERS
// ===================================
(() => {
"use strict";
let mapboxMap = null;
const clusterMarkers = new Map();
let renderToken = 0;
const SOURCE_ID = "photos";
const CLUSTER_LAYER_ID = "photos-clusters";
// ===================================
// SET MAP
// ===================================
function setMap(map) {
mapboxMap = map;
console.log("[PhotoClusters] setMap");
}
// ===================================
// CREA LAYER CLUSTER
// ===================================
function createLayer() {
if (!mapboxMap) {
return;
}
if (!mapboxMap.isStyleLoaded()) {
return;
}
if (!mapboxMap.getSource(SOURCE_ID)) {
console.warn(
"[PhotoClusters] source photos non presente",
);
return;
}
if (mapboxMap.getLayer(CLUSTER_LAYER_ID)) {
return;
}
console.log("[PhotoClusters] creo layer cluster");
mapboxMap.addLayer({
id: CLUSTER_LAYER_ID,
type: "circle",
source: SOURCE_ID,
filter: ["has", "point_count"],
paint: {
// =================================
// IMPORTANTE
// =================================
//
// Il layer deve rimanere renderizzato
// per queryRenderedFeatures().
//
// Ma il pallino blu Mapbox non deve
// essere visibile perché viene
// sostituito dal marker HTML.
//
"circle-color": "#3388ff",
"circle-radius": [
"step",
["get", "point_count"],
22,
10,
28,
50,
38,
],
"circle-stroke-color": "#ffffff",
"circle-stroke-width": 3,
"circle-opacity": 0,
"circle-stroke-opacity": 0,
},
});
// =================================
// NUMERO CLUSTER
// =================================
//
// Anche il numero Mapbox viene nascosto.
// Il numero viene disegnato dal marker HTML.
//
const countLayerId = "photos-cluster-count";
if (!mapboxMap.getLayer(countLayerId)) {
mapboxMap.addLayer({
id: countLayerId,
type: "symbol",
source: SOURCE_ID,
filter: ["has", "point_count"],
layout: {
"text-field": "{point_count_abbreviated}",
"text-size": 14,
},
paint: {
"text-color": "#ffffff",
"text-opacity": 0,
},
});
}
console.log("[PhotoClusters] layer creati");
}
// ===================================
// GET RENDERED CLUSTERS
// ===================================
function getRenderedClusters() {
if (!mapboxMap) {
return [];
}
if (!mapboxMap.getLayer(CLUSTER_LAYER_ID)) {
return [];
}
const rendered = mapboxMap.queryRenderedFeatures({
layers: [CLUSTER_LAYER_ID],
});
const clusters = [];
const seen = new Set();
for (const feature of rendered) {
const clusterId =
feature.properties?.cluster_id;
if (
clusterId === undefined ||
clusterId === null
) {
continue;
}
const id = String(clusterId);
if (seen.has(id)) {
continue;
}
seen.add(id);
clusters.push(feature);
}
return clusters;
}
// ===================================
// CREA ELEMENTO HTML CLUSTER
// ===================================
function createClusterElement(
cluster,
thumbs,
) {
const count =
Number(cluster.properties?.point_count) || 0;
const size = Math.min(
92,
Math.round(28 + Math.sqrt(count) * 6),
);
const cls =
count > 200
? "cluster-xl"
: count > 50
? "cluster-lg"
: count > 10
? "cluster-md"
: "cluster-sm";
const el =
document.createElement("div");
el.className =
"marker-cluster-wrapper mapbox-cluster-wrapper";
el.style.width = `${size}px`;
el.style.height = `${size}px`;
// =================================
// COLLAGE
// =================================
const collage = thumbs.length
? `
<div class="cluster-collage">
${thumbs
.map(
(src, index) => `
<div class="c${index}">
<img
src="${src}"
alt=""
/>
</div>
`,
)
.join("")}
</div>
`
: "";
// =================================
// HTML
// =================================
el.innerHTML = `
<div
class="gp-cluster ${cls}"
style="
width:${size}px;
height:${size}px;
"
>
${collage}
<div class="gp-count">
<span>${count}</span>
</div>
</div>
`;
return el;
}
// ===================================
// CLICK CLUSTER
// ===================================
function handleClusterClick(
clusterId,
coordinates,
) {
console.log(
"[PhotoClusters] click cluster",
{
clusterId,
coordinates,
},
);
if (
window.MapboxPhotoSpiderfy
) {
window.MapboxPhotoSpiderfy.handleClusterClick(
clusterId,
coordinates,
);
return;
}
console.warn(
"[PhotoClusters] PhotoSpiderfy non disponibile",
);
}
// ===================================
// CREA MARKER HTML
// ===================================
function createClusterMarker(
cluster,
thumbs,
) {
if (!mapboxMap) {
return null;
}
const coordinates =
cluster.geometry?.coordinates;
if (
!coordinates ||
coordinates.length < 2
) {
return null;
}
const clusterId = String(
cluster.properties?.cluster_id,
);
const el = createClusterElement(
cluster,
thumbs,
);
// =================================
// CLICK
// =================================
el.addEventListener(
"click",
(event) => {
event.stopPropagation();
handleClusterClick(
Number(clusterId),
coordinates,
);
},
);
// =================================
// MAPBOX MARKER
// =================================
const marker =
new mapboxgl.Marker({
element: el,
anchor: "center",
})
.setLngLat(coordinates)
.addTo(mapboxMap);
return marker;
}
// ===================================
// RECUPERA THUMBNAILS
// ===================================
function getClusterThumbs(
clusterId,
callback,
) {
if (!mapboxMap) {
return;
}
const source =
mapboxMap.getSource(SOURCE_ID);
if (!source) {
return;
}
source.getClusterLeaves(
Number(clusterId),
4,
0,
callback,
);
}
// ===================================
// UPDATE
// ===================================
function update() {
if (!mapboxMap) {
return;
}
if (!mapboxMap.isStyleLoaded()) {
return;
}
if (!mapboxMap.getSource(SOURCE_ID)) {
return;
}
if (!mapboxMap.getLayer(CLUSTER_LAYER_ID)) {
createLayer();
}
if (
window.MapboxPhotoSpiderfy?.isOpen?.()
) {
return;
}
const token = ++renderToken;
const clusters =
getRenderedClusters();
const currentIds = new Set(
clusters.map((cluster) =>
String(
cluster.properties?.cluster_id,
),
),
);
console.log(
"[PhotoClusters] rendered:",
clusters.length,
);
// =================================
// CREA / AGGIORNA
// =================================
for (const cluster of clusters) {
const clusterId = String(
cluster.properties?.cluster_id,
);
const coordinates =
cluster.geometry?.coordinates;
if (
!coordinates ||
coordinates.length < 2
) {
continue;
}
// --------------------------------
// MARKER GIÀ ESISTENTE
// --------------------------------
if (
clusterMarkers.has(clusterId)
) {
const marker =
clusterMarkers.get(clusterId);
marker.setLngLat(
coordinates,
);
marker
.getElement()
.style.display = "";
continue;
}
// --------------------------------
// RECUPERA THUMB
// --------------------------------
getClusterThumbs(
clusterId,
(err, leaves) => {
if (err) {
console.error(
"[PhotoClusters] leaves error",
err,
);
return;
}
// --------------------------------
// TOKEN
// --------------------------------
if (
token !== renderToken
) {
return;
}
// --------------------------------
// CLUSTER ANCORA VISIBILE
// --------------------------------
if (
!currentIds.has(
clusterId,
)
) {
return;
}
const thumbs =
leaves
.map(
(feature) =>
feature.properties?.thumb,
)
.filter(Boolean);
console.log(
"[PhotoClusters] creo marker",
{
clusterId,
count:
cluster.properties
?.point_count,
thumbs:
thumbs.length,
},
);
const marker =
createClusterMarker(
cluster,
thumbs,
);
if (!marker) {
return;
}
clusterMarkers.set(
clusterId,
marker,
);
},
);
}
// =================================
// RIMUOVI CLUSTER NON VISIBILI
// =================================
for (
const [
clusterId,
marker,
] of clusterMarkers
) {
if (
!currentIds.has(
String(clusterId),
)
) {
marker.remove();
clusterMarkers.delete(
clusterId,
);
}
}
}
// ===================================
// HIDE
// ===================================
function hide() {
for (
const marker of clusterMarkers.values()
) {
marker
.getElement()
.style.display = "none";
}
}
// ===================================
// SHOW
// ===================================
function show() {
for (
const marker of clusterMarkers.values()
) {
marker
.getElement()
.style.display = "";
}
}
// ===================================
// CLEAR
// ===================================
function clear() {
renderToken++;
for (
const marker of clusterMarkers.values()
) {
marker.remove();
}
clusterMarkers.clear();
}
// ===================================
// SIZE
// ===================================
function size() {
return clusterMarkers.size;
}
// ===================================
// DEBUG
// ===================================
function debug() {
console.log(
"[PhotoClusters] debug",
{
markers:
clusterMarkers.size,
rendered:
getRenderedClusters().length,
layer:
!!mapboxMap?.getLayer(
CLUSTER_LAYER_ID,
),
source:
!!mapboxMap?.getSource(
SOURCE_ID,
),
},
);
}
// ===================================
// API PUBBLICA
// ===================================
window.MapboxPhotoClusters = {
setMap,
createLayer,
update,
hide,
show,
clear,
size,
debug,
getRenderedClusters,
};
})();