Skip to content

Story: Encode Polygon geometries and validate ring orientation #13

Closed

Description

@Alexandre-T

Objective

Encode Polygon rings and reject incompatible orientation without modifying the geometry.

Acceptance criteria

  1. Encode {"type":"Polygon","coordinates":[[[0,0],[2,0],[0,2],[0,0]]]} unchanged.
  2. Encode {"type":"Polygon","coordinates":[[[0,0,3],[2,0,3],[0,2,3],[0,0,3]]]} unchanged.
  3. Preserve a correctly oriented exterior and hole: {"type":"Polygon","coordinates":[[[0,0],[4,0],[4,4],[0,4],[0,0]],[[1,1],[1,2],[2,2],[2,1],[1,1]]]}.
  4. Reject a clockwise exterior ring such as [[0,0],[0,2],[2,0],[0,0]] with UnsupportedGeometryStructureException.
  5. Reject a counterclockwise interior ring with UnsupportedGeometryStructureException.
  6. EMPTY XY and XYZ Polygons produce {"type":"Polygon","coordinates":[]}.
  7. Do not reverse rings, reorder vertices, close or repair rings, discard members or cut at the antimeridian.
  8. Verify ring requirements against the actual model invariants. Escalate a genuinely unresolved normative case instead of inventing a repair or acceptance policy.

The shared acceptance criteria below also apply.

Implementation and integration

Develop and count the shared ring checks here once. Their private location and algorithms remain implementation decisions. MultiPolygon may reuse them after integration. No functional dependency on MultiPoint. Full verification requires the common infrastructure and operational ring checks; development may begin immediately.

Shared acceptance criteria

  • Cover Geometry and Geography families using real objects supported by the applicable spatial-types version.
  • Support XY and XYZ; preserve coordinate values, position order, member order and Z. Do not reproject, convert units, reorient, split, flatten, omit members or implicitly convert geometry types.
  • Reject XYM and XYZM, including EMPTY, with UnsupportedDimensionException.
  • SRID and declared-reference metadata do not by themselves cause rejection or coordinate changes. Do not serialize them as a GeoJSON coordinate-system extension. The caller supplies coordinates with RFC 7946 semantics and units.
  • Reject LongitudeOne geometry types absent from RFC 7946 with public UnsupportedSpatialTypeException.
  • Reject the approved incompatible structures with UnsupportedGeometryStructureException. Do not return partial output.
  • Expose JSON encoding failures through JsonEncodingException, preserving the original exception as previous.
  • Keep UnsupportedSpatialInterfaceException unchanged; do not implicitly promote internal exceptions.
  • Verify actual model invariants before selecting invalid-value tests. Do not invent a separate NaN/Infinity policy or bypass model invariants to obtain fixtures.
  • Use explicit constructors, literal coordinates and literal expected results, without fixture factories, loops, generators or dynamic class names. Verify JSON structure, numeric values and array order; no canonical whitespace or object-member ordering contract is introduced.
  • Update English documentation with usage, supported dimensions, EMPTY handling, failures and caller responsibilities.
  • Run applicable automated tests and composer quality, preserving existing strategy behavior.

Approved public exception contracts

All classes below are in LongitudeOne\SpatialWriter\Exception and implement ExceptionInterface:

  • UnsupportedDimensionException directly extends \InvalidArgumentException.
  • UnsupportedSpatialTypeException retains its existing \Exception inheritance and signatures; its public promotion is explicit.
  • UnsupportedGeometryStructureException extends \InvalidArgumentException.
  • JsonEncodingException extends \RuntimeException and retains the original exception as previous.

Standards and approved decisions

RFC 7946 defines the output: geometry objects only, with no Feature or FeatureCollection support and no proprietary types.
The caller prepares compatible geometries. Non-RFC types are rejected rather than converted. EMPTY geometries are encoded using an empty coordinates array, or an empty geometries array for GeometryCollection. Measured geometries remain rejected even when EMPTY.
MultiPoint, MultiLineString and MultiPolygon containing any EMPTY member are rejected; aggregates with no members remain accepted.
Polygon exterior rings must be counterclockwise and interior rings clockwise; incompatible orientation is rejected without repair.
Antimeridian crossings are encoded unchanged, without detection for rejection or cutting. Document the deliberate non-application of the SHOULD in RFC 7946 section 3.1.9 and its interoperability implications.
GeometryCollection nesting, singleton forms and homogeneous forms are preserved despite the SHOULD guidance in section 3.1.8; document the interoperability implications.
The future spatial-transformer work is not an implementation dependency.

