UNPKG

rs-react-native-map-clustering

Version:

React Native Map Clustering both for Android and iOS with TypeScript support

392 lines (391 loc) 17.5 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const react_1 = __importStar(require("react")); const react_native_1 = require("react-native"); const react_native_maps_1 = __importStar(require("react-native-maps")); const supercluster_1 = __importDefault(require("supercluster")); const clustered_marker_1 = __importDefault(require("./clustered-marker")); const helpers_1 = require("./helpers"); const { width, height } = react_native_1.Dimensions.get('window'); const ClusteredMapView = (0, react_1.forwardRef)(({ radius = react_native_1.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 = react_native_1.LayoutAnimation.Presets.spring, animationEnabled = true, renderCluster, tracksViewChanges = false, spiralEnabled = true, superClusterRef = { current: null }, edgePadding = { top: 50, left: 50, right: 50, bottom: 50 }, selectedClusterId, selectedClusterColor, mapRef = () => { }, ...restProps }, ref) => { const [markers, updateMarkers] = (0, react_1.useState)([]); const [spiderMarkers, updateSpiderMarker] = (0, react_1.useState)([]); const [otherChildren, updateChildren] = (0, react_1.useState)([]); const [superCluster, setSuperCluster] = (0, react_1.useState)(null); const [currentRegion, updateRegion] = (0, react_1.useState)((restProps.region || restProps.initialRegion)); const [isSpiderfier, updateSpiderfier] = (0, react_1.useState)(false); const [clusterChildren, updateClusterChildren] = (0, react_1.useState)(null); const mapRefCurrent = (0, react_1.useRef)(null); const currentRegionRef = (0, react_1.useRef)(currentRegion); const superClusterRef_ = (0, react_1.useRef)(null); const externalClusterRef = (0, react_1.useRef)(superClusterRef); const previousMarkerKeysRef = (0, react_1.useRef)(''); (0, react_1.useLayoutEffect)(() => { currentRegionRef.current = currentRegion; }, [currentRegion]); (0, react_1.useLayoutEffect)(() => { superClusterRef_.current = superCluster; }, [superCluster]); const propsChildren = (0, react_1.useMemo)(() => react_1.default.Children.toArray(children), [children]); const clusterOptions = (0, react_1.useMemo)(() => ({ radius, maxZoom, minZoom, minPoints, extent, nodeSize }), [radius, maxZoom, minZoom, minPoints, extent, nodeSize]); (0, react_1.useEffect)(() => { const currentMarkerNodes = propsChildren.filter(helpers_1.isMarker); const markerKey = currentMarkerNodes .map((c) => { const el = c; const coord = el.props?.coordinate; return `${el.key}|${coord?.latitude}|${coord?.longitude}`; }) .join(';'); const markersChanged = markerKey !== previousMarkerKeysRef.current; previousMarkerKeysRef.current = markerKey; if (!markersChanged && superClusterRef_.current) { updateChildren(propsChildren.filter((c) => !(0, helpers_1.isMarker)(c))); return; } const rawData = []; const otherChildren = []; if (!clusteringEnabled) { updateSpiderMarker([]); updateMarkers([]); updateChildren(propsChildren); setSuperCluster(null); return; } let cancelled = false; const processChildrenInBatch = () => { const batchSize = 500; const totalChildren = propsChildren.length; let processedCount = 0; const processNextBatch = () => { if (cancelled) return; const endIndex = Math.min(processedCount + batchSize, totalChildren); for (let i = processedCount; i < endIndex; i++) { const child = propsChildren[i]; if ((0, helpers_1.isMarker)(child)) { const feature = (0, helpers_1.markerToGeoJSONFeature)(child, i); rawData.push({ ...feature, properties: feature.properties || {} }); } else { otherChildren.push(child); } } processedCount = endIndex; if (processedCount < totalChildren) { setTimeout(processNextBatch, 0); } else { if (!cancelled) createCluster(); } }; processNextBatch(); }; const createCluster = () => { const cluster = new supercluster_1.default(clusterOptions); cluster.load(rawData); const region = currentRegionRef.current; const bBox = (0, helpers_1.calculateBBox)(region); const zoom = (0, helpers_1.returnMapZoom)(region, bBox, minZoom); const markers = cluster.getClusters(bBox, zoom); updateMarkers(markers); updateChildren(otherChildren); setSuperCluster(cluster); if (externalClusterRef.current) { externalClusterRef.current.current = cluster; } }; if (propsChildren.length <= 1000) { propsChildren.forEach((child, index) => { if ((0, helpers_1.isMarker)(child)) { const feature = (0, helpers_1.markerToGeoJSONFeature)(child, index); rawData.push({ ...feature, properties: feature.properties || {} }); } else { otherChildren.push(child); } }); createCluster(); } else { processChildrenInBatch(); } return () => { cancelled = true; }; }, [propsChildren, clusteringEnabled, clusterOptions, minZoom]); (0, react_1.useEffect)(() => { if (!spiralEnabled) return; if (!isSpiderfier || markers.length === 0) { if (spiderMarkers.length > 0) { updateSpiderMarker([]); } return; } const generateSpiderMarkers = () => { const allSpiderMarkers = []; const processedClusters = new Set(); const markersToProcess = markers.length > 100 ? markers.slice(0, 100) : markers; markersToProcess.forEach((marker, i) => { if (!marker.properties.cluster || !marker.properties.cluster_id) return; if (processedClusters.has(marker.properties.cluster_id)) return; processedClusters.add(marker.properties.cluster_id); let spiralChildren = []; if (superCluster) { spiralChildren = superCluster.getLeaves(marker.properties.cluster_id, Infinity); } const positions = (0, helpers_1.generateSpiral)(marker, spiralChildren, markers, i); allSpiderMarkers.push(...positions); }); updateSpiderMarker(allSpiderMarkers); }; if (react_native_1.Platform.OS === 'web') { generateSpiderMarkers(); } else { requestAnimationFrame(generateSpiderMarkers); } }, [isSpiderfier, markers, spiralEnabled, superCluster, spiderMarkers.length]); const handleRegionChangeComplete = (0, react_1.useCallback)((region) => { const typedRegion = region; if (!superCluster || !typedRegion) { const emptyMarkers = []; if (typeof onRegionChangeComplete === 'function') { onRegionChangeComplete(typedRegion, emptyMarkers); } return; } const bBox = (0, helpers_1.calculateBBox)(typedRegion); const zoom = (0, helpers_1.returnMapZoom)(typedRegion, bBox, minZoom); const clusteredMarkers = superCluster.getClusters(bBox, zoom); if (animationEnabled && react_native_1.Platform.OS === 'ios') { react_native_1.LayoutAnimation.configureNext(layoutAnimationConf); } if (zoom >= 18 && clusteredMarkers.length > 0 && clusterChildren) { if (spiralEnabled) updateSpiderfier(true); } else if (spiralEnabled) { updateSpiderfier(false); } updateMarkers(clusteredMarkers); updateRegion(typedRegion); requestAnimationFrame(() => { if (typeof onMarkersChange === 'function') { onMarkersChange(clusteredMarkers); } if (typeof onRegionChangeComplete === 'function') { onRegionChangeComplete(typedRegion, clusteredMarkers); } }); }, [ superCluster, minZoom, animationEnabled, layoutAnimationConf, spiralEnabled, clusterChildren, onMarkersChange, onRegionChangeComplete ]); const handleClusterPress = (0, react_1.useCallback)((cluster) => () => { const sc = superClusterRef_.current; if (!sc || !mapRefCurrent.current) return; const clusterId = cluster.properties.cluster_id; if (clusterId === undefined) { updateClusterChildren([]); if (typeof onClusterPress === 'function') { onClusterPress(cluster, []); } return; } const leaves = sc.getLeaves(clusterId, Infinity); updateClusterChildren(leaves); if (preserveClusterPressBehavior) { requestAnimationFrame(() => { if (typeof onClusterPress === 'function') { onClusterPress(cluster, leaves); } }); return; } try { const expansionZoom = Math.min(sc.getClusterExpansionZoom(clusterId), maxZoom); const [lng, lat] = cluster.geometry.coordinates; const latDelta = 100 / 2 ** (expansionZoom - 1); const lngDelta = latDelta * (width / height); mapRefCurrent.current.animateToRegion({ latitude: lat, longitude: lng, latitudeDelta: latDelta, longitudeDelta: lngDelta }, 250); if (typeof onClusterPress === 'function') { onClusterPress(cluster, leaves); } } catch { requestAnimationFrame(() => { const coordinates = leaves.map(({ geometry }) => ({ latitude: geometry.coordinates[1], longitude: geometry.coordinates[0] })); if (coordinates.length > 0) { mapRefCurrent.current?.fitToCoordinates(coordinates, { edgePadding }); } if (typeof onClusterPress === 'function') { onClusterPress(cluster, leaves); } }); } }, [preserveClusterPressBehavior, maxZoom, edgePadding, onClusterPress]); const setMapRef = (0, react_1.useCallback)((map) => { mapRefCurrent.current = map; if (ref) { if (typeof ref === 'function') { ref(map); } else { ref.current = map; } } mapRef(map); }, [ref, mapRef]); return (<react_native_maps_1.default {...restProps} ref={setMapRef} onRegionChangeComplete={handleRegionChangeComplete}> {markers.map((marker, idx) => { if (!marker || !marker.properties) { return null; } const key = `marker-item-${marker.properties.cluster_id ?? marker.properties.index ?? idx}`; if (marker.properties.point_count === 0) { const markerIndex = marker.properties.index; if (typeof markerIndex === 'number' && markerIndex >= 0 && markerIndex < propsChildren.length) { const childElement = propsChildren[markerIndex]; if (react_1.default.isValidElement(childElement)) { return react_1.default.cloneElement(childElement, { key }); } } return null; } if (isSpiderfier) { return null; } if (renderCluster) { return renderCluster({ ...marker, key, onPress: handleClusterPress(marker), clusterColor, clusterTextColor, clusterFontFamily }); } const geometry = marker.geometry; const coordinates = geometry?.coordinates; if (!geometry || !Array.isArray(coordinates) || coordinates.length !== 2 || typeof coordinates[0] !== 'number' || typeof coordinates[1] !== 'number') { return null; } return (<clustered_marker_1.default key={key} geometry={marker.geometry} properties={marker.properties} onPress={handleClusterPress(marker)} clusterColor={selectedClusterId === String(marker.properties.cluster_id) ? selectedClusterColor || clusterColor : clusterColor} clusterTextColor={clusterTextColor} clusterFontFamily={clusterFontFamily} tracksViewChanges={tracksViewChanges}/>); })} {otherChildren} {spiderMarkers.map((marker) => { if (!marker || typeof marker.index !== 'number' || marker.index < 0 || marker.index >= propsChildren.length) { return null; } const childElement = propsChildren[marker.index]; if (react_1.default.isValidElement(childElement)) { const newProps = { key: `spider-${marker.index}-${marker.latitude}-${marker.longitude}`, coordinate: { latitude: marker.latitude, longitude: marker.longitude }, style: { ...(typeof childElement.props.style === 'object' && childElement.props.style !== null ? childElement.props.style : {}), zIndex: 1000 } }; return react_1.default.cloneElement(childElement, newProps); } return null; })} {spiderMarkers.map((marker, index) => { if (!marker || !marker.centerPoint || typeof marker.latitude !== 'number' || typeof marker.longitude !== 'number' || typeof marker.centerPoint.latitude !== 'number' || typeof marker.centerPoint.longitude !== 'number') { return null; } return (<react_native_maps_1.Polyline key={`poly-${index}`} coordinates={[ { latitude: marker.centerPoint.latitude, longitude: marker.centerPoint.longitude }, { latitude: marker.latitude, longitude: marker.longitude }, { latitude: marker.centerPoint.latitude, longitude: marker.centerPoint.longitude } ]} strokeColor={spiderLineColor} strokeWidth={1}/>); })} </react_native_maps_1.default>); }); exports.default = (0, react_1.memo)(ClusteredMapView);