Skip to content

Both copy text to your clipboard — Build with AI copies a setup prompt to paste into Claude Code, Cursor, Codex or Copilot; Copy page as Markdown copies this page to paste into a chat. How it works

Display Multiple Geometry Types Together ​

This tutorial shows how to display markers, polylines, polygons, and circles all on the same MapMetrics Android map.

Prerequisites ​

Combined Geometries ​

Add points, lines, polygons, and circles in one view:

kotlin
import android.graphics.Color
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import org.maplibre.android.annotations.MarkerOptions
import org.maplibre.android.camera.CameraUpdateFactory
import org.maplibre.android.geometry.LatLng
import org.maplibre.android.geometry.LatLngBounds
import org.maplibre.android.maps.MapView
import org.maplibre.android.maps.MapLibreMap
import org.maplibre.android.maps.Style
import org.maplibre.android.style.layers.CircleLayer
import org.maplibre.android.style.layers.FillLayer
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.*
import com.google.gson.JsonObject
import kotlin.math.*

class MultiGeometryActivity : AppCompatActivity() {

    private lateinit var mapView: MapView
    private lateinit var map: MapLibreMap

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

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

            map.setStyle(
                Style.Builder().fromUri(
                    "https://gateway.mapmetrics-atlas.net/styles/YOUR_STYLE_ID?token=YOUR_API_KEY"
                )
            ) { style ->
                addPolygonZone(style)
                addRouteLines(style)
                addPointMarkers(style)
                addCircleRadius(style)
                fitToAll()
            }
        }
    }

    // 1. Polygon — park/zone boundary
    private fun addPolygonZone(style: Style) {
        val parkBoundary = Polygon.fromLngLats(listOf(listOf(
            Point.fromLngLat(2.3300, 48.8470),
            Point.fromLngLat(2.3400, 48.8470),
            Point.fromLngLat(2.3420, 48.8440),
            Point.fromLngLat(2.3380, 48.8420),
            Point.fromLngLat(2.3280, 48.8430),
            Point.fromLngLat(2.3300, 48.8470),
        )))

        style.addSource(
            GeoJsonSource("zone-source", Feature.fromGeometry(parkBoundary))
        )

        style.addLayer(
            FillLayer("zone-fill", "zone-source")
                .withProperties(
                    fillColor(Color.parseColor("#34A853")),
                    fillOpacity(0.2f)
                )
        )

        style.addLayer(
            LineLayer("zone-outline", "zone-source")
                .withProperties(
                    lineColor(Color.parseColor("#34A853")),
                    lineWidth(2f)
                )
        )
    }

    // 2. Polylines — walking routes
    private fun addRouteLines(style: Style) {
        val route1 = Feature.fromGeometry(
            LineString.fromLngLats(listOf(
                Point.fromLngLat(2.3350, 48.8560),
                Point.fromLngLat(2.3380, 48.8530),
                Point.fromLngLat(2.3360, 48.8490),
                Point.fromLngLat(2.3340, 48.8460),
            )),
            JsonObject().apply { addProperty("name", "Route A") }
        )

        val route2 = Feature.fromGeometry(
            LineString.fromLngLats(listOf(
                Point.fromLngLat(2.3200, 48.8550),
                Point.fromLngLat(2.3250, 48.8520),
                Point.fromLngLat(2.3300, 48.8500),
                Point.fromLngLat(2.3350, 48.8480),
            )),
            JsonObject().apply { addProperty("name", "Route B") }
        )

        style.addSource(
            GeoJsonSource("routes-source",
                FeatureCollection.fromFeatures(listOf(route1, route2)))
        )

        style.addLayer(
            LineLayer("routes-layer", "routes-source")
                .withProperties(
                    lineColor(Color.parseColor("#4285F4")),
                    lineWidth(3f),
                    lineOpacity(0.8f)
                )
        )
    }

    // 3. Point markers — landmarks
    private fun addPointMarkers(style: Style) {
        val landmarks = listOf(
            Triple(2.3376, 48.8606, "Louvre Museum"),
            Triple(2.3499, 48.8530, "Notre-Dame"),
            Triple(2.3266, 48.8600, "Musée d'Orsay"),
        )

        for ((lng, lat, name) in landmarks) {
            map.addMarker(
                MarkerOptions()
                    .position(LatLng(lat, lng))
                    .title(name)
            )
        }
    }

    // 4. Circle — radius zone around a point
    private fun addCircleRadius(style: Style) {
        val center = LatLng(48.8530, 2.3499) // Notre-Dame
        val circlePolygon = createCirclePolygon(center, 300.0, 64)

        style.addSource(
            GeoJsonSource("circle-source", Feature.fromGeometry(circlePolygon))
        )

        style.addLayer(
            FillLayer("circle-fill", "circle-source")
                .withProperties(
                    fillColor(Color.parseColor("#FF6B35")),
                    fillOpacity(0.15f)
                )
        )

        style.addLayer(
            LineLayer("circle-outline", "circle-source")
                .withProperties(
                    lineColor(Color.parseColor("#FF6B35")),
                    lineWidth(1.5f),
                    lineDasharray(arrayOf(2f, 1f))
                )
        )
    }

    private fun fitToAll() {
        val bounds = LatLngBounds.Builder()
            .include(LatLng(48.8606, 2.3376))
            .include(LatLng(48.8420, 2.3280))
            .include(LatLng(48.8600, 2.3266))
            .include(LatLng(48.8560, 2.3350))
            .build()

        map.easeCamera(
            CameraUpdateFactory.newLatLngBounds(bounds, 80),
            1000
        )
    }

    private fun createCirclePolygon(center: LatLng, radiusMeters: Double, steps: Int): Polygon {
        val points = mutableListOf<Point>()
        val earthRadius = 6371000.0

        for (i in 0..steps) {
            val angle = Math.toRadians((360.0 / steps) * i)
            val lat = Math.toRadians(center.latitude)
            val lng = Math.toRadians(center.longitude)

            val newLat = asin(
                sin(lat) * cos(radiusMeters / earthRadius) +
                cos(lat) * sin(radiusMeters / earthRadius) * cos(angle)
            )
            val newLng = lng + atan2(
                sin(angle) * sin(radiusMeters / earthRadius) * cos(lat),
                cos(radiusMeters / earthRadius) - sin(lat) * sin(newLat)
            )

            points.add(Point.fromLngLat(Math.toDegrees(newLng), Math.toDegrees(newLat)))
        }

        return Polygon.fromLngLats(listOf(points))
    }

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

Layer Rendering Order ​

Layers render in the order they're added. The correct visual stacking:

Top     →  Annotation markers (always on top)
          Symbol layers (icons, labels)
          Circle layers (data points)
          Line layers (routes, borders)
Bottom  →  Fill layers (zones, polygons)

Use addLayerBelow or addLayerAbove for precise control:

kotlin
style.addLayerBelow(fillLayer, "routes-layer")   // fills below lines
style.addLayerAbove(circleLayer, "routes-layer")  // points above lines

Next Steps ​


Tip: Add fills first, then lines, then points. This ensures points remain clickable on top and lines are visible above filled zones.