Skip to content

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

Open
wants to merge 92 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
92 commits
Select commit Hold shift + click to select a range
c5a3b90
add test segment. proper test later
souma4 Feb 11, 2025
e004810
Clean single commit for BentleyOttman algorithm. Essentially, it outp…
souma4 Feb 11, 2025
491a130
test for valid answer
souma4 Feb 11, 2025
840c1a5
moved sweepline algorithm to Intersections since it is dependent on i…
souma4 Feb 11, 2025
6f018a3
Update Doc for bentley ottman implementation to specify output
souma4 Feb 17, 2025
eb0d56b
update project.toml to include BinaryTrees dependency
souma4 Feb 17, 2025
b869b6e
major commit. Moved sweepline from intersections to utils along with …
souma4 Feb 24, 2025
6be63f1
new_type and new_geom didn't meet style, so fixed. Removed an unneede…
souma4 Feb 24, 2025
ed09d87
forgot to rename _intersection to _pushintersection
souma4 Feb 24, 2025
2b83b1a
small commits to improve compatibility and fix mixed up old names
souma4 Mar 4, 2025
7484dc1
small updates to License text, cases, variable and function names, cl…
souma4 Mar 6, 2025
16791eb
Merge branch 'JRC_add_BentleyOttman' of https://github.com/souma4/Mes…
souma4 Mar 6, 2025
ec4c593
Updated algorithm to be simpler, output is points and segment indices…
souma4 Mar 7, 2025
e18eb99
updated tests to reflect changed output type
souma4 Mar 8, 2025
e27d9f1
add compat for BinaryTrees
souma4 Mar 9, 2025
56018d3
fixed names of variables and helper functions. removed output type f…
souma4 Mar 9, 2025
442861b
updated utils include path and refactored if else to new function for…
souma4 Mar 10, 2025
0cc5d91
Minor adjustments before review
juliohm Mar 10, 2025
1c03947
More fixes
juliohm Mar 11, 2025
00e2151
changed S = Tuple{P, P} to V and changed the empty vectors of S to ca…
souma4 Mar 11, 2025
83093e4
the s iter variable was modified to segment using the same name, so …
souma4 Mar 12, 2025
76d8be3
Rename V -> S
juliohm Mar 13, 2025
1e36abe
More refactoring
juliohm Mar 13, 2025
d590d36
Rename \scrT to \scrR to match paper notation, and avoid confusion wi…
juliohm Mar 14, 2025
93ce4a6
Refactor _newevent to improve readability
juliohm Mar 14, 2025
9fdaf93
More refactoring to improve readability
juliohm Mar 14, 2025
353cf70
More refactoring to improve readability and performance
juliohm Mar 14, 2025
d136b6e
More refactoring to improve readability
juliohm Mar 14, 2025
602a579
More refactoring
juliohm Mar 14, 2025
36e2497
More refactoring
juliohm Mar 14, 2025
f5ff53e
More refactoring
juliohm Mar 14, 2025
0004c36
realigned more with original algorithm and reduced recomputation
souma4 Mar 14, 2025
dd88813
More refactoring
juliohm Mar 14, 2025
2fb7e10
More refactoring
juliohm Mar 14, 2025
8e618e0
More refactoring
juliohm Mar 14, 2025
2850538
More refactoring
juliohm Mar 14, 2025
7762044
Add more tests
juliohm Mar 14, 2025
3f42e7d
Merge branch 'JuliaGeometry:master' into JRC_add_BentleyOttman
souma4 Mar 14, 2025
e288e23
edited the processing of intersections to better reflect the original…
souma4 Mar 14, 2025
5a21d3a
More refactoring
juliohm Mar 15, 2025
165ddae
Refactor tests
juliohm Mar 15, 2025
3ed853d
Add more tests
juliohm Mar 15, 2025
859c846
Improve tests
juliohm Mar 15, 2025
9732322
Fix formatting
juliohm Mar 15, 2025
98f0786
More refactoring
juliohm Mar 15, 2025
042e690
Add test with infinite loop
juliohm Mar 15, 2025
c9d933f
Fix formatting
juliohm Mar 15, 2025
219add3
update to stop infinite loops, but has major TODOs to handle floating…
souma4 Mar 15, 2025
89a8d98
reverting prior to dictionary implementation
souma4 Mar 16, 2025
3078412
Merge branch 'JuliaGeometry:master' into JRC_add_BentleyOttman
souma4 Mar 17, 2025
fdb936b
fully functioning not infinitely looping bentleyottman algorithm all …
souma4 Mar 17, 2025
5ef6505
cleaned up test outputs and fixed tests to be T agnostic
souma4 Mar 17, 2025
33f2fba
hopefully fixed failing test on Float32 values
souma4 Mar 19, 2025
e933e9c
I think it's a precision issue, but T wasn't being handled how I thou…
souma4 Mar 19, 2025
5e972fc
shuffled to reduce redundant computations, particularly specifying th…
souma4 Mar 20, 2025
0ed8c5f
Minor adjustments
juliohm Mar 20, 2025
0a18865
removed commented out useless code
souma4 Mar 20, 2025
e38d203
Minor adjustments
juliohm Mar 21, 2025
893fafd
Int is default type for integer literals
souma4 Mar 22, 2025
a10680b
Set digits by default using exponent of absolute tolerance
juliohm Mar 22, 2025
20178a3
Decrease number of digits by one
juliohm Mar 22, 2025
01604f1
updated script to be simpler. updated tests. THIS FAILS THE GRID TEST…
souma4 Mar 23, 2025
8345d1e
WIP, With what little time I've had and will have for a bit, here's a…
souma4 Mar 28, 2025
562c778
complete functional Bentley Ottman Algorithm and passing all local te…
souma4 Apr 18, 2025
5557fbb
fix roundcoords to be coordround
souma4 Apr 18, 2025
5023a34
Merge remote-tracking branch 'origin/master' into JRC_add_BentleyOttman
souma4 Apr 18, 2025
701e8a9
fixed maxsearch and minsearch to not be exhaustive. findintersections…
souma4 Apr 20, 2025
5baeff2
fixed incorrect removal of end and midpoints
souma4 Apr 20, 2025
ee4d2b8
fixed type insecurity
souma4 Apr 20, 2025
fcdaa81
_findintersection appears to no longer exhaustively search the tree. …
souma4 Apr 20, 2025
f2bc9a2
Minor adjustments
juliohm Apr 21, 2025
27a0b17
removed unneeded search
souma4 Apr 21, 2025
d87d91e
updated to use intersect function
souma4 Apr 24, 2025
399aaf4
Major update to BO algorithm. I do not like the performance of handle…
souma4 Jun 1, 2025
d6a9e61
updated intersect method to get 3x speedup from improved type stabili…
souma4 Jun 16, 2025
dee44a4
reduced uneeded computations, there's still room to cut more but that…
souma4 Jun 17, 2025
28815e5
Merge branch 'master' into JRC_add_BentleyOttman
souma4 Jun 27, 2025
4a57da8
updated to use AdaptivePredicates (*NEW DEPENDENCY*) for more robust …
souma4 Jun 28, 2025
e125b9b
cleaned up comments, expanded detail on implementation quirks, built …
souma4 Jun 30, 2025
00f8fa3
simplified and corrected name for the initializing start and end poin…
souma4 Jun 30, 2025
c25247d
updated variable names to be more accurate/simple
souma4 Jul 1, 2025
314980d
removed AdaptivePredicates. Uneeded, didn't realize.
souma4 Jul 1, 2025
74cdbc0
Reorder imports
juliohm Jul 2, 2025
f2f80af
Merge branch 'master' into JRC_add_BentleyOttman
juliohm Jul 2, 2025
4728892
Fix missing end in test/utils.jl
juliohm Jul 2, 2025
03072a4
updated to only output intersection points because we are assuming th…
souma4 Jul 5, 2025
6d95fc4
Cleanup tests
juliohm Jul 7, 2025
d50c1b9
Cleanup implementation
juliohm Jul 7, 2025
a351b2d
updated code to align with style more. kept similar variable names th…
souma4 Jul 13, 2025
b108214
Cleanup tests further
juliohm Jul 15, 2025
82fbcec
More cleanup
juliohm Jul 15, 2025
3ddb02f
Fix formatting issue
juliohm Jul 15, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ version = "0.53.27"

[deps]
Bessels = "0e736298-9ec6-45e8-9647-e4fc86a2fe38"
BinaryTrees = "289e92be-c05a-437b-9e67-5b0c799891f8"
CircularArrays = "7a955b69-7140-5f4e-a0ed-f168c5e2e749"
Colorfy = "03fe91ce-8ec6-4610-8e8d-e7491ccca690"
CoordRefSystems = "b46f11dc-f210-4604-bfba-323c1ec968cb"
Expand All @@ -22,8 +23,15 @@ TiledIteration = "06e1c1a7-607b-532d-9fad-de7d9aa2abac"
TransformsBase = "28dd2a49-a57a-4bfb-84ca-1a49db9b96b8"
Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d"

[weakdeps]
Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a"

[extensions]
MeshesMakieExt = "Makie"

[compat]
Bessels = "0.2"
BinaryTrees = "0.1.2"
CircularArrays = "1.3"
Colorfy = "1.1"
CoordRefSystems = "0.18"
Expand All @@ -42,9 +50,3 @@ TiledIteration = "0.5"
TransformsBase = "1.6"
Unitful = "1.17"
julia = "1.9"

[extensions]
MeshesMakieExt = "Makie"

[weakdeps]
Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a"
1 change: 1 addition & 0 deletions src/Meshes.jl
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ using CoordRefSystems
using StaticArrays
using SparseArrays
using CircularArrays
using BinaryTrees
using LinearAlgebra
using Unitful
using Random
Expand Down
1 change: 1 addition & 0 deletions src/utils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ include("utils/cmp.jl")
include("utils/units.jl")
include("utils/crs.jl")
include("utils/misc.jl")
include("utils/intersect.jl")
346 changes: 346 additions & 0 deletions src/utils/intersect.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,346 @@
# ------------------------------------------------------------------
# Licensed under the MIT License. See LICENSE in the project root.
# ------------------------------------------------------------------