Delivery and review

The architecture, normative behavior, public API and allocation of shared work above were approved by Alexandre.
All seven geometry contributions may start in parallel against these contracts. Integration prerequisites are not a required order of development.
Private helpers and algorithms remain implementation decisions. Escalate any genuinely uncovered architecture, API, compatibility or normative decision discovered during implementation.
Independent review, resolution of blocking findings and merge remain required; implementation completion alone does not make the work Done.

Activity

  1. Alexandre-T commented on Sep 21, 2026

    @Alexandre-T
    ContributorAuthor

    Developer escalation — Normative decision required

    Implementation work has started on feat/13-geojson-polygon, but a normative decision is required before proceeding.

    The current geometry model accepts the following ring structures:

    • collinear ring: [[0,0],[1,0],[2,0],[0,0]];
    • self-intersecting ring: [[0,0],[2,2],[0,2],[2,0],[0,0]].

    The constructor currently validates that a ring contains at least four positions, is closed, and contains no consecutive duplicate positions. It does not validate ring simplicity or non-zero area.

    RFC 7946 §3.1.6 requires Polygon rings to follow the right-hand rule, but the Story does not explicitly define how these degenerate or self-intersecting cases should be handled. AC8 requires such cases to be escalated rather than introducing an implicit validation policy.

    Proposed decision: determine ring orientation from the signed area of the XY coordinates. Require a strictly positive signed area for the exterior ring and a strictly negative signed area for interior rings. Reject rings with a zero signed area using UnsupportedGeometryStructureException. Do not introduce general topological validation in the encoder; simplicity and other topology checks remain the caller's responsibility.

    Should this policy be adopted for the GeoJSON Polygon encoder?

  2. Alexandre-T commented on Sep 21, 2026

    @Alexandre-T
    ContributorAuthor

    Approved decision — GeoJSON Polygon ring orientation

    The GeoJSON encoder MUST generate valid GeoJSON conforming to RFC 7946.

    RFC 7946 §3.1.6 requires Polygon linear rings to follow the right-hand rule: exterior rings MUST be counterclockwise and interior rings MUST be clockwise. Although RFC 7946 recommends that parsers do not reject Polygons that violate this rule for backward compatibility, this tolerance applies to consumers of existing GeoJSON and does not relax the requirements for an encoder producing RFC 7946 output.

    Therefore, the encoder MUST verify the orientation of every Polygon ring before encoding it.

    Ring orientation SHALL be determined from the signed area computed from the XY coordinates:

    • the exterior ring MUST have a strictly positive signed area;
    • each interior ring MUST have a strictly negative signed area.

    A ring whose signed area is zero has no determinable clockwise or counterclockwise orientation and therefore cannot satisfy the RFC 7946 right-hand-rule requirement. Such a ring MUST be rejected with UnsupportedGeometryStructureException.

    This rejection is a LongitudeOne encoding policy derived from the requirement to produce RFC 7946-compliant GeoJSON; RFC 7946 does not itself explicitly prescribe a signed-area calculation or explicitly require zero-area rings to be rejected.

    The signed-area check MUST NOT be treated as general topological validation. In particular, the encoder is not responsible for determining ring simplicity, detecting self-intersections, or performing other general Polygon validity checks. Such validation remains outside the responsibility of the GeoJSON encoder.

    Consequently, a self-intersecting ring MAY be rejected when its signed area is zero as a consequence of the orientation check, but the encoder MUST NOT implement a separate self-intersection or simplicity check as part of this Story.

    This decision resolves the structural-validation escalation defined by AC8 while preserving the responsibility boundary of spatial-writer: the encoder guarantees that the GeoJSON it emits satisfies its RFC 7946 encoding requirements; it is not a general-purpose geometry topology validator.

  3. Alexandre-T commented on Sep 21, 2026

    @Alexandre-T
    ContributorAuthor

    PR created: #22

    • GeoJSON Polygons with XY/XYZ coordinates, EMPTY support, ring orientation, and zero-area rejection according to the approved decision.
    • 38 new tests; documentation updated.
    • composer test: 914 tests, 1,428 assertions passed.
    • composer quality: passed.
    • Commit b15225f pushed; working tree clean.
    • PHP 8.4/8.5 CI is in progress.
  4. changed the title [-]Encode Polygon geometries and validate ring orientation[/-] [+]Story: Encode Polygon geometries and validate ring orientation[/+] on Oct 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    impact:apiAffects the public APIimpact:architectureRequires or affects an architecture decision

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions