12

I know what the input and outputs are, but I'm just not sure how or why it works.

This code is being used to, given a min and max longitude/latitude (a square) that contains a set of points, determine the maximum zoom level on Google Maps that will still display all of those points. The original author is gone, so I'm not sure what some of these numbers are even for (i.e. 6371 and 8). Consider it a puzzle =D

int mapdisplay = 322; //min of height and width of element which contains the map
double dist = (6371 * Math.acos(Math.sin(min_lat / 57.2958) * Math.sin(max_lat / 57.2958) + 
            (Math.cos(min_lat / 57.2958) * Math.cos(max_lat / 57.2958) * Math.cos((max_lon / 57.2958) - (min_lon / 57.2958)))));

double zoom = Math.floor(8 - Math.log(1.6446 * dist / Math.sqrt(2 * (mapdisplay * mapdisplay))) / Math.log (2));

if(numPoints == 1 || ((min_lat == max_lat)&&(min_lon == max_lon))){
    zoom = 11;
}
Gil Birman
  • 35,242
  • 14
  • 75
  • 119
Alex Beardsley
  • 20,988
  • 15
  • 52
  • 67
  • 9
    If you see the original author, slap him with a trout ;) Such extensive usage of magic numbers (http://en.wikipedia.org/wiki/Magic_number_%28programming%29) is considered bad coding style. – schnaader May 09 '11 at 16:53
  • 1
    Here is the equation explained, [Distance Calc](http://www.meridianworlddata.com/Distance-Calculation.asp) 6371 is the radius of the earth in km. – Andrew May 09 '11 at 17:16

6 Answers6

12

I use a simple formula below:

public int getZoomLevel(Circle circle) {
    if (circle != null){
        double radius = circle.getRadius();
        double scale = radius / 500;
        zoomLevel =(int) (16 - Math.log(scale) / Math.log(2));
    }
    return zoomLevel;
}

You can also replace circle with its specific radius.

Alexis Pigeon
  • 7,423
  • 11
  • 39
  • 44
Ken
  • 1,303
  • 13
  • 15
  • 1
    The new Google Maps seem to use a diameter value instead of the zoom. I seem to be able to convert this diameter to the old zoom value (which is also used in maps embeds) using your function, but using 1000 instead of 500 (of course, because radius = diameter / 2) and 19 instead of 16 (max zoom seems to be 19). I tested it with several values. See: https://gist.github.com/panzi/6694200 – panzi Sep 25 '13 at 01:56
  • 1
    thanks this worked for me and give accurate results +1 – Syed Raza Mehdi Apr 14 '14 at 04:45
  • 1
    With radius 2500 on a 720x1080px XHDPI screen the result is not proper. – Rahul Rastogi May 22 '16 at 13:11
  • @RahulRastogi, see my answer here: https://stackoverflow.com/questions/29895875/android-google-maps-zoom-to-a-specific-area-by-lat-and-long/. – CoolMind May 29 '19 at 12:38
  • @panzi, currently `googleMap.getMaxZoomLevel()` = 21. Why do you think we should divide by 1000? I suppose it depends on screen. – CoolMind Jun 05 '19 at 08:36
  • @CoolMind In my function I use diameter. Why it is that constant I don't know. Seems that Google chose it that way? More I don't know anymore since this is from 6 years ago. – panzi Jun 06 '19 at 21:14
  • @panzi, maybe an algorythm has changed. I wrote another one below. – CoolMind Jun 06 '19 at 22:43
12

Some numbers can be explained easily

And again the zoom level doubles the size with each step, i.e. increase the zoomlevel by one halfs the size on the screen.

zoom = 8 - log(factor * dist) / log(2) = 8 - log_2(factor * dist)
=> dist = 2^(8-zoom) / factor

From the numbers we find that zoom level eight corresponds to a distance of 276.89km.

Howard
  • 38,639
  • 9
  • 64
  • 83
  • Suppose I wanted to make default radius of my GeoFence as 2.5miles, minimum radius is 500meter and maximum is 25miles then how would I do that, I know I need to give dynamic zoom level to map at onMove() method but how and what would be the values! – Akash Dubey Oct 28 '21 at 17:20
2

After many attempts I made a solution. I assume that you have a padding outside a radius (for instance, if you have radius = 10000 m, then it will be 2500 m left and right). Also you should have an accuracy in meters. You can set suitable zoom with recoursion (binary search). If you change moveCamera to animateCamera, you will get an interesting animation of search. The larger radius, the more accurate zoom value you will receive. This is a usual binary search.

private fun getCircleZoomValue(latitude: Double, longitude: Double, radius: Double,
                               minZoom: Float, maxZoom: Float): Float {
    val position = LatLng(latitude, longitude)
    val currZoom = (minZoom + maxZoom) / 2
    val camera = CameraUpdateFactory.newLatLngZoom(position, currZoom)
    googleMap!!.moveCamera(camera)
    val results = FloatArray(1)
    val topLeft = googleMap!!.projection.visibleRegion.farLeft
    val topRight = googleMap!!.projection.visibleRegion.farRight
    Location.distanceBetween(topLeft.latitude, topLeft.longitude, topRight.latitude,
        topRight.longitude, results)
    // Difference between visible width in meters and 2.5 * radius.
    val delta = results[0] - 2.5 * radius
    val accuracy = 10 // 10 meters.
    return when {
        delta < -accuracy -> getCircleZoomValue(latitude, longitude, radius, minZoom,
            currZoom)
        delta > accuracy -> getCircleZoomValue(latitude, longitude, radius, currZoom,
            maxZoom)
        else -> currZoom
    }
}

Usage:

if (googleMap != null) {
    val zoom = getCircleZoomValue(latitude, longitude, radius, googleMap!!.minZoomLevel, 
        googleMap!!.maxZoomLevel)
}

You should call this method not earlier than inside first event of googleMap?.setOnCameraIdleListener, see animateCamera works and moveCamera doesn't for GoogleMap - Android. If you call it right after onMapReady, you will have a wrong distance, because the map will not draw itself that time.

Warning! Zoom level depends on location (latitude). So that the circle will have different sizes with the same zoom level depending on distance from equator (see Determine a reasonable zoom level for Google Maps given location accuracy).

enter image description here

CoolMind
  • 26,736
  • 15
  • 188
  • 224
2

This page is extremely helpful for explaining all this stuff (distance between two lat-lng pairs, etc).

6371 is the approximate radius of the earth in kilometers.

57.2958 is 180/pi

also, check out these Mercator projection calculations for converting between latitude-longitude and X-Y: http://wiki.openstreetmap.org/wiki/Mercator

Dave
  • 1,338
  • 12
  • 17
1

I needed the opposite: given a particular radius at a certain zoom level (i.e., 40 meters at zoom level 15), I needed the radii at other zoom levels that showed the same circle size (graphically) in the map. To do this:

// after retrieving the googleMap from either getMap() or getMapAsync()...

// we want a circle with r=40 meters at zoom level 15
double base = 40 / zoomToDistFactor(15);

final Circle circle = googleMap.addCircle(new CircleOptions()
        .center(center)
        .radius(40)
        .fillColor(Color.LTGRAY)
);

googleMap.setOnCameraMoveListener(new GoogleMap.OnCameraMoveListener() {
    @Override
    public void onCameraMove() {
        CameraPosition cameraPosition = googleMap.getCameraPosition();
        LatLng center = cameraPosition.target;
        float z2 = cameraPosition.zoom;
        double newR = base * zoomToDistFactor(z2);
        circle.setRadius(newR);
    }
});

// ...

private double zoomToDistFactor(double z) {
    return Math.pow(2,8-z) / 1.6446;
}

I figure I'd put it here to save anyone else the effort to get this conversion.

As a side note, moving the circle around like this in a cameramovelistener made the circle motion very choppy. I ended up putting a view in the center of the enclosing MapView that simply drew a small circle.

Danny Daglas
  • 1,501
  • 1
  • 9
  • 9
0

I think he gets this function:

 function calculateZoom(WidthPixel,Ratio,Lat,Length){
    // from a segment Length (km), 
    // with size ratio of the of the segment expected on a map (70%),
    // with a map widthpixel size (100px), and a latitude (45°) we can ge the best Zoom
    // earth radius : 6,378,137m, earth is a perfect ball; perimeter at the equator = 40,075,016.7 m
    // the full world on googlemap is available in a box of 256 px; It has a ratio of 156543.03392 (px/m)
    // for Z = 0; 
    // pixel scale at the Lat_level is ( 156543,03392 * cos ( PI * (Lat/180) ))
    // map scale increase at the rate of square root of Z
    //
    Length = Length *1000;                     //Length is in Km
    var k = WidthPixel * 156543.03392 * Math.cos(Lat * Math.PI / 180);        //k = perimeter of the world at the Lat_level, for Z=0 
    var myZoom = Math.round( Math.log( (Ratio * k)/(Length*100) )/Math.LN2 );
    myZoom =  myZoom -1;                   // z start from 0 instead of 1
    //console.log("calculateZoom: width "+WidthPixel+" Ratio "+Ratio+" Lat "+Lat+" length "+Length+" (m) calculated zoom "+ myZoom);

    // not used but it could be usefull for some: Part of the world size a the Lat 
    MapDim = k /Math.pow(2,myZoom);
    //console.log("calculateZoom: size of the map at the Lat: "+MapDim + " meters.");
    //console.log("calculateZoom: world perimeter at the Lat: " +k+ " meters.");
return(myZoom);
}
  • Welcome to Stack Overflow! While this code snippet may solve the question, [including an explanation](//meta.stackexchange.com/questions/114762/explaining-entirely-code-based-answers) really helps to improve the quality of your post. Remember that you are answering the question for readers in the future, and those people might not know the reasons for your code suggestion. Please also try not to crowd your code with explanatory comments, this reduces the readability of both the code and the explanations! – Jonathan Lam Aug 09 '16 at 14:18