Packages

Partition a list of GeoJSON MultiPolygon features until all polygons are below a given area bound

Current section

Files

Jump to
poly_partition lib Geometry.ex
Raw

lib/Geometry.ex

defmodule PolyPartition.Geometry do
alias PolyPartition.Helpers
@moduledoc """
Geometry functions for PolyPartition
"""
@doc """
Calculate the squared length of a segment
Returns float regardless of input
## Examples
iex> PolyPartition.Geometry.sq_length([[0,0], [1,1]])
2.0
"""
def sq_length(seg) do
[[x1, y1], [x2, y2]] = seg
:math.pow((x2 - x1), 2) + :math.pow((y2 - y1), 2)
end
@doc """
Finds the midpoint of a segment
Returns float regardless of input
## Examples
iex> PolyPartition.Geometry.midpoint([[0,0], [4,4]])
[2.0, 2.0]
"""
def midpoint(seg) do
[[x1, y1], [x2, y2]] = seg
[(x1 + x2) / 2, (y1 + y2) / 2]
end
@doc """
Calculate the slope of a line through two given points
Returns float if slope is defined, returns "vert" if line is degenerate or vertical.
## Examples
iex> PolyPartition.Geometry.slope([0,0], [4,4])
1.0
iex> PolyPartition.Geometry.slope([0,0], [4,0])
0.0
iex> PolyPartition.Geometry.slope([0,0], [0,4])
"vert"
iex> PolyPartition.Geometry.slope([0,0], [0,0])
"vert"
"""
def slope(point1, point2) do
[x1, y1] = point1
[x2, y2] = point2
case x2 - x1 do
0 -> "vert"
0.0 -> "vert"
_ -> (y2 - y1) / (x2 - x1)
end
end
defp deg_to_rad(deg) do
deg * 2 * :math.pi / 360
end
@doc """
Approximate (miles/degree)^2 of longitude for a given latitude (in radians)
## Examples
iex> PolyPartition.Geometry.lat_factor(1.4)
1629.2417131835887
"""
def lat_factor(lat_rad) do
:math.pow(((69.0 * :math.cos(lat_rad)) + 69.0) / 2.0, 2)
end
@doc """
Approximate area (square miles) of polygon (long/lat)
## Examples
iex> poly = [ [ -106.19590759277344, 39.35182131761711 ], [ -106.30714416503905, 39.26894242038886 ], [ -106.08879089355467, 39.25671479372372 ] ]
iex> PolyPartition.Geometry.area(poly)
36.41104908437559
"""
def area(poly) do
factor = poly
|> hd
|> List.last
|> deg_to_rad
|> lat_factor
get_segments(poly)
|> Enum.map(fn(x) -> Helpers.det_seg(x) end)
|> List.foldr(0, fn(x, acc) -> x + acc end)
|> Kernel./(2.0)
|> abs
|> Kernel.*(factor)
end
defp rotate90(point) do
[x, y] = point
[-y, x]
end
defp rotate90_seg(seg) do
Enum.map(seg, fn(x) -> rotate90(x) end)
end
@doc """
Determine which side of a line a given point is on
A line is determined by the `sample` point and slope `m`. Two `given` points that
are on opposite sides of this line will yield numbers with opposite signs from
`point_score`.
## Examples
iex> PolyPartition.Geometry.point_score([0,1], [1,1], 1)
1
iex> PolyPartition.Geometry.point_score([1,0], [1,1], 1)
-1
"""
def point_score(given, sample, m) do
[h, k] = sample
[x, y] = given
case m do
"vert" -> h - x
_ -> y - (m * x) - k + (m * h)
end
end
@doc """
Returns a list of segments representing the sides of the polygon
## Examples
iex> PolyPartition.Geometry.get_segments([[0,1], [0,0], [1,0]])
[[[0,1], [0,0]], [[0,0], [1,0]], [[1,0], [0,1]]]
"""
def get_segments(poly) do
poly ++ [hd(poly)]
|> Stream.with_index
|> Enum.map(fn(x) ->
{point, index} = x
cond do
index != 0 -> [Enum.at(poly, index - 1), point]
true -> nil
end
end)
|> List.delete(nil)
end
@doc """
Determine if two segments perpendicular to the axes intersect
## Examples
iex> PolyPartition.Geometry.perp_intersect?([[0,0], [0,1]], [[-1,0.5], [1,0.5]])
true
iex> PolyPartition.Geometry.perp_intersect?([[0,0], [0,1]], [[-1,1.5], [1,1.5]])
false
"""
def perp_intersect?(seg1, seg2) do
[[x11, y11], [x12, y12]] = seg1
[[x21, y21], [x22, _]] = seg2
cond do
x11 != x12 -> perp_intersect?(seg2, seg1)
true ->
horiz = (x21 - x11) * (x22 - x11)
vert = (y11 - y21) * (y12 - y21)
!(horiz >= 0 || vert >= 0)
end
end
@doc """
Determine if two segments share an endpoint
## Examples
iex> PolyPartition.Geometry.share_endpoint?([[1,0], [0,0]], [[1,0], [1,1]])
true
iex> PolyPartition.Geometry.share_endpoint?([[1,0], [0,0]], [[5,0], [1,1]])
false
"""
def share_endpoint?(seg1, seg2) do
[p11, p12] = seg1
[p21, p22] = seg2
p11 == p21 ||
p11 == p22 ||
p12 == p21 ||
p12 == p22
end
defp one_side_intersect?(seg1, seg2) do
cond do
share_endpoint?(seg1, seg2) -> false
true ->
[p11, p12] = seg1
[p21, p22] = seg2
m = slope(p21, p22)
k1 = point_score(p11, p21, m)
k2 = point_score(p12, p22, m)
Helpers.sgn_to_bool(k1, k2)
end
end
@doc """
Determine if two segments non-trivially (i.e., excluding endpoints) intersect
## Examples
iex> PolyPartition.Geometry.intersect?([[0,0], [1,1]], [[0,1],[1,0]])
true
iex> PolyPartition.Geometry.intersect?([[0,1], [1,1]], [[0,1],[1,0]])
false
iex> PolyPartition.Geometry.intersect?([[0,1], [1,1]], [[0,0],[1,0]])
false
"""
def intersect?(seg1, seg2) do
one_side_intersect?(seg1, seg2) && one_side_intersect?(seg2, seg1)
end
@doc """
Determine if a segment intersects a side of the polygon.
Determine if a segment non-trivially (i.e., excluding endpoints) intersects a side
of the given polygon _excepting the sides incident to the first vertex_.
## Examples
iex> poly = [[0,1], [1,1], [1,0], [0,0]]
iex> PolyPartition.Geometry.intersect_side?(poly, [[0.5,0.5], [1.5,0.5]])
true
iex> poly = [[0,1], [1,1], [1,0], [0,0]]
iex> PolyPartition.Geometry.intersect_side?(poly, [[1.5,1.5], [5.5,1.5]])
false
"""
def intersect_side?(poly, seg) do
poly
|> get_segments
|> Enum.slice(1..length(poly) - 1)
|> Enum.map(fn(x) -> intersect?(seg, x) end)
|> List.foldl(false, fn(x, acc) -> x || acc end)
end
@doc """
Determine if a segment is a valid partition boundary in the polygon
Given `opp_index`, determine if the segment from the first vertex to the
vertex at `opp_index` forms a valid partition boundary.
iex> poly = [[0,1], [1, 0], [2, 0], [3,1], [2,2], [2,0.5]]
iex> PolyPartition.Geometry.good_cut?(poly, 2)
true
iex> poly = [[0,1], [1, 0], [2, 0], [3,1], [2,2], [2,0.5]]
iex> PolyPartition.Geometry.good_cut?(poly, 3)
false
"""
def good_cut?(poly, opp_index) do
new1 = [hd(poly), Enum.at(poly, opp_index)] ++ Enum.slice(poly, (opp_index + 1)..length(poly))
new2 = Enum.slice(poly, 0..opp_index - 1) ++ [Enum.at(poly, opp_index)]
cond do
opp_index == 1 || opp_index == length(poly) - 1 -> false
intersect_side?(poly, [hd(poly), Enum.at(poly, opp_index)]) -> false
area(new1) > area(poly) || area(new2) > area(poly) -> false
true -> true
end
end
end