Skip to content

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.MapMetricsMap
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: MapMetricsMap

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

        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 ->
                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.