In Cartopy, we recently switch from manual conversion of Shapefile-to-Shapely to using shapely.geometry.shape, which uses pyshp's implementation of __geo_interface__. On the 10m scale Natural Earth Land feature, this produces bogus results SciTools/cartopy#1578. This is because the __geo_interface__ implementation for polygons returns invalid polygons.
If you load ne_10m_land, and look at the first shape, you get a ring of 98 parts.
>>> import shapefile
>>> r = shapefile.Reader('natural_earth/physical/ne_10m_land')
>>> shape = r.shape(0)
>>> import shapely.geometry as sgeom
>>> shapefile.PARTTYPE_LOOKUP[shape.shapeType]
'RING'
>>> len(shape.parts)
98
>>> shape.parts
[0, 15957, 82433, 83999, 93461, 174970, 175864, 175972, 179585, 180141, 181933, 183376, 185735, 186804, 188298, 188306, 190694, 191670, 194149, 194775, 197939, 198120, 198271, 199303, 201557, 201793, 205500, 208563, 209874, 212082, 213047, 223935, 224492, 225634, 241182, 242117, 242451, 242475, 243635, 244521, 245714, 247283, 248503, 252342, 252630, 252785, 252871, 253078, 253132, 254149, 254211, 254227, 254361, 254868, 256367, 256794, 257497, 257777, 258009, 258359, 258378, 258391, 258406, 258521, 258689, 258802, 258872, 258934, 259183, 259478, 260055, 260160, 260352, 260584, 260749, 260842, 261276, 261587, 262753, 262832, 262868, 262971, 263079, 263280, 263376, 263941, 264041, 264119, 264286, 264346, 264380, 264517, 264884, 266663, 266745, 266791, 266836, 268575]
pyshp takes interior rings and puts them in the preceding exterior ring:
|
for ring in rings[1:]: |
|
if signed_area(ring) < 0: |
|
polys.append(poly) |
|
poly = [ring] |
|
else: |
|
poly.append(ring) |
|
polys.append(poly) |
so here, part 15 is used as an interior ring in part 14:
>>> p14 = sgeom.Polygon(shape.points[shape.parts[14]:shape.parts[15]])
>>> p14.exterior.is_ccw
False
>>> p15 = sgeom.Polygon(shape.points[shape.parts[15]:shape.parts[16]])
>>> p15.exterior.is_ccw
True
>>> p16 = sgeom.Polygon(shape.points[shape.parts[16]:shape.parts[17]])
>>> p16.exterior.is_ccw
False
However, if you check more carefully, that ring is not contained in part 14, but in part 4:
>>> p14.contains(p15)
False
>>> p4 = sgeom.Polygon(shape.points[shape.parts[4]:shape.parts[5]])
>>> p4.exterior.is_ccw
False
>>> p4.contains(p15)
True
The original Cartopy code, traversed all exterior rings and put the interior ring in the one that contains it (i.e., part 15 is put in part 4). It seems that this traversal must be done in pyshp as well, or invalid geometries are produced.
>>> mp = sgeom.shape(shape)
>>> mp.is_valid
False
>>> [(i, j.is_valid) for i, j in enumerate(mp.geoms) if not j.is_valid]
[(14, False)]
If the interior ring is moved to the right place, the geometry is valid again:
>>> geoms = list(mp.geoms)
>>> geoms[4] = sgeom.Polygon(geoms[4].exterior, geoms[14].interiors[:])
>>> geoms[14] = sgeom.Polygon(geoms[14].exterior, [])
>>> mpn = sgeom.MultiPolygon(geoms)
>>> mpn.is_valid
True
In Cartopy, we recently switch from manual conversion of Shapefile-to-Shapely to using
shapely.geometry.shape, which uses pyshp's implementation of__geo_interface__. On the 10m scale Natural Earth Land feature, this produces bogus results SciTools/cartopy#1578. This is because the__geo_interface__implementation for polygons returns invalid polygons.If you load
ne_10m_land, and look at the first shape, you get a ring of 98 parts.pyshp takes interior rings and puts them in the preceding exterior ring:
pyshp/shapefile.py
Lines 244 to 250 in 71231dd
so here, part 15 is used as an interior ring in part 14:
However, if you check more carefully, that ring is not contained in part 14, but in part 4:
The original Cartopy code, traversed all exterior rings and put the interior ring in the one that contains it (i.e., part 15 is put in part 4). It seems that this traversal must be done in pyshp as well, or invalid geometries are produced.
If the interior ring is moved to the right place, the geometry is valid again: