Skip to content

Commit e3de259

Browse files
committed
FIX: create a single inverted polygon when no exteriors found
Fixes #1149, #1674, #2160,
1 parent 0fbb8a9 commit e3de259

File tree

4 files changed

+59
-16
lines changed

4 files changed

+59
-16
lines changed

lib/cartopy/crs.py

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import pyproj
2222
from pyproj import Transformer
2323
from pyproj.exceptions import ProjError
24+
import shapely
2425
import shapely.geometry as sgeom
2526
from shapely.prepared import prep
2627

@@ -1214,34 +1215,52 @@ def _rings_to_multi_polygon(self, rings, is_ccw):
12141215
y3 -= by
12151216
x4 += bx
12161217
y4 += by
1218+
1219+
interior_polys = []
1220+
12171221
for ring in interior_rings:
1218-
# Use shapely buffer in an attempt to fix invalid geometries
1219-
polygon = sgeom.Polygon(ring).buffer(0)
1220-
if not polygon.is_empty and polygon.is_valid:
1222+
polygon = shapely.make_valid(sgeom.Polygon(ring))
1223+
if not polygon.is_empty:
1224+
if isinstance(polygon, sgeom.Polygon):
1225+
interior_polys.append(polygon)
1226+
elif isinstance(polygon, sgeom.MultiPolygon):
1227+
interior_polys.extend(polygon.geoms)
1228+
elif isinstance(polygon, sgeom.GeometryCollection):
1229+
for geom in polygon.geoms:
1230+
if isinstance(geom, sgeom.Polygon):
1231+
interior_polys.append(geom)
1232+
else:
1233+
# make_valid may produce some linestrings. Ignore these
1234+
continue
1235+
12211236
x1, y1, x2, y2 = polygon.bounds
12221237
bx = (x2 - x1) * 0.1
12231238
by = (y2 - y1) * 0.1
12241239
x1 -= bx
12251240
y1 -= by
12261241
x2 += bx
12271242
y2 += by
1228-
box = sgeom.box(min(x1, x3), min(y1, y3),
1229-
max(x2, x4), max(y2, y4))
12301243

1231-
# Invert the polygon
1232-
polygon = box.difference(polygon)
1244+
x3 = min(x1, x3)
1245+
x4 = max(x2, x4)
1246+
y3 = min(y1, y3)
1247+
y4 = max(y2, y4)
12331248

1234-
# Intersect the inverted polygon with the boundary
1235-
polygon = boundary_poly.intersection(polygon)
1249+
box = sgeom.box(x3, y3, x4, y4, ccw=is_ccw)
12361250

1237-
if not polygon.is_empty:
1238-
polygon_bits.append(polygon)
1251+
# Invert the polygons
1252+
polygon = box.difference(sgeom.MultiPolygon(interior_polys))
12391253

1240-
if polygon_bits:
1241-
multi_poly = sgeom.MultiPolygon(polygon_bits)
1242-
else:
1243-
multi_poly = sgeom.MultiPolygon()
1244-
return multi_poly
1254+
# Intersect the inverted polygon with the boundary
1255+
polygon = boundary_poly.intersection(polygon)
1256+
1257+
if not polygon.is_empty:
1258+
if isinstance(polygon, sgeom.MultiPolygon):
1259+
polygon_bits.extend(polygon.geoms)
1260+
else:
1261+
polygon_bits.append(polygon)
1262+
1263+
return sgeom.MultiPolygon(polygon_bits)
12451264

12461265
def quick_vertices_transform(self, vertices, src_crs):
12471266
"""
51.3 KB
Loading

lib/cartopy/tests/mpl/test_features.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,18 @@ def test_natural_earth_custom():
4646
return ax.figure
4747

4848

49+
@pytest.mark.filterwarnings("ignore:Downloading")
50+
@pytest.mark.natural_earth
51+
@pytest.mark.mpl_image_compare(filename='ocean_lambertazimuthalequalarea.png')
52+
def test_ocean_lambertazimuthalequalarea():
53+
proj = ccrs.LambertAzimuthalEqualArea(central_latitude=45, central_longitude=-100)
54+
ax = plt.axes(projection=proj)
55+
ax.add_feature(cfeature.OCEAN)
56+
ax.coastlines()
57+
ax.set_extent([-140, -70, 20, 60])
58+
return ax.figure
59+
60+
4961
@pytest.mark.network
5062
@pytest.mark.skipif(not _HAS_PYKDTREE_OR_SCIPY, reason='pykdtree or scipy is required')
5163
@pytest.mark.mpl_image_compare(filename='gshhs_coastlines.png', tolerance=0.95)

lib/cartopy/tests/test_polygon.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,18 @@ def test_inverted_poly_simple_hole(self):
446446
self._assert_bounds(polygon.interiors[0].bounds,
447447
- 1.2e7, -1.2e7, 1.2e7, 1.2e7, 1e6)
448448

449+
def test_inverted_poly_multi_hole(self):
450+
# Adapted from 1149
451+
proj = ccrs.LambertAzimuthalEqualArea(
452+
central_latitude=45, central_longitude=-100)
453+
poly = sgeom.Polygon([(-180, -80), (180, -80), (180, 90), (-180, 90)],
454+
[[(-50, -50), (-50, 0), (0, 0), (0, -50)]])
455+
multi_polygon = proj.project_geometry(poly)
456+
# Should project to single polygon with multiple holes
457+
assert len(multi_polygon.geoms) == 1
458+
assert len(multi_polygon.geoms[0].interiors) >= 2
459+
460+
449461
def test_inverted_poly_clipped_hole(self):
450462
proj = ccrs.NorthPolarStereo()
451463
poly = sgeom.Polygon([(0, 0), (-90, 0), (-180, 0), (-270, 0)],

0 commit comments

Comments
 (0)