Try Live
Add Docs
Rankings
Pricing
Enterprise
Docs
Install
Install
Docs
Pricing
Enterprise
More...
More...
Try Live
Rankings
Add Docs
MapLibre Compose Multiplatform
https://github.com/maplibre/maplibre-compose
Admin
MapLibre Compose is a Compose Multiplatform wrapper around the MapLibre SDKs for rendering
...
Tokens:
18,908
Snippets:
189
Trust Score:
9.1
Update:
2 weeks ago
Context
Skills
Chat
Benchmark
72.3
Suggestions
Latest
Show doc for...
Code
Info
Show Results
Context Summary (auto-generated)
Raw
Copy
Link
# MapLibre Compose MapLibre Compose is a Compose Multiplatform wrapper around the MapLibre SDKs (MapLibre Native for Android, iOS, and Desktop; MapLibre GL JS for Web) that enables interactive, customizable maps in Kotlin Multiplatform applications. It targets Android, iOS, Desktop (macOS, Windows, Linux via JNI), and Web (Kotlin/JS), sharing map logic across all platforms while respecting each platform's rendering backend. The library is published on Maven Central under the `org.maplibre.compose` group and integrates directly into Compose Multiplatform UIs as standard `@Composable` functions. The library is organized into three main artifacts: `maplibre-compose` (core map, sources, layers, camera, expressions, offline), `maplibre-compose-material3` (Material3 UI controls: compass, scale bar, attribution), and `maplibre-compose-gms` (Google Mobile Services location provider for Android). It exposes a declarative, reactive API: sources and layers react to state changes automatically, the camera is controlled via `CameraState`, and style DSL expressions are typed and compiled at composition time. The project is pre-v1.0 and under active development; Android and iOS have the most complete platform support. --- ## MaplibreMap — Core Map Composable The root composable that renders a MapLibre map. It fills its container by default and accepts a base style, camera state, zoom/pitch constraints, bounding box, click handlers, render/gesture/ornament options, and a `content` lambda for declarative sources and layers. ```kotlin import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier import org.maplibre.compose.camera.CameraPosition import org.maplibre.compose.camera.rememberCameraState import org.maplibre.compose.map.GestureOptions import org.maplibre.compose.map.MapOptions import org.maplibre.compose.map.MaplibreMap import org.maplibre.compose.map.OrnamentOptions import org.maplibre.compose.style.BaseStyle import org.maplibre.compose.util.ClickResult import org.maplibre.spatialk.geojson.BoundingBox import org.maplibre.spatialk.geojson.Position import kotlin.time.Duration.Companion.seconds @Composable fun MyMap() { val cameraState = rememberCameraState( firstPosition = CameraPosition( target = Position(latitude = 47.607, longitude = -122.342), zoom = 12.0, bearing = 0.0, tilt = 0.0, ) ) MaplibreMap( modifier = Modifier.fillMaxSize(), baseStyle = BaseStyle.Uri("https://demotiles.maplibre.org/style.json"), cameraState = cameraState, zoomRange = 3f..18f, pitchRange = 0f..60f, boundingBox = BoundingBox( southwest = Position(-125.0, 24.0), northeast = Position(-66.0, 50.0), ), options = MapOptions( gestureOptions = GestureOptions.Standard, ornamentOptions = OrnamentOptions.OnlyLogo, ), onMapClick = { position, offset -> println("Clicked at $position") ClickResult.Pass }, onMapLongClick = { position, offset -> println("Long click at $position") ClickResult.Consume }, onMapLoadFinished = { println("Map loaded") }, onMapLoadFailed = { reason -> println("Load failed: $reason") }, ) { // Sources and layers go here } } ``` --- ## BaseStyle — Map Style Configuration Defines the base map style as either a URI to a remote style JSON or an inline JSON object. Provides `BaseStyle.Demo` (MapLibre demo tiles) and `BaseStyle.Empty` (blank canvas) presets. ```kotlin import org.maplibre.compose.style.BaseStyle // Remote style URI (e.g., from MapTiler, Stadia, or self-hosted) val remoteStyle = BaseStyle.Uri("https://api.maptiler.com/maps/streets/style.json?key=MY_KEY") // Inline JSON style val customStyle = BaseStyle.Json { put("version", 8) put("name", "Custom") putJsonObject("sources") { putJsonObject("osm") { put("type", "raster") putJsonArray("tiles") { add("https://tile.openstreetmap.org/{z}/{x}/{y}.png") } put("tileSize", 256) } } putJsonArray("layers") { add(buildJsonObject { put("id", "osm-tiles") put("type", "raster") put("source", "osm") }) } } // Blank map (add all sources/layers in the content lambda) val blank = BaseStyle.Empty // Usage MaplibreMap(baseStyle = remoteStyle) { /* layers */ } ``` --- ## CameraState / rememberCameraState — Camera Control `CameraState` exposes the current `CameraPosition` (target, zoom, bearing, tilt, padding) and provides suspend functions for animated and instant camera movements. `rememberCameraState` creates and survives recompositions; state is saved/restored on configuration changes. ```kotlin import androidx.compose.foundation.layout.PaddingValues import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.unit.dp import org.maplibre.compose.camera.CameraPosition import org.maplibre.compose.camera.rememberCameraState import org.maplibre.compose.map.MaplibreMap import org.maplibre.spatialk.geojson.BoundingBox import org.maplibre.spatialk.geojson.Position import kotlin.time.Duration.Companion.seconds @Composable fun CameraDemo() { val camera = rememberCameraState( firstPosition = CameraPosition( target = Position(latitude = 45.52, longitude = -122.68), zoom = 13.0, bearing = 45.0, tilt = 30.0, ) ) // Read camera state println("Current zoom: ${camera.position.zoom}") println("Is moving: ${camera.isCameraMoving}") println("Move reason: ${camera.moveReason}") println("Meters/dp: ${camera.metersPerDpAtTarget}") // Jump instantly (no animation) // camera.position = CameraPosition(target = Position(0.0, 0.0), zoom = 2.0) LaunchedEffect(Unit) { // Animated move to a position camera.animateTo( finalPosition = camera.position.copy( target = Position(latitude = 47.607, longitude = -122.342), zoom = 15.0, ), duration = 2.seconds, ) // Animate to fit a bounding box with padding camera.animateTo( boundingBox = BoundingBox( southwest = Position(-122.5, 47.4), northeast = Position(-122.2, 47.8), ), bearing = 0.0, tilt = 0.0, padding = PaddingValues(32.dp), duration = 1.seconds, ) // Jump to bounding box (no animation) camera.jumpTo( boundingBox = BoundingBox( southwest = Position(-74.1, 40.6), northeast = Position(-73.8, 40.9), ), padding = PaddingValues(16.dp), ) // Query projection after map is ready val projection = camera.awaitProjection() val visibleBbox = projection.queryVisibleBoundingBox() println("Visible area: $visibleBbox") } MaplibreMap(cameraState = camera) } ``` --- ## GeoJsonSource / rememberGeoJsonSource — GeoJSON Data Source Provides map data from inline GeoJSON (features, feature collections, raw JSON string, or URL). Supports point clustering with configurable radius, zoom levels, min points, and custom aggregate cluster properties. ```kotlin import androidx.compose.runtime.Composable import org.maplibre.compose.expressions.dsl.asNumber 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 import org.maplibre.spatialk.geojson.FeatureCollection import org.maplibre.spatialk.geojson.Feature import org.maplibre.spatialk.geojson.Point import org.maplibre.spatialk.geojson.Position @Composable fun GeoJsonSourceDemo() { // From a URL val remoteSource = rememberGeoJsonSource( data = GeoJsonData.Uri("https://example.com/data.geojson"), ) // From in-memory features val features = FeatureCollection( listOf( Feature(geometry = Point(Position(latitude = 51.5, longitude = -0.09))), Feature(geometry = Point(Position(latitude = 40.7, longitude = -74.0))), ) ) val inMemorySource = rememberGeoJsonSource( data = GeoJsonData.Features(features), options = GeoJsonOptions(synchronousUpdate = true), // fast updates on Android ) // Clustered source with custom aggregate property val clusteredSource = rememberGeoJsonSource( data = GeoJsonData.Uri("https://example.com/points.geojson"), options = GeoJsonOptions( cluster = true, clusterRadius = 50, clusterMaxZoom = 14, clusterMinPoints = 2, clusterProperties = mapOf( "total_value" to GeoJsonOptions.ClusterPropertyAggregator( mapper = feature["value"].asNumber(), reducer = feature.accumulated().asNumber() + feature["total_value"].asNumber(), ) ), lineMetrics = true, // required for LineLayer gradient ), ) // Access cluster helpers on clustered GeoJsonSource // val isCluster: Boolean = clusteredSource.isCluster(someFeature) // val expansionZoom: Double = clusteredSource.getClusterExpansionZoom(clusterFeature) // val children: FeatureCollection = clusteredSource.getClusterChildren(clusterFeature) // val leaves: FeatureCollection = clusteredSource.getClusterLeaves(clusterFeature, limit=10, offset=0) } ``` --- ## VectorSource / rememberVectorSource — Vector Tile Source Loads tiled vector data from a TileJSON URI or explicit tile URL list. Vector features are rendered via layer composables that reference the source and a specific `sourceLayer` name. ```kotlin import androidx.compose.runtime.Composable import org.maplibre.compose.expressions.dsl.const import org.maplibre.compose.sources.TileSetOptions import org.maplibre.compose.sources.rememberVectorSource @Composable fun VectorSourceDemo() { // From TileJSON spec URI val tilejsonSource = rememberVectorSource( uri = "https://api.maptiler.com/tiles/v3/tiles.json?key=MY_KEY", ) // From explicit tile URLs val tileSource = rememberVectorSource( tiles = listOf("https://tiles.example.com/{z}/{x}/{y}.pbf"), options = TileSetOptions(minZoom = 0, maxZoom = 14), ) // Query source features programmatically // val features = tilejsonSource.querySourceFeatures( // sourceLayerIds = setOf("water", "roads"), // predicate = feature["class"].asString() eq const("motorway"), // ) } ``` --- ## ComputedSource / rememberComputedSource — Computed (Custom) Vector Source Generates vector tile data on-the-fly from a Kotlin lambda, called with the current visible `BoundingBox` and zoom level. Useful for data that cannot be pre-tiled. Supports explicit tile invalidation. ```kotlin import androidx.compose.runtime.Composable import org.maplibre.compose.sources.ComputedSourceOptions import org.maplibre.compose.sources.rememberComputedSource import org.maplibre.spatialk.geojson.BoundingBox import org.maplibre.spatialk.geojson.Feature import org.maplibre.spatialk.geojson.FeatureCollection import org.maplibre.spatialk.geojson.Point import org.maplibre.spatialk.geojson.Position @Composable fun ComputedSourceDemo() { val computedSource = rememberComputedSource( options = ComputedSourceOptions(minZoom = 0, maxZoom = 16), getFeatures = { bounds: BoundingBox, zoomLevel: Int -> // Generate features for the requested tile bounds/zoom FeatureCollection( listOf( Feature( geometry = Point( Position( latitude = (bounds.northeast.latitude + bounds.southwest.latitude) / 2, longitude = (bounds.northeast.longitude + bounds.southwest.longitude) / 2, ) ), ) ) ) }, ) // Invalidate when underlying data changes: // computedSource.invalidateBounds(myBoundingBox) // computedSource.invalidateTile(zoomLevel = 12, x = 654, y = 1583) } ``` --- ## LineLayer — Polyline and Polygon Outline Layer Renders lines or polygon outlines from a source. Supports color, width, dash patterns, gradient (requires `lineMetrics = true` on the GeoJSON source), caps, joins, gap width, offset, and blur. All paint and layout properties accept the expression DSL. ```kotlin import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import org.maplibre.compose.expressions.dsl.const import org.maplibre.compose.expressions.dsl.exponential import org.maplibre.compose.expressions.dsl.feature 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.sources.rememberVectorSource import org.maplibre.compose.util.ClickResult @Composable fun LineLayerDemo() { val roadsSource = rememberVectorSource(uri = "https://example.com/tiles.json") LineLayer( id = "roads", source = roadsSource, sourceLayer = "transportation", minZoom = 5f, maxZoom = 24f, color = interpolate( exponential(1.5f), zoom(), 5 to const(Color(0xFF888888)), 15 to const(Color(0xFFFFFFFF)), ), width = interpolate( exponential(1.5f), zoom(), 5 to const(0.5.dp), 15 to const(8.dp), ), cap = const(LineCap.Round), join = const(LineJoin.Round), opacity = const(0.9f), dasharray = const(listOf(2, 1)), onClick = { features -> println("Clicked road: ${features.firstOrNull()?.id}") ClickResult.Consume }, ) } ``` --- ## FillLayer — Polygon Fill Layer Renders filled polygons from a source. Supports fill color, opacity, outline color, pattern image, anti-aliasing, and translation. Click events are supported per layer. ```kotlin import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import org.maplibre.compose.expressions.dsl.const import org.maplibre.compose.expressions.dsl.feature import org.maplibre.compose.expressions.dsl.switch import org.maplibre.compose.expressions.dsl.case import org.maplibre.compose.expressions.dsl.asString import org.maplibre.compose.layers.FillLayer import org.maplibre.compose.sources.rememberVectorSource import org.maplibre.compose.util.ClickResult @Composable fun FillLayerDemo() { val buildingsSource = rememberVectorSource(uri = "https://example.com/buildings.json") FillLayer( id = "buildings", source = buildingsSource, sourceLayer = "building", minZoom = 12f, opacity = const(0.7f), color = switch( input = feature["building_type"].asString(), case(label = "residential", output = const(Color(0xFFADD8E6))), case(label = "commercial", output = const(Color(0xFFFFD700))), fallback = const(Color(0xFFCCCCCC)), ), outlineColor = const(Color(0xFF999999)), antialias = const(true), onClick = { features -> println("Building type: ${features.firstOrNull()?.properties?.get("building_type")}") ClickResult.Consume }, ) } ``` --- ## SymbolLayer — Icon and Text Label Layer Renders point features as icons and/or text labels. Provides extensive collision, alignment, rotation, offset, halo, and scaling controls for both icons (including SDF-based color tinting) and text. ```kotlin import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.em 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.value.SymbolAnchor import org.maplibre.compose.expressions.value.SymbolPlacement import org.maplibre.compose.expressions.value.TextJustify 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 SymbolLayerDemo() { val poiSource = rememberGeoJsonSource(data = GeoJsonData.Uri("https://example.com/pois.geojson")) SymbolLayer( id = "poi-labels", source = poiSource, placement = const(SymbolPlacement.Point), // Text label from feature property textField = feature["name"].asString().cast(), textSize = const(1.em), // relative to 16sp default textColor = const(Color.Black), textHaloColor = const(Color.White), textHaloWidth = const(1.5.dp), textAnchor = const(SymbolAnchor.Top), textJustify = const(TextJustify.Center), textAllowOverlap = const(false), // Icon from Compose painter (converted to map image) iconImage = image( value = myMarkerPainter, // Painter from resources or vector ), iconAnchor = const(SymbolAnchor.Bottom), iconSize = const(1.2f), iconAllowOverlap = const(true), iconColor = const(Color.Red), // Only works for SDF icons onClick = { features -> println("POI: ${features.firstOrNull()?.properties?.get("name")}") ClickResult.Consume }, ) } ``` --- ## CircleLayer — Point Circle Layer Renders point features as circles. Controls radius, fill color, stroke color/width, blur, opacity, pitch scale, and pitch alignment. Supports data-driven expressions for all visual properties. ```kotlin import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp 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.expressions.dsl.asNumber import org.maplibre.compose.expressions.value.CirclePitchAlignment import org.maplibre.compose.layers.CircleLayer import org.maplibre.compose.sources.GeoJsonData import org.maplibre.compose.sources.rememberGeoJsonSource import org.maplibre.compose.util.ClickResult @Composable fun CircleLayerDemo() { val stationsSource = rememberGeoJsonSource( data = GeoJsonData.Uri("https://example.com/stations.geojson") ) CircleLayer( id = "stations", source = stationsSource, radius = step( input = feature["ridership"].asNumber(), fallback = const(6.dp), 1000 to const(10.dp), 10000 to const(16.dp), 50000 to const(24.dp), ), color = const(Color(0xFF0066CC)), strokeColor = const(Color.White), strokeWidth = const(2.dp), opacity = const(0.9f), pitchAlignment = const(CirclePitchAlignment.Map), // circle stays flat on map when tilted onClick = { features -> val name = features.firstOrNull()?.properties?.get("name") println("Station: $name") ClickResult.Consume }, ) } ``` --- ## BackgroundLayer — Map Background Layer Renders a solid color or pattern as the map background. Typically used with `BaseStyle.Empty` to set a custom background before adding other layers. ```kotlin import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import org.maplibre.compose.expressions.dsl.const import org.maplibre.compose.layers.BackgroundLayer import org.maplibre.compose.map.MaplibreMap import org.maplibre.compose.style.BaseStyle @Composable fun BackgroundLayerDemo() { MaplibreMap(baseStyle = BaseStyle.Empty) { BackgroundLayer( id = "background", color = const(Color(0xFFE0F0FF)), opacity = const(1f), ) // Add other layers on top } } ``` --- ## Anchor — Layer Positioning Controls where Compose-declared layers are inserted relative to base-style layers. Layers can be placed at the `Top` (default), `Bottom`, `Above` a named layer, `Below` a named layer, or `Replace` a named layer from the base style. ```kotlin import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import org.maplibre.compose.expressions.dsl.const import org.maplibre.compose.layers.Anchor import org.maplibre.compose.layers.FillLayer import org.maplibre.compose.layers.LineLayer import org.maplibre.compose.map.MaplibreMap import org.maplibre.compose.style.BaseStyle import org.maplibre.compose.sources.rememberVectorSource @Composable fun AnchorDemo() { val source = rememberVectorSource(uri = "https://example.com/tiles.json") MaplibreMap(baseStyle = BaseStyle.Uri("https://demotiles.maplibre.org/style.json")) { // Default: placed on top of all base style layers LineLayer(id = "my-line", source = source, sourceLayer = "roads", color = const(Color.Red), width = const(2.dp)) // Insert below the first symbol/label layer in the base style Anchor.Below("country-label") { FillLayer(id = "my-fill", source = source, sourceLayer = "land", color = const(Color(0xFFE8F4D9))) } // Insert above a specific base layer Anchor.Above("water") { FillLayer(id = "my-overlay", source = source, sourceLayer = "water_overlay", color = const(Color(0x660099CC))) } // Place at the very bottom of the layer stack Anchor.Bottom { BackgroundLayer(id = "custom-bg", color = const(Color.LightGray)) } } } ``` --- ## Expression DSL — Data-Driven Styling Type-safe Kotlin DSL for building MapLibre style expressions. Expressions compute layer property values at render time from feature properties, zoom level, or other map state. All layer properties accept `Expression<T>` where `T` constrains the value type. ```kotlin import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import org.maplibre.compose.expressions.dsl.* import org.maplibre.compose.expressions.value.* // Constant literal (required when passing a plain value as an expression) val redColor: Expression<ColorValue> = const(Color.Red) val threeDP: Expression<DpValue> = const(3.dp) // Feature property access val nameExpr = feature["name"].asString() // nullable, null-safe string val populationExpr = feature["population"].asNumber() // Conditional: if population > 1000 → large label, else small val textSizeExpr = switch( condition(test = feature["population"].asNumber() gt const(1000f), output = const(18.dp)), condition(test = feature["population"].asNumber() gt const(100f), output = const(14.dp)), fallback = const(10.dp), ) // Match expression (like a switch on string property) val colorByType: Expression<ColorValue> = switch( input = feature["type"].asString(), case(label = "park", output = const(Color(0xFF4CAF50))), case(label = "water", output = const(Color(0xFF2196F3))), case(label = "road", output = const(Color(0xFF9E9E9E))), fallback = const(Color.LightGray), ) // Zoom interpolation (line width doubles every 2 zoom levels) val zoomWidth: Expression<DpValue> = interpolate( exponential(2f), zoom(), 8 to const(1.dp), 16 to const(16.dp), ) // Step function (discrete zoom breakpoints) val zoomOpacity = step( input = zoom(), fallback = const(0f), 10 to const(0.5f), 14 to const(1f), ) // HCL color interpolation across zoom levels val heatColors: Expression<ColorValue> = interpolateHcl( linear(), zoom(), 1 to const(Color.Blue), 10 to const(Color.Green), 20 to const(Color.Red), ) // Boolean logic val isHighwayExpr = feature["class"].asString() eq const("motorway") val isVisibleExpr = isHighwayExpr and (zoom() gt const(8f)) // Coalesce (first non-null) val labelExpr = coalesce(feature["name_en"], feature["name"], const("Unknown")) // Feature geometry and id val geomType = feature.geometryType() val featureId = feature.id<FloatValue>() // Feature within a geometry val withinCity = feature.within(const(cityPolygonGeoJson)) ``` --- ## UserLocationState / rememberUserLocationState — User Location Tracking Subscribes to a `LocationProvider` (and optional `OrientationProvider`) and exposes the current `Location` and `Orientation` as Compose state, sampled at a configurable interval to throttle recompositions. ```kotlin import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import kotlin.time.Duration.Companion.milliseconds import org.maplibre.compose.location.rememberUserLocationState import org.maplibre.compose.location.AndroidLocationProvider // platform-specific import org.maplibre.compose.location.LocationPuck import org.maplibre.compose.camera.rememberCameraState import org.maplibre.compose.map.MaplibreMap @Composable fun UserLocationDemo() { val locationProvider = remember { AndroidLocationProvider(context) } val locationState = rememberUserLocationState( locationProvider = locationProvider, samplePeriod = 500.milliseconds, ) val camera = rememberCameraState() MaplibreMap(cameraState = camera) { LocationPuck( idPrefix = "user", location = locationState.location, cameraState = camera, showBearing = true, showBearingAccuracy = true, ) } } ``` --- ## LocationPuck — User Location Indicator A `@MaplibreComposable` that renders a dot + accuracy circle + bearing arrow indicator at the user's current location using `CircleLayer` and `SymbolLayer` internally. Colors, sizes, and bearing source are fully configurable. ```kotlin import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import kotlin.time.Duration.Companion.seconds import org.maplibre.compose.camera.rememberCameraState import org.maplibre.compose.location.LocationPuck import org.maplibre.compose.location.LocationPuckColors import org.maplibre.compose.location.LocationPuckSizes import org.maplibre.compose.location.rememberUserLocationState import org.maplibre.compose.map.MaplibreMap @Composable fun LocationPuckDemo(locationProvider: org.maplibre.compose.location.LocationProvider) { val camera = rememberCameraState() val locationState = rememberUserLocationState(locationProvider = locationProvider) MaplibreMap(cameraState = camera) { LocationPuck( idPrefix = "my-location", location = locationState.location, bearing = locationState.location?.course, cameraState = camera, oldLocationThreshold = 30.seconds, accuracyThreshold = 20f, // show accuracy circle when accuracy > 20m showBearing = true, showBearingAccuracy = true, colors = LocationPuckColors( dotFillColorCurrentLocation = Color(0xFF0066FF), dotFillColorOldLocation = Color.Gray, dotStrokeColor = Color.White, accuracyFillColor = Color(0x220066FF), bearingColor = Color(0xFF0066FF), ), sizes = LocationPuckSizes( dotRadius = 8.dp, dotStrokeWidth = 2.dp, ), onClick = { location -> println("Clicked at: ${location.position.value}") }, ) } } ``` --- ## LocationTrackingEffect — Camera–Location Sync A `LaunchedEffect` that calls a suspend lambda whenever the `UserLocationState` changes meaningfully (position delta ≥ `precision`). The `LocationChangeScope` receiver provides `CameraState.updateFromLocation()` to animate the camera to the new location with configurable bearing tracking modes. ```kotlin import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import org.maplibre.compose.camera.rememberCameraState import org.maplibre.compose.location.BearingUpdate import org.maplibre.compose.location.LocationTrackingEffect import org.maplibre.compose.location.rememberUserLocationState import org.maplibre.compose.map.MaplibreMap import kotlin.time.Duration.Companion.milliseconds @Composable fun TrackingDemo(locationProvider: org.maplibre.compose.location.LocationProvider) { val camera = rememberCameraState() val locationState = rememberUserLocationState(locationProvider = locationProvider) var tracking by remember { mutableStateOf(true) } LocationTrackingEffect( locationState = locationState, enabled = tracking, trackBearing = true, precision = 0.00001, // ~1 meter ) { // updateFromLocation animates camera to current location camera.updateFromLocation( animationDuration = 300.milliseconds, updateBearing = BearingUpdate.TRACK_AUTOMATIC, // BearingUpdate options: // IGNORE — keep current bearing // ALWAYS_NORTH — reset to north // TRACK_COURSE — follow GPS course // TRACK_ORIENTATION — follow device compass // TRACK_AUTOMATIC — use whichever is more accurate ) } MaplibreMap(cameraState = camera) } ``` --- ## GestureOptions — Map Gesture Configuration Controls which user gestures are active on the map. Predefined presets cover the most common scenarios; platform-specific constructors allow fine-grained control per platform. ```kotlin import androidx.compose.runtime.Composable import org.maplibre.compose.map.GestureOptions import org.maplibre.compose.map.MapOptions import org.maplibre.compose.map.MaplibreMap @Composable fun GestureDemo() { // Use a preset MaplibreMap(options = MapOptions(gestureOptions = GestureOptions.Standard)) MaplibreMap(options = MapOptions(gestureOptions = GestureOptions.PositionLocked)) // no pan MaplibreMap(options = MapOptions(gestureOptions = GestureOptions.RotationLocked)) // no rotate/tilt MaplibreMap(options = MapOptions(gestureOptions = GestureOptions.ZoomOnly)) // zoom only MaplibreMap(options = MapOptions(gestureOptions = GestureOptions.AllDisabled)) // full control // Android platform-specific fine-grained settings MaplibreMap( options = MapOptions( gestureOptions = GestureOptions( isTiltEnabled = true, isZoomEnabled = true, isRotateEnabled = false, isScrollEnabled = true, ) ) ) } ``` --- ## OrnamentOptions — Map Ornament (UI Controls) Configuration Controls which built-in platform UI controls (compass, scale bar, attribution, logo) are shown and their positioning. Use `OrnamentOptions.OnlyLogo` (or `AllDisabled`) when providing your own Material3 controls. ```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.MapOptions import org.maplibre.compose.map.MaplibreMap import org.maplibre.compose.map.OrnamentOptions @Composable fun OrnamentDemo() { // All built-in controls enabled (default) MaplibreMap(options = MapOptions(ornamentOptions = OrnamentOptions.AllEnabled)) // Only the required MapLibre logo MaplibreMap(options = MapOptions(ornamentOptions = OrnamentOptions.OnlyLogo)) // Custom positions and enabled states MaplibreMap( options = MapOptions( ornamentOptions = OrnamentOptions( padding = PaddingValues(8.dp), isLogoEnabled = true, logoAlignment = Alignment.BottomStart, isAttributionEnabled = true, attributionAlignment = Alignment.BottomEnd, isCompassEnabled = false, // using Material3 CompassButton instead isScaleBarEnabled = false, // using Material3 ScaleBar instead ) ) ) } ``` --- ## OfflineManager / rememberOfflineManager — Offline Map Tiles Manages download, resume, pause, and deletion of offline tile packs (`OfflinePack`). Available on Android and iOS. Each pack is defined by a `OfflinePackDefinition.TilePyramid` (bounding box + style + zoom range) or `OfflinePackDefinition.Shape` (polygon). ```kotlin import androidx.compose.foundation.layout.PaddingValues import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.unit.dp import kotlinx.coroutines.launch import org.maplibre.compose.camera.rememberCameraState import org.maplibre.compose.map.MaplibreMap import org.maplibre.compose.offline.OfflinePackDefinition import org.maplibre.compose.offline.rememberOfflineManager import org.maplibre.spatialk.geojson.BoundingBox import org.maplibre.spatialk.geojson.Position @Composable fun OfflineDemo() { val offlineManager = rememberOfflineManager() val coroutineScope = rememberCoroutineScope() val camera = rememberCameraState() MaplibreMap(cameraState = camera) // Download a pack fun downloadCurrentView() = coroutineScope.launch { val projection = camera.awaitProjection() val bounds = projection.queryVisibleBoundingBox() val pack = offlineManager.create( definition = OfflinePackDefinition.TilePyramid( styleUrl = "https://demotiles.maplibre.org/style.json", bounds = bounds, // minZoom and maxZoom default to a sensible range ), metadata = "My Region".encodeToByteArray(), ) offlineManager.resume(pack) // start/resume download // offlineManager.pause(pack) // pause download // offlineManager.delete(pack) // delete pack // offlineManager.invalidate(pack) // force re-download println("Pack status: ${pack.state}") println("Bytes downloaded: ${pack.progress.completedResourceSize}") } // Read existing packs (Compose State — reacts to changes) offlineManager.packs.forEach { pack -> println("Pack: ${pack.metadata?.decodeToString()}, state: ${pack.state}") } // Cache management coroutineScope.launch { offlineManager.invalidateAmbientCache() offlineManager.clearAmbientCache() offlineManager.setMaximumAmbientCacheSize(50L * 1024 * 1024) // 50 MB offlineManager.setTileCountLimit(6000L) } } ``` --- ## CompassButton / DisappearingCompassButton — Material3 Compass A Material3 `ElevatedButton` that displays a rotating compass needle reflecting the map's current bearing. Clicking it animates the camera back to north (and zero tilt by default). `DisappearingCompassButton` auto-hides when the bearing is near north. ```kotlin import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import kotlin.time.Duration.Companion.seconds import org.maplibre.compose.camera.rememberCameraState import org.maplibre.compose.map.MaplibreMap import org.maplibre.compose.map.MapOptions import org.maplibre.compose.map.OrnamentOptions import org.maplibre.compose.material3.CompassButton import org.maplibre.compose.material3.DisappearingCompassButton @Composable fun CompassDemo() { val camera = rememberCameraState() Box(Modifier.fillMaxSize()) { MaplibreMap( modifier = Modifier.fillMaxSize(), cameraState = camera, options = MapOptions(ornamentOptions = OrnamentOptions.OnlyLogo), ) // Always-visible compass CompassButton( cameraState = camera, modifier = Modifier.align(Alignment.TopEnd).padding(8.dp), size = 48.dp, ) // Auto-hiding compass — disappears 1 second after bearing returns to ~0° DisappearingCompassButton( cameraState = camera, modifier = Modifier.align(Alignment.TopEnd).padding(8.dp), visibilityDuration = 1.seconds, slop = 0.5, // degrees of tolerance before showing ) } } ``` --- ## ScaleBar / DisappearingScaleBar — Material3 Scale Bar Renders a graphical map scale bar showing real-world distance per screen distance. Automatically switches between metric and imperial units, or between primary/secondary units. Connects to `CameraState.metersPerDpAtTarget`. ```kotlin import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import org.maplibre.compose.camera.rememberCameraState import org.maplibre.compose.map.MaplibreMap import org.maplibre.compose.map.MapOptions import org.maplibre.compose.map.OrnamentOptions import org.maplibre.compose.material3.ScaleBar @Composable fun ScaleBarDemo() { val camera = rememberCameraState() Box(Modifier.fillMaxSize()) { MaplibreMap( modifier = Modifier.fillMaxSize(), cameraState = camera, options = MapOptions(ornamentOptions = OrnamentOptions.OnlyLogo), ) ScaleBar( metersPerDp = camera.metersPerDpAtTarget, modifier = Modifier .align(Alignment.BottomStart) .padding(start = 8.dp, bottom = 32.dp), // measures = ScaleBarMeasures(primary = imperialMeasure, secondary = metricMeasure), // color = LocalContentColor.current, // haloColor = backgroundColorFor(color), // haloWidth = 1.dp, ) } } ``` --- ## ExpandingAttributionButton / AttributionLinks — Material3 Attribution Material3 attribution button that expands to show data source attribution links (HTML strings) from the current map style's sources. Auto-dismisses on map gestures. Requires `StyleState` from `MaplibreMap`. ```kotlin import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import org.maplibre.compose.camera.rememberCameraState import org.maplibre.compose.map.MaplibreMap import org.maplibre.compose.map.MapOptions import org.maplibre.compose.map.OrnamentOptions import org.maplibre.compose.material3.ExpandingAttributionButton import org.maplibre.compose.style.rememberStyleState @Composable fun AttributionDemo() { val camera = rememberCameraState() val styleState = rememberStyleState() Box(Modifier.fillMaxSize()) { MaplibreMap( modifier = Modifier.fillMaxSize(), cameraState = camera, styleState = styleState, options = MapOptions(ornamentOptions = OrnamentOptions.AllDisabled), ) ExpandingAttributionButton( cameraState = camera, styleState = styleState, modifier = Modifier .align(Alignment.BottomEnd) .padding(8.dp), contentAlignment = Alignment.BottomEnd, ) } } ``` --- ## rememberFusedLocationProvider — GMS Location Provider (Android) Wraps the Google Mobile Services `FusedLocationProviderClient` as a `LocationProvider` compatible with `rememberUserLocationState`. Requires the `maplibre-compose-gms` artifact and location permissions. ```kotlin import androidx.compose.runtime.Composable import com.google.android.gms.location.LocationRequest import com.google.android.gms.location.Priority import org.maplibre.compose.gms.rememberFusedLocationProvider import org.maplibre.compose.location.LocationPuck import org.maplibre.compose.location.rememberUserLocationState import org.maplibre.compose.camera.rememberCameraState import org.maplibre.compose.map.MaplibreMap @Composable fun GmsLocationDemo() { // Requires ACCESS_FINE_LOCATION or ACCESS_COARSE_LOCATION permission val locationProvider = rememberFusedLocationProvider( locationRequest = LocationRequest.Builder( Priority.PRIORITY_HIGH_ACCURACY, 1000L, // interval ms ).setMinUpdateIntervalMillis(500L).build(), ) val locationState = rememberUserLocationState(locationProvider = locationProvider) val camera = rememberCameraState() MaplibreMap(cameraState = camera) { LocationPuck( idPrefix = "user", location = locationState.location, cameraState = camera, ) } } ``` --- MapLibre Compose is best suited for applications requiring interactive, customizable maps across Android, iOS, desktop, and web from a single Kotlin Multiplatform codebase. Typical use cases include navigation and location tracking apps (using `LocationTrackingEffect`, `LocationPuck`, and `CameraState.animateTo`), data visualization on maps (using GeoJSON sources with clustered points, heatmaps, or choropleth fills styled with the expression DSL), and offline-capable map applications (using `OfflineManager` to download tile packs for use without connectivity). The declarative, reactive model means sources and layer styles update automatically when Compose state changes, making it straightforward to build dynamic, data-driven map UIs. Integration follows the standard Compose Multiplatform pattern: add `maplibre-compose` to `commonMain.dependencies`, add platform-specific dependencies for iOS (CocoaPods or SPM) and Desktop (JNI native bindings), place `MaplibreMap()` in any composable hierarchy, and compose sources and layers declaratively in its `content` lambda. For richer UIs, `maplibre-compose-material3` provides ready-made `CompassButton`, `ScaleBar`, and `ExpandingAttributionButton` controls that integrate with the app's Material3 theme. For production Android location, `maplibre-compose-gms` provides a drop-in `FusedLocationProvider`. The expression DSL (`interpolate`, `step`, `switch`, `feature[...]`, `zoom()`) maps directly to the MapLibre Style Specification, enabling the full power of data-driven styling with Kotlin type safety.