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

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, ,

Migrating to Jetpack Compose – an interop love story

Most of you are familiar with Jetpack Compose and its benefits. If you’re able to start anew and create a Compose-only app, you’re on the right track. But this talk might not be for you…
Watch Video

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer for Jetpack Compose
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engin ...
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer f ...
Google

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
This is the second article in an article series that will discuss the dependency…
READ MORE
blog
Let’s suppose that for some reason we are interested in doing some tests with…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

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

Menu