174

I'm looking for a way to calculate the zoom level for a given bounds using the Google Maps V3 API, similar to getBoundsZoomLevel() in the V2 API.

Here is what I want to do:

// These are exact bounds previously captured from the map object
var sw = new google.maps.LatLng(42.763479, -84.338918);
var ne = new google.maps.LatLng(42.679488, -84.524313);
var bounds = new google.maps.LatLngBounds(sw, ne);
var zoom = // do some magic to calculate the zoom level

// Set the map to these exact bounds
map.setCenter(bounds.getCenter());
map.setZoom(zoom);

// NOTE: fitBounds() will not work

Unfortunately, I can't use the fitBounds() method for my particular use case. It works well for fitting markers on the map, but it does not work well for setting exact bounds. Here is an example of why I can't use the fitBounds() method.

map.fitBounds(map.getBounds()); // not what you expect
Liam
  • 27,717
  • 28
  • 128
  • 190
Nick Clark
  • 4,439
  • 4
  • 23
  • 25
  • Sorry, linked wrong question, [this is the correct link](http://stackoverflow.com/questions/9843732/how-to-affect-the-grace-margin-of-map-fitbounds). – Tomas Mar 24 '12 at 08:09
  • 1
    **This question is not a duplicate of [the other question](http://stackoverflow.com/questions/1556921/google-map-api-v3-set-bounds-and-center).** The answer to the other question is to use `fitBounds()`. This question asks what to do when `fitBounds()` is insufficient -- either because it over zooms or you don't want to zoom (i.e., you just want the zoom level). – John S May 09 '14 at 15:31
  • @Nick Clark: How do you know the sw, ne bounds to be set? How did you capture them before? – varaprakash May 30 '14 at 22:24

14 Answers14

330

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.
John S
  • 21,212
  • 8
  • 46
  • 56
  • 1
    @John S - This is a fantastic solution and I'm contemplating using this over the native google maps fitBounds method available to me as well. I noticed fitBounds is sometimes one zoom level back (zoomed out), but I assume that's from the padding which it's adding. Is the only difference then between this and fitBounds method, just the amount of padding you want to add which accounts for the change in zoom level between the two? – johntrepreneur Jun 25 '13 at 19:40
  • @John S - Is it safe to assume this will start with exactly zero padding then for the contained LatLngBounds bounding box? – johntrepreneur Jun 25 '13 at 19:44
  • @johntrepreneur - I can only imagine that `fitBounds` is allowing for some kind of padding, but I would say yes, this method should be using the same calculations otherwise. – John S Jun 25 '13 at 21:35
  • @johntrepreneur - Besides the over-zooming issue with 'fitBounds`, there are two more advantages of using this function. I will list them in the next two comments. – John S Jun 25 '13 at 21:39
  • 1
    @johntrepreneur - Advantage #1: You can use this method before you even create the map, so you can provide its result to the initial map settings. With `fitBounds`, you need to create the map and then wait for the "bounds_changed" event. – John S Jun 25 '13 at 21:40
  • @johntrepreneur - Advantage #2: This method does not pan or zoom the map like `fitBounds` does. Instead, after calling this function, you can call the `setCenter` and `setZoom` methods. This makes it easy to establish a maximum zoom when centering and zooming on a bounds. Imagine the case where the bounds contains a single point (marker). You may not want to zoom into the maximum zoom level. If you call `fitBounds` you have to wait for the "bounds_changed" event, and then zoom back out to your maximum level. – John S Jun 25 '13 at 21:42
  • @johntrepreneur - There is virtually no padding (unless you adjust the `mapDim` parameter), but I did try the following experiment. Using the jsfiddle in my answer, I called `map.getBounds()`, which should get a bounds that matches the map viewport. I used the north-east and south-west values from that bounds as points for a new [this jsfiddle](http://jsfiddle.net/john_s/BHHs8/22/). You will notice that it zooms the map out one more level. I think that might be because the north-east value from calling `getBounds` is actually just outside the viewport. – John S Jun 25 '13 at 22:02
  • @John S - all good points. On [another SO post](http://stackoverflow.com/questions/9843732/how-to-affect-the-grace-margin-of-map-fitbounds), one user stated that the fitBounds padding/margin is 45px around each edge. I ended up going with your solution so I could adjust that default padding/margin. Works great. – johntrepreneur Jun 26 '13 at 00:29
  • The 45px padding seems to be correct. After subtracting 90 from the mapDim height and width, the returned value equals the eventual zoom level after calling fitBounds. (For Google: custom fitBounds function method) – ricka Jan 06 '14 at 06:46
  • If anybody needs this in Java, I've translated it here: https://stackoverflow.com/questions/10620515/how-do-i-determine-the-zoom-level-of-a-latlngbounds-before-using-map-fitbounds/ – Elroid May 04 '14 at 23:42
  • it does NOT work for bounds: {southwest=lat/lng: (32.05604537053063,-12.897488175961438), northeast=lat/lng: (65.09861008601234,35.39855629119581)} – Marian Paździoch Jan 27 '15 at 08:01
  • 1
    @MarianPaździoch - It does work for that bounds, [see here](http://jsfiddle.net/BHHs8/306/). Are you expecting to be able to zoom so those points are at the exact corners of the map? That is not possible because zoom levels are integer values. The function returns the highest zoom level that will still include the entire bounds on the map. – John S Jan 28 '15 at 00:16
  • @MarianPaździoch - Could you explain your comment? It may be helpful so I could improve my answer. Thanks. – John S Feb 04 '15 at 16:53
  • @JohnS your comment to my comment explains my comment :) I expected it to show exactly my position as I was struggling with this issue: http://stackoverflow.com/questions/28106979/google-map-lite-mode-movecamera-to-lat-lng-bounds-adds-unwanted-map-padding – Marian Paździoch Feb 05 '15 at 08:48
  • This is a great answer, but you don't have to implement all that Mercator projection math yourself, you can just use `var proj = map.getProjection()` and then use e.g. `proj.fromLatLngToPoint(bounds.getNorthEast())` to get the NE and SW points in Google's flat world coordinates (0-256). Then calculating the fractions is just eg `(ne.x - sw.x) / 256`, and the rest is the same. – Carl Meyer Apr 20 '16 at 19:51
  • 1
    @CarlMeyer - I don't mention it in my answer, but in a comment above I state that one advantage of this function is "You can use this method before you even create the map." Using `map.getProjection()` would eliminate some of the math (and the assumption about the projection), but it would mean the function could not be called until after the map has been created and the "projection_changed" event has fired. – John S Apr 21 '16 at 18:01
  • @JohnS I realize this topic has been around for a long time and it just helped me a lot! I am now confronted with an extension to this problem and was wondering if you had any pointers in how I might solve that. I'm trying to calculate the actual bounds of the map from the zoom level that was calculated with your method (plus the center & the map size). I was trying to invert your method to calculate the actual lat/long fractions of the map extent but I haven't been able to get it working. I might open a new question but I wanted to reach out to you through this excellent answer first... – forrert Aug 16 '16 at 00:04
  • @forrert - After you set the center and zoom level for the map, you can call `map.getBounds()`. Or do you need to know the bounds for a given center and zoom level without actually setting those values on the map? – John S Aug 16 '16 at 04:26
  • @JohnS Thanks for your response! I'm actually not working with google maps but with `mapbox-gl` (in node.js). It only allows me to set the map extent by setting a center and zoom level, but my application requires me to set the extent by NE/SW coordinates (that's where I used your code above). I'm now trying to find out the actual bounds of the map from these inputs, because there's no API method that returns the bounds (afaik). – forrert Aug 16 '16 at 06:03
  • @JohnS I created a new question for my problem: http://stackoverflow.com/q/38980638/4108919 thanks again for your help with this answer! – forrert Aug 16 '16 at 17:06
  • @JohnS I used your code sample, but for one particular case, it has a bug. If markers are aligned in a line diagonally, then it calculates zoom where last marker on right is not visible. If I switch longitude and latitude fractions at zoom calculation, then everything is alright. Why is that so? – Imants Volkovs Jan 24 '17 at 15:37
  • @ImantsVolkovs - Could you share a link to a JSFiddle that shows the issue, or include the lat/lng pairs in a comment? Thanks. – John S Jan 24 '17 at 20:02
  • @JohnS here is plunker - http://plnkr.co/edit/C4AT5U?p=preview But you can repeat that by placing 2 markers diagonally, then change zoom, add marker somewhere at end or marker line, where are multiple markers, then calls for zoom calculation. If there are markers at sides (in plunk there is commented coordinates), I placed one marker in code to test. With this marker you method works. Well I kinda made overkill with making static class for these utilities, but i like code to be structured – Imants Volkovs Jan 25 '17 at 09:48
  • @ImantsVolkovs - Perhaps I'm just not familiar with Plunker, but when I run it, it just shows "Loading...". Here is a [jsfiddle with the three diagonal markers from your Plunker](http://jsfiddle.net/BHHs8/663/). It appears to work fine. – John S Jan 26 '17 at 20:00
  • @JohnS That is because, for some reason i did not save latest version of code I made for that :( Next week I will take a look on it, but I don't think it is good idea to spam at comment section of this thread, I will try to repeat that bug, if it works I can try to make my own answer here or make new question thread. My E-mail imants.volkovs@gmail.com if I do not make thread :) – Imants Volkovs Jan 28 '17 at 08:21
  • It is useful way to determine Center and zoom. – Pratik Soni Jun 19 '17 at 13:37
  • @JohnS YOU sir are a magnificent human being, I needed a way to zoom EXACTLY to a set of bounds, and I found that if I yank Math.floor from your `zoom` function I can get a non-integer zoom level (so I can zoom exactly to a set of bounds, with no padding) which was what I've been searching for, THANK YOU!!! – Jared Sep 28 '18 at 00:00
  • Nice research thanks this helped a lot also very good explanation for what happens inside the calculation. – Nicu Dec 08 '20 at 06:26
  • Can somebody explain to what `WORLD_DIM` is? – hugo der hungrige Jan 19 '21 at 16:07
  • 1
    @hugoderhungrige - See note #2 in the answer. When the a Google map is zoomed all the way out (to level 0), the whole world is displayed in an image with dimensions of 256 x 256 pixels. – John S Apr 27 '21 at 22:46
  • I wanted to say thank you for this answer, works like a charm. – hjrshng Jan 14 '22 at 20:48
117

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);
Giles Gardam
  • 1,372
  • 1
  • 9
  • 9
  • I'm not using arbitrary bounds, I'm using the exact bounds taken previously from the map object. So, in theory, I should be able to pan and zoom the map to fit these bounds exactly. Or, in other words, move the map back to where it was before. – Nick Clark May 19 '11 at 12:15
  • In that case, can't you use the map's getters and setters for center and zoom, rather than using bounds? – Giles Gardam May 19 '11 at 12:43
  • For my use case, I would prefer not to store the zoom level. I would like to be able to calculate the zoom level from the bounds. – Nick Clark May 19 '11 at 14:20
  • I think storing zoom would be a lot easier and quicker, but I've added code to calculate the zoom. – Giles Gardam May 20 '11 at 00:36
  • I've just had to search for how to do this and this answer works perfectly - many thanks – James Long Aug 01 '11 at 15:51
  • can you explain exactly what 256 in this represents? is this a constant or is this a pixel width at zoom zero? – tester Oct 19 '11 at 17:08
  • I've edited the answer to make this clearer. It is a constant in the projection (width at zoom zero), that is documented by Google and thus (almost) guaranteed never to change: http://code.google.com/apis/maps/documentation/javascript/maptypes.html#WorldCoordinates – Giles Gardam Oct 20 '11 at 22:24
  • 1
    Wouldn't you have to repeat this for the lat and then choose the min of the result of the 2? I don't think this would work for a tall narrow bounds..... – whiteatom Mar 25 '12 at 03:15
  • 3
    Works great for me with a change of Math.round to Math.floor. Thanks a million. – Pete Apr 27 '12 at 13:54
  • 3
    How can this be right if it doesn't take latitude into account? Near the equator it should be fine but the scale of the map at a given zoom level changes depending on the latitude! – Eyal May 17 '12 at 08:19
  • 1
    @Pete good point, in general you would probably want to round down the zoom level so that you fit a bit more than desired in the map, rather than a bit less. I used Math.round because in the OP's situation the value before rounding should be approximately integral. – Giles Gardam Jul 15 '12 at 08:16
  • @whiteatom you certainly would need to use latitude in general, but for the OP's question you can just longitude, because you already know that your map fitted those bounds previously (the mathematics involved in lat is more involved, but still fairly straightforward). – Giles Gardam Jul 15 '12 at 08:20
  • Works for me in Java code. Just hard code the JavaScript Math.LN2 with double LN2 = 0.6931471805599453; – Anderson Mao Jan 13 '14 at 02:52
  • I voted this down by accident, totally my mistake sorry. This does work thanks. It does worry me a bit that the api doesn't work this out any more in v3 like it did v2 – Adam Spence Jul 04 '14 at 11:22
  • 24
    what is the value for pixelWidth – albert Jegani Feb 23 '15 at 14:27
  • @albertJegani, I assume it is map's width in pixels following the explanation above the code – Igor Bukin Jul 05 '16 at 09:12
  • Although, still the approach didn't work for me unfortunately... However the @john-s's did – Igor Bukin Jul 05 '16 at 09:14
47

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);
}
Neuron
  • 5,141
  • 5
  • 38
  • 59
d.raev
  • 9,216
  • 8
  • 58
  • 79
  • 1
    why not set a minimum zoom level while initialising map, something like: var mapOptions = { maxZoom: 15, }; – Kush Aug 12 '14 at 07:22
  • 3
    @Kush, good point. but `maxZoom` will prevent the user from **manual** zooming. My example only changes the DefaultZoom and only if it is necessary. – d.raev Aug 12 '14 at 09:03
  • 1
    when you do fitBounds, it just jumps to fit the bounds instead of animating there from the current view. the awesome solution is by using already mentioned `getBoundsZoomLevel`. that way, when you call setZoom it animates to the desired zoom level. from there it is not a problem to do the panTo and you end up with a beautiful map animation that fits the bounds – user151496 Feb 26 '15 at 17:24
  • 1
    *animation* is no where discussed in the question nor in my answer. If you have useful example on the topic, just create a constructive answer, with example and how and when it can be used. – d.raev Feb 26 '15 at 17:46
  • For some reason Google map does not zoom when calling setZoom() immediately after the map.fitBounds() call. (gmaps is v3.25 currently) – kashiraja Oct 07 '16 at 23:11
  • I also get different zoom levels occasionally for the same boundary: sometimes 11 and at other times 14 when I do a getZoom() call afterwards. – kashiraja Oct 07 '16 at 23:36
8

Dart Version:

  double latRad(double lat) {
    final double sin = math.sin(lat * math.pi / 180);
    final double radX2 = math.log((1 + sin) / (1 - sin)) / 2;
    return math.max(math.min(radX2, math.pi), -math.pi) / 2;
  }

  double getMapBoundZoom(LatLngBounds bounds, double mapWidth, double mapHeight) {
    final LatLng northEast = bounds.northEast;
    final LatLng southWest = bounds.southWest;

    final double latFraction = (latRad(northEast.latitude) - latRad(southWest.latitude)) / math.pi;

    final double lngDiff = northEast.longitude - southWest.longitude;
    final double lngFraction = ((lngDiff < 0) ? (lngDiff + 360) : lngDiff) / 360;

    final double latZoom = (math.log(mapHeight / 256 / latFraction) / math.ln2).floorToDouble();
    final double lngZoom = (math.log(mapWidth / 256 / lngFraction) / math.ln2).floorToDouble();

    return math.min(latZoom, lngZoom);
  }
ikhsanudinhakim
  • 1,554
  • 16
  • 23
6

Here a Kotlin version of the function:

fun getBoundsZoomLevel(bounds: LatLngBounds, mapDim: Size): Double {
        val WORLD_DIM = Size(256, 256)
        val ZOOM_MAX = 21.toDouble();

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

        fun zoom(mapPx: Int, worldPx: Int, fraction: Double): Double {
            return floor(Math.log(mapPx / worldPx / fraction) / Math.log(2.0))
        }

        val ne = bounds.northeast;
        val sw = bounds.southwest;

        val latFraction = (latRad(ne.latitude) - latRad(sw.latitude)) / Math.PI;

        val lngDiff = ne.longitude - sw.longitude;
        val lngFraction = if (lngDiff < 0) { (lngDiff + 360) / 360 } else { (lngDiff / 360) }

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

        return minOf(latZoom, lngZoom, ZOOM_MAX)
    }
Community
  • 1
  • 1
photograve
  • 61
  • 1
  • 2
5

None of the highly upvoted answers worked for me. They threw various undefined errors and ended up calculating inf/nan for angles. I suspect perhaps the behavior of LatLngBounds has changed over time. In any case, I found this code to work for my needs, perhaps it can help someone:

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 getZoom(lat_a, lng_a, lat_b, lng_b) {

      let latDif = Math.abs(latRad(lat_a) - latRad(lat_b))
      let lngDif = Math.abs(lng_a - lng_b)

      let latFrac = latDif / Math.PI 
      let lngFrac = lngDif / 360 

      let lngZoom = Math.log(1/latFrac) / Math.log(2)
      let latZoom = Math.log(1/lngFrac) / Math.log(2)

      return Math.min(lngZoom, latZoom)

}
xcodesucks123
  • 424
  • 4
  • 8
2

Thanks, that helped me a lot in finding the most suitable zoom factor to correctly display a polyline. I find the maximum and minimum coordinates among the points I have to track and, in case the path is very "vertical", I just added few lines of code:

var GLOBE_WIDTH = 256; // a constant in Google's map projection
var west = <?php echo $minLng; ?>;
var east = <?php echo $maxLng; ?>;
*var north = <?php echo $maxLat; ?>;*
*var south = <?php echo $minLat; ?>;*
var angle = east - west;
if (angle < 0) {
    angle += 360;
}
*var angle2 = north - south;*
*if (angle2 > angle) angle = angle2;*
var zoomfactor = Math.round(Math.log(960 * 360 / angle / GLOBE_WIDTH) / Math.LN2);

Actually, the ideal zoom factor is zoomfactor-1.

  • I liked ````var zoomfactor = Math.floor(Math.log(960 * 360 / angle / GLOBE_WIDTH) / Math.LN2)-1;````. Still, very helpful. – Mojowen Jun 29 '12 at 20:30
1

Since all of the other answers seem to have issues for me with one or another set of circumstances (map width/height, bounds width/height, etc.) I figured I'd put my answer here...

There was a very useful javascript file here: http://www.polyarc.us/adjust.js

I used that as a base for this:

var com = com || {};
com.local = com.local || {};
com.local.gmaps3 = com.local.gmaps3 || {};

com.local.gmaps3.CoordinateUtils = new function() {

   var OFFSET = 268435456;
   var RADIUS = OFFSET / Math.PI;

   /**
    * Gets the minimum zoom level that entirely contains the Lat/Lon bounding rectangle given.
    *
    * @param {google.maps.LatLngBounds} boundary the Lat/Lon bounding rectangle to be contained
    * @param {number} mapWidth the width of the map in pixels
    * @param {number} mapHeight the height of the map in pixels
    * @return {number} the minimum zoom level that entirely contains the given Lat/Lon rectangle boundary
    */
   this.getMinimumZoomLevelContainingBounds = function ( boundary, mapWidth, mapHeight ) {
      var zoomIndependentSouthWestPoint = latLonToZoomLevelIndependentPoint( boundary.getSouthWest() );
      var zoomIndependentNorthEastPoint = latLonToZoomLevelIndependentPoint( boundary.getNorthEast() );
      var zoomIndependentNorthWestPoint = { x: zoomIndependentSouthWestPoint.x, y: zoomIndependentNorthEastPoint.y };
      var zoomIndependentSouthEastPoint = { x: zoomIndependentNorthEastPoint.x, y: zoomIndependentSouthWestPoint.y };
      var zoomLevelDependentSouthEast, zoomLevelDependentNorthWest, zoomLevelWidth, zoomLevelHeight;
      for( var zoom = 21; zoom >= 0; --zoom ) {
         zoomLevelDependentSouthEast = zoomLevelIndependentPointToMapCanvasPoint( zoomIndependentSouthEastPoint, zoom );
         zoomLevelDependentNorthWest = zoomLevelIndependentPointToMapCanvasPoint( zoomIndependentNorthWestPoint, zoom );
         zoomLevelWidth = zoomLevelDependentSouthEast.x - zoomLevelDependentNorthWest.x;
         zoomLevelHeight = zoomLevelDependentSouthEast.y - zoomLevelDependentNorthWest.y;
         if( zoomLevelWidth <= mapWidth && zoomLevelHeight <= mapHeight )
            return zoom;
      }
      return 0;
   };

   function latLonToZoomLevelIndependentPoint ( latLon ) {
      return { x: lonToX( latLon.lng() ), y: latToY( latLon.lat() ) };
   }

   function zoomLevelIndependentPointToMapCanvasPoint ( point, zoomLevel ) {
      return {
         x: zoomLevelIndependentCoordinateToMapCanvasCoordinate( point.x, zoomLevel ),
         y: zoomLevelIndependentCoordinateToMapCanvasCoordinate( point.y, zoomLevel )
      };
   }

   function zoomLevelIndependentCoordinateToMapCanvasCoordinate ( coordinate, zoomLevel ) {
      return coordinate >> ( 21 - zoomLevel );
   }

   function latToY ( lat ) {
      return OFFSET - RADIUS * Math.log( ( 1 + Math.sin( lat * Math.PI / 180 ) ) / ( 1 - Math.sin( lat * Math.PI / 180 ) ) ) / 2;
   }

   function lonToX ( lon ) {
      return OFFSET + RADIUS * lon * Math.PI / 180;
   }

};

You can certainly clean this up or minify it if needed, but I kept the variable names long in an attempt to make it easier to understand.

If you are wondering where OFFSET came from, apparently 268435456 is half of earth's circumference in pixels at zoom level 21 (according to http://www.appelsiini.net/2008/11/introduction-to-marker-clustering-with-google-maps).

Shadow Man
  • 3,234
  • 1
  • 24
  • 35
0

Valerio is almost right with his solution, but there is some logical mistake.

you must firstly check wether angle2 is bigger than angle, before adding 360 at a negative.

otherwise you always have a bigger value than angle

So the correct solution is:

var west = calculateMin(data.longitudes);
var east = calculateMax(data.longitudes);
var angle = east - west;
var north = calculateMax(data.latitudes);
var south = calculateMin(data.latitudes);
var angle2 = north - south;
var zoomfactor;
var delta = 0;
var horizontal = false;

if(angle2 > angle) {
    angle = angle2;
    delta = 3;
}

if (angle < 0) {
    angle += 360;
}

zoomfactor = Math.floor(Math.log(960 * 360 / angle / GLOBE_WIDTH) / Math.LN2) - 2 - delta;

Delta is there, because i have a bigger width than height.

Lukas Olsen
  • 5,294
  • 7
  • 22
  • 28
0

map.getBounds() is not momentary operation, so I use in similar case event handler. Here is my example in Coffeescript

@map.fitBounds(@bounds)
google.maps.event.addListenerOnce @map, 'bounds_changed', =>
  @map.setZoom(12) if @map.getZoom() > 12
mikdiet
  • 9,859
  • 8
  • 59
  • 68
0

Work example to find average default center with react-google-maps on ES6:

const bounds = new google.maps.LatLngBounds();
paths.map((latLng) => bounds.extend(new google.maps.LatLng(latLng)));
const defaultCenter = bounds.getCenter();
<GoogleMap
 defaultZoom={paths.length ? 12 : 4}
 defaultCenter={defaultCenter}
>
 <Marker position={{ lat, lng }} />
</GoogleMap>
Gapur Kassym
  • 1,131
  • 12
  • 10
0

The calculation of the zoom level for the longitudes of Giles Gardam works fine for me. If you want to calculate the zoom factor for latitude, this is an easy solution that works fine:

double minLat = ...;
double maxLat = ...;
double midAngle = (maxLat+minLat)/2;
//alpha is the non-negative angle distance of alpha and beta to midangle
double alpha  = maxLat-midAngle;
//Projection screen is orthogonal to vector with angle midAngle
//portion of horizontal scale:
double yPortion = Math.sin(alpha*Math.pi/180) / 2;
double latZoom = Math.log(mapSize.height / GLOBE_WIDTH / yPortion) / Math.ln2;

//return min (max zoom) of both zoom levels
double zoom = Math.min(lngZoom, latZoom);
Andre
  • 33
  • 6
0

Calculate zoom level to display a map including the two cross corners of the area and display the map on a the part of the screen with a specific height.

Two coordinates max lat/long min lat/long

Display area in pixels height

      double getZoomLevelNew(context, 
             double maxLat, double maxLong, 
             double minLat, double minLong, 
             double height){
  try {
    double _zoom;
    MediaQueryData queryData2;
    queryData2 = MediaQuery.of(context);
    double _zLat =
        Math.log(
            (globals.factor(height) / queryData2.devicePixelRatio / 256.0) *
                180 / (maxLat - minLat).abs()) / Math.log(2);
    double _zLong =
        Math.log((globals.factor(MediaQuery
            .of(context)
            .size
            .width) / queryData2.devicePixelRatio / 256.0) * 360 /
            (maxLong - minLong).abs()) / Math.log(2);
    _zoom = Math.min(_zLat, _zLong)*globals.zoomFactorNew;
    if (_zoom < 0) {
      _zoom = 0;
    }
    return _zoom;
  } catch(e){
    print("getZoomLevelNew - excep - " + e.toString());
  }
Kobi
  • 127
  • 1
  • 11
-1

For swift version

func getBoundsZoomLevel(bounds: GMSCoordinateBounds, mapDim: CGSize) -> Double {
        var bounds = bounds
        let WORLD_DIM = CGSize(width: 256, height: 256)
        let ZOOM_MAX: Double = 21.0
        func latRad(_ lat: Double) -> Double {
            let sin2 = sin(lat * .pi / 180)
            let radX2 = log10((1 + sin2) / (1 - sin2)) / 2
            return max(min(radX2, .pi), -.pi) / 2
        }
        func zoom(_ mapPx: CGFloat,_ worldPx: CGFloat,_ fraction: Double) -> Double {
            return floor(log10(Double(mapPx) / Double(worldPx) / fraction / log10(2.0)))
        }
        let ne = bounds.northEast
        let sw = bounds.southWest
        let latFraction = (latRad(ne.latitude) - latRad(sw.latitude)) / .pi
        let lngDiff = ne.longitude - sw.longitude
        let lngFraction = lngDiff < 0 ? (lngDiff + 360) : (lngDiff / 360)
        let latZoom = zoom(mapDim.height, WORLD_DIM.height, latFraction);
        let lngZoom = zoom(mapDim.width, WORLD_DIM.width, lngFraction);
        return min(latZoom, lngZoom, ZOOM_MAX)
    }