Skip to content

Measure Distances on the Map

This tutorial shows how to build a tap-to-measure tool that calculates distances between points on your MapMetrics Android map using the Haversine formula.

Prerequisites

Tap-to-Measure Tool

Tap points on the map to measure the total distance:

kotlin
import android.graphics.Color
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.geometry.LatLng
import org.maplibre.android.maps.MapView
import org.maplibre.android.maps.MapMetricsMap
import org.maplibre.android.maps.Style
import org.maplibre.android.style.layers.LineLayer
import org.maplibre.android.style.layers.PropertyFactory.*
import org.maplibre.android.style.sources.GeoJsonSource
import org.maplibre.geojson.Feature
import org.maplibre.geojson.LineString
import org.maplibre.geojson.Point
import kotlin.math.*

class MeasureActivity : AppCompatActivity() {

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

    private val measurePoints = mutableListOf<LatLng>()

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

        distanceText = findViewById(R.id.tvDistance)
        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 ->
                setupMeasureLine(style)
                setupMapClick()

                map.cameraPosition = CameraPosition.Builder()
                    .target(LatLng(48.8566, 2.3522))
                    .zoom(12.0)
                    .build()
            }

            // Clear button
            findViewById<Button>(R.id.btnClear).setOnClickListener {
                clearMeasurement()
            }
        }
    }

    private fun setupMeasureLine(style: Style) {
        style.addSource(GeoJsonSource("measure-source"))

        style.addLayer(
            LineLayer("measure-line", "measure-source")
                .withProperties(
                    lineColor(Color.parseColor("#FF6B35")),
                    lineWidth(3f),
                    lineDasharray(arrayOf(2f, 1f))
                )
        )
    }

    private fun setupMapClick() {
        map.addOnMapClickListener { latLng ->
            // Add point
            measurePoints.add(latLng)

            // Add marker at tap point
            map.addMarker(
                MarkerOptions()
                    .position(latLng)
                    .title("Point ${measurePoints.size}")
                    .snippet(
                        "Lat: ${String.format("%.6f", latLng.latitude)}, " +
                        "Lng: ${String.format("%.6f", latLng.longitude)}"
                    )
            )

            // Update line
            if (measurePoints.size >= 2) {
                updateMeasureLine()
            }

            // Update total distance
            updateDistance()

            true
        }
    }

    private fun updateMeasureLine() {
        val points = measurePoints.map {
            Point.fromLngLat(it.longitude, it.latitude)
        }
        val lineString = LineString.fromLngLats(points)

        val source = map.style?.getSource("measure-source") as? GeoJsonSource
        source?.setGeoJson(Feature.fromGeometry(lineString))
    }

    private fun updateDistance() {
        var totalDistance = 0.0

        for (i in 0 until measurePoints.size - 1) {
            totalDistance += haversineDistance(
                measurePoints[i], measurePoints[i + 1]
            )
        }

        distanceText.text = when {
            totalDistance < 1.0 -> "${(totalDistance * 1000).toInt()} m"
            else -> String.format("%.2f km", totalDistance)
        }
    }

    private fun clearMeasurement() {
        measurePoints.clear()
        map.clear()
        distanceText.text = "0 m"

        val source = map.style?.getSource("measure-source") as? GeoJsonSource
        source?.setGeoJson(Feature.fromGeometry(LineString.fromLngLats(emptyList())))
    }

    /**
     * Calculate distance between two points using the Haversine formula.
     * Returns distance in kilometers.
     */
    private fun haversineDistance(point1: LatLng, point2: LatLng): Double {
        val R = 6371.0 // Earth's radius in km

        val lat1 = Math.toRadians(point1.latitude)
        val lat2 = Math.toRadians(point2.latitude)
        val dLat = Math.toRadians(point2.latitude - point1.latitude)
        val dLng = Math.toRadians(point2.longitude - point1.longitude)

        val a = sin(dLat / 2).pow(2) +
                cos(lat1) * cos(lat2) * sin(dLng / 2).pow(2)
        val c = 2 * atan2(sqrt(a), sqrt(1 - a))

        return R * c
    }

    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_measure.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="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="top|center_horizontal"
        android:layout_marginTop="16dp"
        android:background="#CC000000"
        android:padding="12dp"
        android:orientation="horizontal"
        android:gravity="center_vertical">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Distance: "
            android:textColor="#FFFFFF"
            android:textSize="16sp" />

        <TextView
            android:id="@+id/tvDistance"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="0 m"
            android:textColor="#FF6B35"
            android:textStyle="bold"
            android:textSize="18sp" />

        <Button
            android:id="@+id/btnClear"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Clear"
            android:layout_marginStart="12dp" />

    </LinearLayout>

</FrameLayout>

Show Segment Distances

Display distance on each segment:

kotlin
private fun updateDistance() {
    var totalDistance = 0.0
    val segmentInfo = StringBuilder()

    for (i in 0 until measurePoints.size - 1) {
        val segmentDist = haversineDistance(measurePoints[i], measurePoints[i + 1])
        totalDistance += segmentDist

        segmentInfo.append("Segment ${i + 1}: ")
        segmentInfo.append(
            if (segmentDist < 1.0) "${(segmentDist * 1000).toInt()} m"
            else String.format("%.2f km", segmentDist)
        )
        segmentInfo.append("\n")
    }

    distanceText.text = when {
        totalDistance < 1.0 -> "${(totalDistance * 1000).toInt()} m total"
        else -> String.format("%.2f km total", totalDistance)
    }
}

Next Steps


Tip: The Haversine formula gives great-circle distances — straight line over the Earth's surface. For road distances, you would need a routing API. The measurements shown here are accurate for aerial/straight-line distance.