-
Notifications
You must be signed in to change notification settings - Fork 89
Add Bentley-Ottman Algorithm #1168
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
base: master
Are you sure you want to change the base?
Conversation
…uts a dictionary of vertex keys with segment values
…ntersections and ultimately calculates all intersections
@souma4 please let me know when the PR is ready for review. I have some review comments already, just waiting for your green light. |
@juliohm Thanks for reminding 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.
Thank you @souma4 for working on this. It is really nice to see the amazing progress you've made already ❤️
Attached is my first round of review with a couple of suggestions to improve the PR. Let's work on it together slowly, possibly improving the BinaryTrees.jl dependency before coming back here.
Looking forward to the next round. It looks very promising!
…tests. Edited test to be more intensive. updated entire script to meet style and Julia best practices. Removed unneded functions. Shifted BinaryTrees relevant materials to PR JuliaGeometry#12 in BinaryTrees and adjusted code as needed. Reordered point processing to reduce the likelihood of a bug when processing start and intersection segments.
…d function. removed println calls used for debugging
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.
Attaching a few minor comments. Tomorrow I will try to find some time to run tests locally.
The good news is that the output of @code_warntype
is all green.
Tests are failing because the utility function is not exported. We can either export it or qualify the calls in the test suite with the |
Co-authored-by: Júlio Hoffimann <julio.hoffimann@gmail.com>
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.
More basic fixes before the actual review of the algorithm.
…eaner enum handling, and shifted the final dictionary to be unique values
…hes.jl into JRC_add_BentleyOttman
… that connect to the point
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.
Tests are failing due to the latest review changes.
I introduced a helper |
The heuristic for the number of digits worked well. Assuming a tolerance of I noticed that the output of the algorithm is including the original points besides the intersections. For instance, here is what I get in the second test: viz(segs)
viz!(points, color="red") Could you please take a look to make sure we are only returning intersection points? |
Oh if we only want intersections (not all points) then this is an easy change. I'll commit fixes and update tests to reflect. Although I am finding some variance in tests, so I'll need to debug and see if it's real or not. Outside of that, the facetgrid test returns nothing because the facet generator only generates cornerintersections, which are start/endpoints and not included. |
Very good point. I think we need to refactor this test by constructing segments manually in a grid-like arrangement. We could do the following instead: horizontal = [Segment((1, i), (n, i)) for i in 1:n]
vertical = [Segment((i, 1), (i, n)) for i in 1:n]
segs = [horizontal; vertical] |
…. EVENTS ARENT BEING HANDLED PROPERLY. I DONT HAVE TIME TO ADDRESS PROMPTLY
Thank you @souma4 for looking into it ❤️ Tests with random segments show that the algorithm is not there yet. Perhaps we could try to follow the algorithmic structure in Chapter 2 of Berg et al. Computational Geometry - Applications and Algorithms instead. This structure is discussed step-by-step in the video I linked in the issue. More specifically, it is discussed in this second video: https://www.youtube.com/watch?v=qkhUNzCGDt0 (If you need access to the book, I have the PDF and can send it to you directly) We are almost there! |
agreed. My plan was to pull up De Berg to see exactly what they do. I'm fairly busy this week, but, it'll keep moving haha. I think we are pretty close though. |
…s far as I got with De Berg implementation. All tests fail. I think this is occuring between the status structure isn't reflecting the order where the plane crosses, but the total order in x and y (so a line with a low x will always be the minimum even if where it intersects the plane is the maximum). I tried having a sort method to maybe get it but that didn't work. I'm sort of at a loss outside of computing intersections to the plane, which I can't think of an efficient way of doing that right now. Maybe y'all will have some ideas
Merging to newest version of main branch...
… still is, I've tried a lot to no success
…tests appear to be passing. My only latent concern is type inference
src/utils/intersect.jl
Outdated
# Ensure the point is not the endpoint (avoids duplicates) | ||
skip = (x₂ - TOL ≤ x ≤ x₂ + TOL) && (y₂ - TOL ≤ y ≤ y₂ + TOL) | ||
# if collinear and not an endpoint | ||
collinear = dy * (x - x₁) - dx * (y - y₁) | ||
boundcheck = (x₁ - TOL ≤ x ≤ x₂ + TOL) && (y₁ - TOL ≤ y ≤ y₂ + TOL) | ||
|
||
if abs(collinear) ≤ TOL * ℒ #&& boundcheck | ||
skip || push!(ℳₚ, seg) | ||
end |
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.
Did you find an issue with our built-in intersection method? It is not clear to me yet why we need to handle coordinates manually within a custom intersection algorithm. From what I recall, the Bentley-Ottmann algorithm assumes an atom function intersect(s1, s2)
with the usual definition.
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 did. The only consistent way of calculating the x intersect (which now that I think about this again, maybe x intersect and all derivates should be y intersect, because x=# intersect segment
can just be seen as a "y intercept", but my intial logic was "this is the x at the intersect"). Is to either do this cross product solution or to set a Segment((x,-Inf),(x,Inf))
intersection, but in the intersection script, it uses a method that can't compute the solution due to the infinities. Another potential solution is to use a y extent determined in the initialization and send that to find intersection. Right now I'm finding the current intersect
methods struggle with the curved lines, so I felt fine doing the linear thing.
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.
Right now I'm finding the current
intersect
methods struggle with the curved lines, so I felt fine doing the linear thing.
Do you mean our intersect
atom has bugs that we need to tackle first?
Is to either do this cross product solution or to set a
Segment((x,-Inf),(x,Inf))
intersection, but in the intersection script, it uses a method that can't compute the solution due to the infinities.
Handing coordinates at infinity is indeed tricky. I wonder what the code would look like with the alternative solution
Another potential solution is to use a y extent determined in the initialization and send that to find intersection.
I understand it would just call our built-in intersect
with the original segments and 4 additional segments beyond the boundingbox
. For example, segs |> boundingbox |> boundary |> segments |> collect
. We could also add an extra room to the bounding box with Stretch(factor)
.
@souma4 thank you very much for your resilience! I wish Bentley-Ottmann had shared a precise pseudo-code in their original article. That would have saved us a huge amount of time handing these tricky corner cases. Could you please remind what was the issue with that original vanilla implementation? Was it purely caused by floating point arithmetic or there was something else? |
The issue with the original is it struggled with verticals and multiple intersections. Floating point was mainly an implementation detail that didn't transfer over nicely from the good math. But hey, it works, it seems to have good time complexity, woohoo. |
Let's investigate these two issues separately: verticals and multiple intersections. Regarding the verticals, what exactly is the issue? I remember the solution in that video involved a small rotation of the sweep line, which was implemented implicitly in the order relation when we compare "x" and then "y" coordinate in order (lexicographical comparison). Our Regarding the multiple intersections, I believe this is a more serious issue. The algorithm presented in the video (and in the book) seemed ok with multiple intersections. The algorithm implemented in the PR may be doing something slightly different, even though you adapted it from the original paper by Bentley-Ottmann. |
Right now, the algorithm in this PR follows the version from the video and de Berg et al.—essentially modified Bentley-Ottmann. Handling Multiple IntersectionsI'll start with handling multiple segments intersecting at a point, since that ties into vertical segment handling later. When several segments meet at a point p, we need to:
To do this ordering properly, I compute the xintersection in the I tried other methods (hardcoding order, swapping axes, using midpoints, holding segments as values instead of keys), but none handled edge cases well. Luckily, computing the intersection is cheap. Once calculated, comparisons are simple: go right if you're below the segment, left if you're above. Status Structure and StalenessThe status tree is ordered by these computed xintersect values. But I only recompute So, when checking "is this segment active?" we need to recalculate the intersection to be sure. That’s what I considered trying to do what I'll call a "Meta Tree" (there might be a real name for it) where all nodes are recomputed upon a change of some "meta state," for example, a change in the sweepline. But I opted against that because this would cause redundant calculations since the relative order of the tree is still unchanged. That's why I do the recompute on the fly strategy. VerticalsVertical segments are a special case. They must always be considered the "top" segment at any point they pass through. But I couldn’t just set their xintersect to their top endpoint because it would miss every intersection between |
Thank you @souma4 for the detailed comments. I understand that your Could we try that other approach you suggested where we add two additional sentinel segments in the beginning of the algorithm using the Ideally, we could achieve the desired results with two tricks besides the built-in
|
So the auxiliary type is there because we need to order the status structure by where the segments intersect the sweepline. The Where the |
see issue #1126 and PR #1125
I needed to refactor and I messed up with the last draft #1143.
This produces a dictionary of vertices with a vector of segment values. Future needs: A test that the outputs are correct, and general method for rebuilding polygons with this structure.
@juliohm sorry about messing up the prior draft. Live and learn. I'll let you look this over. I think a polygon build method for this output is a new PR. This outputs vertices with segment info. If someone just wants the vertices they can grab the keys. If someone wants a polygon they can use a build method.