"""
bentleyottmann(segments; [digits])

Compute pairwise intersections between n `segments`
with `digits` precision in O((n+k)⋅log(n)) time using
Bentley-Ottmann sweep line algorithm.

By default, set `digits` based on the absolute
tolerance of the length type of the segments.

## References

* Bentley & Ottmann 1979. [Algorithms for reporting and counting
geometric intersections](https://ieeexplore.ieee.org/document/1675432)
"""
function bentleyottmann(segments; digits=_digits(segments))
# orient segments and round coordinates
segs = map(segments) do seg
a, b = coordround.(extrema(seg), digits=digits)
a > b ? (b, a) : (a, b)
end

# retrieve relevant types
P = typeof(first(first(segs)))
ℒ = lentype(P)
S = Tuple{P,P}
U = Set{S}

# event queue: stores points with associated sets of starting, ending, and crossing segments
𝒬 = BinaryTrees.AVLTree{P,Tuple{U,U,U}}()

# status structure: stores segments currently intersecting the sweepline
ℛ = BinaryTrees.AVLTree{SweepSegment{P,ℒ}}()

# lookup table mapping segments to their linear indices
lookup = Dict{S,Int}()

# add points and segments to event queue and lookup table
for (i, seg) in enumerate(segs)
_addpoint!(𝒬, seg[1], [seg], 1)
_addpoint!(𝒬, seg[2], [seg], 2)
lookup[seg] = i
end

# initialize sweepline
pmin, _ = first(extrema(segs))
ybounds = _ybounds(segs)
sweepline = SweepLine{P,ℒ}(pmin, ybounds)
# output dictionary (planar graph 𝐺)
𝐺 = Dict{P,Vector{Int}}()
# vector holding segments intersecting the current event point
bundle = Vector{SweepSegment{P,ℒ}}()
# holds segment indices for output
inds = Set{Int}()

