Drawing path between two points in Google Maps with Kotlin in Android app
Kotlin is a fairly new language that has recently become quite famous since it became an official language for Android application development.
And it is an amazing language indeed!
For a while, when I first heard of it, I thought it was a fad. Yet another newish language that people claim is the next big thing and then quickly fades away. But after two months of working with it and experimenting with it, I am more and more fascinated every day.
Take the task of drawing the path between two points when using a Maps Activity in Android. You can draw a straight line, of course, and that is fairly simple. But unless you’re travelling by Nazgul, it’s not really too useful.
If you want a detailed path, pretty much like the one we see when we use Google Maps, you will have to take the following steps:
1) Build the correct URL string to make the API call
2) Connect to it, download a stream from the contents, and convert it to a string.
3) Parse the string into a JSON object.
4) Parse the JSON object to get the correct polyline objects.
5) Decode each polyline string into a List of points.
6) Build, from all the lists you got in the step above, one final list that will contain all points in the path.
7) Store those points in a Polyline object to produce the path.
If you have ever done this in Java (or seen how it’s done), you will know that this is really a lot of code. I’ll show you how I did this in Kotlin and, hopefully, some of you out there will be as happy as I am with the results.
First, of course, you will create a Maps Activity in Android Studio. For the purposes of keeping this simple, we’ll hardcode the two points of the trajectory. It doesn’t matter how you will acquire those points in real life, we’re not concerned with that today.
Depending on which version of Android Studio you’re using, you may have to configure Kotlin, or you will have chosen Kotlin support when creating the activity. Once the file is created and you have converted it to Kotlin, you may find some errors: just remember that the override keyword is mandatory in Kotlin (Android Studio doesn’t always put it there when it converts to Kotlin) and that the onCreate() method should look like
override fun onCreate(savedInstanceState: Bundle?)
// Bundle should be a nullable argument
instead of
override fun onCreate(savedInstanceState: Bundle)
Once that is fixed, we are almost ready. Kotlin is not only a very cool language in itself, but it also has some very good libraries that we can use to make our lives much simpler. For this project, we will use two libraries: One is anko, which has a lot of features, but we will use it here to perform async tasks (yes, and avoid using the horrible AsyncTask class), the other one is klaxon, which is a very light-weight library that will make JSON parsing really easy.
So you should edit the build.gradle file (the module one), and add these two lines:
compile 'org.jetbrains.anko:anko-sdk15:0.8.2'
compile 'com.beust:klaxon:0.30'
There’s more information in their github repos, in case something was not to work.
And now, we can start coding.
In our onMapReady() method, we should have something like this:
override fun onMapReady(googleMap: GoogleMap) {
mMap = googleMap
// Add a marker in Sydney and move the camera
val sydney = LatLng(-34.0, 151.0)
mMap!!.addMarker(MarkerOptions().position(sydney).title("Marker in Sydney"))
mMap!!.moveCamera(CameraUpdateFactory.newLatLng(sydney))
}
Let’s just stay in Sydney. I’ve always wanted to go to Australia anyway! And since one of the most iconic places in Sydney is the Opera House, let’s make that our destination. We will leave out the moveCamera() function for now. We’ll come back to it later. So now we have:
override fun onMapReady(googleMap: GoogleMap) {
mMap = googleMap val sydney = LatLng(-34.0, 151.0)
val opera = LatLng(-33.9320447,151.1597271)
mMap!!.addMarker(MarkerOptions().position(sydney).title("Marker in Sydney"))
mMap!!.addMarker(MarkerOptions().position(opera).title("Opera House"))
}
The next step is to create a PolylineOptions object, set up the color and width. We will use this object to add the points later.
val options = PolylineOptions()
options.color(Color.RED)
options.width(5f)
Now, we need to build the URL we will use to make the API call. We can put it in a separate function to get it out of the way:
private fun getURL(from : LatLng, to : LatLng) : String {
val origin = "origin=" + from.latitude + "," + from.longitude
val dest = "destination=" + to.latitude + "," + to.longitude
val sensor = "sensor=false"
val params = "$origin&$dest&$sensor"return "https://maps.googleapis.com/maps/api/directions/json?$params"
}
And, of course, we call it by doing:
val url = getURL(sydney, opera)
Now that we have our URL, we need to connect to it, download the contents and put them into a string. This is, of course, something that we don’t want to do in the UI thread, so we will use anko’s async. This makes things much easier and safer than using AsyncTask. Basically, the structure of the code is pretty simple:
async {
// here you put the potentially blocking code
uiThread {
// this will execute in the main thread, after the async call is done }
}
So let’s put all our code in the first part of async, so that we can connect to Google, open a stream, copy the contents of the page into the stream and convert the stream to a string:
async {
val result = URL(url).readText()
uiThread {
// this will execute in the main thread, after the async call is done }
}
Yes. That’s all you need to do. Kotlin gives us the readText() extension function, which does all the steps mentioned above correctly in that simple line. As per the documentation, it has the slight disadvantage that it will not work for huge files. “Huge” here means 2 Gb, so it’s probably safe to use it here, unless you’re planning to drive a lot.
Oh, and in case you were wondering, it also closes the stream for you.
Once we have our string stored and ready, the uiThread part of the code will execute and there is where the rest of the code goes. Now we are ready to extract the JSON object from the string, and we will use klaxon for that. This is also pretty simple:
val parser: Parser = Parser()
val stringBuilder: StringBuilder = StringBuilder(result)
val json: JsonObject = parser.parse(stringBuilder) as JsonObject
We declare the parser, pass our string to a StringBuilder, and then pass this to the parser. Now we have our JSON Object, and we need to find the points to form the path.
What Google returns is a fairly deeply nested object, that has all the information you normally get when using Google Maps. The nested objects are under the keys “routes”/ “legs”/ “steps”, and there we have a JSON array of objects that have different fields. The one we’re interested in is under the key “polyline”, and it has an encoded string under the key “points” from which we can get all the points for that particular step. We will need to decode that string, there’s no way around it. You can find the code for the function in my repo. It’s fairly involved and I couldn’t really find much information about it, so if anybody knows, let me know.
Actually traversing the JSON object to get the points is fairly easy. klaxon is simple to use and its JSON arrays can be used just like any Kotlin List.
val routes = json.array<JsonObject>("routes")
val points = routes!!["legs"]["steps"][0] as JsonArray<JsonObject>
The second line is likely to give an “unchecked cast” warning in AS, but that’s mostly because it has no way of knowing whether or not you’re accessing an element that can be safely converted into a JsonArray. Since we know it can, we can ignore the warning.
What we have in ‘points’ is a JsonArray of JsonObjects. What we need is an List of LatLng points. Which means we need to find the “polyline” object in each element of the array, get its “points” field, call the decoding function for each, which will return an List of LatLng, and ‘blend’ all those Lists into one that holds all the points.
Because JsonArray behaves like Kotlin’s List, we can apply all the methods we would apply to a list, so in order to get all the encoded strings we could use the map() function from List and do something like this:
val polypts = points.map { it.obj("polyline")?.string("points")!! }
But that’s not quite what we need because these are just the strings. So we could actually call the decoding function from the closure itself. Since the decoding function returns a List, we would end up having a List of Lists, which is not ideal because it’s not so easy to traverse. Here comes another Kotlin function to the rescue: Instead of using map, we can use flatMap(), which will convert the List of Lists into a simple list:
val polypts = points.flatMap { decodePoly(it.obj("polyline")?.string("points")!!) }
And we’re pretty much done. All we need to do now is add our first starting point, the points that we have in polypts and the destination to our Polyline object and add the Polyline to the map:
options.add(sydney)
for (point in polypts) options.add(point)
options.add(opera)
mMap!!.addPolyline(options)
And that’s it!
I also used LatLngBounds to make sure the whole path gets centered in the screen, so the final call to moveCamera() looks like this:
mMap!!.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds, 100))
Here’s our map:
This is a 40+km route and it is displayed immediately. The route given is the fastest one, which is the same one you would get by default using Google Maps.
Just for the sake of it, I tested it to draw the route between Madrid and Moscow (that’s 4000 km.). It worked perfectly fine, even when it did take a while to get all the information from Google. I think the readText() extension function is safe enough to use in this context, although in other cases you might want to handle the streams yourselves.
Hope you have enjoyed this. The full code is here.