Shapefile Ring Orientation Reversal¶
A Shapefile holding the same square as simple_valid_polygon, but with its exterior ring wound clockwise instead of counter-clockwise. The Shapefile specification mandates CW exterior rings, so OGR silently reverses the winding of any GeoJSON polygon written through it. Code that reads Polygon.exterior.is_ccw to tell an exterior ring from a hole gets the wrong answer here.
| Property | Value |
|---|---|
| Case ID | shapefile_ring_orientation |
| Category | vector |
| Format | Shapefile |
| Geometry type | Polygon |
| CRS | EPSG:4326 |
| Location | Central Europe (synthetic) — 10.00°E, 50.00°N → 11.00°E, 51.00°N |
| Test tier | unit |
| Size class | tiny |
| Storage class | bundled |
| Redistributable | yes |
| Loader | geopandas |
| Status | validated |
Use this case¶
import pytest
@pytest.mark.geocase_case("shapefile_ring_orientation")
def test_shapefile_ring_orientation(geocase_case) -> None:
data = geocase_case.load()
assert data is not None
Use GeoCase in your tests¶
Install the complete set of vector, raster, and NetCDF dependencies:
What this case checks¶
Expose workflows that assume a polygon read from any format keeps the RFC 7946 right-hand rule, or that infer ring role from orientation rather than from ring position. Provide a fixture where topological equality with the GeoJSON reference holds while orientation differs.
Risk types covered¶
Expected behavior¶
| Assertion | Expected |
|---|---|
expect_loadable |
yes |
expect_valid_geometry |
yes |
expect_crs |
yes |
expected_epsg |
4326 |
expected_geometry_types |
Polygon |
Notes¶
Purpose¶
This case pins the one thing a Shapefile round trip changes about a polygon that is otherwise identical to its source: the winding order of the rings.
The geometry is the same square as simple_valid_polygon —
POLYGON ((10 50, 11 50, 11 51, 10 51, 10 50)) in the GeoJSON reference — and
the two are topologically equal. But the exterior ring here runs clockwise,
so geom.exterior.is_ccw is False where the reference's is True.
Mechanism¶
The Shapefile specification (ESRI, 1998, p. 8) mandates that a polygon's outer ring be ordered clockwise and its holes counter-clockwise. RFC 7946 §3.1.6 and the OGC right-hand rule mandate exactly the opposite. OGR resolves the conflict by rewriting the orientation on write, regardless of what it was given:
# The written .shp comes back with the winding reversed, silently.
gpd.read_file("rfc7946_ccw.geojson").to_file("out.shp")
Nothing warns, nothing fails, and the geometry is not wrong — it is correct for the format.
Why this breaks real code¶
The failure mode is code that infers a ring's role from its orientation:
# Wrong: works on GeoJSON, silently inverts on Shapefile.
outer = next(r for r in rings if r.is_ccw)
holes = [r for r in rings if not r.is_ccw]
Ring role is positional — in both formats and in Shapely, geom.exterior is the
exterior and geom.interiors are the holes, whatever their winding. Orientation
is a serialization detail. Code that conflates the two produces inverted
polygons, negative signed areas, or holes promoted to exteriors, on exactly one
of its input formats.
Provenance¶
These bytes are the pre-plan-13 polygon_shapefile_baseline, preserved rather
than discarded when that family was converged onto its declared canonical. Its
name attribute still reads baseline_polygon for that reason.
The preservation is not sentimentality. The *_baseline families are compared
with shapely.normalize on both sides, which canonicalizes ring orientation by
design — it has to, because the Shapefile members of those families cannot
match a CCW canonical. Inside a baseline family this artifact is therefore not
merely undocumented, it is unassertable. Only as its own case can anything
assert it at all.
Expected behavior¶
- The file loads and the geometry is valid.
geom.equals(simple_valid_polygon_geometry)isTrue— topologically identical.geom.exterior.is_ccwisFalse, and the reference's isTrue.shapely.normalizemakes the two compare equal;equals_exactdoes not.
Related cases¶
simple_valid_polygon— the CCW GeoJSON reference.polygon_shapefile_baseline— the current Shapefile member of the cross-format family, which holds this same geometry and, being a Shapefile, is also CW. It is compared winding-insensitively; this case is where the winding itself is asserted.shapefile_field_truncation,shapefile_encoding_legacy— the other format artifacts promoted to named cases rather than left implicit in a baseline.
Required capabilities¶
loadgeometry-validationattribute-inspection
Files¶
- Primary:
ring_orientation.shp - Sidecar:
ring_orientation.dbf - Sidecar:
ring_orientation.shx - Sidecar:
ring_orientation.prj - Sidecar:
ring_orientation.cpg - Notes:
notes.md
Source and license¶
- Source: geocase-curated
- License: MIT
Tags¶
format_specific polygon ring_orientation shapefile vector winding
Related cases¶
- Format-Limited KML Case --
format_limited_kml_case - Shapefile Legacy DBF Encoding --
shapefile_encoding_legacy - Shapefile Field Name Truncation --
shapefile_field_truncation - Empty Geometry in GeoPackage --
empty_geometry_gpkg - Format-limited Precision Polygon --
format_limited_precision_polygon