Combine Multiple Data Sources
This tutorial shows how to load and display data from multiple GeoJSON sources simultaneously on your MapMetrics Android map — useful for layering different datasets like POIs, routes, and zones.
Prerequisites
- Completed the Getting Started Guide
- A MapMetrics API key and style URL from the MapMetrics Portal
Multiple Sources with Different Layer Types
Add points, lines, and polygons from separate sources:
kotlin
import android.graphics.Color
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.google.gson.JsonObject
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.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.*
class MultiSourceActivity : 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 ->
addZones(style) // Polygons
addRoutes(style) // Lines
addPointsOfInterest(style) // Points
setCamera()
}
}
}
private fun addZones(style: Style) {
val zone1 = Feature.fromGeometry(
Polygon.fromLngLats(listOf(listOf(
Point.fromLngLat(2.32, 48.87),
Point.fromLngLat(2.35, 48.87),
Point.fromLngLat(2.35, 48.855),
Point.fromLngLat(2.32, 48.855),
Point.fromLngLat(2.32, 48.87),
))),
JsonObject().apply { addProperty("name", "Zone A") }
)
val zone2 = Feature.fromGeometry(
Polygon.fromLngLats(listOf(listOf(
Point.fromLngLat(2.35, 48.855),
Point.fromLngLat(2.38, 48.855),
Point.fromLngLat(2.38, 48.84),
Point.fromLngLat(2.35, 48.84),
Point.fromLngLat(2.35, 48.855),
))),
JsonObject().apply { addProperty("name", "Zone B") }
)
style.addSource(
GeoJsonSource("zones-source",
FeatureCollection.fromFeatures(listOf(zone1, zone2)))
)
// Fill layer for zones
style.addLayer(
FillLayer("zones-fill", "zones-source")
.withProperties(
fillColor(Color.parseColor("#4285F4")),
fillOpacity(0.2f)
)
)
// Outline layer for zones
style.addLayer(
LineLayer("zones-outline", "zones-source")
.withProperties(
lineColor(Color.parseColor("#4285F4")),
lineWidth(2f)
)
)
}
private fun addRoutes(style: Style) {
val route = Feature.fromGeometry(
LineString.fromLngLats(listOf(
Point.fromLngLat(2.3200, 48.8600),
Point.fromLngLat(2.3350, 48.8580),
Point.fromLngLat(2.3500, 48.8560),
Point.fromLngLat(2.3600, 48.8500),
Point.fromLngLat(2.3700, 48.8450),
))
)
style.addSource(
GeoJsonSource("routes-source", FeatureCollection.fromFeature(route))
)
style.addLayer(
LineLayer("routes-layer", "routes-source")
.withProperties(
lineColor(Color.parseColor("#FF6B35")),
lineWidth(4f),
lineOpacity(0.8f)
)
)
}
private fun addPointsOfInterest(style: Style) {
val pois = listOf(
Triple(2.3376, 48.8606, "Louvre Museum"),
Triple(2.3266, 48.8600, "Musée d'Orsay"),
Triple(2.3499, 48.8530, "Notre-Dame"),
Triple(2.3464, 48.8462, "Luxembourg Gardens"),
Triple(2.3532, 48.8619, "Centre Pompidou"),
)
val features = pois.map { (lng, lat, name) ->
Feature.fromGeometry(
Point.fromLngLat(lng, lat),
JsonObject().apply { addProperty("name", name) }
)
}
style.addSource(
GeoJsonSource("pois-source", FeatureCollection.fromFeatures(features))
)
style.addLayer(
CircleLayer("pois-layer", "pois-source")
.withProperties(
circleRadius(8f),
circleColor(Color.parseColor("#34A853")),
circleStrokeColor(Color.WHITE),
circleStrokeWidth(2f)
)
)
}
private fun setCamera() {
map.cameraPosition = CameraPosition.Builder()
.target(LatLng(48.855, 2.350))
.zoom(13.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)
}
}Toggle Layer Visibility
Let users show/hide each data layer:
kotlin
import android.widget.ToggleButton
import org.maplibre.android.style.layers.Property
private fun setupLayerToggles(style: Style) {
val layerToggles = mapOf(
R.id.btnZones to listOf("zones-fill", "zones-outline"),
R.id.btnRoutes to listOf("routes-layer"),
R.id.btnPois to listOf("pois-layer"),
)
for ((btnId, layerIds) in layerToggles) {
findViewById<ToggleButton>(btnId).setOnCheckedChangeListener { _, checked ->
for (layerId in layerIds) {
val layer = style.getLayer(layerId)
layer?.setProperties(
visibility(
if (checked) Property.VISIBLE else Property.NONE
)
)
}
}
}
}Mix Local and Remote Sources
Combine bundled data with API data:
kotlin
private fun addMixedSources(style: Style) {
// Remote: live data from API
style.addSource(
GeoJsonSource(
"live-data",
java.net.URI("https://api.example.com/live-events.geojson")
)
)
style.addLayer(
CircleLayer("live-layer", "live-data")
.withProperties(
circleRadius(6f),
circleColor(Color.RED)
)
)
// Local: static boundary from assets
val boundaryJson = assets.open("data/city-boundary.geojson")
.bufferedReader().readText()
style.addSource(
GeoJsonSource("boundary", FeatureCollection.fromJson(boundaryJson))
)
style.addLayer(
LineLayer("boundary-layer", "boundary")
.withProperties(
lineColor(Color.parseColor("#333333")),
lineWidth(2f),
lineDasharray(arrayOf(3f, 2f))
)
)
}Layer Ordering
Control which layers appear on top:
kotlin
// Add below a specific layer
style.addLayerBelow(fillLayer, "road-label")
// Add above a specific layer
style.addLayerAbove(circleLayer, "zones-fill")
// Typical order (bottom to top):
// 1. Fill layers (zones, polygons)
// 2. Line layers (routes, boundaries)
// 3. Circle/Symbol layers (POIs, markers)Next Steps
- GeoJSON from URL — Loading remote data
- GeoJSON Guide — GeoJSON fundamentals
- Filter Features — Filter visible data
Tip: Add layers in the correct visual order — fills first, then lines, then points. Use addLayerBelow or addLayerAbove to insert layers at specific positions in the rendering stack. Points should always be on top so they remain clickable.