Skip to content

Interior rings incorrectly converted to GeoJSON #202

Description

@QuLogic

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:

pyshp/shapefile.py

Lines 244 to 250 in 71231dd

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

Metadata

Metadata

Assignees

Labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions