Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Marker clustering ios #137

Closed

Conversation

yonahforst
Copy link

adds marker clustering for iOS. still trying to get it workong on Android...

@alexHlebnikov
Copy link

@joshblour Hi!
Is there any luck with clustering on Android?
I've figured out how to make clustering with native markers, created in Java class.
But I can't get how to connect React Native Maps markers with ClusterManager in Java :(

@yonahforst
Copy link
Author

@alexHlebnikov check out this branch https://github.com/joshblour/react-native-maps-1/commits/marker_clustering (it's probably what you already have)

The ClusteringManager lets you provide your own ClusterRenderer. They recommend subclassing DefaultClusterRenderer and if you do, you can implement onBeforeClusterItemRendered and provide some of your own marker options.

Note: You'll then need to tell AirMapView not to add it's own markers to the map, otherwise they'll appear twice; clustered and unclustered.

The problem with this approach is that we're limited to plain-ol' markers, and can't use custom views, which blows.

It would be awesome if there was a way to provide our own marker object to DefaultClusterRenderer but there doesn't seem to be a way of doing that. Maybe a pull request somewhere around here: https://github.com/googlemaps/android-maps-utils/blob/master/library/src/com/google/maps/android/clustering/view/DefaultClusterRenderer.java#L813

@alexHlebnikov
Copy link

I think here is a way to reslove the problem with clusetering markers customization in Java.

@yonahforst
Copy link
Author

yes, that's the approach they recommend but it's still just setting markerOptions.

I haven't tried, but it seems that with AirMaps you can have completely custom marker by providing your own view. Anyone using a custom view wouldn't be able to use clustering.

What we really want is to tell DefaultClusterRenderer to use the marker we provide, instead of rendering a new one (even if we can specify the options)

@michaltaberski
Copy link

Hey, whats the status of this PR?

Does the project support already marker clustering? Or maybe do you know if there is an easy way to implement it?

Thanks

@danielreuterwall
Copy link

Would be awesome to get this in, feels like the missing piece

@raphaelsaunier raphaelsaunier mentioned this pull request May 8, 2016
@bcalik
Copy link

bcalik commented May 16, 2016

Any progress on this?

@lucaboieru
Copy link

Hey guys! Any chance you can merge this and release a new version any time soon?

@n23daniel
Copy link

Please merge this, it would be really useful 🙏

@nazywamsiepawel
Copy link

+1, please merge

@bcalik
Copy link

bcalik commented Jul 21, 2016

I will pay for who can make clustering for both platforms. Just say the price.
Currently it is really slow when there are 100+ markers.
Contact: burak@macellan.net

@ptomasroos
Copy link

I can def sponsor as well

On 21 Jul 2016, at 03:10, Burak Çalık notifications@github.com wrote:

I will pay for who can make clustering for both platforms. Just say the price.
Currently it is really slow when there are 100+ markers.


You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub, or mute the thread.

@justim
Copy link

justim commented Jul 21, 2016

I solved this by doing the clustering in javascript, inspired by leaflet and google maps. At the moment it's very much tied into our application and not perfect yet. If you guys don't mind an javascript implementation, I'm happy to explore to open source our solution.

@bcalik
Copy link

bcalik commented Jul 21, 2016

@justim that would be great. I also planned to do on js side, your code would help.

@yonahforst
Copy link
Author

@justim I'd love to see your code too :)

@nazywamsiepawel
Copy link

nazywamsiepawel commented Aug 10, 2016

I'm also looking at javascript implementation - a possible solution might come via supercluster

@justim
Copy link

justim commented Aug 10, 2016

@nazywamsiepawel supercluster looks pretty sweet, my solution can't handle that kind of amounts of markers.

I didn't really get to making a proper repo/PR for the clustering, but since there are people that want to cluster, I created a gist with code that I pulled from our application. There are some magic numbers and the region/bounds stuff is a bit weird, but it works alright. Maybe someone can use it :)

https://gist.github.com/justim/2c834a657553f0e2531949edae03fb83

@veedeo
Copy link

veedeo commented Aug 24, 2016

anyone had a luck using a supercluster?
want to know how much work needs to be done before I jump into it

