MapLibre GL
lastmod 2026-06-18created 2023-08-033 edits
Fixing a specific polygon layer that renders broken in MapLibre
A record of diagnosing and fixing a problem where water areas render broken when rendering a South-Korea PMTiles basemap (a Protomaps planetiler extract) with MapLibre.
| | |
Symptoms
The map itself draws normally, but only the water areas render oddly. Polygon endpoints are cut off midway, or thin/zigzag fills appear across the map. It gets worse at higher zoom levels (z10 and above).
Diagnosis
The PMTiles file is fine
Inspecting the extract directly with the go-pmtiles CLI showed nothing wrong with the file itself.
-
pmtiles verifypasses, 161,203 tile contents, clustered - Header
center/boundscorrectly recalculated for Korea (center128.25, 35.76) -
min zoom 0 / max zoom 15, low-zoom overview tiles preserved -
vector_layersnames match the Protomaps schema the style assumes - The worker's gzip decompression path (
[email protected]+ CloudflareDecompressionStream) is also fine
So the cause is not the file but the layer definitions in the style.
Confirmed cause
Mixed geometry in the water layer
Protomaps' water source-layer contains mixed geometry types.
- Polygon:
ocean,bay,strait,water(lakes/reservoirs),swimming_pool→ areas that should be filled - LineString:
river,stream→ waterways that should be drawn as lines - Point: for labels (occasional)
Evidence from decoded tiles
Results of decoding actual tiles from the extract with @mapbox/vector-tile:
| Tile (z/x/y) | water layer contents (kind / geomType) |
| 7/109/49 | ocean·bay·water = Polygon 5, Point 1 |
| 8/219/101 | ocean·strait·water = Polygon 4, Point 1 |
| 10/875/403 | river = LineString 13, water = Polygon 3 |
| 12/3492/1587 | river = LineString 5, water·swimming_pool = Polygon 5 |
| 13/6984/3175 | stream = LineString 6, water·swimming_pool = Polygon 3 |
Rendering mechanism
Before the fix, the style's water layer was a type: 'fill' with no filter.
{ id: 'water', type: 'fill', source: SOURCE, 'source-layer': 'water',
paint: {'fill-color': c.water} } // 필터 없음 → water의 모든 피처를 채움
When MapLibre's fill renderer receives a LineString feature, classifyRings treats the line as a polygon ring, implicitly connects its start and end points, triangulates it (earcut), and fills it. As a result:
- Thin/zigzag polygons are painted along river paths
- Closing lines connecting river endpoints are drawn across the map
→ This exactly matches what the user saw: "water areas look odd, with endpoints cut off midway". Especially bad at z10+, where there are many waterways.
Secondary cause: dead physical-line layer
The style had a physical-line layer (source-layer: 'physical_line') meant to draw waterways as lines, but this build has no physical_line layer.
- vector_layers: boundaries / buildings / earth / landcover / landuse / places / pois / roads / water
- This Protomaps build stores waterways (rivers/streams) not in
physical_linebut in thewaterlayer askind:river/streamLineStrings
So physical-line was a dead layer pointing to a nonexistent layer; waterway lines were never drawn properly anywhere, and were instead wrongly absorbed into the water fill and painted broken (a schema assumption mismatch).
Fix
Applied patch
src/worker/lib/basemap-style.ts
1) Restrict the water fill to polygons (use $type instead of geometry-type to include Multi — geometry-type does not treat MultiPolygon as 'Polygon', so features may be dropped)
{ id: 'water', type: 'fill', source: SOURCE, 'source-layer': 'water',
filter: ['==', '$type', 'Polygon'], // 추가
paint: {'fill-color': c.water} }
2) Replace the dead physical-line with a water-based waterway line layer
{ id: 'waterway', type: 'line', source: SOURCE, 'source-layer': 'water',
filter: ['==', '$type', 'LineString'],
minzoom: 9,
layout: {'line-cap': 'round', 'line-join': 'round'},
paint: {
'line-color': c.water,
'line-width': ['interpolate', ['linear'], ['zoom'], 9, 0.4, 14, 2],
} }
Verification
-
./npm run buildpasses (types/compile OK) - The style is generated by the worker and the response carries
Cache-Control: no-cache, so the change takes effect from the nextstyle.jsonrequest. Tile bytes are unchanged, so no edge cache invalidation is needed.
Notes
- MapLibre
filllayers do not filter by geometry type by default, so always specify a$typefilter for mixed-geometry source-layers. - The
$typefilter only distinguishes Point / LineString / Polygon (no Multi distinction), so it is safe for area filters. Thegeometry-typeexpression returns MultiPolygon separately, so it is unsuitable for area filters. - Full diagnosis:
docs/plans/basemap-south-korea-render-diagnosis.md
See also
- GIS
- Mapkick.js - Create beautiful, interactive maps with one line of JS code
- Mapbox-GL-js is no longer under the 3-clause BSD license
- MapLibre GL - open-source fork of MapboxGL JS