[Huawei Developers] Sabrina Cara:
View attachment 5302217

Hello friends and welcome back to my series of integrating various Huawei services. In this article I will show the integration of Geocoding API using Retrofit to get the coordinates from a format address followed by the integration of Directions API where we input the aforementioned coordinates to get the directions and steps from origin to destination together with the distance and time calculation.

Introduction

As explained in the previous section, we have to perform various API requests and integrate them using Retrofit. We will take them step by step, starting from explaining these services and how we use them. To start your app development in Huawei, you first have to perform some configurations needed to use the Kits and Services it provides, by following this post.

Geocoding API​

Geocoding API is a service providing two main functionalities:

Forward Geocoding: a service that enables the retrieval of spatial coordinates (latitude, longitude) from a structured address. It can return up to 10 results for any given address, ranking them according to importance and accuracy.

Reverse Geocoding: does the opposite of forward geocoding by providing formatted addresses when given a set of coordinates. This service can return up to 11 formatted addresses for the coordinates given, again according to importance and accuracy.

For the purpose of this article, we will be using Forward Geocoding to retrieve the coordinates of a site based on the formatted address.

Integration

The first thing we need to do after performing the necessary configurations, would be to add the dependencies in the app-level build gradle.

//Retrofit

implementation 'com.google.code.gson:gson:2.8.6'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'

implementation("com.squareup.okhttp3:logging-interceptor:4.2.2")

After that we will set our Geocoding Retrofit Requests and Response data classes to determine what we need to send as a parameter and retrieve as a response.

data class GeocodingRequest(
u/SerializedName("address") val address: String?,
u/SerializedName("language") val language: String?
)
data class Location(
u/SerializedName("lng") val lng: Double?,
u/SerializedName("lat") val lat: Double?
)

data class GeocodingResponse(
u/SerializedName("returnCode") val returnCode: String? ,
u/SerializedName("sites") val sites: Array<Sites>?,
u/SerializedName("returnDesc") val returnDesc: String?
)
data class Sites(
u/SerializedName("formatAddress") val formatAddress: String?,
u/SerializedName("location") val location: Location?
)

You can determine the request and response parameters based on the rules of the API requests and our needs.

After setting the data classes, we will need to establish a Retrofit client that will serve as an authenticator and interactor with the API and send network requests.

class GeocodeRetrofit {

val BASE_URL_DIRECTIONS = "https://siteapi.cloud.huawei.com/mapApi/v1/siteService/"

private val retrofit: Retrofit = Retrofit.Builder()
.baseUrl(BASE_URL_DIRECTIONS)
.client(setInterceptors())
.addConverterFactory(GsonConverterFactory.create())
.build()

fun <S> createService(serviceClass: Class<S>?): S {
return retrofit.create(serviceClass)
}

private fun setInterceptors() : okhttp3.OkHttpClient {
val logger = HttpLoggingInterceptor()
logger.level = HttpLoggingInterceptor.Level.BODY

return okhttp3.OkHttpClient.Builder()
.readTimeout(60, TimeUnit.SECONDS)
.connectTimeout(60, TimeUnit.SECONDS)
.addInterceptor { chain ->
val url: okhttp3.HttpUrl = chain.request().url.newBuilder()
.addQueryParameter("key", API_KEY)
.build()
val request = chain.request().newBuilder()
.header("Content-Type", "application/json")
.url(url)
.build()
chain.proceed(request)
}
.addInterceptor(logger)
.build()
}
}

The base URL is given as below, and we should keep in mind to add the API Key of the agconnect-services.json file.

val BASE_URL_DIRECTIONS = "https://siteapi.cloud.huawei.com/mapApi/v1/siteService/"

The next step would be to create a repo;

class GeocodingBaseRepo {
private var geocodingApis : GeocodingInterface? = null

fun getInstance(): GeocodingInterface?{
if(geocodingApis==null)
setMainApis()
return geocodingApis
}
private fun setMainApis(){
geocodingApis = GeocodeRetrofit().createService(GeocodingInterface::class.java)
}
}

We proceed by creating an interface that will serve as exactly that, an interface between the API and the Retrofit Client.

interface GeocodingInterface {
@Headers("Content-Type: application/json; charset=UTF-8")
@POST("geocode")
fun listPost (
@Body geocodingRequest: GeocodingRequest
): Call<GeocodingResponse>
}

Once we have stablished all of the above, we can finally request the API in our activity or fragment. To adapt it to our case, we have created to editable text fields where user can insert origin and destination addresses. Based on that we make two geocode API calls, for origin and destination respectively, and observe their results through callbacks.

fun performGeocoding(type: String, geocodingRequest: GeocodingRequest, callback: (ResultData<GeocodingResponse>) -> Unit){
GeocodingBaseRepo().getInstance()?.listPost(geocodingRequest)?.enqueue(
object : Callback<GeocodingResponse> {
override fun onFailure(call: Call<GeocodingResponse>, t: Throwable) {
Log.d(TAG, "ERROR GEOCODING" + t.message)
}
override fun onResponse(
call: Call<GeocodingResponse>,
response: Response<GeocodingResponse>
) {
if (response.isSuccessful) {
Log.d(TAG, "SUCCESS GEOCODING" + response.message())
response.body()?.let {
if(type == "parting"){
callback.invoke(ResultData.Success(response.body()))
}
if(type == "destination"){
callback.invoke(ResultData.Success(response.body()))
}
}
}
}
})
}

private fun callOriginData(){
geocodingRequest = GeocodingRequest(partingaddress, "EN")
performGeocoding("parting" ,geocodingRequest, callback = {
it.handleSuccess {
var startingLatitude = it.data?.sites?.get(0)?.location?.lat
var startingLongitude = it.data?.sites?.get(0)?.location?.lng
origin = startingLatitude?.let { it1 -> startingLongitude?.let { it2 ->
LatLngData(it1,
it2
)
} }!!
callDestinationData()
}
})
}

private fun callDestinationData(){
geocodingRequest = GeocodingRequest(destinationaddress, "EN")
performGeocoding("destination", geocodingRequest, {
it.handleSuccess {
var endingLatitude = it.data?.sites?.get(0)?.location?.lat
var endingLongitude = it.data?.sites?.get(0)?.location?.lng
destination = endingLatitude?.let { it1 -> endingLongitude?.let { it2 ->
LatLngData(it1,
it2
)
} }!!
callDirections()
}
})
}

Geocoding Origin Results

View attachment 5302221

Geocoding Destination Results

Directions API​

Directions API is a Huawei service that provides three main functionalities:

Walking Route Planning: Plans an available walking route between two points within 150 km.

Cycling Route Planning: Plans an available cycling route between two points within 500 km.

Driving Route Planning: Plans an available driving route between two points.

Integration​

After being done with Geocoding, we need to use the results data from it and insert it into Directions API requests to be able to get all three route planning available between origin and destination coordinates. Similar to Geocode, we first establish the request and response data classes.

data class DirectionsRequest(
@SerializedName("origin") val origin: LatLngData,
@SerializedName("destination") val destination: LatLngData )
data class LatLngData (
@SerializedName("lat") val lat: Double,
@SerializedName("lng") val lng: Double )

data class DirectionsResponse (@SerializedName("routes") val routes: List<Routes>,
@SerializedName("returnCode") val returnCode: String,
@SerializedName("returnDesc") val returnDesc: String)
data class Routes (@SerializedName("paths") val paths: List<Paths>,
@SerializedName("bounds") val bounds: Bounds)

data class Paths (@SerializedName("duration") val duration: Double,
@SerializedName("durationText") val durationText: String,
@SerializedName("durationInTraffic") val durationInTraffic: Double,
@SerializedName("distance") val distance: Double,
@SerializedName("startLocation") val startLocation: LatLngData,
@SerializedName("startAddress") val startAddress: String,
@SerializedName("distanceText") val distanceText: String,
@SerializedName("steps") val steps: List<Steps>,
@SerializedName("endLocation") val endLocation: LatLngData,
@SerializedName("endAddress") val endAddress: String)

data class Bounds (@SerializedName("southwest") val southwest: LatLngData,
@SerializedName("northeast") val northeast: LatLngData)

data class Steps (@SerializedName("duration") val duration: Double,
@SerializedName("orientation") val orientation: Double,
@SerializedName("durationText") val durationText: String,
@SerializedName("distance") val distance: Double,
@SerializedName("startLocation") val startLocation: LatLngData,
@SerializedName("instruction") val instruction: String,
@SerializedName("action") val action: String,
@SerializedName("distanceText") val distanceText: String,
@SerializedName("endLocation") val endLocation: LatLngData,
@SerializedName("polyline") val polyline: List<LatLngData>,
@SerializedName("roadName") val roadName: String)

We then create a Retrofit Client for Directions API.

class DirectionsRetrofit {
val BASE_URL_DIRECTIONS = "https://mapapi.cloud.huawei.com/mapApi/v1/"

private val retrofit: Retrofit = Retrofit.Builder()
.baseUrl(BASE_URL_DIRECTIONS)
.client(setInterceptors())
.addConverterFactory(GsonConverterFactory.create())
.build()

fun <S> createService(serviceClass: Class<S>?): S {
return retrofit.create(serviceClass)
}

private fun setInterceptors() : okhttp3.OkHttpClient {
val logger = HttpLoggingInterceptor()
logger.level = HttpLoggingInterceptor.Level.BODY

return okhttp3.OkHttpClient.Builder()
.readTimeout(60, TimeUnit.SECONDS)
.connectTimeout(60, TimeUnit.SECONDS)
.addInterceptor { chain ->
val url: okhttp3.HttpUrl = chain.request().url.newBuilder()
.addQueryParameter("key", API_KEY)
.build()
val request = chain.request().newBuilder()
.header("Content-Type", "application/json")
.url(url)
.build()
chain.proceed(request)
}
.addInterceptor(logger)
.build()
}
}

