Adding Layers¶
Your data layers live in app/layers.ts, which exports a layers array that settings.ts imports as settings.layers. (It's a separate file purely to keep settings.ts readable — everything downstream treats it as one array.)
Each entry is a typed object with a type discriminator that selects the loader:
import type { LayerConfig } from '@/core/types'
export const layers: LayerConfig[] = [
{
id: 'parks',
type: 'geojson',
name: 'National Parks',
geometryType: 'polygon',
url: './layers/parks.geojson',
symbology: { kind: 'simple', color: '#2c8a3e' },
},
// …
]
Order matters: it sets the layer-list display order, and groups are positioned by the first appearance of their group name.
Common fields (every layer type)¶
| Field | Type | Default | Description |
|---|---|---|---|
id |
string |
— (required) | Unique id (must be unique across all layers) |
name |
string |
— (required) | Display name in the layer list and legend |
type |
'geojson' \| 'pmtiles' \| 'flatgeobuf' \| 'esri' \| 'geoparquet' |
— (required) | Selects the loader |
symbology |
Symbology |
— | How to draw it (see Symbology). Required for most types; optional for Esri |
group |
string |
none | Group layers under a shared header in the layer list. Same name = same group |
visible |
boolean |
true |
Initial visibility |
opacity |
number (0–1) |
1 |
Initial opacity |
minZoom / maxZoom |
number |
MapLibre defaults | Zoom range at which the layer renders (distinct from the camera clamp in Map & Camera) |
geometryType |
'point' \| 'line' \| 'polygon' |
none | Drives the legend swatch shape; required for vector-tile types |
label |
LabelConfig |
none | Text labels (see Labels) |
popup |
PopupConfig |
none (off) | Feature popups (see Popups) |
hideFromLegend |
boolean |
false |
Keep the layer on the map + in the layer list, but out of the legend (works for any symbology kind) |
actions |
{ opacity?, labels? } |
global default | Per-layer override of the layer-list row action buttons |
Where files live¶
- Local files go in
app/layers/. Reference them as'./layers/<file>'. Vite resolves the path at build time. Supported extensions:.geojson,.pmtiles,.fgb,.parquet/.geoparquet. - Remote URLs (
http:///https://) are used as-is. Remote sources must be CORS-enabled.
GeoJSON file-size reality
A geojson source loads and parses the entire file into memory on the main thread — there's no streaming. Rough thresholds: under 5 MB is fine; 5–20 MB is noticeable startup lag; over ~50 MB isn't viable. minZoom controls rendering, not the parse. For large data, use PMTiles, FlatGeobuf (dynamic), or a tiled Esri service.
geojson¶
The simplest type. One source, automatically split into fill / line / circle sublayers filtered by geometry — so a single file can mix points, lines, and polygons.
| Field | Type | Description |
|---|---|---|
url |
string |
Path to a .geojson file (./layers/…) or a remote URL |
{
id: 'airports',
type: 'geojson',
name: 'US Major Airports',
geometryType: 'point',
url: 'https://example.com/airports.geojson',
symbology: { kind: 'ref', name: 'airports-by-passengers' },
}
pmtiles¶
PMTiles vector tiles — a single cloud-native archive that streams only the tiles in view. Ideal for large datasets.
| Field | Type | Description |
|---|---|---|
url |
string |
Path to a .pmtiles archive (local or remote) |
sourceLayer |
string |
The layer name inside the archive (required) |
geometryType |
'point' \| 'line' \| 'polygon' |
Required (vector tiles carry no geometry hint) |
{
id: 'counties',
type: 'pmtiles',
name: 'US Counties',
geometryType: 'polygon',
url: './layers/US_counties.pmtiles',
sourceLayer: 'US_counties',
symbology: { kind: 'simple', color: '#838291', strokeColor: '#fff', strokeWidth: 1 },
}
flatgeobuf¶
FlatGeobuf (.fgb) — a streaming vector format. Decoded into a GeoJSON source, so it flows through the same sublayer / popup / legend pipeline.
| Field | Type | Default | Description |
|---|---|---|---|
url |
string |
— | Path to a .fgb file (local or remote) |
bbox |
'all' \| 'dynamic' |
'all' |
Loading strategy |
'all'reads the whole file once into memory — fine for small/medium files (same ceiling as GeoJSON).'dynamic'fetches only the features in the current viewport via the file's spatial index (HTTP Range requests), refetched on map move — the way to use a large remote.fgb(hundreds of MB / GB) without downloading it whole.
Dynamic mode needs an uncompressed, Range-friendly host
dynamic reads byte ranges, so the host must serve the file uncompressed over Range requests. Hosts that gzip responses (e.g. GitHub Pages) return a 206 the browser can't decode, and dynamic mode fails. S3 / object stores / most CDNs are fine.
esri¶
ArcGIS REST services via esri-gl. A service discriminator picks the family. esri-gl creates the source; the kit adds the styled layer(s).
service |
Source kind | Capabilities |
|---|---|---|
'feature' |
GeoJSON (tile-indexed, queried per-viewport) | Full client styling + popups + legend |
'dynamic' |
Server-rendered raster (export image) | Visibility + opacity only; no legend, no popups |
'tiled' |
Pre-cached raster tiles | Visibility + opacity only |
Feature service¶
{
id: 'buoys',
type: 'esri',
service: 'feature',
name: 'NDBC Buoys',
geometryType: 'point',
url: 'https://services5.arcgis.com/.../FeatureServer/0',
minZoom: 4,
symbology: { kind: 'simple', color: '#a5d7f7', radius: 5, strokeColor: '#fff', strokeWidth: 1 },
popup: { displayType: 'rich' },
}
For feature services, symbology is optional: provide it for kit styling, or omit it to use the service's own ArcGIS renderer. Extra fields: where (an ArcGIS where clause, default '1=1').
Raster services¶
{
id: 'usa-dynamic', type: 'esri', service: 'dynamic',
name: 'USA States/Counties',
url: 'https://sampleserver6.arcgisonline.com/.../USA/MapServer',
layers: [2, 3], // dynamic only: which MapServer sublayers to draw
opacity: 0.7,
}
dynamic takes an optional layers (sublayer ids). tiled takes only the common fields. Both are visibility + opacity only.
geoparquet¶
GeoParquet — parsed in a Web Worker via parquet-wasm + apache-arrow. The worker decodes the WKB geometry to GeoJSON and feeds a GeoJSON source.
| Field | Type | Description |
|---|---|---|
url |
string |
Path to a .parquet / .geoparquet file (CORS-enabled if remote) |
limit |
number |
Optional cap on features to load (the reader stops early). Omit to read the whole file |
Optional dependencies
parquet-wasm and apache-arrow are optional dependencies — only loaded when a geoparquet layer is used. If they're missing, the loader logs a clear npm install parquet-wasm apache-arrow message and the rest of the map keeps working.
There's no viewport bbox filtering for GeoParquet yet — for large files, set a limit or pre-filter the file.
Labels¶
Any vector layer can carry text labels via a label block. Rendered as a companion MapLibre symbol sublayer.
| Field | Type | Default | Description |
|---|---|---|---|
field |
string |
— | Column to label by (compiles to ['get', field]) |
expression |
ExpressionSpecification |
— | A raw MapLibre expression for text-field (wins over field). Combine columns, e.g. ['concat', ['get', 'name'], ' (', ['get', 'code'], ')'] |
font |
string |
'Noto Sans Regular' |
Must be served by the basemap's glyphs URL (see note) |
fontSize |
number |
12 |
Text size in px |
color |
string |
'#333333' |
Text color |
haloColor |
string |
'#ffffff' |
Outline color for legibility |
haloWidth |
number |
1 |
Halo width (0 = none) |
minZoom |
number |
layer's range | Zoom floor for labels alone, so geometry can show before its labels |
anchor |
'center' \| 'top-left' \| 'top-right' \| 'bottom-left' \| 'bottom-right' |
'center' |
Label position relative to the feature |
allowOverlap |
boolean |
false |
Allow labels to overlap (MapLibre drops colliding labels by default) |
Fonts must be served by the basemap
MapLibre draws labels on the WebGL canvas, so the font must be in the active basemap style's glyphs URL. All OpenFreeMap styles (the defaults) serve Noto Sans Regular / Bold / Italic. The raw OSM raster basemap has no glyphs URL, so labels won't render over it.
Set app-wide label defaults once via ui.layerDefaults.label.
Hiding a layer from the legend¶
Set hideFromLegend: true on any layer to keep it on the map and in the layer list but omit it from the legend. (This works for every symbology kind, unlike the raw-symbology legend: { kind: 'hidden' } which is raw-only — see Symbology.)