// public/js/map/activities/activityClusters.js
// ===================================
// MAPBOX ACTIVITY CLUSTERS
// ===================================
(() => {
"use strict";
let mapboxMap = null;
const clusterMarkers = new Map();
let renderToken = 0;
const SOURCE_ID = "activities";
const CLUSTER_LAYER_ID = "activities-clusters";
const COUNT_LAYER_ID = "activities-cluster-count";
// ===================================
// SET MAP
// ===================================
function setMap(map) {
mapboxMap = map;
console.log("[ActivityClusters] setMap");
}
// ===================================
// CREA LAYER CLUSTER
// ===================================
function createLayer() {
if (!mapboxMap) {
return;
}
if (!mapboxMap.isStyleLoaded()) {
return;
}
if (!mapboxMap.getSource(SOURCE_ID)) {
console.warn(
"[ActivityClusters] source activities non presente",
);
return;
}
// ---------------------------------
// CLUSTER LAYER
// ---------------------------------
if (!mapboxMap.getLayer(CLUSTER_LAYER_ID)) {
console.log(
"[ActivityClusters] creo layer cluster",
);
mapboxMap.addLayer({
id: CLUSTER_LAYER_ID,
type: "circle",
source: SOURCE_ID,
filter: ["has", "point_count"],
paint: {
"circle-color": "#ff3333",
"circle-radius": [
"step",
["get", "point_count"],
22,
10,
28,
50,
38,
],
"circle-stroke-color": "#ffffff",
"circle-stroke-width": 3,
// --------------------------------
// NASCONDI PALLINO MAPBOX
// --------------------------------
//
// Il layer rimane presente per
// queryRenderedFeatures().
//
"circle-opacity": 0,
"circle-stroke-opacity": 0,
},
});
}
// ---------------------------------
// COUNT LAYER
// ---------------------------------
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 numero Mapbox viene
// sostituito dal marker HTML.
"text-color": "#ffffff",
"text-opacity": 0,
},
});
}
console.log(
"[ActivityClusters] layer creati",
);
}
// ===================================
// CLUSTER VISIBILI
// ===================================
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) {
const count =
Number(cluster.properties?.point_count) || 0;
const size = Math.min(
72,
Math.round(30 + Math.sqrt(count) * 5),
);
const cls =
count > 200
? "cluster-xl"
: count > 50
? "cluster-lg"
: count > 10
? "cluster-md"
: "cluster-sm";
const el =
document.createElement("div");
el.className =
"activity-cluster-wrapper";
el.style.width = `${size}px`;
el.style.height = `${size}px`;
el.innerHTML = `
`;
return el;
}
// ===================================
// CLICK CLUSTER
// ===================================
function handleClusterClick(
clusterId,
coordinates,
) {
if (!mapboxMap) {
return;
}
const source =
mapboxMap.getSource(SOURCE_ID);
if (!source) {
return;
}
console.log(
"[ActivityClusters] click cluster",
{
clusterId,
coordinates,
},
);
/*
* Prima proviamo ad espandere
* il cluster con lo zoom Mapbox.
*/
source.getClusterExpansionZoom(
Number(clusterId),
(err, expansionZoom) => {
if (err) {
console.error(
"[ActivityClusters] expansion zoom error",
err,
);
return;
}
const currentZoom =
mapboxMap.getZoom();
const targetZoom = Math.max(
expansionZoom,
currentZoom + 1,
);
mapboxMap.easeTo({
center: coordinates,
zoom: targetZoom,
duration: 600,
});
},
);
}
// ===================================
// CREA MARKER
// ===================================
function createClusterMarker(cluster) {
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);
// ---------------------------------
// CLICK
// ---------------------------------
el.addEventListener(
"click",
(event) => {
event.stopPropagation();
handleClusterClick(
Number(clusterId),
coordinates,
);
},
);
// ---------------------------------
// MARKER
// ---------------------------------
const marker =
new mapboxgl.Marker({
element: el,
anchor: "center",
})
.setLngLat(coordinates)
.addTo(mapboxMap);
marker.__clusterId = clusterId;
return marker;
}
// ===================================
// UPDATE
// ===================================
function update() {
if (!mapboxMap) {
return;
}
if (!mapboxMap.isStyleLoaded()) {
return;
}
if (!mapboxMap.getSource(SOURCE_ID)) {
return;
}
if (!mapboxMap.getLayer(CLUSTER_LAYER_ID)) {
createLayer();
}
const token = ++renderToken;
const clusters =
getRenderedClusters();
const currentIds =
new Set(
clusters.map((cluster) =>
String(
cluster.properties?.cluster_id,
),
),
);
console.log(
"[ActivityClusters] 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 ESISTENTE
// ---------------------------------
if (
clusterMarkers.has(clusterId)
) {
const marker =
clusterMarkers.get(
clusterId,
);
marker.setLngLat(
coordinates,
);
marker
.getElement()
.style.display = "";
continue;
}
// ---------------------------------
// TOKEN
// ---------------------------------
if (token !== renderToken) {
continue;
}
// ---------------------------------
// CREA MARKER
// ---------------------------------
const marker =
createClusterMarker(cluster);
if (!marker) {
continue;
}
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(
"[ActivityClusters] debug",
{
markers:
clusterMarkers.size,
rendered:
getRenderedClusters().length,
layer:
!!mapboxMap?.getLayer(
CLUSTER_LAYER_ID,
),
source:
!!mapboxMap?.getSource(
SOURCE_ID,
),
},
);
}
// ===================================
// API PUBBLICA
// ===================================
window.MapboxActivityClusters = {
setMap,
createLayer,
update,
hide,
show,
clear,
size,
debug,
getRenderedClusters,
};
})();