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
JavaScript
;
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);