# MapLibre Compose MapLibre Compose is a Kotlin Multiplatform library that provides Compose wrappers around MapLibre SDKs for rendering interactive maps across Android, iOS, Desktop, and Web platforms. It enables developers to integrate MapLibre mapping functionality into their Compose Multiplatform applications using a declarative API that follows Compose principles. The library supports camera controls, layers, data sources, gestures, and offline map management. The library uses platform-specific implementations: MapLibre Native SDKs for Android and iOS, MapLibre GL JS for Web via Kotlin/JS bindings, and MapLibre Native Core for Desktop via JNI bindings. It provides a unified API surface across all platforms while exposing platform-specific configuration options where needed. The project includes core modules (`maplibre-compose`), Material 3 themed components (`maplibre-compose-material3`), and platform bindings for JavaScript and native platforms. ## MaplibreMap - Basic Map Display Display an interactive map in a Composable with default settings. The MaplibreMap composable is the main entry point for rendering maps. ```kotlin import androidx.compose.runtime.Composable import org.maplibre.compose.map.MaplibreMap @Composable fun MyApp() { MaplibreMap() } ``` ## MaplibreMap - Custom Style Configure the map with a custom style URL to change the visual appearance, including support for dark mode and third-party tile providers. ```kotlin import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.runtime.Composable import org.maplibre.compose.map.MaplibreMap import org.maplibre.compose.style.BaseStyle @Composable fun StyledMap() { // Static style MaplibreMap(baseStyle = BaseStyle.Uri("https://tiles.openfreemap.org/styles/liberty")) // Dynamic style based on theme val variant = if (isSystemInDarkTheme()) "dark" else "light" MaplibreMap( baseStyle = BaseStyle.Uri("https://api.protomaps.com/styles/v4/$variant/en.json?key=MY_KEY") ) } ``` ## CameraState - Camera Control and Animation Control and observe camera position, including zoom, bearing, tilt, and target location. Supports both immediate updates and animated transitions. ```kotlin import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import io.github.dellisd.spatialk.geojson.Position import org.maplibre.compose.camera.CameraPosition import org.maplibre.compose.camera.rememberCameraState import org.maplibre.compose.map.MaplibreMap import kotlin.time.Duration.Companion.seconds @Composable fun CameraExample() { val camera = rememberCameraState( firstPosition = CameraPosition( target = Position(latitude = 45.521, longitude = -122.675), zoom = 13.0 ) ) MaplibreMap(cameraState = camera) // Animate camera to new position LaunchedEffect(Unit) { camera.animateTo( finalPosition = camera.position.copy( target = Position(latitude = 47.607, longitude = -122.342) ), duration = 3.seconds ) } } ``` ## CircleLayer - Simple Layer from Base Source Add a circle layer to visualize point features from an existing style source. Layers must reference a source and can optionally filter by source layer. ```kotlin import androidx.compose.runtime.Composable import org.maplibre.compose.layers.CircleLayer import org.maplibre.compose.map.MaplibreMap import org.maplibre.compose.sources.getBaseSource import org.maplibre.compose.style.BaseStyle @Composable fun SimpleLayer() { MaplibreMap(baseStyle = BaseStyle.Uri("https://tiles.openfreemap.org/styles/liberty")) { getBaseSource(id = "openmaptiles")?.let { tiles -> CircleLayer(id = "example", source = tiles, sourceLayer = "poi") } } } ``` ## LineLayer - Styled Line with Expressions Style line features with colors, widths, caps, and joins. Use expressions for dynamic styling based on zoom levels. ```kotlin import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import org.jetbrains.compose.resources.ExperimentalResourceApi import org.maplibre.compose.demoapp.generated.Res import org.maplibre.compose.expressions.dsl.const import org.maplibre.compose.expressions.dsl.exponential import org.maplibre.compose.expressions.dsl.interpolate import org.maplibre.compose.expressions.dsl.zoom import org.maplibre.compose.expressions.value.LineCap import org.maplibre.compose.expressions.value.LineJoin import org.maplibre.compose.layers.LineLayer import org.maplibre.compose.map.MaplibreMap import org.maplibre.compose.sources.GeoJsonData import org.maplibre.compose.sources.rememberGeoJsonSource @OptIn(ExperimentalResourceApi::class) @Composable fun StyledLines() { MaplibreMap { val amtrakRoutes = rememberGeoJsonSource( GeoJsonData.Uri(Res.getUri("files/data/amtrak_routes.geojson")) ) // Casing (outline) LineLayer( id = "amtrak-routes-casing", source = amtrakRoutes, color = const(Color.White), width = const(6.dp) ) // Main line with zoom-dependent width LineLayer( id = "amtrak-routes", source = amtrakRoutes, cap = const(LineCap.Round), join = const(LineJoin.Round), color = const(Color.Blue), width = interpolate( type = exponential(1.2f), input = zoom(), 5 to const(0.4.dp), 6 to const(0.7.dp), 7 to const(1.75.dp), 20 to const(22.dp) ) ) } } ``` ## Layer Anchoring Position layers relative to existing style layers using anchors to control z-order without modifying the base style. ```kotlin import androidx.compose.runtime.Composable import org.maplibre.compose.layers.Anchor import org.maplibre.compose.layers.LineLayer import org.maplibre.compose.map.MaplibreMap import org.maplibre.compose.sources.rememberGeoJsonSource import org.maplibre.compose.sources.GeoJsonData @Composable fun LayerAnchoring() { MaplibreMap { val amtrakRoutes = rememberGeoJsonSource( GeoJsonData.Uri("https://example.com/routes.geojson") ) // Place layer above the "road_motorway" layer from the base style Anchor.Above("road_motorway") { LineLayer(id = "amtrak-routes", source = amtrakRoutes) } } } ``` ## GeoJsonSource with Clustering Create a GeoJSON data source with point clustering enabled. Clusters aggregate nearby points and expose properties like point_count. ```kotlin import androidx.compose.runtime.Composable import io.github.dellisd.spatialk.geojson.FeatureCollection import org.maplibre.compose.expressions.dsl.asNumber import org.maplibre.compose.expressions.dsl.convertToNumber import org.maplibre.compose.expressions.dsl.feature import org.maplibre.compose.expressions.dsl.plus import org.maplibre.compose.sources.GeoJsonData import org.maplibre.compose.sources.GeoJsonOptions import org.maplibre.compose.sources.rememberGeoJsonSource @Composable fun ClusteredSource() { val bikeSource = rememberGeoJsonSource( GeoJsonData.JsonString(FeatureCollection().json()), GeoJsonOptions( minZoom = 8, cluster = true, clusterRadius = 32, clusterMaxZoom = 16, clusterProperties = mapOf( "total_range" to GeoJsonOptions.ClusterPropertyAggregator( mapper = feature["current_range_meters"].convertToNumber(), reducer = feature.accumulated().asNumber() + feature["total_range"].convertToNumber() ) ) ) ) } ``` ## CircleLayer with Clustering Visualization Visualize clustered points with dynamic circle sizes and handle click events to zoom into clusters. ```kotlin import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import io.github.dellisd.spatialk.geojson.Point import kotlinx.coroutines.launch import org.maplibre.compose.demoapp.DemoState import org.maplibre.compose.expressions.dsl.asNumber import org.maplibre.compose.expressions.dsl.const import org.maplibre.compose.expressions.dsl.feature import org.maplibre.compose.expressions.dsl.step import org.maplibre.compose.layers.CircleLayer import org.maplibre.compose.util.ClickResult @Composable fun ClusteredCircles(state: DemoState, bikeSource: org.maplibre.compose.sources.GeoJsonSource) { val coroutineScope = rememberCoroutineScope() CircleLayer( id = "clustered-bikes", source = bikeSource, filter = feature.has("point_count"), color = const(Color(50, 205, 5)), opacity = const(0.5f), radius = step( input = feature["point_count"].asNumber(), fallback = const(15.dp), 25 to const(20.dp), 100 to const(30.dp), 500 to const(40.dp), 1000 to const(50.dp), 5000 to const(60.dp) ), onClick = { features -> features.firstOrNull()?.geometry?.let { coroutineScope.launch { state.cameraState.animateTo( state.cameraState.position.copy( target = (it as Point).coordinates, zoom = (state.cameraState.position.zoom + 2).coerceAtMost(20.0) ) ) } ClickResult.Consume } ?: ClickResult.Pass } ) } ``` ## SymbolLayer with Icons and Text Display markers with custom icons and formatted text labels. Supports click handling and feature property access. ```kotlin import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.unit.em import org.jetbrains.compose.resources.painterResource import org.maplibre.compose.demoapp.generated.Res import org.maplibre.compose.demoapp.generated.marker import org.maplibre.compose.expressions.dsl.asString import org.maplibre.compose.expressions.dsl.const import org.maplibre.compose.expressions.dsl.feature import org.maplibre.compose.expressions.dsl.format import org.maplibre.compose.expressions.dsl.image import org.maplibre.compose.expressions.dsl.offset import org.maplibre.compose.expressions.dsl.span import org.maplibre.compose.layers.SymbolLayer import org.maplibre.compose.sources.GeoJsonData import org.maplibre.compose.sources.rememberGeoJsonSource import org.maplibre.compose.util.ClickResult @Composable fun MarkersWithIcons() { val marker = painterResource(Res.drawable.marker) val amtrakStations = rememberGeoJsonSource( data = GeoJsonData.Uri( "https://raw.githubusercontent.com/datanews/amtrak-geojson/refs/heads/master/amtrak-stations.geojson" ) ) SymbolLayer( id = "amtrak-stations", source = amtrakStations, onClick = { features -> println("Clicked on ${features.firstOrNull()?.json()}") ClickResult.Consume }, iconImage = image(marker), textField = format( span(image("railway")), span(" "), span(feature["STNCODE"].asString(), textSize = const(1.2f.em)) ), textFont = const(listOf("Noto Sans Regular")), textColor = const(MaterialTheme.colorScheme.onBackground), textOffset = offset(0.em, 0.6.em) ) } ``` ## Map Gestures and Ornaments Configuration Configure touch gestures (pan, zoom, rotate, tilt) and map ornaments (logo, attribution, compass, scale bar) with positioning. ```kotlin import androidx.compose.foundation.layout.PaddingValues import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.unit.dp import org.maplibre.compose.map.GestureOptions import org.maplibre.compose.map.MapOptions import org.maplibre.compose.map.MaplibreMap import org.maplibre.compose.map.OrnamentOptions @Composable fun ConfiguredMap() { MaplibreMap( options = MapOptions( gestureOptions = GestureOptions( isTiltEnabled = true, isZoomEnabled = true, isRotateEnabled = true, isScrollEnabled = true ), ornamentOptions = OrnamentOptions( padding = PaddingValues(16.dp), isLogoEnabled = true, logoAlignment = Alignment.BottomStart, isAttributionEnabled = true, attributionAlignment = Alignment.BottomEnd, isCompassEnabled = true, compassAlignment = Alignment.TopEnd, isScaleBarEnabled = true, scaleBarAlignment = Alignment.TopStart ) ) ) } ``` ## Map Click Event Handling Handle click and long-click events on the map, query rendered features at the click location, and control event propagation. ```kotlin import androidx.compose.runtime.Composable import io.github.dellisd.spatialk.geojson.Position import org.maplibre.compose.camera.rememberCameraState import org.maplibre.compose.map.MaplibreMap import org.maplibre.compose.util.ClickResult @Composable fun ClickHandling() { val camera = rememberCameraState() MaplibreMap( cameraState = camera, onMapClick = { pos, offset -> val features = camera.projection?.queryRenderedFeatures(offset) if (!features.isNullOrEmpty()) { println("Clicked on ${features[0].json()}") ClickResult.Consume // Prevents propagation to layer listeners } else { ClickResult.Pass } }, onMapLongClick = { pos, offset -> println("Long click at $pos") ClickResult.Pass } ) } ``` ## Main Use Cases and Integration MapLibre Compose is designed for building cross-platform map applications in Kotlin Multiplatform projects using Jetpack/Compose Multiplatform UI. Primary use cases include location-based mobile and desktop applications, data visualization on maps, route planning and navigation interfaces, and geographic information systems. The library supports offline map functionality, real-time data overlays, and interactive map features like markers, clusters, and custom layers. It's particularly suited for applications that need consistent map behavior across Android, iOS, web, and desktop platforms while maintaining a declarative Compose programming model. Integration follows standard Compose patterns with the MaplibreMap composable as the container. Data sources are created using remember functions and passed to layer composables within the map's content lambda. Camera state management uses rememberCameraState() for reactive updates and animations. The library provides both high-level presets (like GestureOptions.Standard) and low-level configuration for platform-specific customization. Styling is managed through BaseStyle URIs pointing to MapLibre style JSON, supporting both remote URLs and local resources. The expression DSL enables dynamic styling based on zoom levels, feature properties, and runtime state, following MapLibre's expression specification while providing type-safe Kotlin builders.