Skip to content

Add an Image Overlay

This tutorial shows how to overlay an image (photo, floor plan, historical map) on top of your MapMetrics Android map, positioned at specific geographic coordinates.

Prerequisites

Basic Image Overlay

Place an image on the map anchored to four corner coordinates:

kotlin
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import org.maplibre.android.camera.CameraPosition
import org.maplibre.android.geometry.LatLng
import org.maplibre.android.geometry.LatLngQuad
import org.maplibre.android.maps.MapView
import org.maplibre.android.maps.MapMetricsMap
import org.maplibre.android.maps.Style
import org.maplibre.android.style.layers.RasterLayer
import org.maplibre.android.style.layers.PropertyFactory.*
import org.maplibre.android.style.sources.ImageSource

class ImageOverlayActivity : 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 ->
                addImageOverlay(style)
            }
        }
    }

    private fun addImageOverlay(style: Style) {
        // Define the four corners of the image placement
        val quad = LatLngQuad(
            LatLng(48.8640, 2.2880),  // Top-left (NW)
            LatLng(48.8640, 2.3010),  // Top-right (NE)
            LatLng(48.8530, 2.3010),  // Bottom-right (SE)
            LatLng(48.8530, 2.2880)   // Bottom-left (SW)
        )

        // Add image source from drawable resource
        style.addSource(
            ImageSource("overlay-source", quad, R.drawable.historic_map)
        )

        // Add raster layer to display the image
        style.addLayer(
            RasterLayer("overlay-layer", "overlay-source")
                .withProperties(
                    rasterOpacity(0.7f)
                )
        )

        // Center on the overlay
        map.cameraPosition = CameraPosition.Builder()
            .target(LatLng(48.8585, 2.2945))
            .zoom(15.0)
            .build()
    }

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

Adjustable Opacity

Let users control the overlay transparency:

kotlin
import android.widget.SeekBar
import android.widget.TextView

private fun setupOpacitySlider(style: Style) {
    val opacityText = findViewById<TextView>(R.id.tvOpacity)

    findViewById<SeekBar>(R.id.seekOpacity).apply {
        max = 100
        progress = 70 // default 70%

        setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
            override fun onProgressChanged(seekBar: SeekBar?, value: Int, user: Boolean) {
                val opacity = value / 100f
                opacityText.text = "Opacity: $value%"

                val layer = style.getLayer("overlay-layer") as? RasterLayer
                layer?.setProperties(rasterOpacity(opacity))
            }
            override fun onStartTrackingTouch(seekBar: SeekBar?) {}
            override fun onStopTrackingTouch(seekBar: SeekBar?) {}
        })
    }
}

Toggle Overlay Visibility

Show or hide the overlay with a button:

kotlin
import android.widget.ToggleButton
import org.maplibre.android.style.layers.Property

private var overlayVisible = true

private fun setupToggle(style: Style) {
    findViewById<ToggleButton>(R.id.btnToggleOverlay).setOnCheckedChangeListener { _, checked ->
        overlayVisible = checked
        val layer = style.getLayer("overlay-layer") as? RasterLayer
        layer?.setProperties(
            visibility(
                if (checked) Property.VISIBLE else Property.NONE
            )
        )
    }
}

Next Steps


Tip: Image overlays work best for small areas (a few blocks). For larger coverage, use raster tile sources instead — they load tiles progressively and don't require loading one massive image into memory.