I am using a customised version of the mapview (OSMDroid version).
I am using custom tiles within it and I only want the user to be able to view the area where I have my custom tiles.
Is there a way to set the boundary lat longs so when they pan the map it doesn't go past these boundaries?
Update: I know this is an old question, but osmdroid now has a setScrollableAreaLimit() method that will achieve what you are looking for. There is also a setMinZoomLevel() and setMaxZoomLevel() method to easily restrict zoom levels.
Original answer:
Please keep an eye on:
http://code.google.com/p/osmdroid/issues/detail?id=209
A patch has already been created, and will likely be integrated shortly.
This is cross-posted from the osmdroid thread. There's now lists a BoundedMapView class, which implements the afortementioned patch.
For those of use using the .jar or otherwise not that familiar with
patches, I cobbled together a subclass of MapView that supports
limiting the user's view to a specific area.
Details on how to use it,
if not obvious, can be found at
http://www.sieswerda.net/2012/08/15/boundedmapview-a-mapview-with-limits/
Incase it helps anyone....
I have sort of a solution that I am using, it works ok, but could definitely be better as the map can go abit too far off the screen before it jumps back!
It uses the lat longs and works out where the map is, I set 4 coordinates which are roughly the 4 corners of the map I have found it works better if you set them slightly into the map rather exactly the corners, I then work out if the lat longs have left the screen completely.. if so it will bounce it halfway back:
I overrode the mapview and the OnTouch event of the map
#Override
public boolean onTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_UP) {
// (only works for north of equator)
// * map right side (lat) can't go past the left (lat) of screen
// get geopoints of the 4 corners of the screen
Projection proj = getProjection();
GeoPoint screenTopLeft = proj.fromPixels(0, 0);
GeoPoint screenTopRight = proj.fromPixels(getWidth(), 0);
GeoPoint screenBottomLeft = proj.fromPixels(0, getHeight());
double screenTopLat = screenTopLeft.getLatitudeE6() / 1E6;
double screenBottomLat = screenBottomLeft.getLatitudeE6() / 1E6;
double screenLeftlong = screenTopLeft.getLongitudeE6() / 1E6;
double screenRightlong = screenTopRight.getLongitudeE6() / 1E6;
double mapTopLat = BoundsTopLeftCorner.getLatitudeE6() / 1E6;
double mapBottomLat = BoundsBottomLeftCorner.getLatitudeE6() / 1E6;
double mapLeftlong = BoundsTopLeftCorner.getLongitudeE6() / 1E6;
double mapRightlong = BoundsTopRightCorner.getLongitudeE6() / 1E6;
// screen bottom greater than map top
// screen top less than map bottom
// screen right less than map left
// screen left greater than map right
boolean movedLeft = false;
boolean movedRight = false;
boolean movedUp = false;
boolean movedDown = false;
boolean offscreen = false;
if (screenBottomLat > mapTopLat) {
movedUp = true;
offscreen = true;
}
if (screenTopLat < mapBottomLat) {
movedDown = true;
offscreen = true;
}
if (screenRightlong < mapLeftlong) {
movedLeft = true;
offscreen = true;
}
if (screenLeftlong > mapRightlong) {
movedRight = true;
offscreen = true;
}
if (offscreen) {
// work out on which plane it's been moved off screen (lat/lng)
if (movedLeft || movedRight) {
double newBottomLat = screenBottomLat;
double newTopLat = screenTopLat;
double centralLat = newBottomLat
+ ((newTopLat - newBottomLat) / 2);
if (movedRight)
this.getController().setCenter(
new GeoPoint(centralLat, mapRightlong));
else
this.getController().setCenter(
new GeoPoint(centralLat, mapLeftlong));
}
if (movedUp || movedDown) {
// longs will all remain the same
double newLeftLong = screenLeftlong;
double newRightLong = screenRightlong;
double centralLong = (newRightLong + newLeftLong) / 2;
if (movedUp)
this.getController().setCenter(
new GeoPoint(mapTopLat, centralLong));
else
this.getController().setCenter(
new GeoPoint(mapBottomLat, centralLong));
}
}
}
return super.onTouchEvent(ev);
}}
A few things I should point out if you are considering using this:
I make no guarentees that it will work for your situation and you should only use it as a starting point.
It will only work for areas north of the equator (I think) due to the way I've done the coords!
It works on the "action up" of the touch event, so it takes the point where the user takes their finger off the screen. This means when the map flings it's completely inaccurate as I could not work out where the map stopped, because of this I turned off the fling by overriding the fling event and not doing anything in it.. this does make the map a bit jolty!
If anyone has any better solutions or can improve my code please feel free!
I'm looking for exactly the same thing.
My best lead is to add an Overlay, which extends boolean onScroll(...). If this returns true, then the scroll is cancelled.
This is exactly how I want it, except for one thing: flinging/flicking. The same approach can be used to cancel fling events, though you only get to hear about it at the start of the fling.
Ideally, you'd be able to listen to the computeScroll() method, and limit the (x, y) of the scroll, based on mScroller.getCurX() and mScroller.getCurY().
Related
I am implementing 2-finger-zoom.
2nd finger down - remember finger distance: (done once)
event.getPointerCoords(0, finger1_start);
event.getPointerCoords(1, finger2_start);
start_distance = VecLength(finger1_start, finger2_start);
2 fingers down + ACTION_MOVE
event.getPointerCoords(0, finger1_now);
event.getPointerCoords(1, finger2_now);
double distance_now = VecLength(finger1_now, finger2_now);
zoom = distance_now / start_distance;
VecLength method - returns length of distance between 2 points
double VecLength (MotionEvent.PointerCoords a, MotionEvent.PointerCoords b)
{
return Math.sqrt(Math.pow(b.x - a.x, 2) + Math.pow(b.y - a.y, 2));
}
Problem, it jitters when I use view.setScaleX. I zommed in as smoothly as possible using my fingers recording the zoom value in logcat.
Not using view.setScaleX/Y
Using view.setScaleX/Y
Which proves that I do see the jitter. I narrowed it down to the pointers actually having different coordinates each frame going back/forth.
I assume the scale somehow affects the view, but I don't understand how to undo that. How do I get "raw" finger coordinates or respect the zoom in my calculation?
Not that I cannot use getRawX as this only returns one finger. I obviously need both.
It seems I can mostly fix this by multiplying the start distance with the current scale:
So change
event.getPointerCoords(0, finger1_now);
event.getPointerCoords(1, finger2_now);
double distance_now = VecLength(finger1_now, finger2_now);
zoom = distance_now / start_distance;
to
event.getPointerCoords(0, finger1_now);
event.getPointerCoords(1, finger2_now);
double distance_now = VecLength(finger1_now, finger2_now);
zoom = distance_now / start_distance * view.getScaleX(); << x and y scale are the same in my case
Background
Suppose I have a Google maps view, and another view on top of it, that covers a part of it, hiding some content of the map.
The problem
I need to make the "camera" of the map, to focus and have a marker on a coordinate , yet let it all be in the middle of the visible part of the map.
Something like this:
The original code was focusing on (about) the center of the entire screen, making the marker almost invisible (as the bottom view covers it).
Thing is, I can't find the proper way to set the correct value to the Y coordinate of the map itself (meaning latitude).
What I've tried
I tried, given the height of the bottom view, and the coordinate that I've put the marker on, to calculate the delta (yet of course not change the marker itself) :
final float neededZoom = 6.5f;
int bottomViewHeight = bottomView.getHeight();
LatLng posToFocusOn = ...;
final Point point = mMap.getProjection().toScreenLocation(posToFocusOn);
final float curZoom = mMap.getCameraPosition().zoom;
point.y += bottomViewHeight * curZoom / neededZoom;
posToFocusOn = mMap.getProjection().fromScreenLocation(point);
final CameraUpdate cameraPosition = CameraUpdateFactory.newCameraPosition(new Builder().target(posToFocusOn).zoom(neededZoom).build());
Sadly, this focuses way above the marker.
The question
What's wrong with what I wrote? What can I do to fix it?
ok, I've found a workaround, which I think works on all devices (tested on 3, each with a different screen resolution and size) :
I've measured how many pixels (and then converted to DP) a change of one degree has on the marker itself.
From this, I measured the height of each view, and calculated the delta needed to move the camera.
In my case, it's this way (supposing the zoom is 6.5f) :
//measured as 223 pixels on Nexus 5, which has xxhdpi, so divide by 3
final float oneDegreeInPixels = convertDpToPixels( 223.0f / 3.0f);
final float mapViewCenter = mapViewHeight / 2.0f;
final float bottomViewHeight = ...;
final float posToFocusInPixelsFromTop = (mapViewHeight - bottomViewHeight) / 2.0f ;// can optionally add the height of the view on the top area
final float deltaLatDegreesToMove = (mapViewCenter - posToFocusInPixelsFromTop) / oneDegreeInPixels;
LatLng posToFocusOn = new LatLng(latitude - deltaLatDegreesToMove, longitude);
final CameraUpdate cameraPosition = CameraUpdateFactory.newCameraPosition(new Builder().target(posToFocusOn).zoom(neededZoom).build());
And it worked.
I wonder if it can be adjusted to support any value of zoom.
Your code is almost right, but it goes above the marker because you are taking into account bottomViewHeight when computing point.y instead of bottomViewHeight/2 (When your view's size is 200px, you only need to displace the map 100px to recenter it):
point.y += (bottomViewHeight / 2) * curZoom / neededZoom;
Update:
This is a more general approach taht takes into account the map bounds and calculates a new map bounds according to the height of your bottomView. This is zoom independent.
public void recenter() {
LatLngBounds mapBounds = mMap.getProjection().getVisibleRegion().latLngBounds;
Point nothEastPoint = mMap.getProjection().toScreenLocation(mapBounds.northeast);
Point souhWestPoint = mMap.getProjection().toScreenLocation(mapBounds.southwest);
Point newNorthEast = new Point(nothEastPoint.x, nothEastPoint.y + bottomView.getHeight() / 2);
Point newSouhWestPoint = new Point(souhWestPoint.x, souhWestPoint.y + bottomView.getHeight() / 2);
LatLngBounds newBounds = LatLngBounds.builder()
.include(mMap.getProjection().fromScreenLocation(newNorthEast))
.include(mMap.getProjection().fromScreenLocation(newSouhWestPoint))
.build();
mMap.moveCamera(CameraUpdateFactory.newLatLngBounds(newBounds, 0));
}
Note that each time you call recenter() the map will move.
I have a screen with google map, on top of screen there is one AutotextView for search place, and make a custom info window to show more details, problem is when user click on any mark custom window popup as their default behaviour (center on screen), so it mixed up autotextview, Can I change info-window position on screen, i.e bottom or anything like that.
If you want to adjust Marker location perfect center on google map screen with popup then please use below code in that i have get popup height and based on that update y axis.i hope it will help you.
public boolean onMarkerClick(Marker marker) {
//Please use fix height popup
float container_height = getResources().getDimension(R.dimen.DIP_300);
Projection projection = mGoogleMap.getProjection();
Point markerScreenPosition = projection.toScreenLocation(marker.getPosition());
Point pointHalfScreenAbove = new Point(markerScreenPosition.x,(int) (markerScreenPosition.y - (container_height / 2)));
LatLng aboveMarkerLatLng = projection.fromScreenLocation(pointHalfScreenAbove);
marker.showInfoWindow();
CameraUpdate center = CameraUpdateFactory.newLatLng(aboveMarkerLatLng);
mGoogleMap.moveCamera(center);
mGoogleMap.animateCamera(center);
marker.showInfoWindow();
return true;
}
Currently you cannot change the info window to be below marker. This is already requested and discussed on gmaps-api-issues.
I can only suggest you to animate marker after being clicked to a different position. Add OnMarkerClickListener and return true (handled) there. After that you can call:
marker.showInfoWindow();
map.animateCamera(...);
to create an effect similar to default but with a different position.
Google Map Info Window Position always on top middle of marker now. If marker is rotated.
double angle = MARKER_ROTATION_ANGLE;
double x = Math.sin(-angle * Math.PI / 180) * 0.5 + 0.5;
double y = -(Math.cos(-angle * Math.PI / 180) * 0.5 - 0.5);
marker.setInfoWindowAnchor((float)x, (float)y);
Explanation:
instead of MARKER_ROTATION_ANGLE you have to give the exact rotation of your marker. Then it will be work fine. :)
I am trying to solve a problem with drawing a path from huge (100k+) set of GeoPoints to a MapView on Android.
Firstly I would like to say, I searched through StackOverflow a lot and haven't found an answer.The bottleneck of my code is not actually drawing into canvas, but Projection.toPixels(GeoPoint, Point) or Rect.contains(point.x, point.y) method..I am skipping points not visible on screen and also displaying only every nth point according to current zoom-level. When the map is zoomed-in I want to display as accurate path as possible so I skipping zero (or nearly to zero) points, so that when finding visible points I need to call the projection method for every single point in the collection. And that is what really takes a lot of time (not seconds, but map panning is not fluid and I am not testing it on HTC Wildfire:)). I tried caching calculated points, but since points be recalculated after every map pan/zoom it haven't helped
at all.
I thought about usage of some kind of prune and search algorithm instead of iterate the array, but I figured out the input data is not sorted (I can't throw away any branch stacked between two invisible points). That could I possible solve with simple sort at the beginning, but I am still not sure even the logarithmic count of getProjection() and Rect.contains(point.x, point.y) calls instead of linear would solve the performance problem.
Bellow is my current code. Please help me if you know how to make this better. Thanks a lot!
public void drawPath(MapView mv, Canvas canvas) {
displayed = false;
tmpPath.reset();
int zoomLevel = mapView.getZoomLevel();
int skippedPoints = (int) Math.pow(2, (Math.max((19 - zoomLevel), 0)));
int mPointsSize = mPoints.size();
int mPointsLastIndex = mPointsSize - 1;
int stop = mPointsLastIndex - skippedPoints;
mapView.getDrawingRect(currentMapBoundsRect);
Projection projection = mv.getProjection();
for (int i = 0; i < mPointsSize; i += skippedPoints) {
if (i > stop) {
break;
}
//HERE IS THE PROBLEM I THINK - THIS METHOD AND THE IF CONDITION BELOW
projection.toPixels(mPoints.get(i), point);
if (currentMapBoundsRect.contains(point.x, point.y)) {
if (!displayed) {
Point tmpPoint = new Point();
projection.toPixels(mPoints.get(Math.max(i - 1, 0)),
tmpPoint);
tmpPath.moveTo(tmpPoint.x, tmpPoint.y);
tmpPath.lineTo(point.x, point.y);
displayed = true;
} else {
tmpPath.lineTo(point.x, point.y);
}
} else if (displayed) {
tmpPath.lineTo(point.x, point.y);
displayed = false;
}
}
canvas.drawPath(tmpPath, this.pathPaint);
}
So I figured out how to make it all much faster!
I will post it here, somebody could possibly found it useful in the future.
It has emerged that usage of projection.toPixels() can really harm application performance. So I figured out that way better than take every single GeoPoint, convert it to Point and then check if it is contained in map viewport is, when I count actuall viewport radius of the map as following:
mapView.getGlobalVisibleRect(currentMapBoundsRect);
GeoPoint point1 = projection.fromPixels(currentMapBoundsRect.centerX(), currentMapBoundsRect.centerY());
GeoPoint point2 = projection.fromPixels(currentMapBoundsRect.left, currentMapBoundsRect.top);
float[] results2 = new float[3];
Location.distanceBetween(point1.getLatitudeE6()/1E6, point1.getLongitudeE6()/1E6, point2.getLatitudeE6()/1E6, point2.getLongitudeE6()/1E6, results2);
The radius is in results2[0]..
Then I can take every single GeoPoint and count the distance between it and the center of the map mapView.getMapCenter(). Then I can compare the radius with computed distance and decide whether ot not diplay the point.
So that's it, hope It will be helpful.
So I have a MapView with a lot of markers, most of which are concentrated in mile wide clusters. When zoomed the markers overlap and appear to only be one. What I want to achieve is at a certain zoom level replace the overlapping markers with a group marker that will display the density of markers and onClick will zoom to display all markers inside. I know I can do this with brute force distance measurements but there must be a more efficient way. Anyone have any solution or smart algorithms on how I can achieve this?
Um... assuming the markers are not grouped, layered or anything: why - before showing them - don't you create a grid of certain density and simply bin the markers into the cells of your grid?
If you then count that several markers fall into the same bin (grid cell) - you can group them. If you need slightly more clever grouping, you might also check the neighbouring cells.
Maybe it sounds a bit primitive but:
No n^2 algorithms
No assumption about ordering of the input
No need to additionally process markers which are not going to be shown
The code for the grid:
Note - I come from the C++ world (got here through [algorithm] tag) so I'll stick to the pseudo-C++. I do not know the API of the mapview. But I would be surprised if this couldn't be efficiently translated into whatever language/library you are using.
Input:
- list of markers
- the rectangle viewing window in world coordinates (section of world we are currently looking at)
In the simplest form, it would look something like this:
void draw(MarkerList mlist, View v) {
//binning:
list<Marker> grid[densityX][densityY]; //2D array with some configurable, fixed density
foreach(Marker m in mlist) {
if (m.within(v)) {
int2 binIdx;
binIdx.x=floor(densityX*(m.coord.x-v.x1)/(v.x2-v.x1));
binIdx.y=floor(densityY*(m.coord.y-v.y1)/(v.y2-v.y1));
grid[binIdx.x][binIdx.y].push(m); //just push the reference
}
//drawing:
for (int i=0; i<densityX; ++i)
for (int j=0; j<densityY; ++j) {
if (grid[i][j].size()>N) {
GroupMarker g;
g.add(grid[i][j]); //process the list of markers belonging to this cell
g.draw();
} else {
foreach (Marker m in grid[i][j])
m.draw()
}
}
}
The problem that might appear is that an unwanted grid split may appear within some clustered group, forming two GroupMarkers. To counter that, you may want to consider not just one grid cell, but also its neighbors in the "\drawing" section, and - if grouped - mark neighboring cells as visited.
The following pragmatic solution based on pixel distance really worked best for me:
http://www.appelsiini.net/2008/11/introduction-to-marker-clustering-with-google-maps
I converted Cygnus X1's answer to Java. Put this method in your custom Overlay and modify drawSingle() and drawGroup() to suit your needs. You improve performance too, like converting the ArrayLists to primitive arrays.
#Override
public void draw(Canvas canvas, MapView mapView, boolean shadow) {
// binning:
int densityX = 10;
int densityY = 10;
// 2D array with some configurable, fixed density
List<List<List<OverlayItem>>> grid = new ArrayList<List<List<OverlayItem>>>(
densityX);
for(int i = 0; i<densityX; i++){
ArrayList<List<OverlayItem>> column = new ArrayList<List<OverlayItem>>(densityY);
for(int j = 0; j < densityY; j++){
column.add(new ArrayList<OverlayItem>());
}
grid.add(column);
}
for (OverlayItem m : mOverlays) {
int binX;
int binY;
Projection proj = mapView.getProjection();
Point p = proj.toPixels(m.getPoint(), null);
if (isWithin(p, mapView)) {
double fractionX = ((double)p.x / (double)mapView.getWidth());
binX = (int) (Math.floor(densityX * fractionX));
double fractionY = ((double)p.y / (double)mapView.getHeight());
binY = (int) (Math
.floor(densityX * fractionY));
// Log.w("PointClusterer absolute", p.x+ ", "+p.y);
// Log.w("PointClusterer relative", fractionX+ ", "+fractionY);
// Log.w("PointClusterer portion", "Marker is in portion: " + binX
// + ", " + binY);
grid.get(binX).get(binY).add(m); // just push the reference
}
}
// drawing:
for (int i = 0; i < densityX; i++) {
for (int j = 0; j < densityY; j++) {
List<OverlayItem> markerList = grid.get(i).get(j);
if (markerList.size() > 1) {
drawGroup(canvas, mapView, markerList);
} else {
// draw single marker
drawSingle(canvas, mapView, markerList);
}
}
}
}
private void drawGroup(Canvas canvas, MapView mapView,
List<OverlayItem> markerList) {
GeoPoint point = markerList.get(0).getPoint();
Point ptScreenCoord = new Point();
mapView.getProjection().toPixels(point, ptScreenCoord);
Paint paint = new Paint();
paint.setTextAlign(Paint.Align.CENTER);
paint.setTextSize(30);
paint.setAntiAlias(true);
paint.setARGB(150, 0, 0, 0);
// show text to the right of the icon
canvas.drawText("GROUP", ptScreenCoord.x, ptScreenCoord.y + 30, paint);
}
private void drawSingle(Canvas canvas, MapView mapView,
List<OverlayItem> markerList) {
for (OverlayItem item : markerList) {
GeoPoint point = item.getPoint();
Point ptScreenCoord = new Point();
mapView.getProjection().toPixels(point, ptScreenCoord);
Paint paint = new Paint();
paint.setTextAlign(Paint.Align.CENTER);
paint.setTextSize(30);
paint.setAntiAlias(true);
paint.setARGB(150, 0, 0, 0);
// show text to the right of the icon
canvas.drawText("SINGLE", ptScreenCoord.x, ptScreenCoord.y + 30,
paint);
}
}
public static boolean isWithin(Point p, MapView mapView) {
return (p.x > 0 & p.x < mapView.getWidth() & p.y > 0 & p.y < mapView
.getHeight());
}
}
Assuming your markers are grouped together in an ItemizedOverlay you could create a method which was called when the map was zoomed. This would compare pixel co-ordinates of each marker to see if they overlap and set a flag. Then in the draw method you could draw either the grouped marker or individuals;
Something like:
//this would need to be wired to be called when the mapview is zoomed
//it sets the drawgrouped flag if co-ordinates are close together
Boolean drawGrouped=false;
public void onMapZoom(MapView mapView){
//loop thru overlay items
Integer i,l=this.size();
OverlayItem item;
Integer deltaX=null,deltaY=null;
Projection proj = mapView.getProjection();
Point p=new Point();
Integer x=null,y=null;
Integer tolerance = 10; //if co-ordinates less than this draw grouped icon
for(i=0;i<l;i++){
//get the item
item=this.getItem(i);
//convert the overlays position to pixels
proj.toPixels(item.getPoint(), p);
proj.toPixels(item.getPoint(), p);
//compare co-ordinates
if(i==0){
x=p.x;
y=p.y;
continue;
}
deltaX=Math.abs(p.x-x);
deltaY=Math.abs(p.y-y);
//if the co-ordinates are too far apart dont draw grouped
if(deltaX>tolerance || deltaY>tolerance){
drawGrouped=false;
return;
}
x=p.x;
y=p.y;
}
//all co-ords are within the tolerance
drawGrouped=true;
}
public void draw(android.graphics.Canvas canvas, MapView mapView, boolean shadow){
if(drawGrouped==true){
//draw the grouped icon *needs to be optimised to only do it once
drawGrouped(canvas,mapView,shadow);
return;
}
//not grouped do regular drawing
super.draw(canvas, mapView, shadow);
}
What you are looking for is usually called clustering. There are common techniques to do this, you can refer, for example, to this SO question, it leads to this post.
The basic idea is to divide the map on squares based on the current zoom level (you can cache calculations based on the zoom level to avoid recalculation when the user starts zooming), and to group them based which square they belong to. So you end up having some sort of grouping based on zoom level, ie for level 1-5 just draw the markers, for level 5-8 group them in squares of 20 miles, for 9-10 in squares of 50 miles, and so on.
Here is another relevant question on SO that you may want to take a look, not sure about the performance of this though: Android Maps Point Clustering
If your markers are grouped, you'll have a fair idea at what zoom level you should be displaying individual markers or the group marker e.g. zoom level > 17 then display individual markers, otherwise display the group marker. I used code something like this in my ItemizedOverlay to change my markers:
#Override
public void draw(Canvas canvas, MapView mapv, boolean shadow)
{
int zoom = mapv.getZoomLevel();
switch(zoom)
{
case 19:
setMarkersForZoomLevel19();
break;
case 18:
setMarkersForZoomLevel18();
break;
case 17:
setMarkersForZoomLevel17();
break;
case 16:
setMarkersForZoomLevel16();
break;
default:
// Hide the markers or remove the overlay from the map view.
mapv.getOverlays().clear();
}
area.drawArea(canvas, mapv);
// Putting this call here rather than at the beginning, ensures that
// the Overlay items are drawn over the top of canvas stuff e.g. route lines.
super.draw(canvas, mapv, false);
}
private void setMarkersForZoomLevel19()
{
for (JourneyOverlayItem item : mOverlays)
{
item.setMarker(areaPointIcon48);
}
}
If its possible to have the individual markers in a collection, you could easily get the largest and smallest latitude and longitude and the difference between them will give you the latitude and longitude span (this could then be used to zoom to the span to show the group of markers). Divide the spans by 2 and you should have the centre point for placing the group marker.
This is the approach that I used. However, it's O(n^2).
The pins must be sorted based on prominent.
Pick pin with the highest prominent. Look at all pins around it. Absorb pins near that pin.
Then move on to the next highest prominent pin. Do the same. Repeat.
Simple.
Things get complicated if you move the map around, zooming in, zooming out, and you want to ensure that the new pins are not being redrawn. So you check each cluster if they have to split during zoom in, and then you check each cluster if they have to merge during zoom out. Then you remove pins that's gone and add new pins. For every pin you add, you check whether they should join a cluster or form their own cluster.