-
Notifications
You must be signed in to change notification settings - Fork 361
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add GEOSDensify to CAPI #391
Conversation
I tend towards a send-an-empty-get-an-empty, send-a-null-get-a-null policy, I don't think things that segfault, but frankly I don't know what our policy is, whether it's "avoid the footguns" or "no footguns". @dbaston ? |
I'd lean towards returning EMPTY on EMPTY (at the Densifier level, not specifically to the C API) and segfaulting on NULL. Dereferencing a null pointer is a programming error and I don't think runtime guards against programming errors are generally a good idea. |
Possible compatibility issue: PostGIS interprets tolerance as A tolerance of 10 in PostGIS matches the inputs for our first test case, whereas GEOS requires >10 (all segments in input geometry are <= 10). It might be good to resolve this before making this publicly available in the CAPI. Should I modify |
This behaviour in GEOS would be coming from JTS. @dr-jts might have an opinion/explanation. |
It looks like the exact tolerance value is not specifically tested in the JTS densifier tests The description there is a bit ambiguous on this front as well:
|
Looks great to me. Thanks for this, @brendan-ward ! |
@brendan-ward As long as we're consistent with JTS behaviour I don't have any particular horse in the race. |
This looks like a bug in the JTS code. I agree that only segments longer than the distance tolerance should be densified. This is the semantics implied by a strict interpretation of:
I will change the JTS behaviour to match this. It's good to have it match PostGIS. |
There you go @brendan-ward, match the postgis behaviour and let me know, I'll merge. |
I am about to pull the PR trigger in JTS, so might want to look at that code when it lands. (Not that it is definitely the World's Best Code, but nice to match it unless there's a good reason). |
JTS PR 676 has this fix. |
Thanks @dr-jts ! I'm not sure the fix was quite complete though; the original approach would create more segments than necessary to satisfy the requirement of <= I adapted the intent here and used This required some minor updates to the existing XMLtests here to match the new segment lengths. |
((0 0, 0 8.75, 0 17.5, 0 26.25, 0 35, 0 43.75, 0 52.5, 0 61.25, 0 70, 8.75 70, 17.5 70, 26.25 70, 35 70, 43.75 70, 52.5 70, 61.25 70, 70 70, 70 61.25, 70 52.5, 70 43.75, 70 35, 70 26.25, 70 17.5, 70 8.75, 70 0, 61.25 0, 52.5 0, 43.75 0, 35 0, 26.25 0, 17.5 0, 8.75 0, 0 0), | ||
(10 10, 16.25 16.25, 22.5 22.5, 28.75 28.75, 35 35, 41.25 41.25, 47.5 47.5, 53.75 53.75, 60 60, 51.66666666666667 60, 43.333333333333336 60, 35 60, 26.666666666666668 60, 18.333333333333336 60, 10 60, 10 51.66666666666667, 10 43.333333333333336, 10 35, 10 26.666666666666668, 10 18.333333333333336, 10 10))) </op></test> | ||
<test><op name="densify" arg1='A' arg2='10.0'> | ||
MULTIPOLYGON (((80 70, 80 80, 80 90, 80 100, 80 110, 90 110, 100 110, 110 110, 120 110, 120 100, 120 90, 120 80, 120 70, 110 70, 100 70, 90 70, 80 70)), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note: first ring normalized (rotated) a bit differently than previous version. The primary change on this ring is that segments are now 10
long, as expected from the tolerance.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you are open to it, I'd like to simplify the coordinates of these densify tests to make it easier to spot and verify the expected output.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Excellent idea. If you fix them up I will copy them to JTS.
Good point. I will follow your lead on the correct code. |
JTS PR 677 has the improved splitting algorithm from this PR. It also adds tests to the XML test file to capture the various additional tests in the unit test code (since it is more portable to test them in the XML files). This could be copied to this PR. Thanks for working on this @brendan-ward - this is a great improvement over the old code. |
geom1_ = GEOSGeomFromWKT("POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0), (1 1, 1 7, 7 7, 7 1, 1 1))"); | ||
ensure(geom1_ != nullptr); | ||
GEOSSetSRID(geom1_, 3857); | ||
|
||
geom2_ = GEOSDensify(geom1_, 5.0); | ||
|
||
ensure("result not null", geom2_ != nullptr); | ||
ensure_geometry_equals( | ||
geom2_, | ||
"POLYGON ((0 0, 5 0, 10 0, 10 5, 10 10, 5 10, 0 10, 0 5, 0 0), (1 1, 1 4, 1 7, 4 7, 7 7, 7 4, 7 1, 4 1, 1 1))"); | ||
|
||
ensure_equals("result SRID == expected SRID", GEOSGetSRID(geom2_), 3857); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure would be nice to reduce the amount of code for tests like this (not just here, for all GEOS unit tests).
Any ideas, @dbaston ? Perhaps some more helper functions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I were writing it I would drop lines 77 and 82, but overall it seems pretty reasonable to me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't have a problem with those lines per se (although if they are redundant would be good to remove them). The real problem is how many lines of code are repeated in each test. The only information content is the input WKT, the densify tolerance, and the result WKT.
In JTS I factor out the boilerplate into a separate function which is called by each test. One thing this supports nicely is that if the test logic needs to be modified or enhanced (say with an extra test on a specific aspect of the result) it can be done in one place only. (In other words, the standard reason for refactoring :)
I'm just not sure if the tut templated approach supports that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It turns out that factoring out the test common code into a support function is very doable. See GEOSMinimumClearanceTest.cpp
for an example.
@brendan-ward how do you feel about making this improvement?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, some of the tests do something like doDensifierTest
. I actually find those harder to work with, mostly because our test framework has no way to run an individual test. So if you want to debug a test and the test code itself is in a common function, you have to comment out all of the remaining tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, it's a balance. I've found having support functions tucked away in another file makes things more painful, but having them in the actual Test file is a-OK. Also, we can run individual tests (not assertions) with ./bin/test_geos_unit geos::io::WKBReader 6
so having more numbered chunks is often preferable to a long list of assertions, or a loop over a data array (my nemesis).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, we can run individual tests (not assertions) with ./bin/test_geos_unit geos::io::WKBReader 6
How am I only learning this now? 🤦
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I only found it out a couple months ago! I was like... "what if I add the number..."
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Also, the numbers don't have to be sequential)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And you could make the opposite argument
Of course. My experience makes me less inclined to write something today the way I wrote the minimum clearance tests 5 years ago. Your experience seems to pull you in the other direction. That's fine.
I've merged this in, but please don't hesitate to submit a new PR if you want to make additional changes. Thanks! |
This adds
GEOSDensify
to the C API.It was necessary to guard for empty and
NULL
geometries, or they result in segfaults from theDensifier
. Is that expected, or should those be checked there instead?I wasn't sure if
NULL
inputs should be guarded, but avoiding segfaults for those seemed like a good idea.The densifier behavior was not exactly as I expected when testing: I expected any segments greater than
tolerance
to be subdivided; the behavior is that any segments greater than or equal totolerance
are subdivided. I tried to clarify that in the function docstring, and I added specific tests for this to the test suite.