@i-tu
Copy link

i-tu commented Aug 31, 2016

@veedeo We used supercluster, and it solved this problem nicely! I didn't do the implementation, but looking at the code it seems pretty simple.

@veedeo
Copy link

veedeo commented Aug 31, 2016

Cool thanks, will look at it later

@danvass
Copy link

danvass commented Sep 2, 2016

@i-tu Can you provide a sample of the implementation you used? More specifically how you converted the region span to a zoom level.

@alexHlebnikov
Copy link

Guys, if you need generalisation instead of clustering, you can use https://github.com/mourner/rbush

@i-tu
Copy link

i-tu commented Sep 20, 2016

@dvassilev I just noticed your message, but in case you or someone else is still working on this, here's an incomplete snippet. HTH https://gist.github.com/i-tu/05e79737adfe36935ec869125a686f82

@danielreuterwall
Copy link

@i-tu I followed your snippet and managed to get clustering in place. Big thanks for that!
However, I would like to be able to click on a cluster and have it zoomed in. Did you solve that? I guest the fitToMarkers could come in handy but I cannot get the markers/points out from the cluster.
Any ideas on how to get that in place? That's the only issue left to get proper clustering support.

@danvass
Copy link

danvass commented Sep 26, 2016

@i-tu Thanks for that, I managed to implement it but noticed that if markers change dynamically the module requires the entire list to be loaded in again which blocks the JS thread and there's a bit of a delay when getting the clusters back. It seems like a native implementation would probably be the best solution.

@olliekav
Copy link

@danielreuterwall or @dvassilev could either of you post a more complete example of getting clustering to work with SuperCluster? Struggling to get it working.

@magrinj
Copy link

magrinj commented Nov 2, 2016

Any news on this ?

@ErkanSensei
Copy link
Contributor

Will this be implemented any time soon?

@GarimaMathur07
Copy link

@yonahforst I have implemented map clustering android code from your branch https://developer.android.com/training/wearables/apps/bt-debugging.html#SetupDevices

But I can't get how to connect React Native Maps markers with Cluster Markers. @yonahforst I didn't get any use of cluster markers in your branch also. cc: @alexHlebnikov Is there any working example.

@danielreuterwall
Copy link

danielreuterwall commented Jan 23, 2017

Sorry for not getting back before. So I did solve this using the supercluster from MapBox. When the region changes on the map I load places from my backend API and the invoke the clustering like this:

  createCluster(places) {
    const cluster = supercluster({
      radius: 60,
      maxZoom: 16,
    });

    try {
      cluster.load(places);
      const padding = 0;
      const markers = cluster.getClusters([
        this.state.region.longitude - (this.state.region.longitudeDelta * (0.5 + padding)),
        this.state.region.latitude - (this.state.region.latitudeDelta * (0.5 + padding)),
        this.state.region.longitude + (this.state.region.longitudeDelta * (0.5 + padding)),
        this.state.region.latitude + (this.state.region.latitudeDelta * (0.5 + padding)),
      ], this.getZoomLevel());
      this.setState({
        markers,
        cluster
      });
    }
    catch(e) {
      console.debug('failed to create cluster', e);
    }
  }

  getZoomLevel(region = this.state.region) {
    const angle = region.longitudeDelta;
    return Math.round(Math.log(360 / angle) / Math.LN2);
  }

Inside the <MapView> component I render the markers/clusters.

<MapView ref={ref => { this.map = ref; }}>
  { this.state.markers.map((marker, index) => {
    return (
      <MapView.Marker 
        coordinate={{ latitude: marker.geometry.coordinates[1], longitude: marker.geometry.coordinates[0] }}
        onPress={() => this.markerPressed(marker)}>
        <Marker model={place} active={this.isSelected(place)} />
      </MapView.Marker>
    );
  })}
</MapView>

The <Marker> component is my custom marker and it checks for ´properties.cluster´ in its model and render itself accordingly.

Finally I have a method that handled taps on the marker/cluster. If its a cluster is centers it and zooms in, if its a regular marker it just centers it.

