Docs / Extension layers / GeoJSON Layer

GeoJSON layer

An extension package for putting GeoJSON straight onto the map. Features are rasterised into tiles, so tens of thousands of them cost no per-feature objects. API names, style defaults and hit-testing behaviour are the same on Android, iOS and React.

ANDROID
com.mapconductor:geojson
IOS
MapConductorGeoJSON
REACT
@mapconductor/react-geojson-layer
Rendering
raster tiles
Platform

01 · Overview

GeoJSON is parsed into lightweight feature models and drawn through MapConductor raster tile pipeline. Whatever the provider — Google Maps, MapLibre, MapKit, HERE — the code and the result are the same.

01

Parse

FeatureCollection, a bare Feature, a bare geometry, and RFC 8142 text sequences. Streaming parsers included.
02

Tile rendering

Features are rasterised into 512 px tiles and shown as a raster layer, with no dependency on provider vector features.
03

Hit testing

Clicks are tested against the same coordinates used to draw, including holes, multiparts and geometry collections.
Point
MultiPoint
LineString
MultiLineString
Polygon
MultiPolygon
GeometryCollection
Every geometry type is supported identically on all three platforms.

02 · Basic usage

Place the layer inside the map view content scope. Parse in the background and hand the result to features.

build.gradle.kts
dependencies {
    implementation("com.mapconductor:geojson:<version>")
}
com.mapconductor.geojson · Compose
val mapViewState = rememberMapLibreMapViewState(
    cameraPosition = MapCameraPosition(
        position = GeoPoint.fromLongLat(139.7671, 35.6812),
        zoom = 12.0,
    ),
)

val layerState = remember { GeoJSONLayerState() }
var features by remember { mutableStateOf(emptyList<GeoJSONFeature>()) }

LaunchedEffect(Unit) {
    features = withContext(Dispatchers.IO) {
        assets.open("wards.geojson").use(GeoJSONParser::parseStream)
    }
}

MapLibreMapView(state = mapViewState) {
    GeoJSONLayer(state = layerState, features = features)
}
When feature data or style changes the layer invalidates its tile URL internally, so the map SDK raster cache never keeps serving a stale picture.

03 · Styling

Style resolves in three layers

Layer-wide defaults, per-feature overrides, and a StyleProvider that decides dynamically. Later layers win, and anything left unspecified is inherited from the one above. Start with the layer defaults and add the lower layers only as you need them.

Figure · resolution order
LAYER 1
Layer defaults
GeoJSONLayerState
The base applied to every feature: strokeColor, fillColor, strokeWidth, pointRadius.
LAYER 2
Per-feature override
GeoJSONFeature.strokeColor …
Same-named fields on each feature. Left null, the layer default is used as is.
LAYER 3
Dynamic resolution
GeoJSONStyleProvider
Called once per feature to return the final style — this is where property-driven colouring lives.
With no provider set, DefaultGeoJSONStyleProvider applies: feature value if present, otherwise layer default. The third layer is really a way to replace that relationship itself.
3-1. Style properties

There are only four style properties, plus opacity, visible and minZoom / maxZoom on the layer for display control.

Property
Type
Default
Description
strokeColor
ARGB Int / UIColor / number
#FF1E88E5
Colour of lines and polygon outlines.
fillColor
ARGB Int / UIColor / number
#801E88E5
Polygon fill and the point circle. Semi-transparent by default.
strokeWidth
Float / CGFloat / number
2.0
Line width in pixels. Because it is rasterised into tiles the width stays constant across zooms.
pointRadius
Float / CGFloat / number
8.0
Radius in pixels of the circle drawn for a point.
opacity
Float / Double / number
1.0
Opacity of the whole raster layer, independent of per-colour alpha.
visible
Boolean
true
Excluded from both drawing and hit testing. Features carry the same field.
minZoom / maxZoom
Int / number
0 / 22
Zoom range in which the generated raster layer is shown; outside it, no tiles are built.
3-2. Setting layer defaults

Start here. Whatever you pass to GeoJSONLayerState becomes the base for every feature, and the state is observable, so assigning later re-renders.

Kotlin · ARGB Int
val layerState = remember {
    GeoJSONLayerState(
        strokeColor = Color.argb(220, 30, 136, 229),
        fillColor   = Color.argb(60, 30, 136, 229),
        strokeWidth = 1.5f,
        pointRadius = 8f,
        opacity     = 1f,
        minZoom = 8, maxZoom = 22,
    )
}

// 状態は observable。あとから代入すればタイルが再生成されます
layerState.fillColor = Color.argb(90, 214, 64, 69)
Colour formats
Android and React use a 32-bit ARGB integer (alpha in the top byte); iOS carries alpha in the UIColor itself. React ships colorArgb(a,r,g,b), colorRgb(r,g,b) and argbToCss(), in the same argument order as Android Color.argb(). All three default to #1E88E5 — opaque stroke, alpha 128 fill.
3-3. Overriding per feature

A feature can carry its own strokeColor, fillColor, strokeWidth, pointRadius and visible. Null means the default; a value wins. When the style is settled at load time and never changes afterwards, this is the simplest and fastest route.

