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

14

175

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
Moina answered 18/5, 2011 at 17:57 Comment(3)
Sorry, linked wrong question, this is the correct link.Gravimetric
This question is not a duplicate of the other question. 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).Rheims
@Nick Clark: How do you know the sw, ne bounds to be set? How did you capture them before?Like
K
119

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);
Kimberelykimberlee answered 19/5, 2011 at 8:9 Comment(17)
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.Moina
In that case, can't you use the map's getters and setters for center and zoom, rather than using bounds?Kimberelykimberlee
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.Moina
I think storing zoom would be a lot easier and quicker, but I've added code to calculate the zoom.Kimberelykimberlee
I've just had to search for how to do this and this answer works perfectly - many thanksEmpathy
can you explain exactly what 256 in this represents? is this a constant or is this a pixel width at zoom zero?Heterosporous
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: code.google.com/apis/maps/documentation/javascript/…Kimberelykimberlee
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.....Griffith
Works great for me with a change of Math.round to Math.floor. Thanks a million.Gory
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!Caseose
@Gory 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.Kimberelykimberlee
@Griffith 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).Kimberelykimberlee
Works for me in Java code. Just hard code the JavaScript Math.LN2 with double LN2 = 0.6931471805599453;Machinegun
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 v2Mcloughlin
what is the value for pixelWidthSciurine
@albertJegani, I assume it is map's width in pixels following the explanation above the codePass
Although, still the approach didn't work for me unfortunately... However the @john-s's didPass
R
334

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.
Rheims answered 7/11, 2012 at 16:51 Comment(31)
@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?Podiatry
@John S - Is it safe to assume this will start with exactly zero padding then for the contained LatLngBounds bounding box?Podiatry
@Podiatry - 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.Rheims
@Podiatry - 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.Rheims
@Podiatry - 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.Rheims
@Podiatry - 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.Rheims
@Podiatry - 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. 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.Rheims
@John S - all good points. On another SO post, 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.Podiatry
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)Dispraise
If anybody needs this in Java, I've translated it here: #10621015Flores
it does NOT work for bounds: {southwest=lat/lng: (32.05604537053063,-12.897488175961438), northeast=lat/lng: (65.09861008601234,35.39855629119581)}Bushel
@MarianPaździoch - It does work for that bounds, see here. 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.Rheims
@MarianPaździoch - Could you explain your comment? It may be helpful so I could improve my answer. Thanks.Rheims
@JohnS your comment to my comment explains my comment :) I expected it to show exactly my position as I was struggling with this issue: #28107479Bushel
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.Meteor
@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.Rheims
@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...Gazette
@Gazette - 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?Rheims
@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).Gazette
@JohnS I created a new question for my problem: https://mcmap.net/q/144439/-mapbox-gl-calculate-map-bounds-from-center-point-zoom-level-and-dimensions/4108919 thanks again for your help with this answer!Gazette
@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?Hydroplane
@ImantsVolkovs - Could you share a link to a JSFiddle that shows the issue, or include the lat/lng pairs in a comment? Thanks.Rheims
@JohnS here is plunker - 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 structuredHydroplane
@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. It appears to work fine.Rheims
@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 [email protected] if I do not make thread :)Hydroplane
It is useful way to determine Center and zoom.Deadeye
@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!!!Kier
Nice research thanks this helped a lot also very good explanation for what happens inside the calculation.Sparteine
Can somebody explain to what WORLD_DIM is?Hedrick
@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.Rheims
I wanted to say thank you for this answer, works like a charm.Karyoplasm
K
119

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);
Kimberelykimberlee answered 19/5, 2011 at 8:9 Comment(17)
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.Moina
In that case, can't you use the map's getters and setters for center and zoom, rather than using bounds?Kimberelykimberlee
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.Moina
I think storing zoom would be a lot easier and quicker, but I've added code to calculate the zoom.Kimberelykimberlee
I've just had to search for how to do this and this answer works perfectly - many thanksEmpathy
can you explain exactly what 256 in this represents? is this a constant or is this a pixel width at zoom zero?Heterosporous
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: code.google.com/apis/maps/documentation/javascript/…Kimberelykimberlee
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.....Griffith
Works great for me with a change of Math.round to Math.floor. Thanks a million.Gory
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!Caseose
@Gory 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.Kimberelykimberlee
@Griffith 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).Kimberelykimberlee
Works for me in Java code. Just hard code the JavaScript Math.LN2 with double LN2 = 0.6931471805599453;Machinegun
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 v2Mcloughlin
what is the value for pixelWidthSciurine
@albertJegani, I assume it is map's width in pixels following the explanation above the codePass
Although, still the approach didn't work for me unfortunately... However the @john-s's didPass
A
51

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);
}
Airplane answered 3/1, 2014 at 13:58 Comment(6)
why not set a minimum zoom level while initialising map, something like: var mapOptions = { maxZoom: 15, };Ron
@Kush, good point. but maxZoom will prevent the user from manual zooming. My example only changes the DefaultZoom and only if it is necessary.Airplane
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 boundsJere
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.Airplane
For some reason Google map does not zoom when calling setZoom() immediately after the map.fitBounds() call. (gmaps is v3.25 currently)Isomagnetic
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.Isomagnetic
C
9

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);
  }
Clothesbasket answered 20/10, 2020 at 16:24 Comment(0)
H
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)
    }
Him answered 18/2, 2020 at 20:25 Comment(1)
Please add the meaning of mapDim and WORLD_DIM, and how to get them?Unwished
T
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)

}
Terrorize answered 28/11, 2020 at 5:11 Comment(1)
I don't know what kind of sorcery this is, but it works like magic!Susette
S
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.

Salade answered 17/5, 2012 at 10:44 Comment(1)
I liked var zoomfactor = Math.floor(Math.log(960 * 360 / angle / GLOBE_WIDTH) / Math.LN2)-1;. Still, very helpful.Mikamikado
D
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).

Decapod answered 13/3, 2013 at 22:31 Comment(0)
M
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.

Mullock answered 31/8, 2012 at 16:25 Comment(0)
W
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
Whoredom answered 14/10, 2012 at 11:18 Comment(0)
K
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>
Kamalakamaria answered 9/4, 2018 at 6:17 Comment(0)
D
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);
Dais answered 23/11, 2019 at 18:16 Comment(0)
D
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());
  }
Deviate answered 4/7, 2022 at 19:54 Comment(0)
C
-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)
    }
Caviness answered 31/8, 2020 at 2:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.