// 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 = `
${count}
`; 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, }; })();