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:
The GPX parser datasource will parse the the file to get the list of attraction points (waypoints in this case).
The datasource module will emit the bounded region information in every 3 seconds
A rectangular bounded area from the centered attraction points with a given radius using a utility method (No dependency to any Map Provider!)
We will move the map camera to the bounded region each time a new bounded region is emitted.
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:
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.
I also realized the need for MarkerOptions and LineOptions entities in our domain module:
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:
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:
After this refactor they look like this:
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.
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:
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:
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:
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:
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:
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:
Then as an extension method to LatLng class, I calculate the LatLngBounds:
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:
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:
Now we have all we need to build our MVVM: Model (UseCases) -View (Fragments) – ViewModel:
- 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.
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