667 lines
12 KiB
JavaScript
667 lines
12 KiB
JavaScript
// 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";
|
|
const COUNT_LAYER_ID = "photos-cluster-count";
|
|
|
|
// ===================================
|
|
// SET MAP
|
|
// ===================================
|
|
|
|
function setMap(map) {
|
|
mapboxMap = map;
|
|
|
|
console.log("[PhotoClusters] setMap");
|
|
}
|
|
|
|
// ===================================
|
|
// CREA LAYER CLUSTER
|
|
// ===================================
|
|
|
|
function createLayer() {
|
|
|
|
|
|
console.group("[PhotoClusters] createLayer CALL");
|
|
console.log("time:", performance.now());
|
|
console.trace();
|
|
console.groupEnd();
|
|
|
|
|
|
|
|
|
|
|
|
if (!mapboxMap) {
|
|
return;
|
|
}
|
|
|
|
if (!mapboxMap.isStyleLoaded()) {
|
|
return;
|
|
}
|
|
|
|
if (!mapboxMap.getSource(SOURCE_ID)) {
|
|
console.warn(
|
|
"[PhotoClusters] source photos non presente",
|
|
);
|
|
|
|
return;
|
|
}
|
|
|
|
// ---------------------------------
|
|
// LAYER CLUSTER
|
|
// ---------------------------------
|
|
|
|
if (!mapboxMap.getLayer(CLUSTER_LAYER_ID)) {
|
|
console.log(
|
|
"[PhotoClusters] creo layer cluster",
|
|
);
|
|
|
|
mapboxMap.addLayer({
|
|
id: CLUSTER_LAYER_ID,
|
|
|
|
type: "circle",
|
|
|
|
source: SOURCE_ID,
|
|
|
|
filter: ["has", "point_count"],
|
|
|
|
paint: {
|
|
/*
|
|
* Il layer Mapbox deve rimanere
|
|
* renderizzato perché viene utilizzato
|
|
* da queryRenderedFeatures().
|
|
*
|
|
* Il pallino Mapbox viene però nascosto.
|
|
*/
|
|
|
|
"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
|
|
// ---------------------------------
|
|
|
|
if (!mapboxMap.getLayer(COUNT_LAYER_ID)) {
|
|
mapboxMap.addLayer({
|
|
id: COUNT_LAYER_ID,
|
|
|
|
type: "symbol",
|
|
|
|
source: SOURCE_ID,
|
|
|
|
filter: ["has", "point_count"],
|
|
|
|
layout: {
|
|
"text-field": "{point_count_abbreviated}",
|
|
|
|
"text-size": 14,
|
|
},
|
|
|
|
paint: {
|
|
/*
|
|
* Anche il testo originale Mapbox
|
|
* viene nascosto perché il numero
|
|
* viene mostrato dal marker HTML.
|
|
*/
|
|
|
|
"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
|
|
// ===================================
|
|
|
|
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
|
|
?.handleClusterClick
|
|
) {
|
|
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,
|
|
);
|
|
},
|
|
);
|
|
|
|
// ---------------------------------
|
|
// MARKER MAPBOX
|
|
// ---------------------------------
|
|
|
|
return new mapboxgl.Marker({
|
|
element: el,
|
|
anchor: "center",
|
|
})
|
|
.setLngLat(coordinates)
|
|
.addTo(mapboxMap);
|
|
}
|
|
|
|
// ===================================
|
|
// 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;
|
|
}
|
|
|
|
//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);
|
|
|
|
const marker =
|
|
createClusterMarker(
|
|
cluster,
|
|
thumbs,
|
|
);
|
|
|
|
if (!marker) {
|
|
return;
|
|
}
|
|
|
|
clusterMarkers.set(
|
|
clusterId,
|
|
marker,
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
// =================================
|
|
// RIMUOVI 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,
|
|
),
|
|
},
|
|
);
|
|
}
|
|
|
|
// ===================================
|
|
// PUBLIC API
|
|
// ===================================
|
|
|
|
window.MapboxPhotoClusters = {
|
|
setMap,
|
|
createLayer,
|
|
update,
|
|
hide,
|
|
show,
|
|
clear,
|
|
size,
|
|
debug,
|
|
getRenderedClusters,
|
|
};
|
|
})();
|