{{announcement.body}}
{{announcement.title}}

All About Maps - Episode 2: Moving Map Camera to Bounded Regions

DZone 's Guide to

All About Maps - Episode 2: Moving Map Camera to Bounded Regions

In this article, we discus how to work with bounded regions and maps in an Android application.

· Web Dev Zone ·
Free Resource

Previously on AllAboutMaps: Episode 1, we looked at:

  • The principles of clean architecture

  • The importance of eliminating map provider dependencies with abstraction

  • Drawing polylines and markers on Mapbox Maps, Google Maps (GMS), and Huawei Maps (HMS)

Episode 2: Bounded Regions

Welcome to the second episode of AllAboutMaps. In order to understand this blog post better, I would first suggest reading the Episode 1. Otherwise, it will be difficult to follow the context. 

In this episode we will talk about bounded regions:

  1. The GPX parser datasource will parse the the file to get the list of attraction points (waypoints in this case).

  2. The datasource module will emit the bounded region information in every 3 seconds

  3. A rectangular bounded area from the centered attraction points with a given radius using a utility method (No dependency to any Map Provider!)

  4. We will move the map camera to the bounded region each time a new bounded region is emitted.


    Map UI mobile

ChangeLog Since Episode 1

As we all know, software development is a continuous process. It helps a lot when you have reviewers who can comment on your code and point out issues or come up with suggestions. Since this project is a one person task, it is not always easy to spot the flaws in the code during implementation. The software gets better and evolves hopefully in a good way when we add new features. Once again, I would like to add the disclaimer that my suggestions here are not silver bullets. There are always better approaches. I am more than happy to hear your suggestions in the comments!

You can see the full code change between episode 1 and 2 here: https://github.com/ulusoyca/AllAboutMaps/compare/episode_1-parse-gpx...episode_2-bounded-region

Here are the main changes I would like to mention:

1- Added MapLifecycleHandlerFragment.kt base class

In episode 1, I had one feature: show the polyline and markers on the map. The base class of all 3 fragments (RouteInfoMapboxFragment, RouteInfoGoogleFragment and RouteInfoHuaweiFragment) called these lifecycle methods. When I added another feature (showing bounded regions) I realized that the new base class of this feature again implemented the same lifecycle methods. This is against the DRY rule (Dont Repeat Yourself)! Here is the base class I introduced so that each feature's base class will extend this one:

Kotlin


Let's see the big picture now:

2- Refactored the Abstraction for Styles, Marker Options, and Line Options

In the first episode, we encapsulated a dark map style inside each custom MapView. When I intended to use outdoor map style for the second episode, I realized that my first approach was a mistake. A specific style should not be encapsulated inside MapView. Each feature should be able to select different style. I took the responsibility to load the style from MapViews to fragments. Once the style is loaded, the style object is passed to MapView.

Kotlin


I also realized the need for MarkerOptions and LineOptions entities in our domain module:

Kotlin
Kotlin


Above entities has properties based on the needs of my project. I only care about the color, text, location, and icon properties of the marker. For polyline, I will customize width, color and text properties. If your project needs to customize the marker offset, opacity, line join type, and other properties, then feel free to add them in your case.

These entities are mapped to corresponding map provider classes:

  • LineOptions:

Kotlin
Kotlin
Kotlin
  • MarkerOptions 
Kotlin
Kotlin
Kotlin


There are minor technical details to handle the differences between map provider APIs but it is out of this blog post's scope.

Earlier our methods for drawing polyline and marker looked like this:

Kotlin


After this refactor they look like this:

Kotlin


It is a code smell when the number of the arguments in a method increases when you add a new feature. That's why we created data holders to pass around.

3- A Secondary Constructor Method for LatLng

While working on this feature, I realized that a secondary method that constructs the LatLng entity from double values would also be useful when mapping the entities with different map providers. I mentioned the reason why I use inline classes for Latitude and Longitude in the first episode.

Kotlin


Bounded Region

A bounded region is used to describe a particular area (in many cases it is rectangular) on a map. We usually need two coordinate pairs to describe a region: Soutwest and Northeast. In this stackoverflow answer (https://stackoverflow.com/a/31029389), it is well described:

Latitude longitude calculator

As expected Mapbox, GMS and HMS maps provide LatLngBounds classes. However, they require a pair of coordinates to construct the bound. In our case we only have one location for each attraction point. We want to show the region with a radius from center on map. We need to do a little bit extra work to calculate the location pair but first let's add LatLngBound entity to our domain module: 

Kotlin


Implementation

Thanks to our clean architecture, it is very easy to add a new feature with a new use case. Let's start with the domain module as always:

Kotlin


The user interacts with the app to start the playback of waypoints. I call this playback because playback is "the reproduction of previously recorded sounds or moving images." We have a list of points to be listened in a given time. We will move map camera periodically from one bounded region to another. The waypoints are emitted from datasource with a given update interval. Domain module doesn't know the implementation details. It sends the request to our datasource module. 

Let's see our datasource module. We added a new method in RouteInfoDataRepository:

Kotlin


Thanks to Kotlin Coroutines, it is very simple to emit the points with a delay. Roman Elizarov describes the flow api in very neat diagram below. If you are interested to learn more about it, his talks are the best to start with.

Long story short, our app module invokes the use case from domain module, domain module forwards the request to datasource module. The corresponding repository class inside datasource module gets the data from GPX datasource and the datasource module orchestrates the data flow

Calculating Bounded Regions for a central point with radius

Now the app layer recevies the data in viewmodel:

Kotlin
 


Each time a point is collected, we will move the camera to a bounded region. Now we need to calculate the bounded region from a center point with a given radius.

As provided in the answer of the aforementioned StackOverFlow post, there is this utility library  'com.google.maps.android:android-maps-utils:0.4.4'  It can do the calculations for us. However, I don't want to add Google dependency to my implementation. I adapted the calculations from their source code to my project using my own domain's LatLng values. I only needed this part of the library to achieve my task:

Kotlin


Then as an extension method to LatLng class, I calculate the LatLngBounds:

Kotlin


Note that each Map provider should have a LatLngBound class. If not, don't use that provider becuase this is one of the core features that a map provider should support. Now, let's see how we map our domain entity to corresponding providers:

Kotlin
 

Calling moveCamera Method in Our Custom MapViews

In AllAboutMapView interface we add this method so that out custom MapView classes namely MapboxMapView, HuaweiMapView, GoogleMapView will implement this method:

Kotlin


  • GoogleMapView 
Kotlin
  • HuaweiMapView
Kotlin
  • MapboxMapView 
Kotlin
 


MVVM

Now we have all we need to build our MVVM: Model (UseCases) -View (Fragments) - ViewModel:

  • Fragment calls start playback method inViewModel and our use case is invoked.

Kotlin
  • Base fragment class observes for the waypoints. Each time a waypoint is observed, a marker is drawn on map and camera is moved to the bounded region. 
Kotlin
  • Huawei Fragment:
Kotlin
  • Google Fragment: 
Kotlin
  • Mapbox Fragment:
Kotlin


Conclusion

In this post, we added a feature to our project so that camera is moved to a bounded region generated from a central point with a radius value. You can see the full source code in the tag: episode_2-bounded-region in the Github page of this project. You can also follow the develop branch for latest version of the project

https://github.com/ulusoyca/AllAboutMaps/tree/episode_2-bounded-region

Topics:
android, hms, integration, java, kotlin, open source

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}