In this case, what will serve as our Base URL will be the URL below:

val BASE_URL_DIRECTIONS = "https://mapapi.cloud.huawei.com/mapApi/v1/"

We create a repo once again;

open class DirectionsBaseRepo {
private var directionsApis : DirectionsInterface? = null

fun getInstance(): DirectionsInterface?{
if(directionsApis==null)
setMainApis()
return directionsApis
}
private fun setMainApis(){
directionsApis = DirectionsRetrofit().createService(DirectionsInterface::class.java)
}
}

And similarly to the previous process we followed in Geocode we need an interface:

interface DirectionsInterface {
@POST("routeService/{type}")
fun getDirectionsWithType(
@Path(value = "type",encoded = true) type : String,
@Body directionRequest: DirectionsRequest
): Call<DirectionsResponse>
}

The only part that is extra from the previous API request is that we need an enumerating class to store the different direction types which will be determined from the user.

enum class DirectionType(val type: String) {
WALKING("walking"),
BICYCLING("bicycling"),
DRIVING("driving")
}

The only thing left for us to do now is to make the API call within the activity / fragment.

For this part we have created three image buttons for three direction types, and we call the direction API based on the type users selected. Basically if user wants to see the driving route, they select the driving type and a Direction API request with type driving is made.

fun getDirections(type: String, directionRequest: DirectionsRequest, callback: (ResultData<DirectionsResponse>) -> Unit){
DirectionsBaseRepo().getInstance()?.getDirectionsWithType(type,directionRequest)?.enqueue(object : Callback<DirectionsResponse>{
override fun onFailure(call: Call<DirectionsResponse>, t: Throwable) {
Log.d(TAG, "ERROR DIRECTIONS" + t.message)
}
override fun onResponse(call: Call<DirectionsResponse>, response: Response<DirectionsResponse>) {
Log.d(TAG, "success DIRECTIONS" + response.message())
if(response.isSuccessful){
response.body()?.let {
callback.invoke(ResultData.Success(it))
}
}
}
})
}

getDirections(DirectionType.DRIVING.type, directionRequest, {
it.handleSuccess {
it.data?.routes?.get(0)?.paths?.get(0)?.steps?.get(0)?.startLocation?.lat?.let { it1 ->
it.data?.routes?.get(0)?.paths?.get(0)?.steps?.get(0)?.startLocation?.lng?.let { it2 ->
commonMap.animateCamera(
it1, it2, 10f )
}
}
it.data?.routes?.get(0)?.paths?.get(0)?.steps?.forEach {
it.polyline.forEach{
commonPolylineCoordinates.add(CommonLatLng(it.lat, it.lng))
}
}
drawPolyline(commonPolylineCoordinates)

carDistance = it.data?.routes?.get(0)?.paths?.get(0)?.distanceText.toString()
binding.distanceByCar.setText(carDistance)
carTime = it.data?.routes?.get(0)?.paths?.get(0)?.durationText.toString()
binding.timebyCar.setText(carTime)
}
})

As a result you can make use of all the response fields, including the steps needed to reach a place, the distance and time, or take the polyline coordinates and draw a route on the map. For this project I have decided to draw the route on the map and calculate the time and distance between the coordinates.

The final result is displayed below:

Geocoding API + Directions API Results

Geocoding API + Directions API Results

Tips and Tricks​

It is a little tricky to work with asynchronous data since you never know when they will return their responses. We need to call geocode APIs for both origin and destination, and we want to make sure that the destination is called after the origin. To perform this you can call the destination geocoding API in the handle success part of the origin geocoding API, this way you make sure when you get a destination, you will definitely have an origin.

Similarly, you want to call the directions API when you have both origin and destination coordinates, hence you can call it in the handle success part of the destination geocoding call. This way you can be sure directions API call will not have empty or static coordinates.

Be careful to clean the polyline after switching between navigation types.

Conclusion​

In this article, we talked about the integration of Geocoding API and performing Forward Geocoding to get the coordinates of a place of origin and destination, based on the formatted addresses. We proceeded by retrieving the origin and destination coordinates and ‘feeding’ them to the Directions API requests to get the route planning for navigation types of driving, cycling and walking. Afterwards we get the response of the Directions API call and use the result data as needed from our use cases. In my case I used the polyline data to draw on the map, and display the distance and time from two places. I hope you give it a shot, let me know what you think. Stay healthy and happy, see you in other articles.

Reference​

https://developer.huawei.com/consum...iretions-api-introduction-0000001050178120-V5

https://developer.huawei.com/consum...ces-V5/webapi-forward-geo-0000001050163921-V5

https://github.com/HMS-Core/hms-mapkit-demo