# sweep line algorithm
while !BinaryTrees.isempty(𝒬)
# current event point
node = BinaryTrees.minnode(𝒬)
p = BinaryTrees.key(node)
BinaryTrees.delete!(𝒬, p)
# sets of beginning, ending, and crossing segments that include the current event point
ℬₚ, ℰₚ, ℳₚ = BinaryTrees.value(node)
# crosses that aren't endpoints (including them can lead to duplicates)
setdiff!(ℳₚ, ℰₚ)
# update the status structure with the current event point
_handlestatus!(ℛ, ℬₚ, ℳₚ, ℰₚ, sweepline, p)

# build bundle of segments crossing the current event point
bundle = empty!(bundle)
for seg in Iterators.flatten((ℬₚ, ℳₚ))
push!(bundle, SweepSegment(seg, sweepline))
end
sort!(bundle)
# process bundled events
if isempty(bundle) # occurs at endpoints
# check newly adjacent segments
nsₗ, nsᵤ = BinaryTrees.prevnext(ℛ, SweepSegment(first(ℰₚ), sweepline))
isnothing(nsₗ) || isnothing(nsᵤ) || _newevent!(𝒬, sweepline, bundle, p, _keyseg(nsₗ), _keyseg(nsᵤ), digits)
else
# check for intersections with adjacent segments below and above the current event point
BinaryTrees.isempty(ℛ) || _handlebottom!(bundle, ℛ, 𝒬, p, digits)
BinaryTrees.isempty(ℛ) || _handletop!(bundle, ℛ, 𝒬, p, digits)
end

# add intersection points and corresponding segment indices to 𝐺
if length(bundle) > length(ℬₚ) # bundle only has ℬₚ unless p is an intersection
# add start and crossing segments
for s in bundle
push!(inds, lookup[_segment(s)])
end
# add ending segments
for seg in ℰₚ
push!(inds, lookup[seg])
end

# add indices to output
indᵥ = collect(inds)
if haskey(𝐺, p)
union!(𝐺[p], indᵥ)
else
𝐺[p] = indᵥ
end
empty!(inds)
end
end
(collect(keys(𝐺)), collect(values(𝐺)))
end

# ------------------------------------
# Sweep line status and event handling
# ------------------------------------

# updates the status structure with the current event point
function _handlestatus!(ℛ, ℬₚ, ℳₚ, ℰₚ, sweepline, p)
# remove end segments that are no longer active and crossings to update
for seg in Iterators.flatten((ℰₚ, ℳₚ))
BinaryTrees.delete!(ℛ, SweepSegment(seg, sweepline))
end
# update sweepline
sweepline.p = p
# insert new and crossing segments into the status structure
for seg in Iterators.flatten((ℬₚ, ℳₚ))
BinaryTrees.insert!(ℛ, SweepSegment(seg, sweepline))
end
end

function _handlebottom!(bundle, ℛ, 𝒬, p, digits)
# bundle is sorted sequence, so the first segment is minimum
s′ = bundle[begin]
# element below s′
nsₗ, _ = !isnothing(s′) ? BinaryTrees.prevnext(ℛ, s′) : (nothing, nothing)
if !isnothing(nsₗ)
_newevent!(𝒬, _sweepline(s′), bundle, p, _segment(s′), _keyseg(nsₗ), digits)
end
end

function _handletop!(bundle, ℛ, 𝒬, p, digits)
# bundle is sorted sequence, so the last segment is maximum
s″ = bundle[end]
# element above s″
_, nsᵤ = !isnothing(s″) ? BinaryTrees.prevnext(ℛ, s″) : (nothing, nothing)
if !isnothing(nsᵤ)
_newevent!(𝒬, _sweepline(s″), bundle, p, _segment(s″), _keyseg(nsᵤ), digits)
end
end

# -----------------
# HELPER FUNCTIONS
# -----------------

