Current section
Files
Jump to
Current section
Files
lib/Helpers.ex
defmodule PolyPartition.Helpers do
alias PolyPartition.Geometry
@moduledoc """
Helper functions for PolyPartition
"""
@doc """
Calculates the determinant of two points provided as a segment
## Examples
iex> PolyPartition.Helpers.det_seg([ [2, 3], [1, -1] ])
-5
"""
def det_seg(seg) do
[p, q] = seg
det(p, q)
end
defp det(p, q) do
[x1, y1] = p
[x2, y2] = q
(x1 * y2) - (x2 * y1)
end
@doc """
Takes two numbers, returns true if their signs differ, false otherwise
## Examples
iex> PolyPartition.Helpers.sgn_to_bool(-1, 2)
true
iex> PolyPartition.Helpers.sgn_to_bool(3, 3)
false
iex> PolyPartition.Helpers.sgn_to_bool(3, 0)
false
"""
def sgn_to_bool(a, b) do
cond do
a * b < 0 -> true
true -> false
end
end
@doc """
Find the index of the vertex to use to split the polygon
`split_coord` assumes there is a vertex that is
- not a neighbor of the first vertex, and
- is "line of sight" from the first vertex.
I believe this is always the case for polygons with more than three vertices,
but haven't proven it.
`split_coord` will start by testing the vertex farthest away (in circular order)
from the first vertex and step out one vertex at a time alternating left and right.
The `step` parameter is incremented by a private function `next`.
## Examples
iex> poly = [[0,1], [1, 0], [2, 0], [3,1], [2,2], [1,2]]
iex> PolyPartition.Helpers.split_coord(poly, 0)
3
iex> poly = [[0,1], [1, 0], [2, 0], [3,1], [2,2], [2,0.5]]
iex> PolyPartition.Helpers.split_coord(poly, 0)
2
"""
def split_coord(poly, step) do
opp_index = round(:math.floor(length(poly) / 2)) + step
case Geometry.good_cut?(poly, opp_index) do
false -> split_coord(poly, next(step))
_ -> opp_index
end
end
defp next(n) do
cond do
n < 0 -> (-1 * n)
true -> -1 * (n + 1)
end
end
@doc """
Add a vertex at the midpoint of a polygon's longest side
If we have a triangle, we need to add a vertex to make a split. We choose the
longest side to keep the polygon as "fat" as possible
The generated point will have float coordinates, regardless of input
## Examples
iex> poly = [[0,1], [1,0], [2,1]]
iex> PolyPartition.Helpers.split_side(poly)
[[1.0, 1.0], [0,1], [1,0], [2,1],]
"""
def split_side(poly) do
{_, pt, ind} = poly
|> Stream.with_index
|> Enum.map(fn(x) ->
{point, index} = x
next = Enum.at(poly, rem((index + 1), length(poly)))
{Geometry.sq_length([point, next]), Geometry.midpoint([point, next]), index}
end)
|> List.foldr({0.0, 0, 0}, fn(x, acc) ->
{length, _, _} = x
{a_length, _, _} = acc
cond do
length > a_length -> x
true -> acc
end
end)
List.insert_at(poly, rem((ind + 1), length(poly)), pt)
end
@doc """
Takes a polygon and returns a list of two polygons forming a partition of the first
If any degenerate polygons are created, we retry with a different initial vertex
## Examples
iex> poly = [[0,1], [1, 0], [2, 0], [3,1], [2,2], [1,2]]
iex> PolyPartition.Helpers.split(poly, 0)
[[[0,1], [1,0], [2,0], [3,1]], [[3,1], [2,2], [1,2], [0,1]]]
"""
def split(poly, retries) do
cond do
retries >= length(poly) -> poly
true -> p = case length(poly) do
3 -> split_side(poly)
_ -> poly
end
opp_index = split_coord(p, 0)
result = []
r = result ++ rotate_list([Enum.slice(p, 0..opp_index)]) ++ [Enum.slice(p, opp_index..length(p)) ++ [hd(p)]]
t = r
|> Enum.map(fn(x) -> Geometry.area(x) end)
|> List.foldr(1, fn(x, acc) -> cond do
x < acc -> x
true -> acc
end
end)
case {t, retries >= length(poly) - 1} do
{0.0, false} -> split(rotate_list(poly), retries + 1) #split failed, try another vertex
_ -> r
end
end
end
defp rotate_list(list) do
list
|> Stream.with_index
|> Enum.map(fn(x) ->
{_, index} = x
Enum.at(list, rem((index + 1), length(list)))
end)
end
end