Skip to content

Commit 7b015b2

Browse files
souma4juliohm
andauthored
Add pairwise intersection between segments with sweep-line algorithm (#1251)
* new PR for line segment intersections. Simpler, not quite as efficient method. * add tests * update utils.jl to include intersect * update names to intervalsweep since this is no longer 'true' bentley-ottmann * add test for coverage when segments stop overlapping * incorporated review feedback. sweep1D is a descriptive name and leaves namespace open for a 2D sweep (if needed) in the future. Also fixed overlaps logic to properly break early * sweep1D doesn't update the initial queue. * simplified implementation, no new structs, moved from sets of indices to vectors of indices because sets are expensive to initialized * consolidated code, cleaned up names, removed maps, improved type stability * disolve _checkintersection function and change P definition * basic cleanup * minor cleanup * fixed addintersection. it gets I * split out the for loops because continue for inline double for loops continues for i, not j * no need for continue * Basic cleanup * Basic cleanup * Improve docstring * Refactor tests * improved docstring and edited @inferred test * Adjust type of key in dictionary of intersections --------- Co-authored-by: Júlio Hoffimann <[email protected]>
1 parent 4db6349 commit 7b015b2

File tree

3 files changed

+186
-0
lines changed

3 files changed

+186
-0
lines changed

src/utils.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ include("utils/cmp.jl")
1111
include("utils/units.jl")
1212
include("utils/crs.jl")
1313
include("utils/misc.jl")
14+
include("utils/intersect.jl")

src/utils/intersect.jl

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# ------------------------------------------------------------------
2+
# Licensed under the MIT License. See LICENSE in the project root.
3+
# ------------------------------------------------------------------
4+
5+
"""
6+
pairwiseintersect(segments; [digits])
7+
8+
Compute pairwise intersections between n `segments`
9+
with `digits` precision in O(n⋅log(n+k)) time
10+
where k is the number of intersections, using
11+
a sweep line algorithm. Similar to an optimal
12+
Bentley-Ottmann algorithm in sparse systems,
13+
and closer to O(n²) in dense systems.
14+
15+
Return intersection points and corresponding indices
16+
of segments involved in the intersection.
17+
18+
By default, set `digits` based on the absolute
19+
tolerance of the length type of the segments.
20+
21+
## Examples
22+
23+
```julia
24+
points, seginds = pairwiseintersect(segments)
25+
points[i] # i-th intersection point
26+
seginds[i] # corresponding segments
27+
```
28+
29+
## References
30+
31+
* Bentley & Ottmann 1979. [Algorithms for reporting and counting
32+
geometric intersections](https://ieeexplore.ieee.org/document/1675432)
33+
"""
34+
function pairwiseintersect(segments; digits=_digits(segments))
35+
# orient segments and round coordinates
36+
segs = map(segments) do s
37+
a, b = coordround.(extrema(s), digits=digits)
38+
a > b ? Segment(b, a) : Segment(a, b)
39+
end
40+
41+
# extract first (or "x") coordinate from
42+
# first and last vertices of segments
43+
x(p) = flat(coords(p)).x
44+
xₛ = [x(first(vertices(s))) for s in segs]
45+
xₑ = [x(last(vertices(s))) for s in segs]
46+
47+
# sort segments based on first coordinates
48+
inds = sortperm(xₛ)
49+
segs = segs[inds]
50+
xₛ = xₛ[inds]
51+
xₑ = xₑ[inds]
52+
53+
# sweepline algorithm
54+
n = length(segs)
55+
P = eltype(vertices(first(segs)))
56+
D = Dict{P,Vector{Int}}()
57+
for i in 1:n
58+
for j in (i + 1):n
59+
# break if segments don't overlap w.r.t. first coordinate
60+
xₛ[i] xₛ[j] xₑ[i] || break
61+
62+
# perform more expensive intersection algorithm
63+
intersection(segs[i], segs[j]) do I
64+
if type(I) == Crossing || type(I) == EdgeTouching
65+
p = coordround(get(I); digits)
66+
if haskey(D, p)
67+
append!(D[p], (inds[i], inds[j]))
68+
else
69+
D[p] = [inds[i], inds[j]]
70+
end
71+
end
72+
end
73+
end
74+
end
75+
76+
# remove duplicate indices
77+
for p in keys(D)
78+
unique!(D[p])
79+
end
80+
81+
collect(keys(D)), collect(values(D))
82+
end
83+
84+
# compute the number of significant digits based on the segment type
85+
# this is used to determine the precision of the points
86+
function _digits(segments)
87+
s = first(segments)
88+
= lentype(s)
89+
τ = ustrip(eps(ℒ))
90+
round(Int, 0.8 * (-log10(τ))) # 0.8 is a heuristic to avoid numerical issues
91+
end

test/utils.jl

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,100 @@ end
8080
@inferred Meshes.coordround(p₁, digits=10)
8181
end
8282

83+
@testitem "pairwiseintersect" setup = [Setup] begin
84+
# helper to sort points and seginds by point coordinates
85+
function sortedintersection(segs)
86+
points, inds = Meshes.pairwiseintersect(segs)
87+
perm = sortperm(points)
88+
points[perm], inds[perm]
89+
end
90+
91+
# simple endpoint case
92+
segs = Segment.([(cart(0, 0), cart(2, 2)), (cart(0, 2), cart(2, 0)), (cart(0, 1), cart(0.5, 1))])
93+
points, seginds = sortedintersection(segs)
94+
@test length(points) == 1
95+
@test length(seginds) == 1
96+
97+
# small number of segments, handling endpoints and precision
98+
segs = Segment.([(cart(0, 0), cart(2, 2)), (cart(1.5, 1), cart(2, 1)), (cart(1.51, 1.3), cart(2, 0.9))])
99+
points, seginds = sortedintersection(segs)
100+
@test length(points) == 1
101+
102+
# box case with one segment outside
103+
segs =
104+
Segment.([
105+
(cart(0, 0), cart(1.1, 1.1)),
106+
(cart(1, 0), cart(0, 1)),
107+
(cart(0, 0), cart(0, 1)),
108+
(cart(0, 0), cart(1, 0)),
109+
(cart(0, 1), cart(1, 1)),
110+
(cart(1, 0), cart(1, 1))
111+
])
112+
points, seginds = sortedintersection(segs)
113+
@test length(points) == 2
114+
@test length(seginds) == 2
115+
@test Set(seginds[1]) == Set([1, 2])
116+
@test Set(seginds[2]) == Set([1, 6, 5])
117+
118+
# multiple intersections, endpoints as intersections
119+
if T === Float64
120+
segs =
121+
Segment.([
122+
(cart(9, 13), cart(6, 9)),
123+
(cart(2, 12), cart(9, 4.8)),
124+
(cart(12, 11), cart(4, 7)),
125+
(cart(2.5, 10), cart(12.5, 2)),
126+
(cart(13, 6), cart(10, 4)),
127+
(cart(10.5, 5.5), cart(9, 1)),
128+
(cart(10, 4), cart(11, -1)),
129+
(cart(10, 3), cart(10, 5))
130+
])
131+
points, seginds = sortedintersection(segs)
132+
@test length(points) == 4
133+
@test length(seginds) == 4
134+
@test points[3] cart(9, 4.8)
135+
@test points[4] cart(10, 4)
136+
@test Set(seginds[1]) == Set([4, 3])
137+
@test Set(seginds[2]) == Set([2, 3])
138+
@test Set(seginds[3]) == Set([4, 2])
139+
@test Set(seginds[4]) == Set([4, 5, 6, 7, 8])
140+
end
141+
142+
# finds all intersections in a grid
143+
n = 10
144+
horizontal = [Segment(cart(1, i), cart(n, i)) for i in 1:n]
145+
vertical = [Segment(cart(i, 1), cart(i, n)) for i in 1:n]
146+
segs = [horizontal; vertical]
147+
points, seginds = sortedintersection(segs)
148+
@test length(points) == n * n - 4
149+
@test length(seginds) == n * n - 4
150+
@test all(==(2), length.(seginds))
151+
152+
# number of intersections is invariant under rotations
153+
for θ in T/ 6):T/ 6):T(2π - π / 6)
154+
# rotation by π in Float32 is not robust, skips test
155+
T === Float32 && θ == T(π) && continue
156+
θpoints, θseginds = sortedintersection(segs |> Rotate(θ))
157+
@test length(θpoints) == n * n - 4
158+
@test length(θseginds) == n * n - 4
159+
@test all(==(2), length.(θseginds))
160+
end
161+
162+
# tests coverage for when intervals don't overlap
163+
segs = [
164+
Segment(cart(0, 2), cart(2, 0)),
165+
Segment(cart(0, 0), cart(2, 2)),
166+
Segment(cart(3, 1), cart(3, 3)),
167+
Segment(cart(3, 3), cart(3, 3))
168+
]
169+
points, seginds = sortedintersection(segs)
170+
@test length(points) == 1
171+
172+
# inference test
173+
segs = facets(cartgrid(10, 10))
174+
@inferred (Meshes.pairwiseintersect(segs))
175+
end
176+
83177
@testitem "isthreaded" setup = [Setup] begin
84178
if Threads.nthreads() > 1
85179
@test Meshes.isthreaded()

0 commit comments

Comments
 (0)