> For the complete documentation index, see [llms.txt](https://docs.mapbox.com/android/maps/llms.txt)

# Add a Line to 3D Terrain

![Example showcasing terrain while animating a route.](https://docs.mapbox.com/android/assets/ideal-img/maps-examples-add-line.86d2e4b.480.png)

This example demonstrates the usage of terrain in **Mapbox Maps SDK for Android** by animating a free camera along a line string route. The `SantaCatalinaActivity` class initializes a Mapbox map with terrain features and loads a satellite style with a line layer to visualize the route. The animation of the free camera is achieved by interpolating along the route using the `TurfMeasurement` library and updating the camera position. The animation duration, camera elevation, and other constants are defined for smooth camera movement. A directions request is executed to retrieve the route geometry, which is then used to animate the camera movement on the map.

The example leverages various Mapbox components such as [`MapboxMap`](https://docs.mapbox.com/android/maps/api/latest/mapbox-maps-android/com.mapbox.maps/-map-controllable/mapbox-map.html), styles, sources, and layers, along with extensions like [`terrain`](https://docs.mapbox.com/android/maps/api/latest/mapbox-maps-android/com.mapbox.maps.extension.style/-style-contract/-style-extension/terrain.html) and [`locationIndicatorLayer`](https://docs.mapbox.com/android/maps/api/latest/mapbox-maps-android/com.mapbox.maps.extension.style.layers.generated/-location-indicator-layer/). The `executeDirectionsRequestForRoute` function fetches the route information and passes it to the `animateRoute` function to visualize and animate the route. The [`FreeCameraOptions`](https://docs.mapbox.com/android/maps/api/latest/mapbox-maps-android/com.mapbox.maps/-free-camera-options/) API is utilized to manipulate the camera position and orientation dynamically. Finally, the line string route is displayed on the map surface to provide a visual representation of the animated camera movement.

> **Note: Android Examples App Available**
> 
> This example code is part of the **Maps SDK for Android Examples App**, a working Android project [available on GitHub](https://github.com/mapbox/mapbox-maps-android/tree/v11.31.0/). Android developers are encouraged to run the examples app locally to interact with this example in an emulator and explore other features of the Maps SDK.
> 
> See our [Run the Maps SDK for Android Examples App](https://docs.mapbox.com/help/tutorials/maps-sdk-android-examples-app/) tutorial for step-by-step instructions.

**Kotlin**

Title: `SantaCatalinaActivity.kt`

[View on GitHub](https://github.com/mapbox/mapbox-maps-android/blob/v11.31.0/app/src/main/java/com/mapbox/maps/testapp/examples/terrain3D/SantaCatalinaActivity.kt)

```kt
package com.mapbox.maps.testapp.examples.terrain3D

import android.animation.TimeAnimator
import android.graphics.Color.rgb
import android.os.Bundle
import android.view.Window
import androidx.appcompat.app.AppCompatActivity
import com.mapbox.api.directions.v5.DirectionsCriteria
import com.mapbox.api.directions.v5.MapboxDirections
import com.mapbox.api.directions.v5.models.DirectionsResponse
import com.mapbox.api.directions.v5.models.RouteOptions
import com.mapbox.common.MapboxOptions
import com.mapbox.core.constants.Constants.PRECISION_6
import com.mapbox.geojson.LineString
import com.mapbox.geojson.Point
import com.mapbox.maps.CameraOptions
import com.mapbox.maps.MapView
import com.mapbox.maps.MapboxMap
import com.mapbox.maps.Style
import com.mapbox.maps.extension.style.image.image
import com.mapbox.maps.extension.style.layers.generated.LocationIndicatorLayer
import com.mapbox.maps.extension.style.layers.generated.SymbolLayer
import com.mapbox.maps.extension.style.layers.generated.lineLayer
import com.mapbox.maps.extension.style.layers.generated.locationIndicatorLayer
import com.mapbox.maps.extension.style.layers.getLayerAs
import com.mapbox.maps.extension.style.layers.properties.generated.Visibility
import com.mapbox.maps.extension.style.sources.addSource
import com.mapbox.maps.extension.style.sources.generated.geoJsonSource
import com.mapbox.maps.extension.style.sources.generated.rasterDemSource
import com.mapbox.maps.extension.style.style
import com.mapbox.maps.extension.style.terrain.generated.terrain
import com.mapbox.maps.plugin.compass.compass
import com.mapbox.maps.plugin.gestures.gestures
import com.mapbox.maps.plugin.scalebar.scalebar
import com.mapbox.maps.testapp.R
import com.mapbox.maps.testapp.utils.BitmapUtils.bitmapFromDrawableRes
import com.mapbox.turf.TurfConstants
import com.mapbox.turf.TurfMeasurement
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response

/**
 * Example showcasing terrain with animating the free camera with a line string.
 */
class SantaCatalinaActivity : AppCompatActivity() {

  private lateinit var mapboxMap: MapboxMap
  private var timeAnimator: TimeAnimator? = null

  override fun onCreate(savedInstanceState: Bundle?) {
    requestWindowFeature(Window.FEATURE_NO_TITLE)
    super.onCreate(savedInstanceState)
    val mapView = MapView(this)
    setContentView(mapView)
    mapView.disablePlugins()

    // get map and setup initial camera
    mapboxMap = mapView.mapboxMap
    mapboxMap.setCamera(
      CameraOptions.Builder()
        .center(POINT_START)
        .zoom(14.0)
        .bearing(215.0)
        .build()
    )

    // load satellite style and add terrain with a line layer to visualize the route
    mapboxMap.loadStyle(
      style(style = Style.STANDARD_SATELLITE) {
        +rasterDemSource(SOURCE) {
          url(TERRAIN_URL_TILE_RESOURCE)
          // 514 specifies padded DEM tile and provides better performance than 512 tiles.
          tileSize(514)
        }
        +terrain(SOURCE) {
          exaggeration(TERRAIN_EXEGERATION)
        }
        +lineLayer(LINE_LAYER_ID, GEOJSON_SOURCE_ID) {
          lineColor(rgb(255, 79, 60))
          lineWidth(5.0)
          slot("middle")
        }
        +locationIndicatorLayer(LOCATION_LAYER_ID) {
          topImage(FOREGROUND_ICON)
          bearingImage(BACKGROUND_ICON)
          imagePitchDisplacement(5.0)
          topImageSize(1.5)
          bearingImageSize(1.5)
        }
        +image(
          FOREGROUND_ICON,
          bitmapFromDrawableRes(R.drawable.mapbox_mylocation_icon_default)
        )
        +image(
          BACKGROUND_ICON,
          bitmapFromDrawableRes(R.drawable.mapbox_mylocation_bg_shape)
        )
      }
    ) { style ->
      // hide road labels
      style.getLayerAs<SymbolLayer>(LAYER_ROAD_ID)?.visibility(Visibility.NONE)

      // execute direction request
      executeDirectionsRequestForRoute {
        // add a source to visualize the route
        addSourceForRoute(style, it)
        // animate the route
        animateRoute(it)
      }
    }
  }

  /**
   * Executes a directions request.
   * propagates the linestring geometry as high order function.
   */
  private fun executeDirectionsRequestForRoute(animateRoute: (LineString) -> Unit) {
    val routeOptions = RouteOptions.builder()
      .coordinatesList(listOf(POINT_START, POINT_END))
      .overview(DirectionsCriteria.OVERVIEW_SIMPLIFIED)
      .profile(DirectionsCriteria.PROFILE_WALKING)
      .steps(true)
      .build()
    val client = MapboxDirections.builder()
      .routeOptions(routeOptions)
      .accessToken(MapboxOptions.accessToken)
      .build()
    client.enqueueCall(object : Callback<DirectionsResponse> {
      override fun onResponse(
        call: Call<DirectionsResponse>,
        response: Response<DirectionsResponse>
      ) {
        response.body()?.let { body ->
          if (body.routes().isNotEmpty()) {
            body.routes()[0].geometry()?.let {
              animateRoute(
                LineString.fromPolyline(it, PRECISION_6)
              )
              return
            }
          }
        } ?: throw RuntimeException("Not able to retrieve a directions route")
      }

      override fun onFailure(call: Call<DirectionsResponse>, t: Throwable) {}
    })
  }

  /**
   * Adds a GeoJsonSource to a style to visualize a LineString
   */
  private fun addSourceForRoute(style: Style, lineString: LineString) {
    style.addSource(
      geoJsonSource(GEOJSON_SOURCE_ID) {
        geometry(lineString)
      }
    )
  }

  /**
   * Animate the route using FreeCamera API
   */
  private fun animateRoute(lineString: LineString) {
    // get the overall distance of each route so we can interpolate along them
    val routeDistance = TurfMeasurement.length(
      lineString, TurfConstants.UNIT_KILOMETERS
    )

    mapboxMap.getStyle {
      // get reference to the location layer
      val locationLayer = it.getLayerAs<LocationIndicatorLayer>(LOCATION_LAYER_ID)

      // cache the camera
      val camera = mapboxMap.getFreeCameraOptions()

      // use time animator to animate the lineString and location layer
      timeAnimator = TimeAnimator().apply {
        setTimeListener { animator, totalTime, _ ->

          // phase determines how far through the animation we are
          val phase: Double = totalTime / ANIMATION_DURATION

          // phase is normalized between 0 and 1
          // when the animation is finished, cancel the animation
          if (phase > 1) {
            animator.cancel()
          }

          // use phase to get a point that is the appropriate distance along the route
          val cameraLookingAt = TurfMeasurement.along(
            lineString, routeDistance * phase,
            TurfConstants.UNIT_KILOMETERS
          )

          // at start, we hover the current position first before trailing it
          var cameraPhase = phase - PHASE_DROP_OFF
          if (cameraPhase < 0) {
            cameraPhase = 0.0
          }

          // use phase that trails behind to be able to create a tilted camera
          val cameraLocation = TurfMeasurement.along(
            lineString, routeDistance * cameraPhase,
            TurfConstants.UNIT_KILOMETERS
          )

          // calculate the elevation of the user location
          val elevation = (ELEVATION_MAX * phase) + ELEVATION_MIN

          // Update location indicator
          locationLayer?.location(
            listOf(
              cameraLookingAt.latitude(),
              cameraLookingAt.longitude(),
              elevation
            )
          )

          // place the camera above the elevation of the user position
          val elevationDifference = BASE_CAMERA_ELEVATION + elevation

          // set the position and altitude of the camera
          camera.setLocation(cameraLocation, elevationDifference)

          // set the position to look with a decreased elevation for creating tilted camera
          camera.lookAtPoint(cameraLookingAt, elevation)

          // set the updated camera position
          mapboxMap.setCamera(camera)
        }
        duration = ANIMATION_DURATION.toLong()
        start()
      }
    }
  }

  override fun onDestroy() {
    super.onDestroy()
    timeAnimator?.cancel()
  }

  companion object {
    // Style constants
    private const val GEOJSON_SOURCE_ID = "geojson"
    private const val LINE_LAYER_ID = "line"
    private const val LOCATION_LAYER_ID = "location"
    private const val SOURCE = "TERRAIN_SOURCE"
    private const val TERRAIN_URL_TILE_RESOURCE = "mapbox://mapbox.mapbox-terrain-dem-v1"
    private const val TERRAIN_EXEGERATION = 1.7
    private const val FOREGROUND_ICON = "mapbox-location-icon"
    private const val BACKGROUND_ICON = "mapbox-location-stroke-icon"
    private const val LAYER_ROAD_ID = "road-label"

    // Camera animation constants
    private const val ANIMATION_DURATION = 75000.0
    private const val BASE_CAMERA_ELEVATION = 45.0
    private const val PHASE_DROP_OFF = 0.15
    private const val ELEVATION_MAX = 225.0
    private const val ELEVATION_MIN = 65.0
    private val POINT_START = Point.fromLngLat(-118.33283, 33.33470)
    private val POINT_END = Point.fromLngLat(-118.34183, 33.32387)
  }
}

fun MapView.disablePlugins() {
  scalebar.enabled = false
  compass.enabled = false

  gestures.pitchEnabled = false
  gestures.rotateEnabled = false
  gestures.pinchToZoomEnabled = false
  gestures.doubleTouchToZoomOutEnabled = false
  gestures.doubleTapToZoomInEnabled = false
  gestures.scrollEnabled = false
}
```