Working with GeoJSON
GeoJSON is the data format that powers everything in MapLibre. Whether you're adding a symbol layer, a cluster, or a heatmap, the data comes from a GeoJSON source. Understanding GeoJSON unlocks the full power of style layers.
What is GeoJSON?
GeoJSON (RFC 7946) is a JSON format for geographic features. A FeatureCollection contains any number of Features, each of which has a Geometry (the shape) and Properties (arbitrary metadata).
flowchart LR
FC["FeatureCollection"] --> F["Feature"]
F --> G["geometry"]
F --> P["properties"]
G --> GT["type<br/><small>Point · LineString · Polygon</small>"]
G --> GC["coordinates<br/><small>[lng, lat] nested by type</small>"]
P --> PV["{ name: 'Paris',<br/>population: 2161000, ... }"]
classDef emphasis fill:#1f6feb,stroke:#1a5fd0,color:#fff;
class FC emphasis
Longitude before latitude
GeoJSON uses [longitude, latitude] order, the opposite of LatLng(lat, lng) in Flutter. This is a common source of bugs.
The three geometry types
Point
A single geographic location.
{
'type': 'Feature',
'properties': {'name': 'Eiffel Tower'},
'geometry': {
'type': 'Point',
'coordinates': [2.2945, 48.8584], // [lng, lat]
},
}
LineString
An ordered sequence of points forming a line.
{
'type': 'Feature',
'properties': {'route': 'Paris - Berlin'},
'geometry': {
'type': 'LineString',
'coordinates': [
[2.3522, 48.8566], // Paris [lng, lat]
[13.4050, 52.5200], // Berlin
],
},
}
Polygon
A closed ring of coordinates (first and last point must be identical).
{
'type': 'Feature',
'properties': {'name': 'Zone A'},
'geometry': {
'type': 'Polygon',
'coordinates': [
[
[2.0, 48.5],
[2.5, 48.5],
[2.5, 49.0],
[2.0, 49.0],
[2.0, 48.5], // close the ring
]
],
},
}
Using properties in expressions
Properties are what make style layers powerful. Any feature property can drive the visual appearance of that feature via expressions.
// Property access in a layer
SymbolLayerProperties(
textField: [Expressions.get, 'name'], // show 'name' property as label
textSize: [Expressions.get, 'font_size'], // drive size from data
iconColor: [Expressions.get, 'color'], // drive color from data
)
Adding and updating a source
Sources are added with addGeoJsonSource and updated with setGeoJsonSource
(whole collection) or setGeoJsonFeature (one feature by id). See
GeoJSON Source for the calls and their options.
Large payloads
On Android and iOS, a payload with long geometries or long properties, such as a line with tens of thousands of points, is encoded on a background isolate, so the UI is not blocked for the whole encode. Handing the payload to the isolate costs a copy on the calling side, and for many small features, such as points, the copy costs more than the encode: those payloads, and small ones, are encoded in place. A very large source can still drop a frame either way.
Feature ids
A feature keeps the id it has in the data, number or string, on every platform: the queries and the cluster calls answer 2.5 and "poi-12" as they were added, and an ["id"] filter or feature state finds the feature by that id. The tap and drag callbacks get the id as text, such as "2" for 2.
GeoJSON loaded from a URL or declared in the style does not pass through the plugin, so it keeps the ids MapLibre gives it: on Android and web only integer ids are sure to come back the same. Vector tile ids are always integers.
To keep the ids, the plugin can add a property whose name starts with maplibre_gl: to a feature. The queries leave it out, but a style expression that reads every property, such as ["properties"], sees it.
Filter by id with ["id"]. On web the legacy filter syntax, such as ["==", "$id", 2], sees only integer ids.
A source added with addSource and generateId: true gives each feature its index as id, over any id it had. MapLibre Native has no generateId, so on Android and iOS the plugin numbers the data it sets, but not data loaded from a URL.
Feature state (setFeatureState, getFeatureState, removeFeatureState) is supported on Android, iOS and web. It lets you restyle individual features, for example recoloring many of them every frame, without re-feeding the whole GeoJSON source.
promoteId: web only
promoteId makes a property the id of each feature:
await controller.addGeoJsonSource(
'my-source',
geojsonData,
promoteId: 'myIdProperty', // promotes this property to be the feature ID
);
Android and iOS ignore it: there every feature must carry a top-level id member in the GeoJSON itself.
Live demo
Capital cities across the Americas rendered from an inline FeatureCollection. Circle radius scales with population using an interpolate expression.
Next steps
- Data-Driven Expressions: use properties to drive styles
- Annotations vs Style Layers: when to use GeoJSON sources vs the annotation API