Hands On

Designing Map Gesture Events in Kotlin and Android to Reverse Geocode Addresses with HERE

By Nic Raboy | 05 February 2019

Over the past few weeks I’ve been writing tutorials around Kotlin and Android that make use of the various HERE APIs. Remember, Java isn’t the only way to develop for Android and it may not be the most popular in the near future.

In the last tutorial we saw how to geocode addresses and display them on a map using Kotlin. This is great for showing markers or other points of interest, but what if we want to know an address based on where the user taps on the map? How do you even determine coordinate information from a tap event on a screen?

We’re going to see how to calculate addresses from where a user taps within an Android application using the HERE Android SDK and the Kotlin programming language.

To get a more realistic idea of what we’re going to accomplish, take the following animated image.

kotlin-android-reverse-geocode

Doing a long-press event will drop a marker on the map after converting the pixel location to latitude and longitude coordinates. Doing a standard tap event on the marker will reverse geocode the coordinates and display an associated nearest address.

Going into this tutorial, the assumption is that you already have the HERE Android SDK configured in your Kotlin application. If you don’t and need help, check out my previous tutorial titled, Getting Started with HERE using Kotlin and the Android SDK.

Calculating Latitude and Longitude Coordinates from Pixel Locations

Before we try to drop any markers or reverse geocode our coordinates, we first need to calculate those coordinates. We can actually make use of the OnGestureListenerAdapter to help us when it comes to gesture events and gathering the positions of those interactions.

Take the following code for example:

class MainActivity : AppCompatActivity() {

    private lateinit var map : Map
    private lateinit var mapFragment : SupportMapFragment
    private lateinit var marker : MapMarker

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        mapFragment = getSupportFragmentManager().findFragmentById(R.id.mapfragment) as SupportMapFragment
        mapFragment.init { error ->
            if (error == OnEngineInitListener.Error.NONE) {
                map = mapFragment.map
                map.setCenter(GeoCoordinate(37.7397, -121.4252, 0.0), Map.Animation.NONE)
                map.zoomLevel = (map.maxZoomLevel + map.minZoomLevel) / 2
                mapFragment.mapGesture.addOnGestureListener(object : MapGesture.OnGestureListener.OnGestureListenerAdapter() {
                    override fun onTapEvent(p: PointF?): Boolean {
                        return false
                    }
                    override fun onLongPressEvent(p: PointF?): Boolean {
                        return false
                    }
                })
            }
        }
    }

}

You’ll notice a few things about the above code. First you’ll notice that we are not configuring our gesture listeners until after the map has initialized. If you try to configure your listeners before you have a functional map, you’re going to run into trouble. You’ll also notice that we’re not using a lambda expression for the various functions within the listener. There are numerous ways to handle interfaces with multiple functions, the approach above just being one of many. Had we only been using a single override, we could have easily used a lambda.

We have the foundation of our gesture events in place, so lets jump forward for a moment and figure out what happens when we need to place a marker on the map. We might have a function like the following:

fun dropMarker(position: GeoCoordinate) {
    if (::marker.isInitialized) {
        map.removeMapObject(marker)
    }
    marker = MapMarker()
    marker.coordinate = position
    map.addMapObject(marker)
}

Given a latitude and longitude coordinate, we see if a marker already exists on the map. If the marker exists, we remove it and then place a new marker based on the position. Nothing too fancy is happening here.

Going back into our gesture listener, we can update it to the following:

mapFragment.mapGesture.addOnGestureListener(object : MapGesture.OnGestureListener.OnGestureListenerAdapter() {
   override fun onTapEvent(p: PointF?): Boolean {
       return false
   }
   override fun onLongPressEvent(p: PointF?): Boolean {
       val position = map.pixelToGeo(p)
       dropMarker(position)
       return false
   }
})

Using the pixelToGeo function we can get the latitude and longitude coordinates of our tap event. These coordinates are then passed to our dropMarker function.

Reverse Geocoding Latitude and Longitude Coordinates to Addresses with HERE for Android

Now that we have latitude and longitude information calculated from the previous step, we can work towards reverse geocoding it to human readable addresses. Before we investigate the onTapEvent in our gesture listener, let’s figure out what is involved in reverse geocoding some coordinates.

fun reverseGeocode(position: GeoCoordinate) {
    val request = ReverseGeocodeRequest(position)
    request.execute { data, error ->
        if (error != ErrorCode.NONE) {
            Log.e("HERE", error.toString())
        } else {
            Toast.makeText(applicationContext, data.text, Toast.LENGTH_LONG).show()
        }
    }
}

Given a position, we can make use of the ReverseGeocodeRequest class that is part of the HERE Android SDK. After executing a request, we’ll get information regarding the closest match to the coordinates provided.

With this in mind, we can update our onTapEvent to contain the following:

mapFragment.mapGesture.addOnGestureListener(object : MapGesture.OnGestureListener.OnGestureListenerAdapter() {
    override fun onTapEvent(p: PointF?): Boolean {
        val viewObjectList = map.getSelectedObjects(p) as ArrayList<ViewObject>
        for (viewObject in viewObjectList) {
            if (viewObject.baseType == ViewObject.Type.USER_OBJECT) {
                val mapObject = viewObject as MapObject
                if (mapObject.type == MapObject.Type.MARKER) {
                    val selectedMarker = mapObject as MapMarker
                    reverseGeocode(selectedMarker.coordinate)
                }
            }
        }
        return false
    }
    override fun onLongPressEvent(p: PointF?): Boolean {
        val position = map.pixelToGeo(p)
        dropMarker(position)
        return false
    }
})

When we create a marker, the coordinate information is stored with the marker. Because we are choosing to get that information from the marker and not a pixel on the map, we need to loop through our map objects and figure out which one was tapped. Once we have that information we can call our reverseGeocode method.

Conclusion

You just saw how to reverse geocode latitude and longitude coordinates using Kotlin and the HERE Android SDK. The star of this tutorial was in the gesture events that are listened for. While we only made use of the OnTapEvent and the OnLongPressEvent there are quite a few other options.

If you are stuck with the configuration of the HERE Android SDK, I encourage you to check out my previous tutorial on the topic. I’m not a Kotlin expert so if you think my Kotlin can be optimized, let me know in the comments.