@@ -10,7 +10,7 @@ import type { Deployment } from "@/types/deployment";
1010import maplibregl from "maplibre-gl" ;
1111import { useTheme } from "next-themes" ;
1212import { useEffect , useRef , useState } from "react" ;
13- import Supercluster from "supercluster" ;
13+ import Supercluster , { ClusterFeature , PointFeature } from "supercluster" ;
1414
1515interface DeploymentMapProps {
1616 deployments : Deployment [ ] ;
@@ -301,17 +301,44 @@ export function DeploymentMap({
301301 markers . current = [ ] ;
302302 markerElementsMap . current . clear ( ) ;
303303
304- // Create supercluster index only if clustering is enabled
305- const cluster = enableClustering
306- ? new Supercluster < Deployment > ( {
307- radius : 60 ,
308- maxZoom : 16 ,
309- minPoints : 2 ,
310- } )
304+ // Group deployments by program for separate clustering
305+ const deploymentsByProgram = deployments . reduce ( ( acc , deployment ) => {
306+ if ( ! acc [ deployment . program ] ) {
307+ acc [ deployment . program ] = [ ] ;
308+ }
309+ acc [ deployment . program ] . push ( deployment ) ;
310+ return acc ;
311+ } , { } as Record < string , Deployment [ ] > ) ;
312+
313+ // Create separate supercluster instances for each program
314+ const clustersByProgram = enableClustering
315+ ? Object . entries ( deploymentsByProgram ) . reduce ( ( acc , [ program , deps ] ) => {
316+ const cluster = new Supercluster < Deployment > ( {
317+ radius : 60 ,
318+ maxZoom : 16 ,
319+ minPoints : 2 ,
320+ } ) ;
321+
322+ const features = deps . map ( ( deployment ) => ( {
323+ type : "Feature" as const ,
324+ geometry : {
325+ type : "Point" as const ,
326+ coordinates : [
327+ deployment . location . longitude ,
328+ deployment . location . latitude ,
329+ ] ,
330+ } ,
331+ properties : deployment ,
332+ } ) ) ;
333+
334+ cluster . load ( features ) ;
335+ acc [ program ] = cluster ;
336+ return acc ;
337+ } , { } as Record < string , Supercluster < Deployment > > )
311338 : null ;
312339
313- // Convert deployments to GeoJSON features
314- const features = deployments . map ( ( deployment ) => ( {
340+ // Convert all deployments to GeoJSON features (for non-clustered mode)
341+ const allFeatures = deployments . map ( ( deployment ) => ( {
315342 type : "Feature" as const ,
316343 geometry : {
317344 type : "Point" as const ,
@@ -323,29 +350,44 @@ export function DeploymentMap({
323350 properties : deployment ,
324351 } ) ) ;
325352
326- if ( cluster ) {
327- cluster . load ( features ) ;
328- }
329-
330353 const updateMarkers = ( ) => {
331354 if ( ! map . current ) return ;
332355
333356 const bounds = map . current . getBounds ( ) ;
334357 const zoom = map . current . getZoom ( ) ;
335358
336359 // Get clusters or individual points based on enableClustering
337- const clusters =
338- enableClustering && cluster
339- ? cluster . getClusters (
340- [
341- bounds . getWest ( ) ,
342- bounds . getSouth ( ) ,
343- bounds . getEast ( ) ,
344- bounds . getNorth ( ) ,
345- ] ,
346- Math . floor ( zoom )
347- )
348- : features ; // If clustering disabled, show all features
360+ let allClusters : Array <
361+ PointFeature < Deployment > | ClusterFeature < { program ?: string } >
362+ > = [ ] ;
363+
364+ if ( enableClustering && clustersByProgram ) {
365+ // Get clusters from each program separately
366+ Object . entries ( clustersByProgram ) . forEach ( ( [ program , cluster ] ) => {
367+ const programClusters = cluster . getClusters (
368+ [
369+ bounds . getWest ( ) ,
370+ bounds . getSouth ( ) ,
371+ bounds . getEast ( ) ,
372+ bounds . getNorth ( ) ,
373+ ] ,
374+ Math . floor ( zoom )
375+ ) ;
376+ // Add program info to each cluster for color coding
377+ programClusters . forEach ( ( c ) => {
378+ if ( c . properties && "cluster" in c . properties ) {
379+ ( c . properties as { program ?: string ; cluster : boolean } ) . program =
380+ program ;
381+ }
382+ } ) ;
383+ allClusters . push ( ...programClusters ) ;
384+ } ) ;
385+ } else {
386+ // If clustering disabled, show all features
387+ allClusters = allFeatures as Array <
388+ PointFeature < Deployment > | ClusterFeature < { program ?: string } >
389+ > ;
390+ }
349391
350392 // Clear existing markers
351393 markers . current . forEach ( ( marker ) => marker . remove ( ) ) ;
@@ -357,7 +399,7 @@ export function DeploymentMap({
357399 lngLat : [ number , number ] ;
358400 } > = [ ] ;
359401
360- clusters . forEach ( ( clusterPoint ) => {
402+ allClusters . forEach ( ( clusterPoint ) => {
361403 const [ lng , lat ] = clusterPoint . geometry . coordinates ;
362404 const properties = clusterPoint . properties ;
363405
@@ -370,25 +412,29 @@ export function DeploymentMap({
370412 const clusterLeaves =
371413 ( properties as { point_count ?: number } ) . point_count || 0 ;
372414 const clusterId = ( properties as { cluster_id ?: number } ) . cluster_id ;
415+ const program = ( properties as { program ?: string } ) . program ;
373416
374- // For simplicity, use first deployment's color
375- const firstDeployment = deployments [ 0 ] ;
376- const color = firstDeployment
377- ? PROGRAM_COLORS [ firstDeployment . program ]
417+ // Use the program's color for the cluster
418+ const color = program
419+ ? PROGRAM_COLORS [ program as keyof typeof PROGRAM_COLORS ]
378420 : "#6B7280" ;
379421
380422 const el = createClusterMarkerElement ( clusterLeaves , color ) ;
381423
382424 el . addEventListener ( "click" , ( ) => {
383- if ( map . current && clusterId && cluster ) {
384- const expansionZoom = Math . min (
385- cluster . getClusterExpansionZoom ?.( clusterId ) || zoom + 2 ,
386- 20
387- ) ;
388- map . current . flyTo ( {
389- center : [ lng , lat ] ,
390- zoom : expansionZoom ,
391- } ) ;
425+ if ( map . current && clusterId && program && clustersByProgram ) {
426+ const programCluster = clustersByProgram [ program ] ;
427+ if ( programCluster ) {
428+ const expansionZoom = Math . min (
429+ programCluster . getClusterExpansionZoom ?.( clusterId ) ||
430+ zoom + 2 ,
431+ 20
432+ ) ;
433+ map . current . flyTo ( {
434+ center : [ lng , lat ] ,
435+ zoom : expansionZoom ,
436+ } ) ;
437+ }
392438 }
393439 if ( ! introAnimationComplete ) {
394440 setIntroAnimationComplete ( true ) ;
0 commit comments