Kotlin · GeoJSONFeature.copy
val parsed = GeoJSONParser.parseStream(input)

// パース後にプロパティを見てスタイルを焼き込む
val styled = parsed.map { f ->
    when (f.properties["status"]) {
        "alert"  -> f.copy(fillColor = Color.argb(120, 214, 64, 69), strokeWidth = 3f)
        "closed" -> f.copy(visible = false)
        else     -> f   // null のままなのでレイヤー既定値が使われる
    }
}

GeoJSONLayer(state = layerState, features = styled)
3-4. Deciding dynamically with a StyleProvider

Colour by a property value, highlight the selected feature, move a threshold from the UI — style that follows a rule belongs in a StyleProvider. It is called once per feature, receives the layer defaults, and returns the final style.

You receive
The feature (properties included) and the current layer defaults. Copying the defaults and changing one field is the usual shape.
You return
A LayerStyle with all four fields filled. Return the default for anything you do not touch and the first layer keeps working.
Kotlin · GeoJSONStyleProviderInterface
// fun interface なのでラムダ1つで書けます
val densityStyle = GeoJSONStyleProviderInterface { feature, defaultStyle ->
    val pop = (feature.properties["population"] as? Number)?.toInt() ?: 0
    val fill = when {
        pop > 500_000 -> Color.argb(150, 173, 20, 87)
        pop > 200_000 -> Color.argb(120, 244, 143, 177)
        else          -> Color.argb(80, 248, 187, 208)
    }
    defaultStyle.copy(fillColor = fill)   // 触らない項目は既定値のまま
}

val layerState = remember {
    GeoJSONLayerState(styleProvider = densityStyle)
}

// あとから差し替えると全フィーチャが再評価されます
layerState.styleProvider = DefaultGeoJSONStyleProvider
Swapping the provider — or changing state it reads — re-evaluates every feature and rebuilds the tiles. Since it runs per feature, pre-compute anything expensive (regex, network, date parsing) outside the provider.
3-5. Which one to use
Approach
Good for
Notes
LayerState
Drawing the whole dataset with one appearance.
The cheapest. Start here.
Feature override
Style fixed at load time and never changing.
Just map the parse result — no provider call per feature.
StyleProvider
Style follows a rule, and the rule itself can change at runtime.
Keeps the logic in one place. On RN it must be registered natively.

04 · Hit testing

MapConductor exposes a single map click slot, so forwarding to the layer is left to the app — deliberately not automatic. processClick returns true only when a feature was hit.

Kotlin
val layerState = remember {
    GeoJSONLayerState(
        onClick = { feature, position -> selected = feature },
    )
}

MapLibreMapView(
    state = mapViewState,
    onMapClick = { point ->
        // 15px 相当の許容範囲で判定(ズームに追従)
        val consumed = layerState.processClick(point, 15.0, mapViewState.zoom)
        if (!consumed) selected = null
    },
) {
    GeoJSONLayer(state = layerState, features = features)
}
Pixel tolerance
Pass a pixel tolerance and the current zoom to processClick for a zoom-aware threshold. Omitted, the world-coordinate default (~0.0002°) applies.
Overlaps
The last drawn — topmost — matching feature is returned.
Geometries
Points, lines, polygons with holes, multiparts and geometry collections.

05 · Data volume and loading

For large data use the streaming parsers and parse off the main thread. There are also two ways to hold features.

GeoJSONFeature
Static and bulk
Immutable data objects — no state object per feature, so tens of thousands stay light. Use this for big GeoJSON.
GeoJSONFeatureState
Few and frequently changing
Reactive per-feature updates. State management costs add up in volume, so use it only where needed.
Kotlin · streaming
// 大きな FeatureCollection は parseStream
val features = withContext(Dispatchers.IO) {
    GeoJSONParser.parseStream(input)
}

// RFC 8142 の GeoJSON Text Sequences
val seq = withContext(Dispatchers.IO) { GeoJSONSeqParser.parse(file) }
GeoJSONSeqParser.streamParse(file) { feature -> buffer.add(feature) }
What the video shows · Android + MapLibreKotlin · Jetpack Compose
val layerState = remember { GeoJSONLayerState() }
var features by remember { mutableStateOf(emptyList<GeoJSONFeature>()) }
var loading by remember { mutableStateOf(true) }

// parseStream returns immutable GeoJSONFeature values — no state object per feature
LaunchedEffect(Unit) {
    features = withContext(Dispatchers.IO) {
        context.assets.open("tokyo-buildings.geojson")
            .use(GeoJSONParser::parseStream)
    }
    loading = false
}

MapLibreMapView(state = mapViewState) {
    GeoJSONLayer(state = layerState, features = features)
}

if (loading) {
    LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
}
Sample video · loading a large GeoJSON as tiles
Video not shot yetTiles arriving one after another as the map is panned, filling the drawing in. That the map stays responsive as the data grows is the part a still cannot show.

06 · Current limitations

Layer-level click listeners are not registered automatically; forward to processClick (by design).
Default line and point hit tolerances are internal constants; pass a pixel tolerance per call to adjust.
Rendering is raster tiles, so native SDK vector feature querying is not used.
Parsers return an empty feature list for malformed input rather than throwing (iOS).

Related pages