35

I'd like to be able add padding to a map view after calling a map.fitBounds(), so all markers can be visible regardless of map controls or things like sliding panels that would cover markers when opened. Leaftlet has an option to add padding to fitBounds, but Google Maps does not.

Sometimes the northmost markers partially hide above the viewport. The westmost markers also often lay under the zoom slider. With API 2 it was possible to form a virtual viewport by reducing given paddings from the map viewport and then call the method showBounds() to calculate and perform zooming and centering based on that virtual viewport:

map.showBounds(bounds, {top:30,right:10,left:50});

A working example of this for API 2 can be found here under the showBounds() example link.

I cannot find similar functionality in API V3, but hopefully there is another way this can be accomplished. Maybe I could grab the northeast and southwest points, then add fake coordinates to extend the bounds further after including them?

UPDATE

(Codepen in case the code below doesn't work)

function initMap() {
  var map = new google.maps.Map(document.getElementById('map'), {
    draggable: true,
    streetViewControl: false,
    zoomControl: false
  });

  var marker1 = new google.maps.Marker({
    position: {lat: 37, lng: -121},
    map: map,
  });

  var marker2 = new google.maps.Marker({
    position: {lat: 39.3, lng: -122},
    map: map,
  });
 
  var bounds = new google.maps.LatLngBounds();
  bounds.extend(marker1.position);
  bounds.extend(marker2.position);
  map.fitBounds(bounds);
}
#map {
  height: 640px;
  width: 360px;
}
#overlays {
  position: absolute;
  height: 50px;
  width: 340px;
  background: white;
  margin: -80px 10px;
  text-align: center;
  line-height: 50px;
}
/* Optional: Makes the sample page fill the window. */
html, body {
  height: 100%;
  margin: 0;
  padding: 0;
}
<html>
  <head>
    <meta name="viewport" content="initial-scale=1.0, user-scalable=no">
    <meta charset="utf-8">
    <title>Simple markers</title>

  </head>
  <body>
    <div id="map"></div>
    <div id="overlays">Controls / Order pizza / ETA / etc.</div>
  
    <script async defer
    src="https://maps.googleapis.com/maps/api/js?&callback=initMap">
    </script>
  </body>
</html>

The problem is this:

enter image description here

I've tried adding a control as documented at Custom controls, but the map isn't exactly aware of it - see this fiddle forked from the Maps custom control example. One of the markers is still obscured by the control.

Dan Dascalescu
  • 143,271
  • 52
  • 317
  • 404
jsurf
  • 575
  • 1
  • 8
  • 20

8 Answers8

26

This is some kind of a hack-ish solution, but after the fitBounds, you could zoom one level out, so you get enough padding for your markers.

Assume map variable is your reference to the map object;

map.setZoom(map.getZoom() - 1);
keune
  • 5,779
  • 4
  • 34
  • 50
  • 4
    Cheap, quick and dirty. i like it! – shababhsiddique Jun 21 '14 at 02:58
  • This is not working here, weird! It is outputting the zoom level, and the zoom level - 1, but not zooming map.fitBounds(bounds); console.log(map.getZoom()); map.setZoom(map.getZoom() - 5); console.log(map.getZoom()); – Miguel Stevens Aug 20 '14 at 09:38
  • Also works great for gmaps4rails in rails. To find the zoomed level I used `var zoom_level = handler.getMap().getZoom()` and set the zoom level with `handler.getMap().setZoom(zoom_level - 1)` after the fitMapToBounds call. – Adam Cooper May 13 '15 at 09:58
  • 3
    Because fitBounds() is asynchronous, to get this to work you may need to add a listener to your code as described in http://stackoverflow.com/a/11976581/286486 – Jeff Steil Feb 29 '16 at 18:56
17

As of June 2017 the Maps JavaScript API is supporting the padding parameter in the fitBounds() method.

fitBounds(bounds:LatLngBounds|LatLngBoundsLiteral, padding?:number)

Please refer to the documentation for further details

https://developers.google.com/maps/documentation/javascript/reference#Map

Soviut
  • 88,194
  • 49
  • 192
  • 260
xomena
  • 31,125
  • 6
  • 88
  • 117
  • 2
    That's good to see, but still an inadequate solution for many use cases as often only some areas need padding and that (poorly documented) feature appears to add padding to all sides. – El Yobo Jul 12 '17 at 22:59
  • It seems to me, that padding is added to all sides of map. – exoslav Sep 01 '17 at 11:22
  • 4
    "refer to the documentation for further details" - That would be nice, but there are no details. There's only a mention of a parameter called `padding` that takes a number. No details at all about its semantics or default value. While the guides for Google Maps API are good, the reference docs are lacking, with this case being no exception. – Edward Brey Feb 20 '18 at 13:51
  • Hilarious. I got here searching for "fitBounds padding" because the documentation contains no further details for padding and I'm referred to the documentation for further details. – Parker Ault Feb 21 '18 at 03:48
  • 1
    Guys, if you think that documentation is not clear please file documentation bug in Google [issue tracker](https://issuetracker.google.com/issues/new?component=188853&template=788207). I understand padding parameter represents padding in pixels from all edges. – xomena Feb 21 '18 at 08:15
  • 4
    https://developers.google.com/maps/documentation/javascript/reference/coordinates#Padding You may add a number or {left:50, top:200} // right, bottom etc.. – yairniz Mar 06 '19 at 13:34
  • for me the zoom out did not stop with a zero padding. Instead i had to use a negative padding which thankfully did not zoom in but didnt zoom out either which worked for me. – Valentin Rapp Mar 14 '19 at 09:38
  • Padding can be a number or a `Padding` object. https://developers.google.com/maps/documentation/javascript/reference/coordinates#Padding which allows different padding for top, right, bottom and left. – gunwin Apr 25 '19 at 11:51
12

You can use map.fitBounds() with API V3 with the same padding syntax as you mentioned with map.showBounds().

Simply using map.fitBounds(bounds, {top:30,right:10,left:50}); worked for me.

(this could be comment to xomena's or Roman86' post, I don't have enough reputation to comment)

Milan Švehla
  • 515
  • 5
  • 15
10

I solved this problem by extended the map bounds to include a latlng that sufficiently pushed the markers into view.

Firstly you need to create an overlay view

var overlayHelper = new google.maps.OverlayView();
overlayHelper.onAdd = function() {};
overlayHelper.onRemove = function() {};
overlayHelper.draw = function() {};
overlayHelper.setMap(map);

Once you have an overlay helper you need to get the map projection and perform calcs based on that.

Note that the control that I have on my map is a 420 pixel wide, 100% height div on the far right of the map. You will obviously need to change the code to accomodate your controls.

var mapCanvas = $("#map_canvas"),
    controlLeft = mapCanvas.width() - 420, // canvas width minus width of the overlayed control
    projection = overlayHelper.getProjection(),
    widestPoint = 0, 
    latlng, 
    point;

// the markers were created elsewhere and already extended the bounds on creation
map.fitBounds(mapBounds);

// check if any markers are hidden behind the overlayed control
for (var m in markers) {
    point = projection.fromLatLngToContainerPixel(markers[m].getPosition());
    if (point.x > controlLeft && point.x > widestPoint) {
        widestPoint = point.x;
    }
}

if (widestPoint > 0) {
    point = new google.maps.Point(
                mapCanvas.width() + (widestPoint - controlLeft), 
                mapCanvas.height() / 2); // middle of map height, since we only want to reposition bounds to the left and not up and down

    latlng = projection.fromContainerPixelToLatLng(point);
    mapBounds.extend(latlng);
    map.fitBounds(mapBounds);
}

If you're doing this when the map loads for the first time, then you will need to wrap this in a map event to wait for idle. This allows the overlay view to initialize. Don't include the overlay helper creation within the event callback.

google.maps.event.addListenerOnce(map, 'idle', function() { <above code> });
Seain Malkin
  • 2,273
  • 19
  • 20
9

Updated

Google Maps API now supports a native "padding" param in the fitBounds method (from version 3.32, correct me if earlier).

I had no chance yet to test it, but if you're able to upgrade - I would recommend to use a native way. If you're using version < 3.32 and can't upgrade - my solution is for you.


I took working solution by erzzo and improved it a little bit.
Example

fitBoundsWithPadding(googleMapInstance, PolygonLatLngBounds, {left:250, bottom:10});

Arguments description:

  1. gMap - google map instance
  2. bounds - google maps LatLngBounds object to fit
  3. paddingXY - Object Literal: 2 possible formats:
    • {x, y} - for horizontal and vertical paddings (x=left=right, y=top=bottom)
    • {left, right, top, bottom}

function listing to copy

function fitBoundsWithPadding(gMap, bounds, paddingXY) {
        var projection = gMap.getProjection();
        if (projection) {
            if (!$.isPlainObject(paddingXY))
                paddingXY = {x: 0, y: 0};

            var paddings = {
                top: 0,
                right: 0,
                bottom: 0,
                left: 0
            };

            if (paddingXY.left){
                paddings.left = paddingXY.left;
            } else if (paddingXY.x) {
                paddings.left = paddingXY.x;
                paddings.right = paddingXY.x;
            }

            if (paddingXY.right){
                paddings.right = paddingXY.right;
            }

            if (paddingXY.top){
                paddings.top = paddingXY.top;
            } else if (paddingXY.y) {
                paddings.top = paddingXY.y;
                paddings.bottom = paddingXY.y;
            }

            if (paddingXY.bottom){
                paddings.bottom = paddingXY.bottom;
            }

            // copying the bounds object, since we will extend it
            bounds = new google.maps.LatLngBounds(bounds.getSouthWest(), bounds.getNorthEast());

            // SW
            var point1 = projection.fromLatLngToPoint(bounds.getSouthWest());


            // we must call fitBounds 2 times - first is necessary to set up a projection with initial (actual) bounds
            // and then calculate new bounds by adding our pixel-sized paddings to the resulting viewport
            gMap.fitBounds(bounds);

            var point2 = new google.maps.Point(
                ( (typeof(paddings.left) == 'number' ? paddings.left : 0) / Math.pow(2, gMap.getZoom()) ) || 0,
                ( (typeof(paddings.bottom) == 'number' ? paddings.bottom : 0) / Math.pow(2, gMap.getZoom()) ) || 0
            );

            var newPoint = projection.fromPointToLatLng(new google.maps.Point(
                point1.x - point2.x,
                point1.y + point2.y
            ));

            bounds.extend(newPoint);

            // NE
            point1 = projection.fromLatLngToPoint(bounds.getNorthEast());
            point2 = new google.maps.Point(
                ( (typeof(paddings.right) == 'number' ? paddings.right : 0) / Math.pow(2, gMap.getZoom()) ) || 0,
                ( (typeof(paddings.top) == 'number' ? paddings.top : 0) / Math.pow(2, gMap.getZoom()) ) || 0
            );
            newPoint = projection.fromPointToLatLng(new google.maps.Point(
                point1.x + point2.x,
                point1.y - point2.y
            ));

            bounds.extend(newPoint);

            gMap.fitBounds(bounds);
        }
    }
Roman86
  • 1,990
  • 23
  • 22
  • Perfect solution. Thank you! – Алексей Матвеев Nov 17 '17 at 09:44
  • Thank you for this solution. How would I adapt this to incorporate a maxZoom parameter? I want to be able to restrict how close fitBoundsWithPadding can zoom in. – juliusbangert Apr 10 '18 at 15:31
  • hi @julius! I'm glad that it helped =) As a fast solution I can propose to check the zoom level after fitBounds. Like this: if (gMap.getZoom() > maxZoom) { gMap.setZoom(maxZoom); } Also, there is a map option "maxZoom" you may set globally for your map – Roman86 Apr 11 '18 at 17:49
  • I actually solved it using **map.setOptions({ maxZoom: maxZoom });** before any fitBounds and then unsetting it afterwards. – juliusbangert Apr 11 '18 at 18:59
  • 1
    @enorl76 thanks for a good question! It even made me doubt a little. Here's the answer: first fitBounds is necessary to set up a projection with initial (actual) bounds and then calculate new bounds by adding our pixel-sized paddings to the resulting viewport. I updated the answer. Thanks again – Roman86 Jul 09 '18 at 12:21
1

I will provide a more generic solution for this issue. If we have a position e.g. marker.getPosition(), we can find a another position (x, y) pixel away from it using this function.

function extendedLocation(position, x, y) {
    var projection = map.getProjection();
    var newMarkerX = projection.fromLatLngToPoint(position).x + x/(Math.pow(2, map.getZoom()))
    var newMarkerY = projection.fromLatLngToPoint(position).y + y/(Math.pow(2, map.getZoom()))
    var newMarkerPoint = new google.maps.Point(newMarkerX, newMarkerY);
    return projection.fromPointToLatLng(newMarkerPoint)
}

Note: positive x is in right direction and positive y is in down direction. So, in general case, to bring marker in view, we need to pass negative value of y e.g. extendedLocation(marker.getPosition(), 4, -18)

If you have a persistent slider or any such element at the top of suppose 30 px height, just use y parameter in the function as -30.

A more generalised function can be created which return array of 4 points, each (x, y) pixels away from the given pointing up, down, right and left direction.

function getAllExtendedPosition(position, x, y){
   var positionArray = [];
   postitionArray.push(extendedLocation(position, x, y);
   postitionArray.push(extendedLocation(position, -x, -y);
   postitionArray.push(extendedLocation(position, -x, y);
   postitionArray.push(extendedLocation(position, x, -y);
   return positionArray
}
Gaurav Mukherjee
  • 6,205
  • 2
  • 23
  • 33
0

The way I've done it seems pretty clean. E.g. Apply a 10% pad using a pixel coordinate basis on each side of the map. This is with Maps API v3.44:

map.fitBounds(bounds)
let mapDiv = map.getDiv()

let padding = {
  bottom: mapDiv.offsetHeight * 0.1,
  left: mapDiv.offsetWidth * 0.1,
  right: mapDiv.offsetWidth * 0.1,
  top: mapDiv.offsetHeight * 0.1,
}
map.fitBounds(bounds, padding);
errolflynn
  • 641
  • 2
  • 11
  • 24
-1

Another way to do this would be to extend your boundaries with an additional LatLong point a calculated distance away. The google.maps.LatLngBounds() object has functions to get the SouthWest and NorthEast points of the bounding box, and that can be used to calculate a distance X miles North, South, East or West.

For example, if you were trying to push your markers to the right to account for overlay elements on the left side of the map, you might try the following:

// create your LatLngBounds object, like you're already probably doing
var bounds = new google.maps.LatLngBounds();

// for each marker call bounds.extend(pos) to extend the base boundaries

// once your loop is complete
// *** add a calculated LatLng point to the west of your boundary ***
bounds.extend(new google.maps.LatLng(bounds.getSouthWest().lat(), bounds.getSouthWest().lng() - .9));

// finally, center your map on the modified boundaries
map.fitBounds(bounds);

In the example above, adding a LatLong point to the bounds by subtracting .9 from the longitude of the western-most point moves the boundary about 52 miles further to the west.

A whole point (pos.lng() - 1.0) is about 58 miles, so you can either guess a good distance or use some other method to calculate that longitudinal offset when figuring out what kind of padding you need.

mcnejef
  • 25
  • 3
  • Adding a point with an absolute distance is bad design, the value will change depending the zoom. To add this point properly you have to calculate the bounds and zoom yourself… – Kustolovic Dec 05 '16 at 14:44
  • accounting for zoom and/or canvas size was sort of implied - hence the last sentence that suggests using "some other method to calculate the longitudinal offset when figuring out what kind of padding you need". – mcnejef Dec 05 '16 at 18:34