Layers & Loaders¶
How settings.layers becomes rendered map layers. Lives in app/src/core/layers/.
Registration¶
registerLayers(map, layers) is the entry point, called by MapView once the style loads (and again on every style.load). It:
- Validates ids — duplicate layer ids throw at boot.
- Validates symbology refs — a
refwhose name isn't insymbology.tsthrows eagerly (clear message at boot, not a silent paint failure later). - Preserves runtime state — for each layer, if the store already has metadata (a basemap switch), it reuses that layer's
visible/opacityinstead of the settings defaults, so user toggles survive. - Dispatches to a per-type loader based on
layer.type. - Writes
LayerMetadatainto the store (what the layer list and legend read). - Returns
RegisteredLayer[]— each may carry ateardown()for persistent resources.
A failing loader (e.g. a missing local file) is caught and skipped with a warning — one bad layer never blanks the whole map; it's just left out of the layer list/legend.
The loader dispatch¶
type === 'geojson' → loaders/geojson.ts addGeoJsonLayer
type === 'pmtiles' → loaders/pmtiles.ts addPMTilesLayer
type === 'flatgeobuf' → loaders/flatgeobuf.ts addFlatGeobufLayer
type === 'esri' → loaders/esri.ts addEsriLayer
type === 'geoparquet' → loaders/geoparquet.ts addGeoParquetLayer
Each loader creates the MapLibre source and the styled sublayers, then returns a RegisteredLayer.
The shared sublayer model¶
A single logical layer emits up to several MapLibre sublayers, named by suffix off the layer id:
| Suffix | Type | Used by |
|---|---|---|
-fill |
fill | polygons |
-line |
line | lines + polygon outlines |
-point |
circle | points |
-label |
symbol | labels (Phase 15) |
-raster |
raster | server-rendered Esri raster services |
For GeoJSON, the loader adds all three geometry sublayers, each filtered by geometry type (['==', ['geometry-type'], 'Point' | 'LineString' | 'Polygon']). That's why one GeoJSON file can mix points, lines, and polygons without extra config — the polygon outline reuses the -line layer (its filter matches both LineStrings and Polygons).
Vector formats that decode to GeoJSON in the browser — FlatGeobuf and GeoParquet — reuse the same shared sublayer builder (loaders/geojsonSublayers.ts). So popups, legend, opacity, labels, and basemap-switch survival all work for those formats with zero extra wiring. (There's no streaming-vector path in browser JS for these two: they ultimately hit the same in-memory GeoJSON ceiling as a .geojson file. Only PMTiles/MVT and raster truly stream large data.)
Layer operations — layerOps.ts¶
Runtime visibility and opacity walk the sublayer suffixes:
setLayerVisibilitytoggles the geometry sublayers'visibility. The-labelsublayer is handled separately by default (the layer list has an independent labels button), unlessincludeLabelis set — used for the draw widget's text drawings, where the text is the geometry.setLayerOpacitysets the right opacity paint prop per sublayer type (fill-opacity,line-opacity,circle-opacity+circle-stroke-opacity,text-opacity,raster-opacity). It skips any property whose value is an expression — arawsymbology's data-driven paint is the user's source of truth and must not be overwritten with a flat number.
Missing sublayers are skipped silently, so the same op works regardless of which sublayers a given layer actually has.
URL resolution¶
resolveLayerUrl.ts turns a user path like './layers/parks.geojson' into a bundled URL via Vite's import.meta.glob('/layers/**/*', { query: '?url' }). Absolute URLs pass through unchanged. A mistyped local path throws with a helpful message listing the files it does know about. Vite inlines small layer files as data URLs and emits larger ones as separate hashed assets.
Basemap-switch survival¶
map.setStyle() (the basemap widget) wipes all sources and layers. MapView listens on style.load and re-runs registerLayers + popup registration, calling each RegisteredLayer.teardown() first so persistent per-layer listeners don't stack. Because registration reads visible/opacity from the store's metadata when present, the user's toggles carry across the switch.