Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
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")
85 changes: 85 additions & 0 deletions src/utils/intersect.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# ------------------------------------------------------------------
# Licensed under the MIT License. See LICENSE in the project root.
# ------------------------------------------------------------------

"""
pairwiseintersect(segments; [digits])
Compute pairwise intersections between n `segments`
with `digits` precision in O(n⋅log(n+k)) time using
Copy link
Member

Choose a reason for hiding this comment

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

Need to define k in the docstring

a sweep line algorithm. Similar to an optimal
Bentley-Ottmann algorithm in sparse systems,
and closer to O(n²) in dense systems.
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 pairwiseintersect(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
P = typeof(first(segs)[1])

starts = [CoordRefSystems.values(coords(seg[1]))[1] for seg in segs]
stops = [CoordRefSystems.values(coords(seg[2]))[1] for seg in segs]
# sort indices based on start x coordinates
inds = sortperm(starts)
# reorder everything based on sorted indices
starts = starts[inds]
stops = stops[inds]
segs = segs[inds]
# keep track of original indices
n = length(segs)
oldindices = (1:n)[inds]
Copy link
Member

Choose a reason for hiding this comment

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

Isn't this equal to inds? How could we sort 1:n according to inds and not get inds as the result?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It actually is equivalent 🤦 . I think I had it in my head that sortperm wasn't sortperm, the indices for the sorted order, which is what is used for old indices. thanks as always for catching these.


# sweepline algorithm
𝐺 = Dict{P,Vector{Int}}()
for i in eachindex(segs)
for k in (i + 1):n
overlap = _overlaps(starts[i], stops[i], starts[k])
# if no overlap, break inner loop
overlap || break

I = intersection(Segment(segs[i]), Segment(segs[k])) do 𝑖
t = type(𝑖)
(t === Crossing || t === EdgeTouching) ? get(𝑖) : nothing
end

# continue if no intersection, add intersection otherwise
if isnothing(I)
continue
else
_addintersection!(𝐺, I, oldindices[i], oldindices[k]; digits=digits)
end
end
end
(collect(keys(𝐺)), collect(values(𝐺)))
end

_overlaps(startᵢ, stopᵢ, startₖ) = (startᵢ startₖ stopᵢ)
# 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
end

# add an intersection point to the dictionary with segment indices
function _addintersection!(𝐺, I::Point, index₁::Int, index₂::Int; digits=10)
p = coordround(I, digits=digits)
if haskey(𝐺, p)
append!(𝐺[p], (index₁, index₂))
unique!(𝐺[p])
else
𝐺[p] = Vector{Int}([index₁, index₂])
end
end
94 changes: 94 additions & 0 deletions test/utils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,100 @@ end
@inferred Meshes.coordround(p₁, digits=10)
end

@testitem "pairwiseintersect" setup = [Setup] begin
# helper to sort points and seginds by point coordinates
function sortedintersection(segs)
points, inds = Meshes.pairwiseintersect(segs)
perm = sortperm(points)
points[perm], inds[perm]
end

# simple endpoint case
segs = Segment.([(cart(0, 0), cart(2, 2)), (cart(0, 2), cart(2, 0)), (cart(0, 1), cart(0.5, 1))])
points, seginds = sortedintersection(segs)
@test length(points) == 1
@test length(seginds) == 1

# small number of segments, handling endpoints and precision
segs = Segment.([(cart(0, 0), cart(2, 2)), (cart(1.5, 1), cart(2, 1)), (cart(1.51, 1.3), cart(2, 0.9))])
points, seginds = sortedintersection(segs)
@test length(points) == 1

# box case with one segment outside
segs =
Segment.([
(cart(0, 0), cart(1.1, 1.1)),
(cart(1, 0), cart(0, 1)),
(cart(0, 0), cart(0, 1)),
(cart(0, 0), cart(1, 0)),
(cart(0, 1), cart(1, 1)),
(cart(1, 0), cart(1, 1))
])
points, seginds = sortedintersection(segs)
@test length(points) == 2
@test length(seginds) == 2
@test Set(seginds[1]) == Set([1, 2])
@test Set(seginds[2]) == Set([1, 6, 5])

# multiple intersections, endpoints as intersections
if T === Float64
segs =
Segment.([
(cart(9, 13), cart(6, 9)),
(cart(2, 12), cart(9, 4.8)),
(cart(12, 11), cart(4, 7)),
(cart(2.5, 10), cart(12.5, 2)),
(cart(13, 6), cart(10, 4)),
(cart(10.5, 5.5), cart(9, 1)),
(cart(10, 4), cart(11, -1)),
(cart(10, 3), cart(10, 5))
])
points, seginds = sortedintersection(segs)
@test length(points) == 4
@test length(seginds) == 4
@test points[3] ≈ cart(9, 4.8)
@test points[4] ≈ cart(10, 4)
@test Set(seginds[1]) == Set([4, 3])
@test Set(seginds[2]) == Set([2, 3])
@test Set(seginds[3]) == Set([4, 2])
@test Set(seginds[4]) == Set([4, 5, 6, 7, 8])
end

# finds all intersections in a grid
n = 10
horizontal = [Segment(cart(1, i), cart(n, i)) for i in 1:n]
vertical = [Segment(cart(i, 1), cart(i, n)) for i in 1:n]
segs = [horizontal; vertical]
points, seginds = sortedintersection(segs)
@test length(points) == n * n - 4
@test length(seginds) == n * n - 4
@test Set(length.(seginds)) == Set([2])

# number of intersections is invariant under rotations
for θ in T(π / 6):T(π / 6):T(2π - π / 6)
# rotation by π in Float32 is not robust, skips test
T === Float32 && θ == T(π) && continue
θpoints, θseginds = sortedintersection(segs |> Rotate(θ))
@test length(θpoints) == n * n - 4
@test length(θseginds) == n * n - 4
@test Set(length.(θseginds)) == Set([2])
end

# tests coverage for when intervals don't overlap
segs = [
Segment(cart(0, 2), cart(2, 0)),
Segment(cart(0, 0), cart(2, 2)),
Segment(cart(3, 1), cart(3, 3)),
Segment(cart(3, 3), cart(3, 3))
]
points, seginds = sortedintersection(segs)
@test length(points) == 1

# inference test
segs = facets(cartgrid(10, 10))
@inferred Nothing (Meshes.pairwiseintersect(segs))
end

@testitem "isthreaded" setup = [Setup] begin
if Threads.nthreads() > 1
@test Meshes.isthreaded()
Expand Down
Loading