Skip to content

Set Pitch and Bearing

This tutorial shows how to control the 3D perspective (pitch/tilt) and compass direction (bearing/rotation) of your MapMetrics Android map.

Prerequisites

Set Pitch and Bearing on Load

Configure the initial 3D view:

kotlin
import android.os.Bundle
import android.widget.Button
import android.widget.SeekBar
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import org.maplibre.android.camera.CameraPosition
import org.maplibre.android.camera.CameraUpdateFactory
import org.maplibre.android.geometry.LatLng
import org.maplibre.android.maps.MapView
import org.maplibre.android.maps.MapMetricsMap
import org.maplibre.android.maps.Style

class PitchBearingActivity : AppCompatActivity() {

    private lateinit var mapView: MapView
    private lateinit var map: MapMetricsMap

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_pitch_bearing)

        mapView = findViewById(R.id.mapView)
        mapView.onCreate(savedInstanceState)
        mapView.getMapAsync { mapMetricsMap ->
            map = mapMetricsMap

            map.setStyle(
                Style.Builder().fromUri(
                    "https://gateway.mapmetrics.org/styles/YOUR_STYLE_ID?token=YOUR_API_KEY"
                )
            )

            // Set initial 3D perspective
            map.cameraPosition = CameraPosition.Builder()
                .target(LatLng(48.8584, 2.2945))
                .zoom(16.0)
                .tilt(60.0)     // Pitch: 0 = top-down, 60 = dramatic 3D
                .bearing(45.0)  // Bearing: 0 = north up, 45 = northeast
                .build()

            setupControls()
        }
    }

    private fun setupControls() {
        val pitchText = findViewById<TextView>(R.id.tvPitch)
        val bearingText = findViewById<TextView>(R.id.tvBearing)

        // Pitch slider (0-60)
        findViewById<SeekBar>(R.id.seekPitch).apply {
            max = 60
            progress = map.cameraPosition.tilt.toInt()
            setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
                override fun onProgressChanged(seekBar: SeekBar?, value: Int, user: Boolean) {
                    if (!user) return
                    pitchText.text = "Pitch: $value°"
                    map.moveCamera(
                        CameraUpdateFactory.newCameraPosition(
                            CameraPosition.Builder(map.cameraPosition)
                                .tilt(value.toDouble())
                                .build()
                        )
                    )
                }
                override fun onStartTrackingTouch(seekBar: SeekBar?) {}
                override fun onStopTrackingTouch(seekBar: SeekBar?) {}
            })
        }

        // Bearing slider (0-360)
        findViewById<SeekBar>(R.id.seekBearing).apply {
            max = 360
            progress = map.cameraPosition.bearing.toInt()
            setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
                override fun onProgressChanged(seekBar: SeekBar?, value: Int, user: Boolean) {
                    if (!user) return
                    bearingText.text = "Bearing: $value°"
                    map.moveCamera(
                        CameraUpdateFactory.newCameraPosition(
                            CameraPosition.Builder(map.cameraPosition)
                                .bearing(value.toDouble())
                                .build()
                        )
                    )
                }
                override fun onStartTrackingTouch(seekBar: SeekBar?) {}
                override fun onStopTrackingTouch(seekBar: SeekBar?) {}
            })
        }
    }

    override fun onStart() { super.onStart(); mapView.onStart() }
    override fun onResume() { super.onResume(); mapView.onResume() }
    override fun onPause() { super.onPause(); mapView.onPause() }
    override fun onStop() { super.onStop(); mapView.onStop() }
    override fun onDestroy() { super.onDestroy(); mapView.onDestroy() }
    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        mapView.onSaveInstanceState(outState)
    }
}

Preset Camera Views

Offer buttons for common perspective angles:

kotlin
private fun setupPresets() {
    // Top-down (2D)
    findViewById<Button>(R.id.btnTopDown).setOnClickListener {
        animateTo(tilt = 0.0, bearing = 0.0, zoom = 14.0)
    }

    // Gentle 3D
    findViewById<Button>(R.id.btnGentle3d).setOnClickListener {
        animateTo(tilt = 30.0, bearing = 0.0, zoom = 15.0)
    }

    // Dramatic 3D
    findViewById<Button>(R.id.btnDramatic3d).setOnClickListener {
        animateTo(tilt = 60.0, bearing = -30.0, zoom = 16.0)
    }

    // Street level
    findViewById<Button>(R.id.btnStreetLevel).setOnClickListener {
        animateTo(tilt = 60.0, bearing = 90.0, zoom = 18.0)
    }

    // Reset to north
    findViewById<Button>(R.id.btnResetNorth).setOnClickListener {
        animateTo(tilt = 0.0, bearing = 0.0, zoom = map.cameraPosition.zoom)
    }
}

private fun animateTo(tilt: Double, bearing: Double, zoom: Double) {
    map.animateCamera(
        CameraUpdateFactory.newCameraPosition(
            CameraPosition.Builder()
                .target(map.cameraPosition.target)
                .zoom(zoom)
                .tilt(tilt)
                .bearing(bearing)
                .build()
        ),
        1500
    )
}

Limit Pitch Range via XML

Set pitch limits in the layout:

xml
<org.maplibre.android.maps.MapView
    android:id="@+id/mapView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:maplibre_cameraPitchMax="60"
    app:maplibre_cameraPitchMin="0"
    app:maplibre_cameraTilt="45"
    app:maplibre_cameraBearing="0"
    app:maplibre_cameraZoom="15" />

Limit Pitch Range Programmatically

kotlin
val options = MapMetricsMapOptions.createFromAttributes(this, null)
    .maxPitchPreference(60.0)
    .minPitchPreference(0.0)
    .camera(
        CameraPosition.Builder()
            .target(LatLng(48.8584, 2.2945))
            .zoom(15.0)
            .tilt(45.0)
            .build()
    )

mapView = MapView(this, options)

Pitch and Bearing Reference

PropertyRangeDescription
Tilt/Pitch0 - 600 = top-down, 60 = maximum 3D perspective
Bearing0 - 3600/360 = north up, 90 = east up, 180 = south up, 270 = west up

Common Perspective Combinations

ViewTiltBearingZoomEffect
Standard 2D12-14Classic flat map
Gentle 3D30°14-16Subtle depth
Dramatic 3D60°Any15-17Skyline visible
Street Level60°Route direction17-19Navigation feel
Cinematic55°Slowly rotating16Showcase mode

Next Steps


Tip: 3D buildings and fill-extrusion layers are most impressive at tilt 45-60°. At tilt 0° (top-down), 3D extrusions are invisible since you're looking straight down on them.