Skip to content

Instantly share code, notes, and snippets.

@mussaiin
Last active August 18, 2021 19:07
Show Gist options
  • Save mussaiin/05e99093adb0e168c963af2ae897411a to your computer and use it in GitHub Desktop.
Save mussaiin/05e99093adb0e168c963af2ae897411a to your computer and use it in GitHub Desktop.
Clustered MapView for React-Native Maps
import React, { forwardRef, memo, useEffect, useMemo, useRef, useState } from 'react'
import { Dimensions, LayoutAnimation, Platform } from 'react-native'
import MapView, { MapViewProps, Polyline } from 'react-native-maps'
import SuperCluster from 'supercluster'
import { MapClusteringProps } from './ClusteredMapViewTypes'
import ClusterMarker from './ClusteredMarker'
import {
calculateBBox,
generateSpiral,
isMarker,
markerToGeoJSONFeature,
returnMapZoom,
} from './helpers'
const ClusteredMapView = forwardRef<MapClusteringProps & MapViewProps, any>(
(
{
radius,
maxZoom,
minZoom,
minPoints,
extent,
nodeSize,
children,
onClusterPress,
onRegionChangeComplete,
onMarkersChange,
preserveClusterPressBehavior,
clusteringEnabled,
clusterColor,
clusterTextColor,
clusterFontFamily,
spiderLineColor,
layoutAnimationConf,
animationEnabled,
renderCluster,
tracksViewChanges,
spiralEnabled,
superClusterRef,
...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.current = superCluster
}, [propsChildren, clusteringEnabled])
useEffect(() => {
if (!spiralEnabled) {
return
}
if (isSpiderfier && markers.length > 0) {
const allSpiderMarkers = []
let spiralChildren = []
markers.map((marker, i) => {
if (marker.properties.cluster) {
spiralChildren = superCluster.getLeaves(marker.properties.cluster_id, Infinity)
}
const 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,
)}
{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,
// 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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment