defmodule Scurry.Geo do
@moduledoc """
Functions related to lines relevant for 2D map pathfinding.
"""
alias Scurry.Vector
# For explanation of a lot of the math here;
# * https://khorbushko.github.io/article/2021/07/15/the-area-polygon-or-how-to-detect-line-segments-intersection.html
# * https://stackoverflow.com/questions/563198/how-do-you-detect-where-two-line-segments-intersect/1968345#1968345
@doc """
Determine if, where and how two lines intersect.
## Params
* `line1` a `{{x1, y1}, {x2, y2}}` line segment
* `line2` a `{{x3, y13, {x4, y4}}` line segment
Returns
* `:on_segment` one line is on the other.
* `:parallel` the lines are parallel and do not intersect.
* `{:point_intersection, {x, y}}` either line has an endpoint (`{x, y}`) on
the other line.
* `{:intersection, {x, y}}` the lines intersect at `{x, y}`.
* `:none` no intersection.
## Examples
iex> Geo.line_segment_intersection({{1, 2}, {4, 2}}, {{2, 0}, {3, 0}})
:parallel
iex> Geo.line_segment_intersection({{1, 2}, {4, 2}}, {{2, 2}, {3, 2}})
:on_segment
iex> Geo.line_segment_intersection({{1, 2}, {4, 2}}, {{2, 0}, {2, 1}})
:none
iex> Geo.line_segment_intersection({{1, 2}, {4, 2}}, {{2, 0}, {2, 2}})
{:point_intersection, {2.0, 2.0}}
iex> Geo.line_segment_intersection({{1, 2}, {4, 2}}, {{2, 0}, {2, 3}})
{:intersection, {2.0, 2.0}}
"""
def line_segment_intersection(line1, line2) do
{{ax1, ay1}, {ax2, ay2}} = line1
{{bx1, by1}, {bx2, by2}} = line2
den = (by2 - by1) * (ax2 - ax1) - (bx2 - bx1) * (ay2 - ay1)
if den == 0 do
if (by1 - ay1) * (ax2 - ax1) == (bx1 - ax1) * (ay2 - ay1) do
:on_segment
else
:parallel
end
else
ua = ((bx2 - bx1) * (ay1 - by1) - (by2 - by1) * (ax1 - bx1)) / den
ub = ((ax2 - ax1) * (ay1 - by1) - (ay2 - ay1) * (ax1 - bx1)) / den
if ua >= 0.0 and ua <= 1.0 and ub >= 0.0 and ub <= 1.0 do
{x, y} = {ax1 + ua * (ax2 - ax1), ay1 + ua * (ay2 - ay1)}
if ua == 0.0 or ub == 1.0 or ua == 1.0 or ub == 0.0 do
{:point_intersection, {x, y}}
else
{:intersection, {x, y}}
end
else
:none
end
end
end
@doc """
Get the distance squared from a point to a line/segment.
## Params
* `line` a tuple of points (`{{ax, ay}, {bx, by}}`) describing a line.
* `point` a tuple `{x, y}` describing a point
This returns the square of the distance beween the given point and segment as
a float.
## Examples
iex> Geo.distance_to_segment_squared({{2, 0}, {2, 2}}, {0, 1})
4.0
"""
def distance_to_segment_squared({{vx, vy}=v, {wx, wy}=w}=_line, {px, py}=point) do
l2 = Vector.distance_squared(v, w)
if l2 == 0.0 do
Vector.distance_squared(point, v)
else
t = ((px - vx) * (wx - vx) + (py - vy) * (wy - vy)) / l2
cond do
t < 0 -> Vector.distance_squared(point, v)
t > 1 -> Vector.distance_squared(point, w)
true -> Vector.distance_squared(point, {vx + t * (wx - vx), vy + t * (wy - vy)})
end
end
end
@doc """
Get the distance from a point to a line/segment, aka the square root of
`distance_squared/2`.
## Params
* `line` a tuple of points (`{{ax, ay}, {bx, by}}`) describing a line.
* `point` a tuple `{x, y}` describing a point
This returns the distance beween the given point and segment as a float.
## Examples
iex> Geo.distance_to_segment({{2, 0}, {2, 2}}, {0, 1})
2.0
"""
def distance_to_segment(line, point) do
:math.sqrt(distance_to_segment_squared(line, point))
end
# ported from http://www.david-gouveia.com/portfolio/pathfinding-on-a-2d-polygonal-map/
@doc """
Check if two lines intersect
This is a simpler version of `line_segment_intersection/2`, which is typically
a better choice since it handles endpoints and segment overlap too.
## Params
* `line1` a `{{x1, y1}, {x2, y2}}` line segment
* `line2` a `{{x3, y13, {x4, y4}}` line segment
Returns `true` if they intersect, `false` otherwise.
Note that this doesn't handle segment overlap or points touching. Use
`line_segment_intersection/2` instead for that level of detail.
"""
def do_lines_intersect?({{ax1, ay1}, {ax2, ay2}}=_l1, {{bx1, by1}, {bx2, by2}}=_l2) do
den = ((ax2 - ax1) * (by2 - by1)) - ((ay2 - ay1) * (bx2 - bx1))
if den == 0 do
false
else
num1 = ((ay1 - by1) * (bx2 - bx1)) - ((ax1 - bx1) * (by2 - by1))
num2 = ((ay1 - by1) * (ax2 - ax1)) - ((ax1 - bx1) * (ay2 - ay1))
if (num1 == 0 or num2 == 0) do
false
else
r = num1 / den
s = num2 / den
(r > 0 and r < 1) and (s > 0 and s < 1)
end
end
end
end