# function to add new events to the event queue as needed
function _newevent!(𝒬, sweepline, bundle, p, seg₁, seg₂, digits)
intersection(Segment(seg₁), Segment(seg₂)) do I
t = type(I)
if t === Crossing || t === EdgeTouching
i = coordround(get(I), digits=digits)
if i ≈ p # helps with vertical+horizontal intersections
push!(bundle, SweepSegment(seg₂, sweepline))
push!(bundle, SweepSegment(seg₁, sweepline))
elseif i > p # add to 𝒬, update existing point if needed
_addpoint!(𝒬, i, [seg₁, seg₂], 3)
end
end
end
end

# compute the number of significant digits based on the segment type
# this is used to determine the precision of the points
function _digits(segments)
seg = first(segments)
ℒ = lentype(seg)
τ = ustrip(eps(ℒ))
round(Int, 0.8 * (-log10(τ))) # 0.8 is a heuristic to avoid numerical issues
Copy link
Member

Choose a reason for hiding this comment

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

This 0.8 was not here before. Can we simply use tau = atol(numtype(lentype(seg)))?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That does work, however it does seem to make one more Float32 test fail (easy to skip it). The 0.8 arose because I had switched to using machine epsilon. If I remember right that was largely to more specifically reflect the precision of the numtype. But this atol switch is a bit more forgiving and seems to work. I'm cool with tau = atol(numtype(lentype(seg)) rather than being reliant on a tuned parameter from eps.

Copy link
Member

Choose a reason for hiding this comment

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

I don't remember the code that was there before, but the 0.8 factor is very ad-hoc. We shouldn't have that.

end

# add an endpoint and corresponding segment to the event queue at a given position (1=start, 2=end, 3=crossing)
function _addpoint!(𝒬, p, segs, pos)
U = Set{typeof(first(segs))} # set of segments
node = BinaryTrees.search(𝒬, p)
# updates or adds event point based on existing events in 𝒬
if !isnothing(node)
union!(BinaryTrees.value(node)[pos], segs)
else
# create a new event point with the value of (U(), U(), U()). pos determines whether inserted segments are starts or ends
vals = ntuple(i -> i == pos ? U(segs) : U(), 3) # (Starts, Ends, Crossings)
BinaryTrees.insert!(𝒬, p, vals)
end
end

# compute y bounds of the segments
function _ybounds(segs)
# compute bounding box
bbox = boundingbox(segs)

# stretch bounding bbox
T = numtype(lentype(bbox))
sbox = bbox |> Stretch(T(1.05))

# extract y coordinate values
map(p -> coords(p).y, extrema(sbox))
end

# convenience function to get the segment from a our AVLNode{SweepSegment} structure
_keyseg(ns) = _segment(BinaryTrees.key(ns))

# handles the degenerate case of trivial (0-length) segments
_istrivial(seg) = seg[1] == seg[2]
Copy link
Member

Choose a reason for hiding this comment

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

I am concerned that I am still reviewing dead code. This function isn't used anywhere.


# ----------------
# DATA STRUCTURES
# ----------------

# tracks the event point and constructs the sweepline lazily
mutable struct SweepLine{P<:Point,ℒ<:Number}
p::P
ybounds::Tuple{ℒ,ℒ}
end
# getters
_sweeppoint(sweepline::SweepLine) = getfield(sweepline, :p)
_sweepx(sweepline::SweepLine) = CoordRefSystems.values(coords(_sweeppoint(sweepline)))[1]
_sweepy(sweepline::SweepLine) = CoordRefSystems.values(coords(_sweeppoint(sweepline)))[2]
_sweepbounds(sweepline::SweepLine) = getfield(sweepline, :ybounds)

#= Sweepline Definition ---

defined as the line segments
|
└——
o|
|
where o is the sweepline point and lines are the sweepline

This definition is used to
handle intersections elegantly. It ensures that y coordinates
of non vertical segments are always correct,
vertical segments are always on top of non-vertical segments next to p,
ending segments don't intersect, and all other segments are correctly ordered.

