📜 ⬆️ ⬇️

Android - we filter pins on the map, depending on the distance from each other

It was like a recent project using google maps. At one point, I saw a picture like this:




')

This happened, you guessed it, when there were too many places to be displayed nearby. Well, nothing - the problem is something like a standard one - there should have been many who had such a thing, for sure the developers provided for grouping and filtering ... Yeah, it was not there! After reviewing the documentation (I immediately admit that only briefly - perhaps I missed what I needed) and asking Google - to my surprise, it was not possible to quickly find anything I needed. Well, nothing - not the gods burn the pots - then we write ourselves.

First, create a project and connect it with google maps api. Here I think it is not necessary to explain anything - much has already been written about this, by the way, the links on the Habré are one , two , three .

First we decide what and how it should work. Go…

Obviously, we need to come up with a certain algorithm that will not display all the pins, but only those that are at a certain distance from each other. And you also need to learn how to unite them in groups. Recalculation of all this economy is necessary to occur:

1) Upon initial loading of pins onto the map.
2) When zooming

With the first paragraph, in principle, everything is clear - let's define the second. Declare interface:

public interface IOnZoomListener { void onZoomChanged(); } 


And we modify our MapView like this:

 public class MyMapView extends MapView { int oldZoomLevel = -1; IOnZoomListener onZoomListener; public MyMapView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } public MyMapView(Context context, String apiKey) { super(context, apiKey); } public MyMapView(Context context, AttributeSet attrs) { super(context, attrs); } public void setOnZoomListener(IOnZoomListener onZoomListener) { this.onZoomListener = onZoomListener; } @Override public void dispatchDraw(Canvas canvas) { super.dispatchDraw(canvas); int newZoom = this.getZoomLevel(); if (newZoom != oldZoomLevel) { if (oldZoomLevel != -1 && onZoomListener != null) { onZoomListener.onZoomChanged(); } oldZoomLevel = getZoomLevel(); } } } 


Here we simply added the ability to register our interface and check when drawing on a ZoomLevel change (if ZoomLevel has changed, pull the method of our interface).

Now we will define how we will display pins on the map and how to combine them into groups. To do this, create a class MyOverlayItem inherited from OverlayItem with such additions:

 public class MyOverlayItem extends OverlayItem { private String name; private ArrayList<MyOverlayItem> list = new ArrayList<MyOverlayItem>(); public MyOverlayItem(GeoPoint point, String name) { super(point, "", ""); this.name = name; } public String getName() { if (list.size() > 0) { return "There are " + (list.size() + 1) + " places."; } else { return name; } } public void addList(MyOverlayItem item) { list.add(item); } public ArrayList<MyOverlayItem> getList() { return list; } } 


The ArrayList list will store the list of grouped pins, and the getName method will return to us either the name of the object, or their number in the group.

Now we will describe what, for the sake of what, in fact, all this was started - our modified ItemizedOverlay.
The essence of the filtering algorithm is pretty simple: we just run in a loop over all existing elements and check each element for a close distance with an existing group of elements. If we find such a group, the element is added to it, if not, a new group is created with this element:

  boolean isImposition; for (MyOverlayItem itemFromAll : myOverlaysAll) { isImposition = false; for (MyOverlayItem item : myOverlays) { if (itemFromAll == item) { isImposition = true; break; } if (isImposition(itemFromAll, item)) { item.addList(itemFromAll); isImposition = true; break; } } if (!isImposition) { myOverlays.add(itemFromAll); } } 


First, to check the distance, I just wanted to use the coordinates of the pins (the error that arises depending on the latitude can be neglected, since the distances are not great), but then I would have to control ZoomLevel.
For my tasks, the mapView.getLatitudeSpan method is quite suitable, which returns the distance of the visible width of the screen in the required coordinate system. It remains only to divide this distance by a certain factor (how many pins should "fit" into the screen in width) - this will be the minimum distance between the pins:

 private boolean isImposition(MyOverlayItem item1, MyOverlayItem item2) { int latspan = mapView.getLatitudeSpan(); int delta = latspan / KOEFF; int dx = item1.getPoint().getLatitudeE6() - item2.getPoint().getLatitudeE6(); int dy = item1.getPoint().getLongitudeE6() - item2.getPoint().getLongitudeE6(); double dist = Math.sqrt(dx * dx + dy * dy); if (dist < delta) { return true; } else { return false; } } 


Here is just in case the full source code of the class:

 public class PlaceOverlay extends ItemizedOverlay<MyOverlayItem> { private static final int KOEFF = 20; private ArrayList<MyOverlayItem> myOverlaysAll = new ArrayList<MyOverlayItem>(); private ArrayList<MyOverlayItem> myOverlays = new ArrayList<MyOverlayItem>(); private MapView mapView; public PlaceOverlay(Drawable defaultMarker, MapView mapView) { super(boundCenterBottom(defaultMarker)); this.mapView = mapView; populate(); } public void addOverlay(MyOverlayItem overlay) { myOverlaysAll.add(overlay); myOverlays.add(overlay); } public void doPopulate() { populate(); setLastFocusedIndex(-1); } @Override protected MyOverlayItem createItem(int i) { return myOverlays.get(i); } @Override public int size() { return myOverlays.size(); } private boolean isImposition(MyOverlayItem item1, MyOverlayItem item2) { int latspan = mapView.getLatitudeSpan(); int delta = latspan / KOEFF; int dx = item1.getPoint().getLatitudeE6() - item2.getPoint().getLatitudeE6(); int dy = item1.getPoint().getLongitudeE6() - item2.getPoint().getLongitudeE6(); double dist = Math.sqrt(dx * dx + dy * dy); if (dist < delta) { return true; } else { return false; } } public void clear() { myOverlaysAll.clear(); myOverlays.clear(); } public void calculateItems() { myOverlaysClear(); boolean isImposition; for (MyOverlayItem itemFromAll : myOverlaysAll) { isImposition = false; for (MyOverlayItem item : myOverlays) { if (itemFromAll == item) { isImposition = true; break; } if (isImposition(itemFromAll, item)) { item.addList(itemFromAll); isImposition = true; break; } } if (!isImposition) { myOverlays.add(itemFromAll); } } doPopulate(); } private void myOverlaysClear() { for (MyOverlayItem item : myOverlaysAll) { item.getList().clear(); } myOverlays.clear(); } @Override protected boolean onTap(int index) { Toast.makeText(mapView.getContext(), myOverlays.get(index).getName(), Toast.LENGTH_SHORT).show(); return true; } } 


