// 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 ? `
${thumbs .map( (src, index) => `
`, ) .join("")}
` : ""; // ================================= // HTML // ================================= el.innerHTML = `
${collage}
${count}
`; 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, }; })();