Syncing MapLibre Filters with Renamed Source Layers
Tippecanoe derives each tile layer’s name from the input file’s basename unless you pass --layer (or -l) explicitly, so renaming an input from roads.geojson to road_network.geojson silently changes the tile layer ID from roads to road_network — and every MapLibre layer whose source-layer still says "roads" renders nothing, filters and all. The fix is to make the tile layer name an explicit, pinned value and to diff the style’s source-layer references against the tile’s actual layer IDs in CI.
When to Use This
Reach for this whenever the tile layer name is not under your explicit control:
- Pipelines where input filenames can change between runs (dated dumps like
roads_2026_07.geojson, or a source system that renames exports). - Builds that omit
--layer, so the layer ID is whatever the basename happens to be that day. - Multi-input builds where a
--layervalue is set per file and a copy-paste error pluralizes or re-cases one of them (roadvsroads). - Any handoff where the person editing the style is not the person who runs Tippecanoe, so a rename on one side never reaches the other.
If your layer names are already pinned and stable, this is a five-minute CI guard rather than a debugging session; the point is to move it from the second category to the first.
Specification Detail: How the Tile Layer Name Is Resolved
The tile layer ID that a MapLibre source-layer must match is resolved by Tippecanoe in this order:
| Precedence | Source of the name | Example | Result |
|---|---|---|---|
| 1 (highest) | --layer=NAME / -l NAME |
tippecanoe -l roads … |
Layer ID is exactly roads |
| 2 | -L NAME:FILE (per-input) |
-L water:lakes.geojson |
That input’s features land in layer water |
| 3 (fallback) | Input file basename, extension stripped | road_network.geojson |
Layer ID becomes road_network |
| — | stdin with no --layer |
… - |
Layer ID defaults to the literal ""-style basename of the source, often unstable |
The MapLibre side is a plain string equality: layers[].source-layer must equal the tile layer ID, byte for byte, case included. There is no fuzzy match and no error when it fails — the layer simply produces zero features.
The stdin case in the last row is the most treacherous. When you pipe NDJSON into Tippecanoe with a trailing - and no --layer, the layer name falls back to a value derived from the source description rather than a filename you control, and it can differ between environments or Tippecanoe versions. Any pipeline that streams GeoParquet or GeoJSON through stdin — the pattern used throughout the generation guides — should treat --layer as mandatory, not optional, precisely because there is no stable basename to fall back on.
It is worth being explicit about what a rename does not touch. The feature geometry, the attribute values, the zoom range, and the tile addressing are all unchanged; only the string key under which the features are grouped inside each tile has moved. That is why the tiles look perfectly healthy to every tool that does not compare the layer name against the style — pmtiles show reports the right feature counts, the tile server returns HTTP 200, and the bytes on the wire are valid MVT. The defect exists only in the relationship between two artifacts, which is why it has to be checked as a relationship.
To list the layer names a tile actually contains, read the vector_layers array from the tileset metadata:
# PMTiles
pmtiles show roads.pmtiles --metadata | \
jq -r '.vector_layers[].id'
# MBTiles
tippecanoe-json-tool roads.mbtiles | \
jq -r '(.json | fromjson).vector_layers[].id'
# Or straight from a single decoded .pbf (layer keys are the layer names)
tippecanoe-decode roads.mbtiles 12 2094 1361 | jq -r 'keys[]'
Whichever tool you use, the output is the authoritative set of names your source-layer values must be drawn from.
Production Command Block
The script below reads the tile layer names, scans style.json for every source-layer plus the property keys each filter references, reports drift, and — when a --rename old:new mapping is supplied — rewrites the style in place. It pairs with pinning the name at build time via tippecanoe --layer.
#!/usr/bin/env python3
"""remap_source_layers.py — detect and fix renamed source-layers."""
import argparse, json, subprocess, sys
def tile_layer_ids(pmtiles_path: str) -> set[str]:
raw = subprocess.check_output(["pmtiles", "show", pmtiles_path, "--metadata"])
return {l["id"] for l in json.loads(raw)["vector_layers"]}
def filter_keys(expr, out: set[str]) -> None:
if not isinstance(expr, list) or not expr:
return
if expr[0] == "get" and len(expr) == 2 and isinstance(expr[1], str):
out.add(expr[1])
for item in expr[1:]:
filter_keys(item, out)
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--tiles", required=True)
ap.add_argument("--style", required=True)
ap.add_argument("--rename", action="append", default=[],
help="old:new source-layer remap; repeatable")
ap.add_argument("--write", action="store_true", help="apply remaps in place")
args = ap.parse_args()
remap = dict(r.split(":", 1) for r in args.rename)
present = tile_layer_ids(args.tiles)
style = json.load(open(args.style))
errors, changed = [], 0
for layer in style.get("layers", []):
sl = layer.get("source-layer")
if sl is None:
continue
if sl in remap:
layer["source-layer"] = remap[sl]
sl, changed = remap[sl], changed + 1
if sl not in present:
keys: set[str] = set()
filter_keys(layer.get("filter", []), keys)
errors.append(
f"{layer['id']}: source-layer '{sl}' not in tile "
f"{sorted(present)} (filter reads {sorted(keys) or '—'})")
if args.write and changed:
json.dump(style, open(args.style, "w"), indent=2)
print(f"remapped {changed} layer(s) → {args.style}", file=sys.stderr)
if errors:
print("\n".join(errors), file=sys.stderr)
return 1
print("all source-layer references resolve", file=sys.stderr)
return 0
if __name__ == "__main__":
sys.exit(main())
Run it two ways. As a report-only CI gate:
python3 remap_source_layers.py --tiles roads.pmtiles --style style.json
To auto-remap a known rename and rewrite the style:
python3 remap_source_layers.py --tiles roads.pmtiles --style style.json \
--rename roads:road_network --write
And pin the name at build time so the drift cannot recur:
tippecanoe --output=roads.pmtiles \
--layer=roads \
--minimum-zoom=4 --maximum-zoom=14 \
--drop-densest-as-needed \
road_network_2026_07.geojson
With --layer=roads, the input can be renamed to anything — road_network_2026_07.geojson above — and the tile layer ID stays roads, so no source-layer in the style ever needs to move.
For a multi-input build, use the per-file -L name:file form so each source lands in a deterministically named layer regardless of its filename:
tippecanoe --output=basemap.pmtiles \
--minimum-zoom=4 --maximum-zoom=14 \
-L roads:road_network_2026_07.geojson \
-L water:hydro_polygons_2026_07.geojson \
-L places:populated_places.geojson
Here the style’s source-layer values are roads, water, and places no matter how the dated input files are named, so a monthly re-export never reaches the style. Commit the --layer/-L values next to the build script and treat them as a fixed interface: changing one is a deliberate, reviewed act that must land in the same commit as the corresponding source-layer edit.
Interaction Effects
The schema-agreement gate. This check is a slice of the same CI gate that validates property keys and zoom ranges. Wire remap_source_layers.py into the job described in validating styles in CI with the GL style spec so a rename fails the build alongside every other schema drift, on the exact tiles the build just produced.
Attribute renames. A layer rename and an attribute rename produce the identical blank-layer symptom, so treat them together — the parent guide on attribute filtering rules covers the -y/--exclude flags that decide which property keys a filter is even allowed to read once the source-layer resolves. When binding those keys into paint expressions, the patterns in binding data-driven properties to vector layers apply to the same references this check validates.
Versioned deploy. Ship the corrected style and the renamed tiles as a versioned pair. Embedding the schema hash in both the tile URL prefix and the style source URL guarantees a fixed style is never served against a cached tile built under the old layer name.
Performance Impact
There is no runtime cost — a source-layer string equality is free, and the fix does not change tile size, build time, or render fidelity. The payoff is correctness and debugging time. A silent blank layer typically costs an hour of bisecting tiles, styles, and CDN caches because nothing errors; a metadata diff catches it in the seconds the CI step takes to run, before the blank map ever ships. The pmtiles show call the check depends on reads only the metadata block, not the tile data, so it is effectively instant even on multi-gigabyte tilesets. The one place to be deliberate is if you extend the guard to decode a sample tile to confirm the remapped layer actually contains features — that reads real tile bytes and should run once in CI rather than on every local edit.
Common Mistakes
Relying on basename-derived names. Leaving --layer off means the layer ID is an accident of the filename. The moment an upstream export is renamed, every referencing layer blanks. Always pin --layer (or -L name:file per input) so the ID is a deliberate contract, not a side effect.
Renaming the input without updating the style. Changing roads.geojson to road_network.geojson and rebuilding produces valid tiles with a new layer ID and a now-stale style. The tiles load, the layer is empty, and there is no error message. Run the diff against the freshly built tiles so the rename is caught in the same commit that caused it.
Case and pluralization drift. roads vs road, Water vs water, landuse vs land_use — source-layer matching is exact and case-sensitive, so a one-character difference is as fatal as a total mismatch. Confirm the stored name with jq -r '.vector_layers[].id' rather than trusting memory, and normalize a naming convention (lowercase, singular or plural — pick one) across every --layer value in the build.
Parent: Layer Filter Synchronization with Tile Attributes
Related
- Validating Styles in CI with the GL Style Spec — the CI job this rename check plugs into, alongside JSON-schema linting and expression validation.
- Binding Data-Driven Properties to Vector Layers — how the property keys behind a resolved
source-layerbind into paint and layout expressions. - Attribute Filtering Rules — the upstream flags that decide which layers and attributes exist in the tile for a filter to reference.