Skip to content

Preserve geometry API compatibility while replacing snap-based preprocessing with validation hooks#34

Open
kasperg3 with Copilot wants to merge 3 commits into
mainfrom
copilot/ensure-valid-geometry-when-changing-crs
Open

Preserve geometry API compatibility while replacing snap-based preprocessing with validation hooks#34
kasperg3 with Copilot wants to merge 3 commits into
mainfrom
copilot/ensure-valid-geometry-when-changing-crs

Conversation

Copilot AI commented Apr 14, 2026

Copy link
Copy Markdown
Contributor

This change ensures decompose_polygon and generate_sweep_pattern remain backward compatible while tightening geometry handling internally. Existing call sites keep working unchanged; new behavior is additive and optional.

  • Public API compatibility

    • Kept existing required parameters and return shapes for:
      • decompose_polygon(boundary, obstacles=None, ...)
      • generate_sweep_pattern(polygon, sweep_offset, clockwise=True, connect_sweeps=False, ...)
    • Added optional validation_strategy with safe default ("repair"), so legacy callers do not need updates.
  • Geometry preprocessing refactor

    • Removed internal coordinate snapping from decomposition/sweep paths.
    • Introduced internal boundary validation/repair via a shared coercion path:
      • accepts polygonal inputs,
      • repairs invalid geometry in "repair" mode,
      • raises ValueError in "strict" mode.
  • Legacy call-path preservation

    • Kept obstacle merge flow and decomposition/sweep invocation flow at the interface level.
    • Maintained existing exception class behavior (ValueError) while improving invalid-geometry specificity.
  • Docs and compatibility coverage

    • Added API docs note stating interfaces remain stable and validation is internal.
    • Expanded geometry tests to cover:
      • old-style calls with no new options,
      • default-vs-explicit repair parity,
      • strict-mode invalid geometry behavior.
# Existing usage remains valid (no call-site changes)
cells = decompose_polygon(boundary, obstacles=obstacles)
sweeps = generate_sweep_pattern(cells[0], sweep_offset)

# New optional behavior (additive)
cells = decompose_polygon(boundary, validation_strategy="strict")
sweeps = generate_sweep_pattern(cell, sweep_offset, validation_strategy="repair")

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors geometry preprocessing in trajgenpy.Geometries to preserve the existing public planning APIs while replacing the prior snap-based approach with an internal validation/repair hook (validation_strategy) for polygon inputs.

Changes:

  • Added _coerce_valid_polygons(...) and integrated it into decompose_polygon(...) and generate_sweep_pattern(...) to validate/repair polygonal inputs.
  • Introduced optional validation_strategy ("repair" default, "strict" to raise on invalid geometry) on both public planning functions.
  • Updated docs and tests to reflect the new validation behavior and compatibility expectations.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
trajgenpy/Geometries.py Replaces coordinate snapping with a shared validation/repair coercion path; adds validation_strategy parameters.
tests/test_geometries.py Updates area assertion expectations (no snap) and adds tests for default vs explicit repair and strict-mode failures.
docs/api/geometries.md Notes backward-compatible interfaces and internal validity/repair behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tests/test_geometries.py
Comment on lines +305 to +314
offset = Geometries.get_sweep_offset(0.1, 30, 90)

default_sweeps = Geometries.generate_sweep_pattern(poly, offset)
repair_sweeps = Geometries.generate_sweep_pattern(
poly, offset, validation_strategy="repair"
)
assert len(default_sweeps) == len(repair_sweeps)

default_cells = Geometries.decompose_polygon(poly)
repair_cells = Geometries.decompose_polygon(poly, validation_strategy="repair")

Copilot AI Apr 14, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test calls generate_sweep_pattern() with an unprojected lon/lat polygon while using a sweep offset computed in metres (get_sweep_offset(...)), so the units are inconsistent (degrees vs metres). To keep the test stable and representative of intended usage, consider projecting the polygon to the same metric CRS used elsewhere in this test module (e.g., wrap in GeoPolygon and set_crs("EPSG:3857")) before generating sweeps.

Suggested change
offset = Geometries.get_sweep_offset(0.1, 30, 90)
default_sweeps = Geometries.generate_sweep_pattern(poly, offset)
repair_sweeps = Geometries.generate_sweep_pattern(
poly, offset, validation_strategy="repair"
)
assert len(default_sweeps) == len(repair_sweeps)
default_cells = Geometries.decompose_polygon(poly)
repair_cells = Geometries.decompose_polygon(poly, validation_strategy="repair")
geo_poly = Geometries.GeoPolygon(poly)
geo_poly.set_crs("EPSG:3857")
offset = Geometries.get_sweep_offset(0.1, 30, 90)
default_sweeps = Geometries.generate_sweep_pattern(
geo_poly.get_geometry(), offset
)
repair_sweeps = Geometries.generate_sweep_pattern(
geo_poly.get_geometry(), offset, validation_strategy="repair"
)
assert len(default_sweeps) == len(repair_sweeps)
default_cells = Geometries.decompose_polygon(geo_poly.get_geometry())
repair_cells = Geometries.decompose_polygon(
geo_poly.get_geometry(), validation_strategy="repair"
)

Copilot uses AI. Check for mistakes.
Comment thread trajgenpy/Geometries.py
Comment on lines +587 to +602
current = shapely.make_valid(current)
if current.is_empty:
msg = f"{where} could not be repaired."
raise ValueError(msg)

Returns:
A new Shapely Polygon with snapped coordinates.
"""
exterior = [
(round(x, precision), round(y, precision))
for x, y in polygon.exterior.coords[:-1]
]
interiors = [
[(round(x, precision), round(y, precision)) for x, y in ring.coords[:-1]]
for ring in polygon.interiors
]
return shapely.Polygon(exterior, interiors)
if isinstance(current, shapely.Polygon):
return [current]
if isinstance(current, shapely.MultiPolygon):
polygons = [poly for poly in current.geoms if not poly.is_empty]
if not polygons:
msg = f"{where} has no non-empty polygon parts."
raise ValueError(msg)
return polygons

msg = f"{where} could not be represented as Polygon or MultiPolygon."
raise ValueError(msg)

Copilot AI Apr 14, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In repair mode, shapely.make_valid() can return a GeometryCollection (e.g., polygons + lines) rather than a Polygon/MultiPolygon. The current implementation will always raise ValueError in that case, even when valid polygonal parts are present. Consider handling GeometryCollection by extracting polygonal components (and discarding non-area geometries) before deciding whether repair succeeded, so validation_strategy="repair" behaves consistently.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants