react-native-map-clustering-flex
Version:
React Native Map Clustering both for Android and iOS
291 lines (263 loc) • 8.53 kB
JavaScript
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);