Oh yeah - in the onTap method we display Toast with the name of the group - to demonstrate the operation of the algorithm.

I want to add that this algorithm is not the ultimate truth - it can and should be improved - for example, to draw a pin not in the place of the first element of a group, but to calculate its location depending on its content. But you are already implementing this yourself in your own projects.

Now let's see how to put it all together.
Create ManyPinsProjectActivity that we inherit from MapActivity and implement the following interfaces: LocationListener, IOnZoomListener. But I will not paint everything in detail - the source code will tell everything for me:

 public class ManyPinsProjectActivity extends MapActivity implements LocationListener, IOnZoomListener { private static final int DEFAULT_ZOOM = 15; private MyMapView mapView = null; private Drawable myCurrentMarker = null; private Drawable placeMarker = null; private List<Overlay> mapOverlays; private PlaceOverlay placeOverlay; private MyCurrentLocationOverlay myCurrentLocationOverlay; double currentLatitude, currentLongitude; private MapController mapController; private LocationManager locationManager; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); locationManager = (LocationManager) this.getSystemService(Context.LOCATION_SERVICE); mapView = (MyMapView) findViewById(R.id.mapview); myCurrentMarker = this.getResources().getDrawable(R.drawable.my_pin_red); placeMarker = this.getResources().getDrawable(R.drawable.my_pin); myCurrentLocationOverlay = new MyCurrentLocationOverlay(myCurrentMarker, mapView); placeOverlay = new PlaceOverlay(placeMarker, mapView); mapOverlays = mapView.getOverlays(); mapController = mapView.getController(); mapView.setBuiltInZoomControls(true); mapView.setOnZoomListener(this); } private void animateToPlaceOnMap(final GeoPoint geopoint) { mapView.post(new Runnable() { @Override public void run() { mapView.invalidate(); mapController.animateTo(geopoint); mapController.setZoom(DEFAULT_ZOOM); } }); } private void setCurrentGeopoint(double myLatitude, double myLongitude) { currentLatitude = myLatitude; currentLongitude = myLongitude; final GeoPoint myCurrentGeoPoint = new GeoPoint((int) (myLatitude * 1E6), (int) (myLongitude * 1E6)); MyOverlayItem myCurrentItem = new MyOverlayItem(myCurrentGeoPoint, "Current Location"); myCurrentLocationOverlay.addOverlay(myCurrentItem); mapOverlays.add(myCurrentLocationOverlay); animateToPlaceOnMap(myCurrentGeoPoint); } @Override protected void onPause() { super.onPause(); locationManager.removeUpdates(this); } @Override protected void onResume() { super.onResume(); locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 5000, 100, this); } private ArrayList<PlaceInfo> generatePlaces(){ Random random = new Random(); int x, y; ArrayList<PlaceInfo> places = new ArrayList<PlaceInfo>(); PlaceInfo p; for(int i = 0; i < 100; i++){ x = random.nextInt(2000); y = random.nextInt(2000); p = new PlaceInfo(); p.setLatitude(currentLatitude + x/100000f); p.setLongitude(currentLongitude - y/100000f); p.setName("Place № " + i); places.add(p); } return places; } private void displayPlacesOnMap() { ArrayList<PlaceInfo> places = generatePlaces(); mapOverlays.remove(placeOverlay); GeoPoint point = null; MyOverlayItem overlayitem = null; placeOverlay.clear(); for (PlaceInfo place : places) { point = new GeoPoint((int) (place.getLatitude() * 1E6), (int) (place.getLongitude() * 1E6)); overlayitem = new MyOverlayItem(point, place.getName()); placeOverlay.addOverlay(overlayitem); } placeOverlay.calculateItems(); placeOverlay.doPopulate(); if (placeOverlay.size() > 0) { mapOverlays.add(placeOverlay); mapView.postInvalidate(); } } @Override public void onLocationChanged(Location location) { locationManager.removeUpdates(this); double myLatitude = location.getLatitude(); double myLongitude = location.getLongitude(); setCurrentGeopoint(myLatitude, myLongitude); displayPlacesOnMap(); } @Override public void onStatusChanged(String provider, int status, Bundle extras) { } @Override public void onProviderEnabled(String provider) { } @Override public void onProviderDisabled(String provider) { } @Override protected boolean isRouteDisplayed() { return false; } @Override public void onZoomChanged() { if (placeOverlay != null) { placeOverlay.calculateItems(); } } } 


Here it is worth adding that MyCurrentLocationOverlay is the usual ItemizedOne with one element, and PlaceInfo is the usual wrapper class containing:

  private String name; private double latitude; private double longitude; 


After all these manipulations, this is how our map with pins began to look:






I hope the article will be useful to you.

The entire project can be found at the link .

Source: https://habr.com/ru/post/139929/


All Articles