Google Maps V3 - How to calculate the zoom level for a given bounds

For version 3 of the API, this is simple and working:

var latlngList = [];
latlngList.push(new google.maps.LatLng(lat, lng));

var bounds = new google.maps.LatLngBounds();
latlngList.each(function(n) {
    bounds.extend(n);
});

map.setCenter(bounds.getCenter()); //or use custom center
map.fitBounds(bounds);

and some optional tricks:

//remove one zoom level to ensure no marker is on the edge.
map.setZoom(map.getZoom() - 1); 

// set a minimum zoom 
// if you got only 1 marker or all markers are on the same address map will be zoomed too much.
if(map.getZoom() > 15){
    map.setZoom(15);
}

A similar question has been asked on the Google group: http://groups.google.com/group/google-maps-js-api-v3/browse_thread/thread/e6448fc197c3c892

The zoom levels are discrete, with the scale doubling in each step. So in general you cannot fit the bounds you want exactly (unless you are very lucky with the particular map size).

Another issue is the ratio between side lengths e.g. you cannot fit the bounds exactly to a thin rectangle inside a square map.

There's no easy answer for how to fit exact bounds, because even if you are willing to change the size of the map div, you have to choose which size and corresponding zoom level you change to (roughly speaking, do you make it larger or smaller than it currently is?).

If you really need to calculate the zoom, rather than store it, this should do the trick:

The Mercator projection warps latitude, but any difference in longitude always represents the same fraction of the width of the map (the angle difference in degrees / 360). At zoom zero, the whole world map is 256x256 pixels, and zooming each level doubles both width and height. So after a little algebra we can calculate the zoom as follows, provided we know the map's width in pixels. Note that because longitude wraps around, we have to make sure the angle is positive.

var GLOBE_WIDTH = 256; // a constant in Google's map projection
var west = sw.lng();
var east = ne.lng();
var angle = east - west;
if (angle < 0) {
  angle += 360;
}
var zoom = Math.round(Math.log(pixelWidth * 360 / angle / GLOBE_WIDTH) / Math.LN2);

Thanks to Giles Gardam for his answer, but it addresses only longitude and not latitude. A complete solution should calculate the zoom level needed for latitude and the zoom level needed for longitude, and then take the smaller (further out) of the two.

Here is a function that uses both latitude and longitude:

function getBoundsZoomLevel(bounds, mapDim) {
    var WORLD_DIM = { height: 256, width: 256 };
    var ZOOM_MAX = 21;

    function latRad(lat) {
        var sin = Math.sin(lat * Math.PI / 180);
        var radX2 = Math.log((1 + sin) / (1 - sin)) / 2;
        return Math.max(Math.min(radX2, Math.PI), -Math.PI) / 2;
    }

    function zoom(mapPx, worldPx, fraction) {
        return Math.floor(Math.log(mapPx / worldPx / fraction) / Math.LN2);
    }

    var ne = bounds.getNorthEast();
    var sw = bounds.getSouthWest();

    var latFraction = (latRad(ne.lat()) - latRad(sw.lat())) / Math.PI;

    var lngDiff = ne.lng() - sw.lng();
    var lngFraction = ((lngDiff < 0) ? (lngDiff + 360) : lngDiff) / 360;

    var latZoom = zoom(mapDim.height, WORLD_DIM.height, latFraction);
    var lngZoom = zoom(mapDim.width, WORLD_DIM.width, lngFraction);

    return Math.min(latZoom, lngZoom, ZOOM_MAX);
}

Demo on jsfiddle

Parameters:

The "bounds" parameter value should be a google.maps.LatLngBounds object.

The "mapDim" parameter value should be an object with "height" and "width" properties that represent the height and width of the DOM element that displays the map. You may want to decrease these values if you want to ensure padding. That is, you may not want map markers within the bounds to be too close to the edge of the map.

If you are using the jQuery library, the mapDim value can be obtained as follows:

var $mapDiv = $('#mapElementId');
var mapDim = { height: $mapDiv.height(), width: $mapDiv.width() };

If you are using the Prototype library, the mapDim value can be obtained as follows:

var mapDim = $('mapElementId').getDimensions();

Return Value:

The return value is the maximum zoom level that will still display the entire bounds. This value will be between 0 and the maximum zoom level, inclusive.

The maximum zoom level is 21. (I believe it was only 19 for Google Maps API v2.)


Explanation:

Google Maps uses a Mercator projection. In a Mercator projection the lines of longitude are equally spaced, but the lines of latitude are not. The distance between lines of latitude increase as they go from the equator to the poles. In fact the distance tends towards infinity as it reaches the poles. A Google Maps map, however, does not show latitudes above approximately 85 degrees North or below approximately -85 degrees South. (reference) (I calculate the actual cutoff at +/-85.05112877980658 degrees.)

This makes the calculation of the fractions for the bounds more complicated for latitude than for longitude. I used a formula from Wikipedia to calculate the latitude fraction. I am assuming this matches the projection used by Google Maps. After all, the Google Maps documentation page I link to above contains a link to the same Wikipedia page.

Other Notes:

  1. Zoom levels range from 0 to the maximum zoom level. Zoom level 0 is the map fully zoomed out. Higher levels zoom the map in further. (reference)
  2. At zoom level 0 the entire world can be displayed in an area that is 256 x 256 pixels. (reference)
  3. For each higher zoom level the number of pixels needed to display the same area doubles in both width and height. (reference)
  4. Maps wrap in the longitudinal direction, but not in the latitudinal direction.