Skip to content
Search

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.

MapLibre_GL_-_Water_Layer_Problem_1.png
MapLibre_GL_-_Water_Layer_Problem_2.png

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 verify passes, 161,203 tile contents, clustered
  • Header center/bounds correctly recalculated for Korea (center 128.25, 35.76)
  • min zoom 0 / max zoom 15, low-zoom overview tiles preserved
  • vector_layers names match the Protomaps schema the style assumes
  • The worker's gzip decompression path ([email protected] + Cloudflare DecompressionStream) 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_line but in the water layer as kind:river/stream LineStrings

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 build passes (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 next style.json request. Tile bytes are unchanged, so no edge cache invalidation is needed.

Notes

  • MapLibre fill layers do not filter by geometry type by default, so always specify a $type filter for mixed-geometry source-layers.
  • The $type filter only distinguishes Point / LineString / Polygon (no Multi distinction), so it is safe for area filters. The geometry-type expression 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
author Zer0 sitemap