Skip to content

Commit

Permalink
New major release 🎉
Browse files Browse the repository at this point in the history
  • Loading branch information
tomekvenits committed May 5, 2020
1 parent 13eaecd commit 94f4d1b
Show file tree
Hide file tree
Showing 8 changed files with 167 additions and 109 deletions.
40 changes: 16 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,37 +4,22 @@ React Native module that handles map clustering for you.

Works with **Expo** and **react-native-cli** 🚀

This repo is proudly sponsored by:

<a href="https://nativeforms.com" rel="nofollow" target="_blank">
<img src="https://raw.githubusercontent.com/venits/native-forms/master/assets/sponsor.png" width="350"><br />
Build forms, surveys and polls for React Native apps.
</a>

## Demo

![Demo](https://raw.githubusercontent.com/venits/react-native-map-clustering/assets/assets/demo.gif)

## Spiral

### Converting same locations in spiral view (done automatically)

![Spiral](https://raw.githubusercontent.com/venits/react-native-map-clustering/assets/assets/spider_lib.png)

## Installation

```js
// clustering module
npm install react-native-map-clustering --save
// yarn add react-native-map-clustering

// and only if you haven't installed it before
npm install react-native-maps --save
```

### Full example

Example of how to use clustering.

```js
import React from "react";
import MapView from "react-native-map-clustering";
Expand All @@ -44,7 +29,7 @@ const INITIAL_REGION = {
latitude: 52.5,
longitude: 19.2,
latitudeDelta: 8.5,
longitudeDelta: 8.5
longitudeDelta: 8.5,
};

const App = () => (
Expand All @@ -58,10 +43,6 @@ const App = () => (
<Marker coordinate={{ latitude: 52.2, longitude: 21 }} />
<Marker coordinate={{ latitude: 52.4, longitude: 21 }} />
<Marker coordinate={{ latitude: 51.8, longitude: 20 }} />
<Marker coordinate={{ latitude: 51.8, longitude: 20 }} />
<Marker coordinate={{ latitude: 51.8, longitude: 20 }} />
<Marker coordinate={{ latitude: 51.8, longitude: 20 }} />
<Marker coordinate={{ latitude: 51.8, longitude: 20 }} />
</MapView>
);

Expand All @@ -74,7 +55,9 @@ export default App;
| ------------------------------------------- | --------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **clusterColor** | String | #00B386 | Background color of cluster. |
| **clusterTextColor** | String | #FFFFFF | Color of text in cluster. |
| **clusterFontFamily** | String | undefined | Font family of text in cluster. |
| **onClusterPress(cluster, markers)** | Function | () => {} | Allows you to control cluster on click event. Function returns information about cluster and its markers. |
| **tracksClusterViewChanges** | Bool | false | Sets whether the cluster markers should track view changes. It's turned off by default to improve cluster markers performance. |
| **width** | Number | window width | map's width. |
| **height** | Number | window height | map's height. |
| **radius** | Number | window.width \* 6% | [SuperCluster radius](https://github.com/mapbox/supercluster#options). |
Expand All @@ -86,14 +69,23 @@ export default App;
| **animationEnabled** | Bool | true | Animate imploding/exploding of clusters' markers and clusters size change. **Works only on iOS**. |
| **layoutAnimationConf** | LayoutAnimationConfig | LayoutAnimation.Presets.spring | `LayoutAnimation.Presets.spring` | Custom Layout animation configuration object for clusters animation during implode / explode **Works only on iOS**. |
| **onRegionChangeComplete(region, markers)** | Function | () => {} | Called when map's region changes. In return you get current region and markers data. |
| **onMarkersChange(markers)** | Function | () => {} | Called when markers change. In return you get markers data. |
| **mapRef(ref)** | Function | () => {} | Return reference to `react-native-maps` MapView component. |
| **clusteringEnabled** | Bool | true | Set true to enable and false to disable clustering. |
| **spiralEnabled** | Bool | true | Set true to enable and false to disable spiral view. |
| **renderCluster** | Function | undefined | Enables you to render custom cluster with custom styles and logic. |
| **spiderLineColor** | String | #FF0000 | Enables you to set color of spider line which joins spiral location with center location. |
| **spiderLineColor** | String | #FF0000 | Enables you to set color of spider line which joins spiral location with center location. |

#### This repo is proudly sponsored by:

<a href="https://nativeforms.com" rel="nofollow" target="_blank">
<img src="https://raw.githubusercontent.com/venits/native-forms/master/assets/sponsor.png" width="350"><br />
Build forms, surveys and polls for React Native apps.
</a>

## Support

Feel free to create issues and pull requests. I will try to provide as much support as possible over Github. In case of questions or problems, contact me at:
[t.przybyl@venits.com](t.przybyl@venits.com)
[tony@venits.com](tony@venits.com)

### Happy Coding 💖
### Happy Coding 💖🚀
2 changes: 1 addition & 1 deletion example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"react": "16.8.3",
"react-dom": "16.8.3",
"react-native": "https://github.com/expo/react-native/archive/sdk-35.0.0.tar.gz",
"react-native-map-clustering": "^3.0.6",
"react-native-map-clustering": "^3.2.0",
"react-native-maps": "^0.26.1",
"react-native-web": "^0.11.7"
},
Expand Down
34 changes: 34 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
declare module "react-native-map-clustering" {
import * as React from "react";
import { LayoutAnimationConfig } from "react-native";
import Map, { MapViewProps, Marker } from "react-native-maps";

export type Cluster = {};

interface MapClusteringProps {
clusteringEnabled?: boolean;
spiralEnabled?: boolean;
animationEnabled?: boolean;
preserveClusterPressBehavior?: boolean;
tracksClusterViewChanges?: boolean;
layoutAnimationConf?: LayoutAnimationConfig;
radius?: number;
maxZoom?: number;
minZoom?: number;
extent?: number;
nodeSize?: number;
edgePadding?: { top: number; left: number; right: number; bottom: number };
clusterColor?: string;
clusterTextColor?: string;
clusterFontFamily?: string;
spiderLineColor?: string;
onClusterPress?: (cluster: Marker, markers?: Marker[]) => void;
mapRef?: (ref: React.Ref<Map>) => void;
onMarkersChange?: (markers?: Marker[]) => void;
}

export default class MapView extends React.Component<
MapViewProps & MapClusteringProps,
any
> {}
}
1 change: 0 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
import * as MapView from "./lib/ClusteredMapView";

module.exports = MapView;
106 changes: 59 additions & 47 deletions lib/ClusteredMapView.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { memo, useState, useEffect, useMemo, createRef } from "react";
import React, { memo, useState, useEffect, useMemo, useRef } from "react";
import { Dimensions, LayoutAnimation, Platform } from "react-native";
import MapView, { Marker, Polyline } from "react-native-maps";
import SuperCluster from "supercluster";
Expand All @@ -8,7 +8,7 @@ import {
markerToGeoJSONFeature,
calculateBBox,
returnMapZoom,
generateSpiral
generateSpiral,
} from "./helpers";

const ClusteredMapView = ({
Expand All @@ -20,14 +20,18 @@ const ClusteredMapView = ({
children,
onClusterPress,
onRegionChangeComplete,
onMarkersChange,
preserveClusterPressBehavior,
clusteringEnabled,
clusterColor,
clusterTextColor,
clusterFontFamily,
spiderLineColor,
layoutAnimationConf,
animationEnabled,
renderCluster,
tracksClusterViewChanges,
spiralEnabled,
...restProps
}) => {
const [markers, updateMarkers] = useState([]);
Expand All @@ -39,26 +43,27 @@ const ClusteredMapView = ({
);

const [isSpiderfier, updateSpiderfier] = useState(false);
const [spiderfierMarker, updateSpiderfierMarker] = useState(null);
const [clusterChildren, updateClusterChildren] = useState(null);
const mapRef = createRef();
const mapRef = useRef();

const propsChildren = useMemo(() => React.Children.toArray(children), [
children
children,
]);

useEffect(() => {
const rawData = [];
const otherChildren = [];

if (!clusteringEnabled) {
updateSpiderMarker([]);
updateMarkers([]);
updateChildren(propsChildren);
return;
}

React.Children.forEach(children, (child, i) => {
React.Children.forEach(children, (child, index) => {
if (isMarker(child)) {
rawData.push(markerToGeoJSONFeature(child, i));
rawData.push(markerToGeoJSONFeature(child, index));
} else {
otherChildren.push(child);
}
Expand All @@ -69,7 +74,7 @@ const ClusteredMapView = ({
maxZoom,
minZoom,
extent,
nodeSize
nodeSize,
});

superCluster.load(rawData);
Expand All @@ -81,26 +86,26 @@ const ClusteredMapView = ({
updateMarkers(markers);
updateChildren(otherChildren);
setSuperCluster(superCluster);
}, [children, restProps.region, restProps.initialRegion]);
}, [children, restProps.region, restProps.initialRegion, clusteringEnabled]);

useEffect(() => {
if (!spiralEnabled) return;

if (isSpiderfier && markers.length > 0) {
let positions = generateSpiral(
markers[0].properties.point_count,
markers[0].geometry.coordinates,
clusterChildren
);
updateSpiderMarker(positions);
updateSpiderfierMarker({
latitude: markers[0].geometry.coordinates[1],
longitude: markers[0].geometry.coordinates[0]
let allSpiderMarkers;
markers.map((marker) => {
let positions = generateSpiral(marker, clusterChildren);
if (allSpiderMarkers instanceof Array)
allSpiderMarkers = allSpiderMarkers.concat(positions);
else allSpiderMarkers = positions;
});
updateSpiderMarker(allSpiderMarkers);
} else {
updateSpiderMarker([]);
}
}, [isSpiderfier]);

const _onRegionChangeComplete = region => {
const _onRegionChangeComplete = (region) => {
if (superCluster) {
const bBox = calculateBBox(region);
const zoom = returnMapZoom(region, bBox, minZoom);
Expand All @@ -109,20 +114,23 @@ const ClusteredMapView = ({
if (animationEnabled && Platform.OS === "ios") {
LayoutAnimation.configureNext(layoutAnimationConf);
}
if (zoom >= 17 && markers.length === 1 && clusterChildren) {
updateSpiderfier(true);
if (zoom >= 17 && markers.length > 0 && clusterChildren) {
if (spiralEnabled) updateSpiderfier(true);
} else {
updateSpiderfier(false);
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, (limit = Infinity));
const _onClusterPress = (cluster) => () => {
const children = superCluster.getLeaves(cluster.id, Infinity);
updateClusterChildren(children);

if (preserveClusterPressBehavior) {
Expand All @@ -132,11 +140,11 @@ const ClusteredMapView = ({

const coordinates = children.map(({ geometry }) => ({
latitude: geometry.coordinates[1],
longitude: geometry.coordinates[0]
longitude: geometry.coordinates[0],
}));

mapRef.current.fitToCoordinates(coordinates, {
edgePadding: restProps.edgePadding
edgePadding: restProps.edgePadding,
});

onClusterPress(cluster, children);
Expand All @@ -145,12 +153,13 @@ const ClusteredMapView = ({
return (
<MapView
{...restProps}
ref={map => {
ref={(map) => {
restProps.mapRef(map);
mapRef.current = map;
}}
onRegionChangeComplete={_onRegionChangeComplete}>
{markers.map(marker =>
onRegionChangeComplete={_onRegionChangeComplete}
>
{markers.map((marker) =>
marker.properties.point_count === 0 ? (
propsChildren[marker.properties.index]
) : !isSpiderfier ? (
Expand All @@ -159,7 +168,8 @@ const ClusteredMapView = ({
onPress: _onClusterPress(marker),
clusterColor,
clusterTextColor,
...marker
clusterFontFamily,
...marker,
})
) : (
<ClusterMarker
Expand All @@ -168,30 +178,29 @@ const ClusteredMapView = ({
onPress={_onClusterPress(marker)}
clusterColor={clusterColor}
clusterTextColor={clusterTextColor}
clusterFontFamily={clusterFontFamily}
tracksClusterViewChanges={tracksClusterViewChanges}
/>
)
) : null
)}
{otherChildren}
{spiderMarkers.map(marker => (
<Marker
key={marker.latitude}
coordinate={marker}
image={marker.image}
onPress={marker.onPress}
></Marker>
))}
{spiderMarkers.map((marker) => {
return propsChildren[marker.index]
? React.cloneElement(propsChildren[marker.index], {
coordinate: { ...marker },
})
: null;
})}
{spiderMarkers.map((marker, index) => {
{
return (
spiderfierMarker && (
<Polyline
key={index}
coordinates={[spiderfierMarker, marker, spiderfierMarker]}
strokeColor={spiderLineColor}
strokeWidth={1}
/>
)
<Polyline
key={index}
coordinates={[marker.centerPoint, marker, marker.centerPoint]}
strokeColor={spiderLineColor}
strokeWidth={1}
/>
);
}
})}
Expand All @@ -201,9 +210,11 @@ const ClusteredMapView = ({

ClusteredMapView.defaultProps = {
clusteringEnabled: true,
spiralEnabled: true,
animationEnabled: true,
preserveClusterPressBehavior: false,
layoutAnimationConf: LayoutAnimation.Presets.spring,
tracksClusterViewChanges: false,
// SuperCluster parameters
radius: Dimensions.get("window").width * 0.06,
maxZoom: 20,
Expand All @@ -219,7 +230,8 @@ ClusteredMapView.defaultProps = {
// Callbacks
onRegionChangeComplete: () => {},
onClusterPress: () => {},
mapRef: () => {}
onMarkersChange: () => {},
mapRef: () => {},
};

export default memo(ClusteredMapView);
Loading

0 comments on commit 94f4d1b

Please sign in to comment.