Preserve geometry API compatibility while replacing snap-based preprocessing with validation hooks#34
Conversation
Agent-Logs-Url: https://github.com/kasperg3/trajgenpy/sessions/042f2bee-4d46-4789-bebc-e18a104a34bb Co-authored-by: kasperg3 <34032375+kasperg3@users.noreply.github.com>
Agent-Logs-Url: https://github.com/kasperg3/trajgenpy/sessions/042f2bee-4d46-4789-bebc-e18a104a34bb Co-authored-by: kasperg3 <34032375+kasperg3@users.noreply.github.com>
Agent-Logs-Url: https://github.com/kasperg3/trajgenpy/sessions/042f2bee-4d46-4789-bebc-e18a104a34bb Co-authored-by: kasperg3 <34032375+kasperg3@users.noreply.github.com>
There was a problem hiding this comment.
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 intodecompose_polygon(...)andgenerate_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.
| 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") |
There was a problem hiding this comment.
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.
| 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" | |
| ) |
| 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) |
There was a problem hiding this comment.
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.
This change ensures
decompose_polygonandgenerate_sweep_patternremain backward compatible while tightening geometry handling internally. Existing call sites keep working unchanged; new behavior is additive and optional.Public API compatibility
decompose_polygon(boundary, obstacles=None, ...)generate_sweep_pattern(polygon, sweep_offset, clockwise=True, connect_sweeps=False, ...)validation_strategywith safe default ("repair"), so legacy callers do not need updates.Geometry preprocessing refactor
"repair"mode,ValueErrorin"strict"mode.Legacy call-path preservation
ValueError) while improving invalid-geometry specificity.Docs and compatibility coverage