markerPressed(marker) {
  let isCluster = marker.properties && marker.properties.cluster;
  let region = this.state.region;

  this.map.animateToRegion({
    longitude: marker.geometry.coordinates[0],
    latitude: marker.geometry.coordinates[1],
    longitudeDelta: isCluster ? region.longitudeDelta/5 : region.longitudeDelta,
    latitudeDelta: isCluster ? region.latitudeDelta/5 : region.latitudeDelta,
  }, 200);
}

Hope this helps.

@proProbe
Copy link

how is it going with this pr? Would love to import this to our current application!

@note89
Copy link

note89 commented Mar 7, 2017

I have a working Hack (sofar) for doing this in android.
you need to make getIcon in and add implements ClusterItem in AirMapMarker public.
then in AirMapView add something like this

    private class MarkerRenderer extends DefaultClusterRenderer<AirMapMarker> {

        public MarkerRenderer(ThemedReactContext reactContext) {
            super(reactContext.getApplicationContext(), map, mClusterManager);
        }

        @Override
        protected void onBeforeClusterItemRendered(AirMapMarker post, MarkerOptions markerOptions) {
            markerOptions.icon(post.getIcon());
        }

        @Override
        protected void onBeforeClusterRendered(Cluster<AirMapMarker> cluster, MarkerOptions markerOptions) {

            AirMapMarker first = cluster.getItems().iterator().next();
            markerOptions.icon(first.getIcon());
        }

        @Override
        protected boolean shouldRenderAsCluster(Cluster cluster) {
            // Always render clusters.
            return cluster.getSize() > 1;
        }
    }

    @Override
    public boolean onClusterClick(Cluster<AirMapMarker> cluster) {
        AirMapMarker item = cluster.getItems().iterator().next();
        WritableMap event;
        event = makeClickEventData(item.getPosition());
        event.putString("action", "marker-press");
        event.putString("id", item.getIdentifier());
        manager.pushEvent(this, "onMarkerPress", event);
        return true;
    }

    @Override
    public void onClusterInfoWindowClick(Cluster<AirMapMarker> cluster) {
        // Does nothing, but you could go to a list of the users.
    }

    @Override
    public boolean onClusterItemClick(AirMapMarker item) {
        WritableMap event;
        event = makeClickEventData(item.getPosition());
        event.putString("action", "marker-press");
        event.putString("id", item.getIdentifier());
        manager.pushEvent(this, "onMarkerPress", event);
        return true;
    }

    @Override
    public void onClusterItemInfoWindowClick(AirMapMarker item) {
        // Does nothing, but you could go into the user's profile page, for example.
    }
    @Override
    public void onMapReady(final GoogleMap map) {
        this.map = map;
        this.map.setInfoWindowAdapter(this);
        this.map.setOnMarkerDragListener(this);
        this.mClusterManager = new ClusterManager<AirMapMarker>(this.getContext(), map);
        mClusterManager.setRenderer(new MarkerRenderer(this.context));

        manager.pushEvent(this, "onMapReady", new WritableNativeMap());

        final AirMapView view = this;
        map.setOnCameraIdleListener(mClusterManager);
        map.setOnMarkerClickListener(mClusterManager);
        map.setOnInfoWindowClickListener(mClusterManager);
        mClusterManager.setOnClusterClickListener(this);
        mClusterManager.setOnClusterInfoWindowClickListener(this);
        mClusterManager.setOnClusterItemClickListener(this);
        mClusterManager.setOnClusterItemInfoWindowClickListener(this);
        mClusterManager.cluster();
   public void addFeature(View child, int index) {
        // Our desired API is to pass up annotations/overlays as children to the mapview component.
        // This is where we intercept them and do the appropriate underlying mapview action.
          if(child instanceof AirMapMarker) {
            AirMapMarker annotation = (AirMapMarker) child;
//            annotation.addToMap(map);
            mClusterManager.addItem(annotation);
            features.add(index, annotation);
            Marker marker = (Marker) annotation.getFeature();
            markerMap.put(marker, annotation);
    public void removeFeatureAt(int index) {
        AirMapFeature feature = features.remove(index);
        if (feature instanceof AirMapMarker) {
             mClusterManager.removeItem((AirMapMarker) feature);
        }else{
            feature.removeFromMap(map);
        }
    }

Maybe there where some more minor things but that is pretty much it.
Also i commented out

map.setOnInfoWindowClickListener
map.setOnMarkerClickListener

I would like to collaborate on how one would do the cluster indicator in an efficent way.

@nehvaleem
Copy link

nehvaleem commented Mar 27, 2017

Any progress with this PR? I am dying to see this feature since I am dealing with thousands of markers and for now performance is simply tragic.

Also what about clustering on Android? I've seen discussion, but this PR doesn't seem to contain any android related changes.

@lelandrichardson
Copy link
Collaborator

@nehvaleem it looks like some others in this thread have investigated the android side of things but nothing has made it into this branch.

If we can come up with a good general clustering solution that doesn't inhibit the non-clustering use case, then I am all for adding it to this library.

@jayesbe
Copy link

jayesbe commented May 3, 2017

My full implementation working on android using supercluster

import isEqual from 'lodash.isequal';
import React, { Component } from 'react';
import { StyleSheet, View, Dimensions } from 'react-native';

import MapView from 'react-native-maps';
import supercluster from 'supercluster';
import MapClusterMarker from './map-cluster-marker';

const { width, height } = Dimensions.get('window');
const ASPECT_RATIO = width / height;
const LATITUDE = 31.8018895; // your starting lat
const LONGITUDE = -85.9572283; // your starting lng
const LATITUDE_DELTA = 0.0922;
const LONGITUDE_DELTA = LATITUDE_DELTA * ASPECT_RATIO;
const SPACE = 0.01;

export default class Map extends Component {
  constructor(props) {
    super(props);

    this.createCluster = this.createCluster.bind(this);
    this.getMarkers = this.getMarkers.bind(this);
    this.getZoomLevel = this.getZoomLevel.bind(this);
    this.onRegionChange = this.onRegionChange.bind(this);
    
    this.state = {
      region: {
        latitude: LATITUDE,
        longitude: LONGITUDE,
        latitudeDelta: LATITUDE_DELTA,
        longitudeDelta: LONGITUDE_DELTA,
      }
    };
    
    const cluster = this.createCluster(this.props.screenProps);
    const markers = this.getMarkers(cluster, this.state.region);

    this.state['cluster'] = cluster;
    this.state['markers'] = markers;
  }

  createCluster(props) {
    const cluster = supercluster({
      radius: 75,
      maxZoom: 16,
    });
    
    const { geoData } = props;
    
    const places = geoData .map( geo => {
      return {
        "type": "Feature",
        "geometry": {"type": "Point", "coordinates": [geo.lng, geo.lat]},
      };
    });

    try {
      cluster.load(places);
      return cluster;
    }
    catch(e) {
      console.debug('failed to create cluster', e);
    }
  }
  
  getMarkers(cluster, region) {
    const padding = 0;
    return cluster.getClusters([
      region.longitude - (region.longitudeDelta * (0.5 + padding)),
      region.latitude - (region.latitudeDelta * (0.5 + padding)),
      region.longitude + (region.longitudeDelta * (0.5 + padding)),
      region.latitude + (region.latitudeDelta * (0.5 + padding)),
    ], this.getZoomLevel());
  }
  
  getZoomLevel(region = this.state.region) {
    // http://stackoverflow.com/a/6055653
    const angle = region.longitudeDelta;

    // 0.95 for finetuning zoomlevel grouping
    return Math.round(Math.log(360 / angle) / Math.LN2);
  }
  
  componentWillReceiveProps(nextProps) {
    const cluster = this.createCluster(nextProps.screenProps);
    const markers = this.getMarkers(cluster, this.state.region);
    
    this.setState({
      cluster: cluster,
      markers: markers
    });
  }
  
  onRegionChange(region) {
    if (this._regionChangeTimer !== null) {
      clearTimeout(this._regionChangeTimer);
    }
    this._regionChangeTimer = setTimeout(() => {
      this.setState({ 
        region: region,
      });     
      this._regionChangeTimer = null;
    }, 150);
  }
  
  componentDidUpdate(prevProps, prevState) {
    if (!isEqual(prevState.region, this.state.region)) {     
      this.setState({
        markers: this.getMarkers(this.state.cluster, this.state.region)        
      });
    }
  }
    
  renderMarkers() {
    return this.state.markers.map( (marker, i) => {
      return (
        <MapView.Marker
          key={i}
          coordinate={{
            latitude: marker.geometry.coordinates[1],
            longitude: marker.geometry.coordinates[0],
          }}
        >
          <MapClusterMarker {...marker} />
        </MapView.Marker>
      );
    })
  }
  
  render() {    
    return (
      <View style={styles.container}>
        <MapView
          style={styles.map}
          region={this.state.region}
          onRegionChange={this.onRegionChange}
        >
        {this.renderMarkers()}
        </MapView>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    ...StyleSheet.absoluteFillObject,
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#f4f4f4',
  },
  map: {
    ...StyleSheet.absoluteFillObject,
  },
});

Thanks to @danielreuterwall and @i-tu above.

@kajal-mittal
Copy link

@jayesbe ,
I have initialized supercluster and react-native maps.But still,I am not able to
import MapClusterMarker from './map-cluster-marker';

@jayesbe
Copy link

jayesbe commented Jun 7, 2017

@KajalMittal .. the map-cluster-marker is your custom marker component. did you create one ?

eg


import React, { Component } from 'react';
import { StyleSheet, View } from 'react-native';
import Svg, { Circle, Text } from 'react-native-svg';
import Icon from 'react-native-vector-icons/MaterialIcons';

export default class MapClusterMarker extends Component { 
  
  setNativeProps(props) {
    this.refs['marker'].setNativeProps(props);
  }
  
  render() {    
    const { circleColour, textColour } = this.props;
  
    if (!this.props.hasOwnProperty('properties') ||
        !this.props.properties.hasOwnProperty('cluster') ||
        !this.props.properties.cluster) 
    {
      return (
        <Icon ref="marker" name="place" size={24} color={circleColour} />
      );
    } 
    
    const pointCount = this.props.properties.point_count_abbreviated;
    const height = 50;
    const width = 50;
    const fontSize = 20;
    
    return (
      <View ref="marker">
        <Svg
          height={height}
          width={width}
        >
          <Circle
            cx={width / 2}
            cy={height / 2}
            strokeWidth={0}
            r="25"
            fill={circleColour}
            fillOpacity={0.5}
          />
          <Circle
            cx={width / 2}
            cy={height / 2}
            strokeWidth={0}
            r="20"
            fill={circleColour}
          />
          <Text
            fill={textColour}
            fontSize={fontSize}
            // fontWeight="bold"
            strokeWidth={0}
            x={width / 2}
            y={(height / 2) - (fontSize / 2)}
            dy={fontSize * -0.25}
            textAnchor="middle"
          >
          {pointCount}
          </Text>
        </Svg>
      </View>
    );
  }
}

MapClusterMarker.propTypes = {
  circleColour: React.PropTypes.string,
  textColour: React.PropTypes.string,
};

MapClusterMarker.defaultProps = {
  circleColour: "#333", // #2b87a2
  textColour: 'white'
};

@luco
Copy link

luco commented Aug 4, 2017

@jayesbe Nice code, thanks a lot. But I find that you didn't define _regionChangeTimer anytime.
Also, do you know why sometimes the map resets position to greenwich?

@venits
Copy link

venits commented Sep 2, 2017

You can try this. It works for me :) https://github.com/venits/react-native-map-clustering
It is the simplest solution right now.

@rborn
Copy link
Collaborator

rborn commented Mar 12, 2018

@yonahforst this PR is really old and sadly somehow it didn't make it in the repo. Lot of things changed in the module since then and the supercluster solution works pretty well.

I will close the PR (we're trying to clean up as much as possible) but please ping me if you think you could update it to work with the last code and also android side 🤗
Thanks for all your effort :)

@rborn rborn closed this Mar 12, 2018
@KonstantinGreat
Copy link

const cluster = this.createCluster(this.props.screenProps);

What is this.props.screenProps ? logically looks like marker list

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet