Skip to content

Commit ca807f2

Browse files
committed
test and clean up line intersection code
1 parent 89b9921 commit ca807f2

File tree

2 files changed

+51
-26
lines changed

2 files changed

+51
-26
lines changed

src/lines.jl

Lines changed: 27 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ Returns `(intersection_found::Bool, intersection_point::Point)`
77
"""
88
# 2D Line-segment intersection algorithm by Paul Bourke and many others.
99
# http://paulbourke.net/geometry/pointlineplane/
10-
function intersects(a::Line{2,T1}, b::Line{2,T2}) where {T1,T2}
10+
function intersects(a::Line{2,T1}, b::Line{2,T2}; eps = 0) where {T1,T2}
1111
T = promote_type(T1, T2)
1212
p0 = zero(Point2{T})
1313

@@ -34,7 +34,7 @@ function intersects(a::Line{2,T1}, b::Line{2,T2}) where {T1,T2}
3434

3535
# Values between [0, 1] mean the intersection point of the lines rests on
3636
# both of the line segments.
37-
if 0 <= unknown_a <= 1 && 0 <= unknown_b <= 1
37+
if eps <= unknown_a <= 1-eps && eps <= unknown_b <= 1-eps
3838
# Substituting an unknown back lets us find the intersection point.
3939
x = x1 + (unknown_a * (x2 - x1))
4040
y = y1 + (unknown_a * (y2 - y1))
@@ -62,27 +62,29 @@ end
6262
"""
6363
self_intersections(points::AbstractVector{<:Point})
6464
65-
Finds all self intersections of polygon `points`
65+
Finds all self intersections of in a continuous line described by `points`.
66+
67+
Note that if two points are the same, they will generate a self intersection
68+
unless they are the first and last point or part of consecutive segments.
6669
"""
67-
function self_intersections(points::AbstractVector{<:Point})
70+
function self_intersections(points::AbstractVector{<:VecTypes{D, T}}) where {D, T}
6871
sections = similar(points, 0)
69-
intersections = Int[]
70-
71-
wraparound(i) = mod1(i, length(points) - 1)
72-
73-
for (i, (a, b)) in enumerate(consecutive_pairs(points))
74-
for (j, (a2, b2)) in enumerate(consecutive_pairs(points))
75-
is1, is2 = wraparound(i + 1), wraparound(i - 1)
76-
if i != j &&
77-
is1 != j &&
78-
is2 != j &&
79-
!(i in intersections) &&
80-
!(j in intersections)
81-
intersected, p = intersects(Line(a, b), Line(a2, b2))
82-
if intersected
83-
push!(intersections, i, j)
84-
push!(sections, p)
85-
end
72+
intersections = Tuple{Int, Int}[]
73+
74+
N = length(points)
75+
76+
for i in 1:N-3
77+
a = points[i]; b = points[i+1]
78+
# i+1 == j describes consecutive segments which are always "intersecting"
79+
# at point i+1/j. Skip those (start at i+2)
80+
# Special case: We assume points[1] == points[end] so 1 -> 2 and N-1 -> N
81+
# always "intersect" at 1/N. Skip this too (end at N-2 in this case)
82+
for j in i+2 : N-1 - (i == 1)
83+
a2 = points[j]; b2 = points[j+1]
84+
intersected, p = intersects(Line(a, b), Line(a2, b2))
85+
if intersected
86+
push!(intersections, (i, j))
87+
push!(sections, p)
8688
end
8789
end
8890
end
@@ -95,15 +97,14 @@ end
9597
Splits polygon `points` into it's self intersecting parts. Only 1 intersection
9698
is handled right now.
9799
"""
98-
function split_intersections(points::AbstractVector{<:Point})
100+
function split_intersections(points::AbstractVector{<:VecTypes{N, T}}) where {N, T}
99101
intersections, sections = self_intersections(points)
100102
return if isempty(intersections)
101103
return [points]
102-
elseif length(intersections) == 2 && length(sections) == 1
103-
a, b = intersections
104+
elseif length(intersections) == 1 && length(sections) == 1
105+
a, b = intersections[1]
104106
p = sections[1]
105-
a, b = min(a, b), max(a, b)
106-
poly1 = simple_concat(points, (a + 1):(b - 1), p)
107+
poly1 = simple_concat(points, (a + 1):b, p)
107108
poly2 = simple_concat(points, (b + 1):(length(points) + a), p)
108109
return [poly1, poly2]
109110
else

test/runtests.jl

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,8 @@ end
258258
found, point = intersects(d, e)
259259
@test found && point Point(0.0, 4.0)
260260
@test intersects(a, f) === (false, Point(0.0, 0.0))
261+
@test intersects(a, d) === (true, Point(0.0, 0.0))
262+
@test intersects(a, d, eps = 1e-6) === (false, Point(0.0, 0.0))
261263

262264
# issue #168
263265
# If these tests fail then you can increase the tolerance on the checks so
@@ -273,6 +275,28 @@ end
273275
@test b
274276
@test loc Point(5760.0, 127.27922061357884)
275277
end
278+
279+
ps = [Point2f(1), Point2f(2)]
280+
@test GeometryBasics.simple_concat(ps, 2:2, Point2f(3)) == [Point2f(2), Point2f(3)]
281+
ps = [Point2f(i) for i in 1:4]
282+
@test collect(GeometryBasics.consecutive_pairs(ps)) == collect(zip(ps[1:end-1], ps[2:end]))
283+
284+
ps = Point2f[(0,0), (1,0), (0,1), (1,2), (0,2), (1,1), (0,0)]
285+
idxs, ips = self_intersections(ps)
286+
@test idxs == [(2, 6), (3, 5)]
287+
@test ips == [Point2f(0.5), Point2f(0.5, 1.5)]
288+
289+
ps = [Point2f(cos(x), sin(x)) for x in 0:4pi/5:4pi+0.1]
290+
idxs, ips = self_intersections(ps)
291+
@test idxs == [(1, 3), (1, 4), (2, 4), (2, 5), (3, 5)]
292+
@test all(ips .≈ Point2f[(0.30901694, 0.2245140), (-0.118034005, 0.36327127), (-0.38196602, 0), (-0.118033946, -0.3632713), (0.309017, -0.22451389)])
293+
294+
@test_throws ErrorException split_intersections(ps)
295+
ps = Point2f[(0,0), (1,0), (0,1), (1,1), (0, 0)]
296+
idxs, ips = self_intersections(ps)
297+
sps = split_intersections(ps)
298+
@test sps[1] == [ps[3], ps[4], ips[1]]
299+
@test sps[2] == [ps[5], ps[1], ps[2], ips[1]]
276300
end
277301

278302

0 commit comments

Comments
 (0)