Skip to content

Fix MLT MultiPolygon ring grouping for PostGIS-clipped geometries - #53

Closed
trasch wants to merge 3 commits into
mainfrom
fix/mlt-multipolygon-ring-grouping
Closed

trasch wants to merge 3 commits into
mainfrom
fix/mlt-multipolygon-ring-grouping

Conversation

@trasch

@trasch trasch commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Problem

MLT tiles showed triangular artifacts in maplibre-gl-js when rendering fill polygons at specific zoom levels (e.g. z7). The PBF (MVT) version of the same tile rendered correctly.

Root cause

PostGIS ST_ClipByBox2D can produce MultiPolygons where interior rings (holes) are stored as separate single-ring polygons instead of being associated with their parent exterior ring.

  • The MVT encoder flattens all rings into one list; the MVT decoder re-groups them using winding order heuristics (classifyRings), which accidentally fixes the broken PostGIS structure.
  • The MLT encoder preserved the PostGIS structure as-is, keeping holes as separate filled polygons. maplibre rendered these holes as filled polygons, causing overlapping triangular artifacts.

Fix

In MLTEncoder.encode, before encoding a MultiPolygon:

  1. Detect whether any single-ring polygon has a clockwise (interior) ring — a sign that holes were separated from their parent.
  2. If so, re-group all rings using the same winding-order heuristic as the MVT decoder: non-CW rings start a new polygon, CW rings become holes of the current polygon.
  3. Correctly structured MultiPolygons are left untouched.

Verification

  • Tile 7/65/47 landuse_a layer: went from 2 → 26 MultiPolygons with holes, matching the PBF output exactly.
  • All 57 existing MLT tests pass.
  • Added 3 regression tests covering the fix.

Files changed

  • Sources/MVTTools/Coders/MLT/MLTEncoder.swift — the fix
  • Tests/MVTToolsTests/MLT/MLTRoundtripGeometryTests.swift — regression tests

PostGIS ST_ClipByBox2D can produce MultiPolygons where interior rings
(holes) are stored as separate single-ring polygons instead of being
associated with their parent exterior ring.  The MVT encoder flattens
all rings and the MVT decoder re-groups them via winding order, which
accidentally fixes the structure.  The MLT encoder preserved the broken
PostGIS structure as-is, causing maplibre to render holes as filled
polygons and produce triangular artifacts at specific zoom levels.

Fix: before encoding a MultiPolygon, detect whether any single-ring
polygon has a clockwise (interior) ring — a sign that holes were
separated from their parent.  If so, re-group all rings using the same
winding-order heuristic as the MVT decoder (classifyRings): non-CW
rings start a new polygon, CW rings become holes of the current
polygon.  Correctly structured MultiPolygons are left untouched.

Verified on tile 7/65/47: landuse_a went from 2 to 26 MultiPolygons
with holes, matching the PBF output exactly.
@trasch trasch self-assigned this Aug 18, 2026
@trasch trasch added the bug Something isn't working label Aug 18, 2026
@trasch

trasch commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

Closing: the winding-order "regrouping" heuristic in this PR is actively harmful. It merges legitimate MultiPolygons that contain several separate same-winding components (e.g. 28 disjoint forest patches) into a single polygon with bogus holes, producing the very triangular artifacts it aimed to fix. The correct hole/interior-ring association is guaranteed by the data source (PostGIS ST_MakeValid(method=structure)); the MLT encoder simply passes the structure through unchanged.

@trasch trasch closed this Aug 18, 2026
@trasch
trasch deleted the fix/mlt-multipolygon-ring-grouping branch August 18, 2026 15:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant