Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: novalabio/react-native-maps-super-cluster
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v1.2.2
Choose a base ref
...
head repository: novalabio/react-native-maps-super-cluster
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: master
Choose a head ref
Loading
Showing with 204 additions and 168 deletions.
  1. +9 −66 ClusterMarker.js
  2. +50 −69 ClusteredMapView.js
  3. +66 −13 README.md
  4. +5 −5 package.json
  5. +47 −15 util.js
  6. +27 −0 yarn.lock
75 changes: 9 additions & 66 deletions ClusterMarker.js
Original file line number Diff line number Diff line change
@@ -3,8 +3,6 @@
// base libs
import PropTypes from 'prop-types'
import React, { Component } from 'react'
import { Marker } from 'react-native-maps'
import { Text, View, StyleSheet } from 'react-native'

export default class ClusterMarker extends Component {
constructor(props) {
@@ -18,81 +16,26 @@ export default class ClusterMarker extends Component {
}

render() {

const pointCount = this.props.properties.point_count // eslint-disable-line camelcase
const latitude = this.props.geometry.coordinates[1],
longitude = this.props.geometry.coordinates[0]

let scaleUpRatio = this.props.scaleUpRatio ? this.props.scaleUpRatio(pointCount) : (1 + (Math.min(pointCount, 999) / 100))
if (isNaN(scaleUpRatio)) {
console.warn('scaleUpRatio must return a Number, falling back to default') // eslint-disable-line
scaleUpRatio = 1 + (Math.min(pointCount, 999) / 100)
}

let textForCluster = '1'

let width = Math.floor(this.props.clusterInitialDimension * scaleUpRatio),
height = Math.floor(this.props.clusterInitialDimension * scaleUpRatio),
fontSize = Math.floor(this.props.clusterInitialFontSize * scaleUpRatio),
borderRadius = Math.floor(width / 2)

// cluster dimension upper limit
width = width <= (this.props.clusterInitialDimension * 2) ? width : this.props.clusterInitialDimension * 2
height = height <= (this.props.clusterInitialDimension * 2) ? height : this.props.clusterInitialDimension * 2
fontSize = fontSize <= 18 ? fontSize : 18

if (pointCount >= 2 && pointCount <= 10) {
textForCluster = pointCount.toString()
} if (pointCount > 10 && pointCount <= 25) {
textForCluster = '10+'
} if (pointCount > 25 && pointCount <= 50) {
textForCluster = '25+'
} if (pointCount > 50 && pointCount <= 100) {
textForCluster = '50+'
} if (pointCount > 100) {
textForCluster = '100+'
if (this.props.renderCluster) {
const cluster = {
pointCount,
coordinate: { latitude, longitude },
clusterId: this.props.properties.cluster_id,
}
return this.props.renderCluster(cluster, this.onPress)
}

const { containerStyle, textStyle } = this.props

return (
<Marker coordinate={{ latitude, longitude }} onPress={this.onPress}>
<View style={[styles.container, containerStyle, { width, height, borderRadius }]}>
<Text style={[styles.counterText, textStyle, { fontSize }]}>{textForCluster}</Text>
</View>
</Marker>
)
throw "Implement renderCluster method prop to render correctly cluster marker!"
}
}

ClusterMarker.defaultProps = {
textStyle: {},
containerStyle: {}
}

ClusterMarker.propTypes = {
scaleUpRatio: PropTypes.func,
renderCluster: PropTypes.func,
onPress: PropTypes.func.isRequired,
geometry: PropTypes.object.isRequired,
textStyle: PropTypes.object.isRequired,
properties: PropTypes.object.isRequired,
renderMarker: PropTypes.func.isRequired,
containerStyle: PropTypes.object.isRequired,
clusterInitialFontSize: PropTypes.number.isRequired,
clusterInitialDimension: PropTypes.number.isRequired,
}

const styles = StyleSheet.create({
container: {
borderWidth: 1,
alignItems: 'center',
borderColor: '#65bc46',
justifyContent: 'center',
backgroundColor: '#fff'
},
counterText: {
fontSize: 16,
color: '#65bc46',
fontWeight: '400'
}
})
119 changes: 50 additions & 69 deletions ClusteredMapView.js
Original file line number Diff line number Diff line change
@@ -17,8 +17,8 @@ import ClusterMarker from './ClusterMarker'
// libs / utils
import {
regionToBoundingBox,
boundingBoxToRegion,
itemToGeoJSONFeature
itemToGeoJSONFeature,
getCoordinatesFromItem,
} from './util'

export default class ClusteredMapView extends PureComponent {
@@ -28,21 +28,17 @@ export default class ClusteredMapView extends PureComponent {

this.state = {
data: [], // helds renderable clusters and markers
region: {}, // helds current map region
region: props.region || props.initialRegion, // helds current map region
}

this.isAndroid = Platform.OS === 'android'
this.dimensions = [props.width, props.height]

this.mapRef = this.mapRef.bind(this)
this.onClusterPress = this.onClusterPress.bind(this)
this.onRegionChangeComplete = this.onRegionChangeComplete.bind(this)
}

componentWillMount() {
this.dimensions = [this.props.width, this.props.height]
this.isAndroid = Platform.OS === 'android'

this.setState({ region: this.props.region || this.props.initialRegion })
}

componentDidMount() {
this.clusterize(this.props.data)
}
@@ -53,29 +49,32 @@ export default class ClusteredMapView extends PureComponent {
}

componentWillUpdate(nextProps, nextState) {
!this.isAndroid && this.props.animateClusters
&& this.clustersChanged(nextState)
&& LayoutAnimation.configureNext(LayoutAnimation.Presets.spring)
if (!this.isAndroid && this.props.animateClusters && this.clustersChanged(nextState))
LayoutAnimation.configureNext(this.props.layoutAnimationConf)
}

mapRef = (ref) => {
mapRef(ref) {
this.mapview = ref
}

getMapRef = () => this.mapview
getMapRef() {
return this.mapview
}

getClusteringEngine = () => this.index
getClusteringEngine() {
return this.index
}

clusterize = (dataset) => {
this.index = SuperCluster({ // eslint-disable-line new-cap
clusterize(dataset) {
this.index = new SuperCluster({ // eslint-disable-line new-cap
extent: this.props.extent,
minZoom: this.props.minZoom,
maxZoom: this.props.maxZoom,
radius: this.props.radius || (this.dimensions[0] * .045), // 4.5% of screen width
})

// get formatted GeoPoints for cluster
const rawData = dataset.map(itemToGeoJSONFeature)
const rawData = dataset.map(item => itemToGeoJSONFeature(item, this.props.accessor))

// load geopoints into SuperCluster
this.index.load(rawData)
@@ -84,66 +83,54 @@ export default class ClusteredMapView extends PureComponent {
this.setState({ data })
}

clustersChanged = (nextState) => this.state.data.length !== nextState.data.length
clustersChanged(nextState) {
return this.state.data.length !== nextState.data.length
}

onRegionChangeComplete = (region) => {
if (region.longitudeDelta <= 80) {
const data = this.getClusters(region)
this.setState({ region, data })
}
this.props.onRegionChangeComplete && this.props.onRegionChangeComplete(region)
onRegionChangeComplete(region) {
let data = this.getClusters(region)
this.setState({ region, data }, () => {
this.props.onRegionChangeComplete && this.props.onRegionChangeComplete(region, data)
})
}

getClusters = (region) => {
getClusters(region) {
const bbox = regionToBoundingBox(region),
viewport = (region.longitudeDelta) >= 40 ? { zoom: this.props.minZoom } : GeoViewport.viewport(bbox, this.dimensions)

return this.index.getClusters(bbox, viewport.zoom)
}

onClusterPress = (cluster) => {
onClusterPress(cluster) {

// cluster press behavior might be extremely custom.
if (this.props.onClusterPress && !this.props.preserveClusterPressBehavior) {
this.props.onClusterPress(cluster.properties.cluster_id)
if (!this.props.preserveClusterPressBehavior) {
this.props.onClusterPress && this.props.onClusterPress(cluster.properties.cluster_id)
return
}

// //////////////////////////////////////////////////////////////////////////////////
// NEW IMPLEMENTATION (with fitToCoordinates)
// //////////////////////////////////////////////////////////////////////////////////
// get cluster children
const children = this.index.getLeaves(cluster.properties.cluster_id, this.props.clusterPressMaxChildren),
markers = children.map(c => c.properties.item)

// fit right around them, keeping edge padding into account
this.mapview.fitToCoordinates(markers.map(m => m.location), { edgePadding: this.props.edgePadding })

this.props.onClusterPress && this.props.onClusterPress(cluster.properties.cluster_id, markers)

// //////////////////////////////////////////////////////////////////////////////////
// OLD, LESS ACCURATE, IMPLEMENTATION (with animateToRegion)
// //////////////////////////////////////////////////////////////////////////////////
// let ne = { latitude: 0, longitude: 0 },
// sw = { latitude: 1000, longitude: 1000 }

// children.forEach(c => {
// const location = c.properties.item.location
const children = this.index.getLeaves(cluster.properties.cluster_id, this.props.clusterPressMaxChildren)
const markers = children.map(c => c.properties.item)

// ne.latitude = Math.max(ne.latitude, location.latitude)
// ne.longitude = Math.max(ne.longitude, location.longitude)
const coordinates = markers.map(item => getCoordinatesFromItem(item, this.props.accessor, false))

// sw.latitude = Math.min(sw.latitude, location.latitude)
// sw.longitude = Math.min(sw.longitude, location.longitude)
// })
// fit right around them, considering edge padding
this.mapview.fitToCoordinates(coordinates, { edgePadding: this.props.edgePadding })

// this.mapview.animateToRegion(boundingBoxToRegion({ ne, sw }))
// this.props.onClusterPress && this.props.onClusterPress(cluster.properties.cluster_id, children)
this.props.onClusterPress && this.props.onClusterPress(cluster.properties.cluster_id, markers)
}

render() {
const { style, ...props } = this.props

return (
<MapView
{ ...this.props}
{...props}
style={style}
ref={this.mapRef}
onRegionChangeComplete={this.onRegionChangeComplete}>
{
@@ -155,13 +142,8 @@ export default class ClusteredMapView extends PureComponent {
<ClusterMarker
{...d}
onPress={this.onClusterPress}
textStyle={this.props.textStyle}
scaleUpRatio={this.props.scaleUpRatio}
renderMarker={this.props.renderMarker}
key={`cluster-${d.properties.cluster_id}`}
containerStyle={this.props.containerStyle}
clusterInitialFontSize={this.props.clusterInitialFontSize}
clusterInitialDimension={this.props.clusterInitialDimension} />
renderCluster={this.props.renderCluster}
key={`cluster-${d.properties.cluster_id}`} />
)
})
}
@@ -176,16 +158,16 @@ export default class ClusteredMapView extends PureComponent {

ClusteredMapView.defaultProps = {
minZoom: 1,
maxZoom: 20,
maxZoom: 16,
extent: 512,
accessor: 'location',
animateClusters: true,
clusteringEnabled: true,
clusterInitialFontSize: 12,
clusterInitialDimension: 30,
clusterPressMaxChildren: 100,
preserveClusterPressBehavior: true,
width: Dimensions.get('window').width,
height: Dimensions.get('window').height,
layoutAnimationConf: LayoutAnimation.Presets.spring,
edgePadding: { top: 10, left: 10, right: 10, bottom: 10 }
}

@@ -198,24 +180,23 @@ ClusteredMapView.propTypes = {
extent: PropTypes.number.isRequired,
minZoom: PropTypes.number.isRequired,
maxZoom: PropTypes.number.isRequired,
clusterInitialFontSize: PropTypes.number.isRequired,
clusterPressMaxChildren: PropTypes.number.isRequired,
clusterInitialDimension: PropTypes.number.isRequired,
// array
data: PropTypes.array.isRequired,
// func
onExplode: PropTypes.func,
onImplode: PropTypes.func,
scaleUpRatio: PropTypes.func,
onClusterPress: PropTypes.func,
renderMarker: PropTypes.func.isRequired,
renderCluster: PropTypes.func.isRequired,
// bool
animateClusters: PropTypes.bool.isRequired,
clusteringEnabled: PropTypes.bool.isRequired,
preserveClusterPressBehavior: PropTypes.bool.isRequired,
// object
textStyle: PropTypes.object.isRequired,
layoutAnimationConf: PropTypes.object,
edgePadding: PropTypes.object.isRequired,
containerStyle: PropTypes.object.isRequired,
// string
// mutiple
accessor: PropTypes.oneOfType([PropTypes.string, PropTypes.func])
}
Loading