Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Draw circles of constant size on screen in Google Maps API v2

I'm working on an Android Application using Gooogle Maps API v2. I have markers on my map, and I'd like to circle one of them. I managed to do that easily, by using the Circle and Circle Options classes. But I'd also like my circle to keep the same size on the screen when zooming or unzooming, just like the markers do. It means that the circle must have a constant radius in terms of pixels. Sadly, we cannot set a radius in pixels in the API v2.

I have tried several solutions, but I'm not satisfied.

In the first one, I just multiply or divide the radius :

@Override
public void onCameraChange(CameraPosition position) 
{
    if(previousZoom > position.zoom) {
        mSelectionCircle.setRadius(Math.abs(position.zoom - previousZoom)*2*mSelectionCircle.getRadius());
    }
    else if(previousZoom < position.zoom) {
        mSelectionCircle.setRadius(Math.abs(position.zoom - previousZoom)*mSelectionCircle.getRadius()/2);
    }

    previousZoom = position.zoom;
}

It seemed to work at first, but produces wrong results when zooming quickly or zooming with fingers. Moreover, the scaling is clearly visible on the screen.

My second solution uses pixel-meter conversions. The idea is to recalculate the radius in meters when zooming/unzooming, so the circle has a constant size on the screen. To do that, I get the current position of the Circle on the screen:

 Point p1 = mMap.getProjection().toScreenLocation(mSelectionCircle.getCenter());

Then I create another point which is on the edge of the circle:

 Point p2 = new Point(p1.x + radiusInPixels, p1.y);

Where

 int radiusInPixels = 40;

After that, I use a function which returns the distance between these two points in meters.

private double convertPixelsToMeters(Point point1, Point point2) {

    double angle = Math.acos(Math.sin(point1.x) * Math.sin(point2.x)
                  + Math.cos(point1.x) * Math.cos(point2.x) * Math.cos(point1.y- point2.y));
    return angle * Math.PI * 6378100.0; // distance in meters
}

6378100 is average Earth radius. Finally, I set the new radius of the Circle :

  mSelectionCircle.setRadius(convertPixelsToMeters(p1, p2));

It should work in theory but I get ridiculous radius values (10^7 m!). The conversion function may be wrong?

So is there a simpler method to do that, or if not, may you help me to understand why my second soluton doesn't work?

Thanks!

like image 786
Christophe Longeanie Avatar asked May 07 '13 14:05

Christophe Longeanie


3 Answers

You probably don't really care about an exact pixel size, just that it looks the same for all zoom levels and device rotations.

Here is a fairly simple way to do this. Draw (and redraw if the zoom is changed) a circle whose radius is some percentage of the diagonal of the visible screen.

The Google Maps API v2 has a getProjection() function that will return the lat/long coordinates of the 4 corners of the visible screen. Then using the super convenient Location class, you can calculate the distance of the diagonal of what is visible on the screen, and use a percentage of that diagonal as the radius of your circle. Your circle will be the same size no matter what the zoom scale is or which way the device is rotated.

Here is the code in Java:

public Circle drawMapCircle(GoogleMap googleMap,LatLng latLng,Circle currentCircle) {

    // get 2 of the visible diagonal corners of the map (could also use farRight and nearLeft)
    LatLng topLeft = googleMap.getProjection().getVisibleRegion().farLeft;
    LatLng bottomRight = googleMap.getProjection().getVisibleRegion().nearRight;

    // use the Location class to calculate the distance between the 2 diagonal map points
    float results[] = new float[4]; // probably only need 3
    Location.distanceBetween(topLeft.latitude,topLeft.longitude,bottomRight.latitude,bottomRight.longitude,results);
    float diagonal = results[0];

    // use 5% of the diagonal for the radius (gives a 10% circle diameter)
    float radius = diagonal / 20;

    Circle circle = null;
    if (currentCircle != null) {
        // change the radius if the circle already exists (result of a zoom change)
        circle = currentCircle;

        circle.setRadius(radius);
    } else {
        // draw a new circle
        circle = googleMap.addCircle(new CircleOptions()
                .center(latLng)
                .radius(radius)
                .strokeColor(Color.BLACK)
                .strokeWidth(2)
                .fillColor(Color.LTGRAY));
    }

    return circle;
}
like image 147
ByteSlinger Avatar answered Nov 20 '22 02:11

ByteSlinger


Use a custom icon for Marker instead. You can create Bitmap and Canvas, draw on the latter and use it as a Marker icon:

new MarkerOptions().icon(BitmapDescriptorFactory.fromBitmap(bitmap))...
like image 30
MaciejGórski Avatar answered Nov 20 '22 01:11

MaciejGórski


EDIT:

My previous answer is no longer valid. As Jean-Philippe Jodoin brought up, you can simply do that with markers and setting their anchor to 0.5/0.5. It's a way cleaner solution.

Pasting the suggested code snippet here for reference:

marker = mMap.addMarker(new MarkerOptions().position(latlng).anchor(0.5f, 0.5f));

Old answer:

I came accross the same problem and could not find a solution, so I did it myself, I will post in the hope that it is helpful to some other people.

The "marker" approach did not work for me because I wanted circles to be centered on a specific lat/lng, and you cannot do that with a marker: if you set a circle icon for your marker, the circle edge will touch the lat/lng, but the circle will not be centered on the lat/lng.

I created a function to compute what should be the size of the circle in meters given the latitude and the camera zoom level, then added a camera listener on the map to update the size of the circle each time the camera changes zoom level. The result is a circle not changing in size (to the bare eye at least).

Here is my code:

public static double calculateCircleRadiusMeterForMapCircle(final int _targetRadiusDip, final double _circleCenterLatitude,
    final float _currentMapZoom) {
    //That base value seems to work for computing the meter length of a DIP
    final double arbitraryValueForDip = 156000D;
    final double oneDipDistance = Math.abs(Math.cos(Math.toRadians(_circleCenterLatitude))) * arbitraryValueForDip / Math.pow(2, _currentMapZoom);
    return oneDipDistance * (double) _targetRadiusDip;
}

public void addCircleWithConstantSize(){
    final GoogleMap googleMap = ...//Retrieve your GoogleMap object here

    //Creating a circle for the example
    final CircleOptions co = new CircleOptions();
    co.center(new LatLng(0,0));
    co.fillColor(Color.BLUE);
    final Circle circle = googleMap.addCircle(co);

    //Setting a listener on the map camera to monitor when the camera changes
    googleMap.setOnCameraMoveListener(new GoogleMap.OnCameraMoveListener() {
        @Override
        public void onCameraMove() {
            //Use the function to calculate the radius
            final double radius = calculateCircleRadiusMeterForMapCircle(12, co.getCenter().latitude, googleMap.getCameraPosition().zoom);
            //Apply the radius to the circle
            circle.setRadius(radius);
        }
    });
}
like image 3
androidseb Avatar answered Nov 20 '22 02:11

androidseb