Inspired by LEDA implementation, but modified for Julia
=#
function _sweepline(sweepline::SweepLine)
x = _sweepx(sweepline)
y = _sweepy(sweepline)
# perturbation to avoid numerical issues
ϵ = atol(x) + eps(x)
lower, upper = _sweepbounds(sweepline)

p₁ = Point(x + ϵ, lower) # lowest point
p₂ = Point(x + ϵ, y + 2ϵ) # doubled to avoid precision issues
p₃ = Point(x - ϵ, y + 2ϵ)
p₄ = Point(x - ϵ, upper) # highest point
# tuple of segments representing the sweepline
# ((lower vertical), (horizontal), (upper vertical))
((p₁, p₂), (p₂, p₃), (p₃, p₄))
end

# sweepline intersection with segment
function _sweepintersect(seg::Tuple{P,P}, sweepline::SweepLine{P,ℒ}) where {P<:Point,ℒ<:Number}
rope = _sweepline(sweepline)
I = nothing
# check for intersections between the segment and the sweepline.
# `Rope()` intersections are not type stable; this loop is a workaround.
for segᵣ in rope
I = intersection(Segment(seg), Segment(segᵣ)) do I
t = type(I)
if t === Crossing || t === EdgeTouching || t === CornerTouching
get(I)
else
nothing
end
end
if !isnothing(I)
break
end
end

# if I isn't found, then we are handling an ending segment, so seg[2] is used instead
i = isnothing(I) ? seg[2] : I
_, y = CoordRefSystems.values(coords(i))
y
end

# tracks a segment and its intersection with the sweepline at the latest calculated event point
mutable struct SweepSegment{P<:Point,ℒ<:Number}
const seg::Tuple{P,P}
const sweepline::SweepLine{P,ℒ}
yintersect::ℒ # information about the intersection with the sweepline
latestpoint::P # latest point of the sweepline used to calculate the intersection
end

# constructor for SweepSegment using Sweepline
function SweepSegment(seg::Tuple{P,P}, sweepline::SweepLine{P,ℒ}) where {P<:Point,ℒ<:Number}
y = _sweepintersect(seg, sweepline)
SweepSegment{P,ℒ}(seg, sweepline, y, _sweeppoint(sweepline))
end

# getters for SweepSegment
_segment(s::SweepSegment) = getfield(s, :seg)
_yintersect(s::SweepSegment) = getfield(s, :yintersect)
_sweepline(s::SweepSegment) = getfield(s, :sweepline)
_sweeppoint(s::SweepSegment) = _sweeppoint(getfield(s, :sweepline))

Base.:(==)(s₁::SweepSegment, s₂::SweepSegment) = _segment(s₁) == _segment(s₂)

# compare two segments based on their sweepline intersection relative to the current event point.
function Base.isless(s₁::SweepSegment{P,ℒ}, s₂::SweepSegment{P,ℒ}) where {P<:Point,ℒ}
# if segments same, return false
if s₁ == s₂
return false
end
#* calculating the y intersect is the largest performance bottleneck
ya = _yintersect(s₁) # s₁ is always up-to-date
yb = _ycalc!(s₂)

diff = ustrip(abs(ya - yb))
tol = eps(ℒ)
# if segments are separated over y, check ya < yb
if diff > tol
ya < yb
else
# fallback to lexicographic ordering of segments
seg₁, seg₂ = _segment(s₁), _segment(s₂)
seg₁ < seg₂
end
end

# calculate y-coordinate of intersection with sweepline
function _ycalc!(s::SweepSegment{P,ℒ}) where {P<:Point,ℒ<:Number}
sweepline = _sweepline(s)
# if the latest point is the sweepline point, use the precalculated intersection
if s.latestpoint === _sweeppoint(sweepline)
y = s.yintersect
else
# otherwise, calculate the intersection with the sweepline
# and update
y = convert(ℒ, _sweepintersect(_segment(s), sweepline))
s.latestpoint = _sweeppoint(sweepline)
s.yintersect = y
end
y
end
Loading
Loading