point order in polygons - ObjectVision/GeoDMS GitHub Wiki

For some functions (like union_polygon) the sequence order of points in a polygon matters.

rules

The following rules apply to the sequence of points in a polygon:

  1. outer rings need to be configured clock wise
  2. inner rings (lakes) are configured counter clock wise
  3. the first and the last point need to have the same coordinate

If the sequence of points is configured manually and used in the sequence2points, always configure the correct sequence, although for the map and functions like point_in_polygon an incorrect sequence might not matter.

Rule 3 is the one the engine repairs for you on the way out: since GeoDMS 20.19.2 both shapefile writers close a ring that the value left open, so an ESRI Shapefile the GeoDMS writes can be read back by gdal.vect, QGIS and ArcGIS. Rules 1 and 2 are never touched by a writer — see fix_winding_order and reverse_polygon below.

multi-polygons

A multi-polygon lives in the same single point sequence. Each ring is written closed, one after the other, and the reader recognises a new ring when a point repeats the ring's own first point. After the rings of a polygon, the sequence walks back to where it came from with backtrack points, so that the whole sequence starts and ends at the very first point:

outer(closed) hole0(closed) hole1(closed) hole0[0] outer[0]      // one polygon with two holes
A(closed) B(closed) C(closed) B[0] A[0]                          // three separate polygons

Which polygon a hole belongs to follows from this order: an inner ring belongs to the outer ring that precedes it, not to the multi-polygon as a whole. That distinction matters whenever a polygon is nested inside another polygon's hole — a shed on a courtyard enclosed by a building block, for instance.

what the readers accept

Since GeoDMS 20.19.0 all three float64 geometry families read a ring's role relative to the feature it belongs to: the first ring that survives decides what "outer" means, a ring wound the other way is a hole of it, and a ring wound like it opens the next polygon. A feature whose rings are all flipped — the usual cause is a source that lists coordinates in latitude/longitude order — therefore arrives with the right topology. geos_polygon and cgal_polygon even hand back a correctly wound result, because both normalise the winding of what they produce; bg_polygon preserves the shape it read, so its result stays negatively wound.

Before 20.19.0 — 20.18.0 included, whose binaries predate the repair — the geos and cgal readers judged each ring absolutely: outer if and only if clockwise. On a uniformly counter-clockwise feature that skipped the shell before any polygon was open, so the next ring — a lake — was promoted to shell and the island inside that lake became its hole. What came back was ordinary-looking geometry with a smaller area (36 − 4 rather than 100 − 36 + 4 for a 10×10 shell with a 6×6 lake and a 2×2 island) and no message of any kind, so a configuration could carry the loss unnoticed. If areas change when you upgrade a configuration that reads flipped source data, this is the likely reason, and the older numbers were the wrong ones. See GeoDMS issue #1212.

Only a uniformly flipped feature is covered by this. When some rings of a feature are flipped and others are not, no rule based on orientation can recover the roles; use fix_winding_order, which derives them from the nesting instead.

Such a partly flipped feature also changes on upgrade, and — unlike the uniform case — it can get smaller, not larger. The old absolute rule dropped a leading counter-clockwise ring outright instead of cutting it out, so the feature was read too large; the relative rule keeps that ring and treats the rings wound the other way as its holes. Measured on the national CBS land-use polygons (NBBG2020, 358,753 features, none of them uniformly flipped): 20.18.0 read 232 features a combined 2,298 ha too large, while 20.19.0 reproduces the stored area of the whole layer exactly. So an area that shrinks on upgrade is the expected result for this class of feature, and the older number was again the wrong one. These features are irregular in other ways too — outer_multi_polygon reports overlapping outer rings on them — so fix_winding_order remains the right treatment rather than relying on either reader rule.

validation

Use has_correct_winding: it is True only when every ring's orientation agrees with its nesting, no ring collapsed, and the geometry is topologically valid.

attribute<bool> winding_ok (district) := has_correct_winding(district/geometry);

The older test was to request the area of the polygon and check its sign: positive means a correct sequence, negative means the points are in the wrong order. That still works for a feature whose rings are all flipped, but it is not sufficient. area is a single signed sum over the whole sequence, so a feature where only the lake is flipped comes out too large rather than negative and passes the test unnoticed.

repairing the point order

Which operator to reach for depends on what is wrong:

symptom operator
every ring of the feature is flipped, e.g. a source in latitude/longitude order reverse_polygon — since 20.19.0 the readers cope with this on their own, so this is about storing the layer the documented way
some rings flipped and others not; islands in lakes ending up as the wrong role fix_winding_order
self-intersecting rings, holes outside their shell, overlapping outer rings fix_polygon

reverse_polygon and fix_winding_order never move a vertex, so they are safe to apply to a whole layer. fix_polygon does move coordinates, because repairing a self-intersection means introducing the intersection point.

Note that reversing the whole point sequence of a multi-polygon - for instance with sequence2points, reverse and points2sequence - does not work: it destroys the ring structure described above, because the reader recognises a new ring where a point repeats the ring's own first point. reverse_polygon reverses each ring in place instead, which leaves every delimiter and backtrack point untouched.

⚠️ **GitHub.com Fallback** ⚠️