UNPKG

react-native-map-clustering-flex

Version:
291 lines (263 loc) 8.53 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 ClusteredMapView = forwardRef( ( { radius, maxZoom, minZoom, minPoints, extent, nodeSize, children, onClusterPress, onRegionChangeComplete, onMarkersChange, preserveClusterPressBehavior, clusteringEnabled, clusterColor, clusterTextColor, clusterFontFamily, spiderLineColor, layoutAnimationConf, animationEnabled, renderCluster, tracksViewChanges, spiralEnabled, superClusterRef, notToCluster, ...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] ); //new state to store markers you dont want to cluster const [notInCluster, setNotInCluster] = useState([]); function getStringFromReactFormat(reactKey) { // Find the index of the first "$" const dollarIndex = reactKey.indexOf("$"); // Return the substring after "$" or the whole string if "$" is not present return dollarIndex !== -1 ? reactKey.substring(dollarIndex + 1) : reactKey; } useEffect(() => { const rawData = []; const otherChildren = []; const markerNotInCluster = []; if (!clusteringEnabled) { updateSpiderMarker([]); updateMarkers([]); updateChildren(propsChildren); setSuperCluster(null); return; } propsChildren.forEach((child, index) => { if (isMarker(child)) { if (notToCluster.includes(getStringFromReactFormat(child.key))) { // check if key is in notToCluster prop and if so add to a temp array markerNotInCluster.push(child); return; } 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); //update state setNotInCluster(markerNotInCluster); superClusterRef.current = superCluster; }, [propsChildren, clusteringEnabled]); 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(markers); onRegionChangeComplete(region, markers); updateRegion(region); } else { onRegionChangeComplete(region); } }; const _onClusterPress = (cluster) => () => { const children = superCluster.getLeaves(cluster.id, Infinity); updateClusterChildren(children); if (preserveClusterPressBehavior) { 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(cluster, children); }; return ( <MapView {...restProps} ref={(map) => { mapRef.current = map; if (ref) ref.current = map; 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 )} {/* render not clustered markers */} {notInCluster.map((marker) => marker)} {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> ); } ); ClusteredMapView.defaultProps = { clusteringEnabled: true, spiralEnabled: true, animationEnabled: true, preserveClusterPressBehavior: false, layoutAnimationConf: LayoutAnimation.Presets.spring, tracksViewChanges: false, // Not To cluster notToCluster: [], // SuperCluster parameters radius: Dimensions.get("window").width * 0.06, maxZoom: 20, minZoom: 1, minPoints: 2, extent: 512, nodeSize: 64, // Map parameters edgePadding: { top: 50, left: 50, right: 50, bottom: 50 }, // Cluster styles clusterColor: "#00B386", clusterTextColor: "#FFFFFF", spiderLineColor: "#FF0000", // Callbacks onRegionChangeComplete: () => {}, onClusterPress: () => {}, onMarkersChange: () => {}, superClusterRef: {}, mapRef: () => {}, }; export default memo(ClusteredMapView);