Blog Infos
Author
Published
Topics
, ,
Author
Published

Hello everyone, in this article I will cover how to limit the entire map window to a bounding box using Mapbox or Maplibre SDK on Android.

I’ve been working on building offline maps for my app. The way it would work is that the users would download a country or a region. Initially I had thought I would show the entire planet with very minimal details and the offline area with actual details. I thought this would provide context on the map as to where the offline map is and also help user see the planet as a whole. It would look something like this:

Offline map for Bhutan

However, I realised this would be a bad idea because

  1. Users are used to dynamic maps where tiles are downloaded on the go, so if they navigated to other areas in the map, they would be confused as to why other area tiles are not downloading
  2. This would increase the size of the offline map file by about 10MB
  3. It would be difficult to identify which area is the actual offline map
  4. Having the rest of the planet with very low detail made no sense and frankly looked ugly.

So I decided to limit the map to just the area with the offline map. This is where the challenge begins.

What I wanted to do was to limit the map view only to a bounding box. A bounding box is a pair of coordinates (north-west and south-east). These coordinates make a box which contains a certain area. By limit the map view, I meant that I did not want the whole map to go out of this bounding box. Here are a couple of tools to explore bounding boxes — http://bboxfinder.com/https://boundingbox.klokantech.com/.

Bounding box around Paris

Mapbox API does provide a method to limit the map view to a bbox.

mapboxMap.setLatLngBoundsForCameraTarget(bounds)

The full code can be found here. The demo can be tried from the Mapbox Demo app on the play store and then by going to Camera (in the side bar) then “Restrict Map panning” example.

 

 

However, as you’ll see in the demo, this method has a big flaw. The way this works is that only the center of the map is not allowed to leave the bounding box. You can see that the green dot is not allowed to leave the red bounding box.

What I wanted is that the whole map never moves out of the red bounding box area itself since the area outside of that bounding box wont have any map data.

On closer thought, I realised I needed to create a new bounding box which would dynamically change size. This bbox would only be to restrict the map movement and it would be smaller than the actual bounding box. The size of this new smaller bbox will need to depend upon the scale of the map and hence the zoom. I realised that the trick was.

new_bbox_width = actual_bbox_width - map_width (at that zoom level)
new_bbox_height = actual_bbox_height - map_height (at that zoom level)

Let me explain how I came to this. Here I have generated the offline map for the Australian capital territory. This shows the complete offline map. Now at this zoom level, according to my requirement, is the user stays at this zoom level, the user should not be able to move the map left or right. There must be no wiggle room.

Red box is actual bbox, Grey box is the limited bbox.

Job Offers

Job Offers


    Android Build Engineer

    Pinterest
    San Francisco, CA | Seattle, WA
    • Full Time
    apply now

    Android Developer

    Small and Modern GmbH
    Hamburg, Remote (Germany)
    • Full Time
    apply now

    Senior Android Developer (Remote)

    Komoot
    Europe
    • Full Time
    apply now
Load more listings

OUR VIDEO RECOMMENDATION

,

Leveling Up Your Tests

We all know about TDD and Unit Testing, and even screenshot testing, but sometimes we do not need to embrace a new paradigm to make our tests better. These are several techniques I have adopted…
Watch Video

Leveling Up Your Tests

Jobs

As we zoom in more, the size of the new bbox would increase giving us more room to move around but still ensure that the actual bounding box should remain at the edge of the screen and not come to center of the screen.

 

 

So the new bounding that we create needs to be half-screen width less than the actual bounding box. This will happen on both sides so we multiply by 2.

new_bbox_width = actual_bbox_width - (map_screen_width/2) * 2
               = actual_bbox_width - map_screen_width

With this information, we can find the lat/lng pairs for the new bounding box. See this diagram:

Coordinates expressed in XY plane

 

Here the actual bbox is P1(x1, y1), P2(x2, y2). And the new bbox we need to create for the panning limit is P3(x3, y3), P4(x4, y4). We already know x1, y1, x2, y2. So to find x3, y4, x4, y4 this is the formula we use:

x3 = x1 - (H1 - H2) / 2
y3 = y2 - (W1 - W2) / 2 - W2
x4 = x1 - (H1 - H2) / 2 - H2
y4 = y2 - (W1 - W2) / 2

In Android code, it translates to this:

fun MapboxMap.limitViewToBounds(bounds: LatLngBounds) {
val newBoundsHeight = bounds.latitudeSpan - projection.visibleRegion.latLngBounds.latitudeSpan
val newBoundsWidth = bounds.longitudeSpan - projection.visibleRegion.latLngBounds.longitudeSpan
val leftTopLatLng = LatLng(
bounds.latNorth - (bounds.latitudeSpan - newBoundsHeight) / 2,
bounds.lonEast - (bounds.longitudeSpan - newBoundsWidth) / 2 - newBoundsWidth,
)
val rightBottomLatLng = LatLng(
bounds.latNorth - (bounds.latitudeSpan - newBoundsHeight) / 2 - newBoundsHeight,
bounds.lonEast - (bounds.longitudeSpan - newBoundsWidth) / 2,
)
val newBounds = LatLngBounds.Builder()
.include(leftTopLatLng)
.include(rightBottomLatLng)
.build()
setLatLngBoundsForCameraTarget(newBounds)
}

What this function does is, it takes the actual bounding box that you want to restrict the map to, and then create new bounding box using the logic previously shown and restrict the map to those new smaller bounds.

The final part is about when and how to call this method :

map.animateCamera(CameraUpdateFactory.newLatLngBounds(bounds, 0),
object : MapboxMap.CancelableCallback {
override fun onCancel() {}
override fun onFinish() {
map.setMinZoomPreference(map.cameraPosition.zoom)
map.limitViewToBounds(bounds)
map.addOnScaleListener(object : MapboxMap.OnScaleListener {
override fun onScaleBegin(detector: StandardScaleGestureDetector) {}
override fun onScale(detector: StandardScaleGestureDetector) {
map.limitViewToBounds(bounds)
}
override fun onScaleEnd(detector: StandardScaleGestureDetector) {}
})
map.addOnCameraIdleListener { map.limitViewToBounds(bounds) }
}
})
view raw map.kt hosted with ❤ by GitHub

There are multiple things happening in this function:

  1. Immediately after the map is done setting the camera to the bounds, and the camera is done moving, I set the current zoom to the maps’s min zoom. This way user cant zoom out further.
  2. Calling `limitViewToBounds()` once right after map view is ready.
  3. Finally, the new bounds is dependent on the zoom value. The more the zoom, the bigger the bbox. So I attached a scaleListener and each time the zoom changes, the new bbox is created.
  4. Additionally, also added the call to a map-camera-idle listener. This ensured a couple of scenarios where the map view was moving out of the bounding box to not happen anymore.

I hope you enjoyed reading this and getting a peek into my thought process.

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
In this part of our series on introducing Jetpack Compose into an existing project,…
READ MORE
blog
Nowadays authentication has become common in almost all apps. And many of us know…
READ MORE
blog
Collections are a set of interfaces and classes that implement highly optimised data structures.…
READ MORE

Leave a Reply

Your email address will not be published.

Fill out this field
Fill out this field
Please enter a valid email address.

Menu