Skip to main content

Route Terrain Profile

Last updated: June 19, 2026 | 6 minutes read

This example computes a route with its terrain profile and presents the elevation and road characteristics in an interactive panel beside the map. The terrain profile describes how the route behaves along its length: the elevation above sea level, the total ascent and descent, the individual climbs (with their rating, length and average grade), and how the distance breaks down by surface type, road (way) type and steepness. Selecting a point or section in the panel highlights the corresponding place on the map, and panning or zooming the elevation chart zooms the route to match.

Elevation chart
Climb details
Roads info

Enabling and computing the terrain profile

The route is computed with a RoutingService. The terrain profile is not calculated by default - preferences.buildTerrainProfile must be set to true before calling calculateRoute, otherwise the route comes back without any elevation data. The waypoints here run from Brașov to Bucharest.

calculateRoute returns synchronously whether the calculation could be started. When it returns an error the onCompleted callback never fires, so that case is handled right here - the error message is shown in a dialog. A successful start is reported later through onCompleted.

MainActivity.ktView on Github
private fun calculateRoute() = SdkCall.execute {
val waypoints = arrayListOf(
Landmark("Brasov", 45.65085, 25.60471),
Landmark("Bucharest", 44.43614, 26.10268),
)

/**
* Setting buildTerrainProfile to true is mandatory to receive terrain profile data;
* without it the terrain profile is not calculated during routing.
*/
routingService.preferences.buildTerrainProfile = true

// calculateRoute returns synchronously whether the calculation could be started. On
// failure onCompleted never fires, so report the error and stop the idling wait here.
val errorCode = routingService.calculateRoute(waypoints)
if (errorCode != GemError.NoError) {
val errorMessage = GemError.getMessage(errorCode, this)
runOnAliveUi { showDialog(getString(R.string.routing_failed_to_start, errorMessage)) }
EspressoIdlingResource.decrement()
}
}

Retrieving the profile from the route

When routing completes, the RoutingService callback inspects the result. Even with buildTerrainProfile enabled the profile can be absent (for example when the SDK has no elevation data for the area), so route.terrainProfile is checked before the panel is shown; if it is missing, a message is displayed instead.

MainActivity.ktView on Github
onCompleted = { routes, errorCode, _ ->
runOnAliveUi {
binding.progressBar.visibility = View.GONE

when (errorCode) {
GemError.NoError -> {
val route = routes.firstOrNull()
// The terrain profile is only available when buildTerrainProfile was
// enabled and the SDK could compute elevation data for the route.
val terrain = route?.let { SdkCall.execute { it.terrainProfile } }
when {
route != null && terrain != null -> displayTerrainInfo(route)
route != null -> {
showDialog(getString(R.string.terrain_profile_not_available))
EspressoIdlingResource.decrement()
}
else -> EspressoIdlingResource.decrement()
}
}
// ... Cancel / error handling
}
}
}

displayTerrainInfo reveals the panel, draws the route on the map, and hands the route to a RouteProfile helper that reads the terrain data and builds the charts.

MainActivity.ktView on Github
private fun displayTerrainInfo(route: Route) {
// Show the layout that contains the elevation views.
binding.routeProfilePanel.visibility = View.VISIBLE

// Present the route after the panel is visible so edge insets are computed correctly.
binding.routeProfilePanel.post {
displayRoute(route)
}

routingProfile = RouteProfile(this, route)
EspressoIdlingResource.decrement()
}

Reading the terrain data

RouteProfile obtains the RouteTerrainProfile from the route once and reads everything it needs from it. Everything the panel shows comes from this object; the rest of RouteProfile.kt only renders those values with a charting library.

RouteProfile.ktView on Github
init {
SdkCall.execute {
routeTerrainProfile = route.terrainProfile!!
routeLength = route.timeDistance?.totalDistance ?: 0
highlightPathsColor = Rgba(239, 38, 81, 255)
}
// ...
}

Elevation. The summary values - minimum and maximum elevation (and the distances at which they occur), total ascent and total descent - are read directly from the profile. For the chart itself, getElevation(distanceM) returns the elevation at a single point along the route, and getElevationSamples(count, startM, endM) returns a series of evenly spaced elevation samples between two distances, which is what gives the chart its resolution.

RouteProfile.ktView on Github
private fun getElevationChartYValue(distance: Double): Int {
return routeTerrainProfile.getElevation(distance.toInt()).toInt()
}

private fun getElevationChartYValues(valuesCount: Int): IntArray {
val yValuesArray = IntArray(valuesCount)

val samples = routeTerrainProfile.getElevationSamples(
valuesCount,
chartMinX.toInt(),
chartMaxX.toInt(),
)
samples?.first?.size?.let {
if (valuesCount <= it) {
for (i in 0 until valuesCount) {
yValuesArray[i] = (samples.first[i]).toInt()
}
}
}

return yValuesArray
}

Sectioned data. The rest of the panel is driven by lists of sections, each spanning a range of the route. The profile exposes:

  • climbSections - the significant climbs, each carrying a rating (grade), start/end distance, start/end elevation, length and average slope. These populate the Climb details table.
  • surfaceSections - the route split by surface type (paved, unpaved, …), summarised in the Surfaces bar.
  • roadTypeSections - the route split by road (way) type (motorway, road, …), summarised in the Ways bar.
  • getSteepSections(intervals) - the route split into steepness categories for the grade thresholds you pass in, summarised in the Steepness bar.

Each section only reports where it begins (startDistanceM), not how long it is. The example derives each section's length by subtracting its own start distance from the next section's start distance; the final section has no successor, so it runs from its start to the end of the route (routeLength).

RouteProfile.ktView on Github
private fun loadData() = SdkCall.execute {
// ...
val steepnessIntervals = arrayListOf(-16f, -10f, -7f, -4f, -1f, 1f, 4f, 7f, 10f, 16f)

routeTerrainProfile.surfaceSections?.let { surfacesSectionList ->
for ((i, item) in surfacesSectionList.withIndex()) {
val length = if (i < surfacesSectionList.size - 1) {
surfacesSectionList[i + 1].startDistanceM - item.startDistanceM
} else {
routeLength - item.startDistanceM
}
// group item.type -> CSectionItem(item.startDistanceM, length)
}
}

routeTerrainProfile.roadTypeSections?.let { /* same pattern, grouped by road type */ }

routeTerrainProfile.getSteepSections(steepnessIntervals)?.let { /* grouped by steepness category */ }
}

Map highlighting

The panel drives the map. Touching the elevation chart, tapping a row in the climb table, or selecting a segment in the surfaces / ways / steepness bars highlights the matching coordinate or path on the map (via activateHighlightLandmarks and the map's path collection). Zooming and panning the elevation chart likewise zooms and pans the map to cover the area the chart is currently showing. This panel-to-map behaviour is the bulk of RouteProfile.kt, layered on top of the terrain-profile data described above.