Skip to content

_rings_to_multi_polygon produces inside-out polygons for features crossing the ObliqueMercator cut line #2650

@jackbenn

Description

@jackbenn

Description

When projecting Natural Earth land (or ocean) features into an ObliqueMercator projection whose cut line passes through large land masses, some polygons are returned as their own complement — a shape covering ~80–95% of the projection domain with a small hole where the land should be. The result is that ocean areas render as land color (tan).

Reproducer

import matplotlib.pyplot as plt
import cartopy.crs as ccrs
import cartopy.feature as cfeature

# ObliqueMercator along the Seattle–Tokyo great-circle arc.
# The cut line (antipodal great circle) passes through Alaska and
# eastern Russia, so several large land polygons cross it.
projection = ccrs.ObliqueMercator(
    central_longitude=-161.07,
    central_latitude=54.55,
    azimuth=90.0,
    scale_factor=1,
)

fig, axes = plt.subplots(1, 2, subplot_kw={'projection': projection},
                         figsize=(14, 4))
fig.suptitle('Inside-out polygon bug — ObliqueMercator over North Pacific')

for ax, label, x_half, y_half in [
    (axes[0], 'larger extent (less affected)',    6_460_000, 1_960_000),
    (axes[1], 'smaller extent (ocean = tan bug)', 5_070_000, 1_170_000),
]:
    ax.set_facecolor(cfeature.COLORS['water'])
    ax.add_feature(cfeature.LAND)
    ax.add_feature(cfeature.COASTLINE)
    ax.set_extent([-x_half, x_half, -y_half, y_half], crs=projection)
    ax.set_title(label)

plt.tight_layout()
plt.show()

The right-hand axes shows the ocean rendered as tan (land color): Image

The bug is triggered when AdaptiveScaler selects 50m data (which happens when the map extent is small enough — roughly < 50° in the shortest dimension). It does not affect 110m data.

Root cause

_attach_lines_to_boundary in crs.py correctly connects projected line segments to the projection boundary, but when a ring crosses the cut line the resulting ring traces the complement of the intended shape. Crucially, the ring retains the correct CCW/CW winding flag, so _rings_to_multi_polygon classifies it as an exterior ring and sgeom.Polygon(exterior_ring) produces a polygon with area ~80–95% of the full projection domain — the inside-out version of the land mass.

This can be detected reliably: a genuine land or ocean polygon projected into a bounded CRS cannot have area greater than half the projection domain. Any exterior ring whose polygon exceeds that threshold has been inverted by the cut-line crossing.

Fix

In _rings_to_multi_polygon, before the normal hole-slurping logic, check each exterior ring's polygon area against 0.5 * self.domain.area. If it exceeds that threshold, replace it with boundary_poly.difference(ring_polygon) to recover the correct shape.

I have a branch with the fix and a regression test ready

Note

The source of this bug was diagnosed, and the fix was written, with the assistance of Claude Code.

Environment

  • cartopy version: 0.25.1
  • shapely version: 2.0.2
  • OS: macOS

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions