mapbox-gl-js
mapbox-gl-js copied to clipboard
alternative maxBounds implementations
Map.maxBounds (red box) is implemented as never show the area outside the maxBounds (blue box, must be strictly within red box) . While useful in some contexts it has limitations on zooming out, which is sometimes not desirable.
An alternative approach to maxBounds is to ensure that at least some part of these bounds is shown in the map, potentially with a padding (green box must have some part in the red box).

Another alternative is to stick with the current approach but add a padding in pixels to the maxBounds.
I'm inclined to keep it simple. Do any other mapping libraries use this alternative definition?
I'm inclined to keep it simple.
I'll see if I can experiment with the options to play with some alternatives to see if they are handy or not, even if it doesn't land that's fine.
Do any other mapping libraries use this alternative definition?
I'm not sure actually.
If I understand this right, then #4268 was implemented, you could achieve this by setting half the width and height as the "global padding" options.
The current solution is definitely too restrictive. I think the padding option is better than this "alternative definition" option, because it allows for alternative options between the two extremes:
- View nothing at all outside the bounds (current solution)
- View a bit/a lot outside the bounds (with global padding)
- View up to a whole screen outside the bounds (this proposal, also achievable with global padding)
When I used maxBounds for the first time, it felt like it exhibited behavior inconsistent with fitBounds.
For example, if I supply a bounding box for the country Italy ([6.749955, 36.619987, 18.480247, 47.115393]) to fitBounds I can see the whole country, but when I use that same bounding max for maxBounds the whole country doesn't fit within my viewport. In this scenario, I can I call getBounds() and see that the bounding box actually being used is [-1.087394611812897, 36.496491345756425, 26.31759661181337, 47.21991711120498] not the original bounds I supplied.
I prefer the behavior for fitBounds and initially expected maxBounds to mimic it; however, it seems more important that the behavior between fitBounds and maxBounds is consistent. If there were a simple option to specify how I expected them to work, that might be more ideal since use cases for something more restrictive (i.e. the way maxBounds works today) and something more lax (i.e. the way fitBounds works today where the entire bbox is visible within the map viewport) exist.
The "global padding" option @stevage mentioned above is not intuitive to me. I'm relatively new to mapbox and digital map building in general and it's not obvious how this is a viable solution. So I use setPadding in combination with maxBounds? While it might work (I haven't tried it yet) the bigger problem for me is that the functionality feels inconsistent -- why do I have to set separate "global padding" options with maxBounds and yet fitBounds accepts these same optional padding parameters but I don't have to supply them because it interprets the bounding box differently?
Perhaps, I'm missing something that's just not intuitive yet. I'm open to better solutions or idiomatic ways to make maxBounds effectively work the same as fitBounds.
Related: #5997, #10209, #11630
I found a workaround that allows me to calculate a maxBounds that is similar to the bounds resulting from fitBounds.
import bbox from '@turf/bbox'
import { bounds as viewportBounds } from '@mapbox/geo-viewport'
const countryBoundingBox = bbox(countryGeoJson)
const { center, zoom } = map.cameraForBounds(countryBoundingBox)
const dimensions = [
Math.floor(map.getCanvas().offsetWidth),
Math.floor(map.getCanvas().offsetHeight),
]
// Mapbox vector tiles are 512x512, as opposed to the library's assumed default of 256x256, hence the final `512` argument
const bounds = viewportBounds(center.toArray(), zoom, dimensions, 512)
I dug into the code for fitBounds and noticed that internally it leverages cameraForBounds which in turn leverages an internal function called _cameraForBoxAndBearing. I don't quite understand all the logic in that function, but it made me realize that something like this might work. I imagine there's probably a more straightforward solution?
I also tried using getBounds immediately after executing fitBounds but the results weren't accurate. If I setTimeout and waited long enough, I would get desired bounds, but I wasn't comfortable with that kind of hack and wanted something reliable and synchronous.
I also tried using
getBoundsimmediately after executingfitBoundsbut the results weren't accurate. If IsetTimeoutand waited long enough, I would get desired bounds, but I wasn't comfortable with that kind of hack and wanted something reliable and synchronous.
@tmcgann you may want to wait for the moveend event, which should ensure fitBounds has finished fitting, no need for setTimeout:
import turfBboxPolygon from '@turf/bbox-polygon';
map.once('moveend', () => {
const mapBounds = map.getBounds();
const bboxSquarePolygon = turfBboxPolygon(
[mapBounds.getWest(), mapBounds.getSouth(), mapBounds.getEast(), mapBounds.getNorth()]
);
map.setMaxBounds(bboxSquarePolygon);
});
maxBounds are very restricting on the zoom level. There is no way to zoom out to see the full bounds on most map display ratio's.
Something like this works much better for me but it's not perfect as dragging stops while your drag operation is out of bounds:
map.on('move', function(){
let center = map.getCenter()
if(!boundsobj.contains(center)){
if(center.lng > boundsobj.getEast()){
center.lng = boundsobj.getEast();
}else if(center.lng < boundsobj.getWest()){
center.lng = boundsobj.getWest();
}
if(center.lat > boundsobj.getNorth()){
center.lat = boundsobj.getNorth();
}else if(center.lat < boundsobj.getSouth()){
center.lat = boundsobj.getSouth();
}
map.stop();
map.setCenter(center);
}
})