Compute centroid and signed area relative to a local origin - #380
Compute centroid and signed area relative to a local origin#380binggao1230 wants to merge 3 commits into
Conversation
Both shoelace sums in functions.py accumulate in absolute coordinates. The
per-edge terms then grow with the square of the coordinate magnitude while
their sum only grows with the area, so the significant digits cancel away for
rings that sit far from the origin. centroid() is affected worst because it
multiplies before it subtracts:
area = (coord[0] * next_coord[1]) - (next_coord[0] * coord[1])
Taking a 10x10 square and moving it to x = y = 1e10 gives a centroid of
(6646321618.7, 6646321618.7) instead of (10000000005.0, 10000000005.0), and
LinearRing.centroid returns None for the same valid ring because the two area
computations no longer agree. A 10 m long, 1 mm thick ring already loses its
centroid at an offset of 1e4.
Subtracting the first vertex before accumulating keeps every term the size of
the ring itself; the centroid is shifted back at the end. Against an exact
rational evaluation over the same vertices the error is now within a few ulp of
the coordinate magnitude, which is the representation limit.
signed_area() differences the y coordinates before multiplying and so degrades
much more slowly, but it is the same accumulation and shares the fix: its
relative error on a 24-gon at an offset of 1e13 goes from 9e-5 to 0.
The cast in signed_area is no longer needed now that the helper is typed.
Changed Files
|
Reviewer's GuideThis PR refactors centroid and signed area computations to use a local coordinate system relative to the first vertex, improving numerical stability for rings far from the origin, and adds precise regression and property tests (including an exact rational oracle) to validate correctness and translation invariance of these operations, especially for thin rings and large offsets. File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughThe change improves ChangesGeometry precision fix
Estimated code review effort: 3 (Moderate) | ~20 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
PR Summary
|
PR Summary by QodoImprove centroid/signed_area numeric stability using local-origin shoelace sums
AI Description
Diagram
High-Level Assessment
Files changed (5)
|
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 0 |
| Duplication | 0 |
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@tests/test_functions.py`:
- Around line 111-135: Update exact_centroid_and_area, offset_ring, and
ulp_tolerance with explicit strict-compatible annotations for coordinate
sequences, scalar offsets, and their tuple/list or numeric return values. Use
the project’s existing coordinate and numeric type conventions where available,
and ensure local variables and inferred returns satisfy strict mypy without
leaving untyped function definitions.
- Around line 156-208: Replace the deterministic loops in
tests/test_functions.py lines 156-208 with custom Hypothesis strategies for
translated rings and offsets, applying `@given` while preserving the exact-oracle,
translation-invariance, signed-area, and empty-centroid assertions; update
tests/test_linear_ring.py lines 194-222 to generate far-origin and thin-ring
inputs through custom strategies while retaining the existing
LinearRing.centroid assertions.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 2033e3d9-32ed-40fd-9be2-a1c7a46b2da9
📒 Files selected for processing (5)
docs/HISTORY.rstpygeoif/functions.pytests/hypothesis/test_functions.pytests/test_functions.pytests/test_linear_ring.py
| def exact_centroid_and_area(coords): | ||
| """ | ||
| Centroid and area of a ring in exact rational arithmetic. | ||
|
|
||
| Independent oracle for the floating point implementation: the vertices are | ||
| the same but no rounding takes place anywhere in the computation. | ||
| """ | ||
| points = [(Fraction(coord[0]), Fraction(coord[1])) for coord in coords] | ||
| double_area = Fraction(0) | ||
| acc_x = Fraction(0) | ||
| acc_y = Fraction(0) | ||
| for (x0, y0), (x1, y1) in zip(points, points[1:] + points[:1], strict=True): | ||
| cross = x0 * y1 - x1 * y0 | ||
| double_area += cross | ||
| acc_x += (x0 + x1) * cross | ||
| acc_y += (y0 + y1) * cross | ||
| return acc_x / (3 * double_area), acc_y / (3 * double_area), double_area / 2 | ||
|
|
||
|
|
||
| def offset_ring(ring, offset): | ||
| return [(x + offset, y + offset) for x, y in ring] | ||
|
|
||
|
|
||
| def ulp_tolerance(ring, ulps=32): | ||
| return ulps * math.ulp(max(abs(value) for point in ring for value in point)) |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Add strict type annotations to the test helpers.
exact_centroid_and_area, offset_ring, and ulp_tolerance have untyped parameters and return values. Strict mypy rejects untyped function definitions. Add explicit coordinate, scalar, and return types.
As per coding guidelines, **/*.py: “All code must pass mypy strict type checking.”
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/test_functions.py` around lines 111 - 135, Update
exact_centroid_and_area, offset_ring, and ulp_tolerance with explicit
strict-compatible annotations for coordinate sequences, scalar offsets, and
their tuple/list or numeric return values. Use the project’s existing coordinate
and numeric type conventions where available, and ensure local variables and
inferred returns satisfy strict mypy without leaving untyped function
definitions.
Source: Coding guidelines
| def test_centroid_far_from_the_origin() -> None: | ||
| for ring in RINGS: | ||
| for offset in OFFSETS: | ||
| moved = offset_ring(ring, offset) | ||
|
|
||
| center, area = centroid(moved) | ||
| expected_x, expected_y, expected_area = exact_centroid_and_area(moved) | ||
|
|
||
| tolerance = ulp_tolerance(moved) | ||
| assert abs(center[0] - float(expected_x)) <= tolerance | ||
| assert abs(center[1] - float(expected_y)) <= tolerance | ||
| assert math.isclose(area, float(expected_area)) | ||
|
|
||
|
|
||
| def test_centroid_is_translation_invariant() -> None: | ||
| for ring in RINGS: | ||
| base, _ = centroid(ring) | ||
| for offset in OFFSETS: | ||
| moved = offset_ring(ring, offset) | ||
|
|
||
| center, _ = centroid(moved) | ||
|
|
||
| tolerance = ulp_tolerance(moved) | ||
| assert abs(center[0] - (base[0] + offset)) <= tolerance | ||
| assert abs(center[1] - (base[1] + offset)) <= tolerance | ||
|
|
||
|
|
||
| def test_centroid_area_agrees_with_signed_area_far_from_the_origin() -> None: | ||
| for ring in RINGS: | ||
| for offset in OFFSETS: | ||
| moved = offset_ring(ring, offset) | ||
|
|
||
| assert math.isclose(centroid(moved)[1], signed_area(moved)) | ||
|
|
||
|
|
||
| def test_signed_area_far_from_the_origin() -> None: | ||
| ring = circle_ish(0, 0, 10, 24) | ||
| for offset in OFFSETS: | ||
| moved = offset_ring(ring, offset) | ||
|
|
||
| *_, expected_area = exact_centroid_and_area(moved) | ||
|
|
||
| assert math.isclose(signed_area(moved), float(expected_area), rel_tol=1e-15) | ||
|
|
||
|
|
||
| def test_centroid_empty() -> None: | ||
| center, area = centroid([]) | ||
|
|
||
| assert math.isnan(center[0]) | ||
| assert math.isnan(center[1]) | ||
| assert area == 0 | ||
|
|
||
|
|
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Use custom Hypothesis strategies for the new regression cases.
The new tests use deterministic loops and fixed fixtures. They do not use @given with custom strategies.
tests/test_functions.py#L156-L208: Define custom strategies for translated rings and offsets. Use them with@givenwhile keeping the exact-oracle assertions.tests/test_linear_ring.py#L194-L222: Generate far-origin and thin-ring inputs from custom strategies. Keep theLinearRing.centroidassertions.
As per coding guidelines, tests/test_*.py: “Tests must use pytest framework with @given decorators using custom strategies and aim for 100% branch coverage.”
📍 Affects 2 files
tests/test_functions.py#L156-L208(this comment)tests/test_linear_ring.py#L194-L222
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/test_functions.py` around lines 156 - 208, Replace the deterministic
loops in tests/test_functions.py lines 156-208 with custom Hypothesis strategies
for translated rings and offsets, applying `@given` while preserving the
exact-oracle, translation-invariance, signed-area, and empty-centroid
assertions; update tests/test_linear_ring.py lines 194-222 to generate
far-origin and thin-ring inputs through custom strategies while retaining the
existing LinearRing.centroid assertions.
Source: Coding guidelines
There was a problem hiding this comment.
Hey - I've found 1 issue, and left some high level feedback:
- The new
_relative_coordinateshelper assumescoordsis non-empty; since this is now a core precondition for bothsigned_areaandcentroid, consider making this explicit (e.g. via an assertion or docstring note) to avoid accidental misuse from future call sites. - The ULP-based tolerance logic in
test_centroid_far_from_the_originandtest_centroid_of_a_regular_polygon_is_its_centeris nearly identical; you could reduce duplication and make future adjustments easier by reusing theulp_tolerancehelper in the Hypothesis test as well.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The new `_relative_coordinates` helper assumes `coords` is non-empty; since this is now a core precondition for both `signed_area` and `centroid`, consider making this explicit (e.g. via an assertion or docstring note) to avoid accidental misuse from future call sites.
- The ULP-based tolerance logic in `test_centroid_far_from_the_origin` and `test_centroid_of_a_regular_polygon_is_its_center` is nearly identical; you could reduce duplication and make future adjustments easier by reusing the `ulp_tolerance` helper in the Hypothesis test as well.
## Individual Comments
### Comment 1
<location path="tests/test_functions.py" line_range="201-206" />
<code_context>
+ assert math.isclose(signed_area(moved), float(expected_area), rel_tol=1e-15)
+
+
+def test_centroid_empty() -> None:
+ center, area = centroid([])
+
+ assert math.isnan(center[0])
+ assert math.isnan(center[1])
+ assert area == 0
+
+
</code_context>
<issue_to_address>
**suggestion (testing):** Extend centroid empty/degenerate tests to cover very small rings and collinear vertices explicitly.
The empty case looks good, but we should also cover “almost empty” inputs that hit the `signed_area == 0 or math.isnan(signed_area)` path. Please add tests for: (1) a two-point line, (2) a three-point collinear ring, and (3) a closed ring whose area is numerically ~0, to verify this branch behaves as expected.
Suggested implementation:
```python
def test_centroid_empty() -> None:
center, area = centroid([])
assert math.isnan(center[0])
assert math.isnan(center[1])
assert area == 0
def test_centroid_two_point_line_is_degenerate() -> None:
line = [(0.0, 0.0), (1.0, 0.0)]
center, area = centroid(line)
assert math.isnan(center[0])
assert math.isnan(center[1])
assert area == 0
def test_centroid_three_point_collinear_ring_is_degenerate() -> None:
# Three collinear vertices; centroid should follow the degenerate branch
ring = [(0.0, 0.0), (1.0, 0.0), (2.0, 0.0)]
center, area = centroid(ring)
assert math.isnan(center[0])
assert math.isnan(center[1])
assert area == 0
def test_centroid_near_zero_area_ring_is_degenerate() -> None:
# Very thin rectangle whose area underflows to ~0 in floating point
# This is intended to exercise the `signed_area == 0` branch.
h = 1e-308
ring = [
(0.0, 0.0),
(1.0, 0.0),
(1.0, h),
(0.0, h),
(0.0, 0.0),
]
center, area = centroid(ring)
assert math.isnan(center[0])
assert math.isnan(center[1])
assert area == 0
```
These tests assume that `centroid` returns `(nan, nan)` and an area of `0` whenever the underlying `signed_area` is exactly zero or `nan`, for non-empty/degenerate inputs as well as the empty case. If the actual intended behavior is different (e.g. returning some other sentinel or raising), you should adjust the assertions accordingly. No additional imports should be necessary if `math` and `centroid` are already imported in this module.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| def test_centroid_empty() -> None: | ||
| center, area = centroid([]) | ||
|
|
||
| assert math.isnan(center[0]) | ||
| assert math.isnan(center[1]) | ||
| assert area == 0 |
There was a problem hiding this comment.
suggestion (testing): Extend centroid empty/degenerate tests to cover very small rings and collinear vertices explicitly.
The empty case looks good, but we should also cover “almost empty” inputs that hit the signed_area == 0 or math.isnan(signed_area) path. Please add tests for: (1) a two-point line, (2) a three-point collinear ring, and (3) a closed ring whose area is numerically ~0, to verify this branch behaves as expected.
Suggested implementation:
def test_centroid_empty() -> None:
center, area = centroid([])
assert math.isnan(center[0])
assert math.isnan(center[1])
assert area == 0
def test_centroid_two_point_line_is_degenerate() -> None:
line = [(0.0, 0.0), (1.0, 0.0)]
center, area = centroid(line)
assert math.isnan(center[0])
assert math.isnan(center[1])
assert area == 0
def test_centroid_three_point_collinear_ring_is_degenerate() -> None:
# Three collinear vertices; centroid should follow the degenerate branch
ring = [(0.0, 0.0), (1.0, 0.0), (2.0, 0.0)]
center, area = centroid(ring)
assert math.isnan(center[0])
assert math.isnan(center[1])
assert area == 0
def test_centroid_near_zero_area_ring_is_degenerate() -> None:
# Very thin rectangle whose area underflows to ~0 in floating point
# This is intended to exercise the `signed_area == 0` branch.
h = 1e-308
ring = [
(0.0, 0.0),
(1.0, 0.0),
(1.0, h),
(0.0, h),
(0.0, 0.0),
]
center, area = centroid(ring)
assert math.isnan(center[0])
assert math.isnan(center[1])
assert area == 0These tests assume that centroid returns (nan, nan) and an area of 0 whenever the underlying signed_area is exactly zero or nan, for non-empty/degenerate inputs as well as the empty case. If the actual intended behavior is different (e.g. returning some other sentinel or raising), you should adjust the assertions accordingly. No additional imports should be necessary if math and centroid are already imported in this module.
|
Both pre-commit.ci failures are pre-existing on
Nothing in the diff adds a lint, format or type finding: |
Greptile SummaryThe PR improves numerical stability by calculating polygon centroid and signed area relative to the first vertex, then translating the centroid back to absolute coordinates.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| pygeoif/functions.py | Introduces local-origin coordinate normalization for centroid and signed-area calculations while preserving supported closed-ring behavior. |
| tests/test_functions.py | Adds exact rational oracles and broad regression coverage for translated and thin rings. |
| tests/hypothesis/test_functions.py | Adds property-based validation that translated regular polygons retain their expected centroid. |
| tests/test_linear_ring.py | Verifies that valid large-offset and thin rings continue to expose the correct centroid. |
| docs/HISTORY.rst | Records the centroid and signed-area precision correction. |
Reviews (3): Last reviewed commit: "Exclude unreachable sequence guards from..." | Re-trigger Greptile
Code Review by Qodo
1.
|
There was a problem hiding this comment.
LlamaPReview — No blocking issues found
Safe to merge: the change is mathematically equivalent for closed rings, strictly reduces floating-point cancellation far from the origin, and fixes the reported None centroid symptom without regressing ordinary inputs.
1 non-blocking finding retained — highest: signed_area result changes for open coordinate sequences with nonzero first x.
Change flow
The rebased shoelace pipeline shows how the PR change fixes the None guard by keeping both compared values in the same local frame.
sequenceDiagram
participant C as Client
participant L as LinearRing.centroid
participant F as functions.centroid
participant A as signed_area
participant R as Result
C->>L: centroid()
L->>F: centroid(coords)
F->>F: _relative_coordinates(coords)
note over F: PR change — rebase by first vertex<br/>then shift back
F-->>L: (center, area)
L->>A: signed_area(coords)
note over A: PR change — same rebased frame<br/>as centroid
A-->>L: area
alt Math.isclose(area, signed_area)
L->>R: Return Point
else
L->>R: Return None (was spurious for far-origin rings)
end
Review details and evidence
| Priority | File | Finding | Evidence |
|---|---|---|---|
| P2 | pygeoif/functions.py |
signed_area result changes for open coordinate sequences with nonzero first x |
confirmed |
Finding details
P2 · signed_area result changes for open coordinate sequences with nonzero first x
pygeoif/functions.py
Rebasing the trapezoid sum by the first vertex introduces the constant term -origin_x · (y_last − y_first) relative to the old absolute-frame result. The term vanishes for closed inputs (zero delta) but not for open sequences. signed_area is a public documented API typed over LineType; nothing in this PR validates or closes inputs, so an external caller passing an open LineString's coords with a nonzero first-x receives the constant-shifted value. In practice every in-repo caller passes closed rings (LinearRing.__init__ auto-closes, LinearRing.centroid/is_ccw pass self.coords), and the docstring states 'area enclosed by a ring'. So for out-of-contract data this is an unannounced fix, not a corruption.
Owner action: Nonblocking optional: add one sentence to docs/codedocs/api-reference/functions.md noting inputs are treated as closed rings.
Verification boundary: confirmed; scope: changed region.
Conceptual guidance (not a committable GitHub suggestion):
Document in the docstring or API reference that input sequences are treated as closed rings for area computation.
Material unknowns
- Cause of the
pre-commit.ci - prfailure — the status carries no annotation or mechanism. If a rerun or diagnostic shows a PR-caused lint/test failure, confidence (and possibly posture) would change; a green rerun would raise decision confidence to High.- Check: Check the
pre-commit.ci - prstatus for a diagnostic or rerun it to confirm cleanliness.
- Check: Check the
LlamaPReview checks
- Read bounded PR-head context from
pygeoif/functions.py. - Read bounded PR-head context from
pygeoif/geometry.py. - Inspected matching PR-head repository snippets in
pygeoif/functions.py,docs/codedocs/architecture.md,docs/HISTORY.rstand 2 more path(s). - Inspected matching PR-head repository snippets in
.github/workflows/codesee-arch-diagram.yml.
Automated review by LlamaPReview · Free for public open-source projects.
|
Note on the red pre-commit.ci: it is not from this PR's diff. The 08-04 autoupdate (2ae7ce7, ruff-pre-commit → v0.16.1) turned on CPY001 via |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## develop #380 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 32 32
Lines 2674 2766 +92
Branches 91 101 +10
=========================================
+ Hits 2674 2766 +92 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Our agent can fix these. Install it.
Gates Passed
6 Quality Gates Passed
Quality Gate Profile: Customizable Safeguards
Install CodeScene MCP: safeguard and uplift AI-generated code. Catch issues early with our IDE extension and CLI tool.
Both shoelace sums in
functions.pyaccumulate in absolute coordinates. The per-edge terms grow with the square of the coordinate magnitude while their sum only grows with the area, so for a ring that sits far from the origin the significant digits cancel away.centroid()is hit hardest because it multiplies before it subtracts:A 10x10 square moved to
x = y = 1e10:Two different symptoms, both from the same sum. Either the coordinates are wrong and nothing says so, or
centroid()'s internal area drifts far enough fromsigned_area()that themath.isclosecheck inLinearRing.centroidrejects a perfectly valid ring. The check is not a safety net for the coordinates: at an offset of1e8the two areas still agree exactly at100.0while the centroid is already off by 0.04.The threshold depends on how thin the ring is, not only on the offset. A 10 m long, 1 mm thick ring — a wall in a metre-based projected system — loses its centroid at an offset of
1e4:Fix: subtract the first vertex from every coordinate before accumulating, so the terms stay the size of the ring, and shift the centroid back at the end. Areas are translation invariant so they need no correction. shapely, on GEOS, returns the exact value for every case above.
Extent of the problem
I checked the whole of
functions.pyrather than just the reported case, against an exactfractions.Fractionevaluation over the same vertices, on a grid of {square, triangle, L-shape, 1 mm sliver, 24-gon} x {shift x, shift y, shift both} x offsets1e0..1e15:centroid()— wrong in every shape/direction combination. Included.signed_area()— same accumulation, but it differences the y coordinates before multiplying, so it degrades much more slowly: relative error9e-5on the 24-gon at an offset of1e13,3e-4at1e14. Small, but it is the same bug and the same one-line fix, so it is in. I also confirmed by exact evaluation that its formula is mathematically identical to the standard one, i.e. only the rounding was at fault. After the fix it is exact on the whole grid._orientation()/convex_hull()— already difference before multiplying and the magnitudes never cancel. Zero sign errors across the entire grid, so left alone. The only degeneracies are at1e14+ where the 1 mm sliver is no longer representable at all, which no arithmetic can fix.move_coordinate(s),Bounds— no products, unaffected.Two claims I could not support and want to flag rather than have you find them: this is not a problem for ordinary Web-Mercator or UTM data. A 20x12 m footprint at EPSG:3857
(1489200, 6894000)comes out exact. What does break is large false eastings, millimetre-based CAD/IFC coordinates, and thin rings, which is what the tests use.Tests
test_centroid_far_from_the_origin,test_centroid_is_translation_invariant,test_signed_area_far_from_the_origin— 4 rings x 13 offsets against exact rational arithmetic and against the untranslated result, to within 32 ulp of the coordinate magnitude.test_centroid_valid_far_from_the_origin,test_centroid_thin_ring_far_from_the_origin— the twoLinearRing.centroidsymptoms above.test_centroid_of_a_regular_polygon_is_its_center— a hypothesis test with a closed-form expectation.test_fuzz_centroidis the only property test that reachescentroid()and its assertion is shape-only (area is naniffcenter is nan), so it cannot see this class of bug at all; I left it alone because its unboundedst.floats()draws leave nothing numeric to assert, and added the new one next to it instead. It survives--hypothesis-profile=exhaustive(10 000 examples), with a worst observed error of 4.5 ulp; before the fix 77% of that domain exceeds the 32 ulp bound and 4% comes backnan.I mutated the fix in six ways (no translation at all, no shift back, wrong sign, shifting back twice, translating only x, leaving
signed_areaalone) — each reddens. Taking the last vertex as the origin instead of the first stays green, so the tests pin the property and not my particular choice.pytest tests --hypothesis-profile=ci437 pass, coverage stays at 100%,mypy,ruff format --check,complexipy,radon,lizardclean.ruff checkreports the same 28 pre-existingCPY001findings asdevelopdoes.This was prepared with the assistance of an AI coding agent working under my direction; I have reviewed the change and the reasoning above and I am accountable for it.
Summary by Sourcery
Improve numerical robustness of centroid and signed area computations for rings, especially far from the origin, and add comprehensive tests for these behaviors.
Bug Fixes:
Enhancements:
Tests:
Summary by CodeRabbit
Bug Fixes
LinearRingcentroid results for projected coordinates with large offsets.Tests
Documentation