import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useState } from 'react' import { debounce, isFunction, uuid } from '@rcuse/core' import { LngLatBounds } from 'mapbox-gl' import Supercluster from 'supercluster' import bbox from '@turf/bbox' import { featureCollection } from '@turf/helpers' import { Marker } from '../Marker' import { useMap } from '../../hooks/useMap' import { defaultSuperclusterOptions } from './config' import type { AnyObject, Feature, LngLatLike, MarkerClusterProps, RefMarkerCluster } from './types' function InternalMarkerCluster(props: MarkerClusterProps, ref: React.Ref) { const { cluster, render, renderCluster, data = [], zoomOnClick, zoomOnClickPadding = 20, onClick, onClusterClick, } = props const { map } = useMap() const [list, setList] = useState[]>([]) const supercluster = useMemo(() => { return new Supercluster({ ...defaultSuperclusterOptions, ...cluster, }) }, []) const getMapBoundary = (): [[number, number, number, number], number] => { const bounds = map.getBounds() const zoom = map.getZoom() return [ // @ts-expect-error [bounds.getWest(), bounds.getSouth(), bounds.getEast(), bounds.getNorth()], Math.round(zoom), ] } const handleChangeBoundary = useCallback( debounce({ delay: 500 }, () => { const mapBoundary = getMapBoundary() const result = supercluster.getClusters(...mapBoundary) setList( result.map((item) => { return { ...item, id: item.id || uuid(), } }), ) }), [], ) useEffect(() => { if (map) { map.on('zoom', handleChangeBoundary) map.on('move', handleChangeBoundary) map.on('resize', handleChangeBoundary) return () => { map.off('zoom', handleChangeBoundary) map.off('move', handleChangeBoundary) map.off('resize', handleChangeBoundary) } } return undefined }, [map]) useEffect(() => { if (data) { supercluster.load(data) handleChangeBoundary() } }, [data]) useImperativeHandle(ref, () => supercluster, []) const handleClusterMarkerClick = (data: any) => { const { properties } = data if (!properties?.cluster) return const children = supercluster.getLeaves(properties.cluster_id, Number.POSITIVE_INFINITY) const childrenBbox = bbox(featureCollection(children)) as [number, number, number, number] map.fitBounds(LngLatBounds.convert(childrenBbox), { padding: zoomOnClickPadding, }) } return ( <> {list.map((item) => { const { geometry, properties } = item const { point_count, cluster, cluster_id } = properties if (cluster) { return ( { onClusterClick?.(point_count, cluster_id) if (zoomOnClick) handleClusterMarkerClick(item) }} > {isFunction(renderCluster) ? renderCluster(point_count, cluster_id) : renderCluster} ) } return ( { onClick?.(item) }} > {isFunction(render) ? render(item) : render} ) })} ) } export const MarkerCluster = forwardRef(InternalMarkerCluster) as RefMarkerCluster