UNPKG

@allisonadam81/react-native-super-clusters

Version:
239 lines (215 loc) 7.03 kB
import React, { memo, useState, useEffect, useMemo, useRef, forwardRef, } from 'react'; import { Dimensions, LayoutAnimation, Platform } from 'react-native'; import MapView, { Marker, Polyline } from 'react-native-maps'; import SuperCluster from 'supercluster'; import ClusterMarker from './ClusteredMarker'; import { isMarker, markerToGeoJSONFeature, calculateBBox, returnMapZoom, generateSpiral, } from './helpers'; const defaultEdgePadding = { top: 50, left: 50, right: 50, bottom: 50 }; const ClusteredMapView = forwardRef( ( { radius = Dimensions.get('window').width * 0.06, maxZoom = 20, minZoom = 1, minPoints = 2, extent = 512, nodeSize = 64, children, onClusterPress, onRegionChangeComplete, onMarkersChange, preserveClusterPressBehavior = false, clusteringEnabled = true, clusterColor = '#00B386', clusterTextColor = '#FFFFFF', clusterFontFamily, spiderLineColor = '#FF0000', layoutAnimationConf = LayoutAnimation.Presets.spring, animationEnabled = true, renderCluster, tracksViewChanges = false, spiralEnabled = true, superClusterRef, edgePadding = defaultEdgePadding, ...restProps }, ref ) => { const [markers, updateMarkers] = useState([]); const [spiderMarkers, updateSpiderMarker] = useState([]); const [otherChildren, updateChildren] = useState([]); const [superCluster, setSuperCluster] = useState(null); const [currentRegion, updateRegion] = useState( restProps.region || restProps.initialRegion ); const [isSpiderfier, updateSpiderfier] = useState(false); const [clusterChildren, updateClusterChildren] = useState(null); const mapRef = useRef(); const propsChildren = useMemo( () => React.Children.toArray(children), [children] ); useEffect(() => { const rawData = []; const otherChildren = []; if (!clusteringEnabled) { updateSpiderMarker([]); updateMarkers([]); updateChildren(propsChildren); setSuperCluster(null); return; } propsChildren.forEach((child, index) => { if (isMarker(child)) { rawData.push(markerToGeoJSONFeature(child, index)); } else { otherChildren.push(child); } }); const superCluster = new SuperCluster({ radius, maxZoom, minZoom, minPoints, extent, nodeSize, }); superCluster.load(rawData); const bBox = calculateBBox(currentRegion); const zoom = returnMapZoom(currentRegion, bBox, minZoom); const markers = superCluster.getClusters(bBox, zoom); updateMarkers(markers); updateChildren(otherChildren); setSuperCluster(superCluster); superClusterRef && (superClusterRef.current = superCluster); }, [propsChildren, clusteringEnabled, superClusterRef]); useEffect(() => { if (!spiralEnabled) return; if (isSpiderfier && markers.length > 0) { let allSpiderMarkers = []; let spiralChildren = []; markers.map((marker, i) => { if (marker.properties.cluster) { spiralChildren = superCluster.getLeaves( marker.properties.cluster_id, Infinity ); } let positions = generateSpiral(marker, spiralChildren, markers, i); allSpiderMarkers.push(...positions); }); updateSpiderMarker(allSpiderMarkers); } else { updateSpiderMarker([]); } }, [isSpiderfier, markers]); const _onRegionChangeComplete = (region) => { if (superCluster && region) { const bBox = calculateBBox(region); const zoom = returnMapZoom(region, bBox, minZoom); const markers = superCluster.getClusters(bBox, zoom); if (animationEnabled && Platform.OS === 'ios') { LayoutAnimation.configureNext(layoutAnimationConf); } if (zoom >= 18 && markers.length > 0 && clusterChildren) { if (spiralEnabled) updateSpiderfier(true); } else { if (spiralEnabled) updateSpiderfier(false); } updateMarkers(markers); onMarkersChange && onMarkersChange(markers); onRegionChangeComplete && onRegionChangeComplete(region, markers); updateRegion(region); } else { onRegionChangeComplete && onRegionChangeComplete(region); } }; const _onClusterPress = (cluster) => () => { const children = superCluster.getLeaves(cluster.id, Infinity); updateClusterChildren(children); if (preserveClusterPressBehavior) { onClusterPress && onClusterPress(cluster, children); return; } const coordinates = children.map(({ geometry }) => ({ latitude: geometry.coordinates[1], longitude: geometry.coordinates[0], })); mapRef.current.fitToCoordinates(coordinates, { edgePadding: restProps.edgePadding, }); onClusterPress && onClusterPress(cluster, children); }; return ( <MapView {...restProps} ref={(map) => { mapRef.current = map; if (ref) ref.current = map; restProps.mapRef && restProps.mapRef(map); }} onRegionChangeComplete={_onRegionChangeComplete} > {markers.map((marker) => marker.properties.point_count === 0 ? ( propsChildren[marker.properties.index] ) : !isSpiderfier ? ( renderCluster ? ( renderCluster({ onPress: _onClusterPress(marker), clusterColor, clusterTextColor, clusterFontFamily, ...marker, }) ) : ( <ClusterMarker key={`cluster-${marker.id}`} {...marker} onPress={_onClusterPress(marker)} clusterColor={ restProps.selectedClusterId === marker.id ? restProps.selectedClusterColor : clusterColor } clusterTextColor={clusterTextColor} clusterFontFamily={clusterFontFamily} tracksViewChanges={tracksViewChanges} /> ) ) : null )} {otherChildren} {spiderMarkers.map((marker) => { return propsChildren[marker.index] ? React.cloneElement(propsChildren[marker.index], { coordinate: { ...marker }, }) : null; })} {spiderMarkers.map((marker, index) => ( <Polyline key={index} coordinates={[marker.centerPoint, marker, marker.centerPoint]} strokeColor={spiderLineColor} strokeWidth={1} /> ))} </MapView> ); } ); export default memo(ClusteredMapView);