Skip to content

Jump Through a Series of Locations

This tutorial shows how to navigate through a list of locations one by one — useful for guided tours, location lists, or step-by-step waypoints.

Prerequisites

Step Through Locations

Navigate forward and backward through a list:

kotlin
import android.os.Bundle
import android.widget.Button
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import org.maplibre.android.annotations.MarkerOptions
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 JumpLocationsActivity : AppCompatActivity() {

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

    data class Location(
        val position: LatLng,
        val name: String,
        val description: String,
        val zoom: Double
    )

    private val locations = listOf(
        Location(LatLng(48.8584, 2.2945), "Eiffel Tower", "324m iron lattice tower", 16.0),
        Location(LatLng(48.8606, 2.3376), "Louvre Museum", "World's largest art museum", 16.0),
        Location(LatLng(48.8530, 2.3499), "Notre-Dame", "Medieval Catholic cathedral", 17.0),
        Location(LatLng(48.8738, 2.2950), "Arc de Triomphe", "Triumphal arch monument", 16.0),
        Location(LatLng(48.8867, 2.3431), "Sacré-Cœur", "White-domed basilica", 16.0),
        Location(LatLng(48.8462, 2.3464), "Luxembourg Gardens", "Historic public garden", 15.0),
    )

    private var currentIndex = 0

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

        locationLabel = findViewById(R.id.tvLocation)
        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"
                )
            ) { style ->
                // Add markers for all locations
                for (loc in locations) {
                    map.addMarker(
                        MarkerOptions()
                            .position(loc.position)
                            .title(loc.name)
                            .snippet(loc.description)
                    )
                }

                // Go to first location
                jumpToLocation(0)
            }

            // Navigation buttons
            findViewById<Button>(R.id.btnPrev).setOnClickListener {
                if (currentIndex > 0) jumpToLocation(currentIndex - 1)
            }

            findViewById<Button>(R.id.btnNext).setOnClickListener {
                if (currentIndex < locations.size - 1) jumpToLocation(currentIndex + 1)
            }
        }
    }

    private fun jumpToLocation(index: Int) {
        currentIndex = index
        val loc = locations[index]

        // Update label
        locationLabel.text = "${index + 1}/${locations.size} — ${loc.name}"

        // Animate camera
        map.animateCamera(
            CameraUpdateFactory.newCameraPosition(
                CameraPosition.Builder()
                    .target(loc.position)
                    .zoom(loc.zoom)
                    .tilt(45.0)
                    .bearing(0.0)
                    .build()
            ),
            1500
        )

        // Update button states
        findViewById<Button>(R.id.btnPrev).isEnabled = index > 0
        findViewById<Button>(R.id.btnNext).isEnabled = index < locations.size - 1
    }

    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)
    }
}

Layout (res/layout/activity_jump_locations.xml):

xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <org.maplibre.android.maps.MapView
        android:id="@+id/mapView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        android:background="#DD000000"
        android:padding="16dp"
        android:gravity="center"
        android:orientation="horizontal">

        <Button
            android:id="@+id/btnPrev"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="← Previous" />

        <TextView
            android:id="@+id/tvLocation"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="center"
            android:textColor="#FFFFFF"
            android:textSize="16sp" />

        <Button
            android:id="@+id/btnNext"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Next →" />

    </LinearLayout>

</FrameLayout>

Auto-Play Tour

Automatically cycle through locations:

kotlin
import android.os.Handler
import android.os.Looper

private val autoPlayHandler = Handler(Looper.getMainLooper())
private var isAutoPlaying = false

private fun startAutoPlay() {
    isAutoPlaying = true

    val autoPlayRunnable = object : Runnable {
        override fun run() {
            if (!isAutoPlaying) return

            val nextIndex = (currentIndex + 1) % locations.size
            jumpToLocation(nextIndex)

            autoPlayHandler.postDelayed(this, 4000) // 4s per stop
        }
    }

    autoPlayHandler.postDelayed(autoPlayRunnable, 4000)
}

private fun stopAutoPlay() {
    isAutoPlaying = false
    autoPlayHandler.removeCallbacksAndMessages(null)
}

Next Steps


Tip: Vary the zoom level per location for a more engaging tour — zoom in close (17-18) for small landmarks and zoom out (13-14) for districts or parks.