Current section
Files
Jump to
Current section
Files
lib/envelope.ex
defmodule Envelope do
@moduledoc ~S"""
A library for calculating envelopes of geometries and tools to compare them.
This is most useful as an approximation of spacial relationships between more
complicated geometries.
iex> Envelope.from_geo( %Geo.Polygon{coordinates: [[{2, -2}, {20, -2}, {11, 11}, {2, -2}]]} )
%Envelope{ min_x: 2, min_y: -2, max_x: 20, max_y: 11 }
iex> Envelope.from_geo( %Geo.LineString{coordinates: [{1, 3}, {2, -1}, {0, -1}, {1, 3}]} )
%Envelope{ min_x: 0, min_y: -1, max_x: 2, max_y: 3 }
You can also expand an existing Envelope with a geometry or another Envelope
iex> a = Envelope.from_geo( %Geo.Polygon{coordinates: [[{2, -2}, {20, -2}, {11, 11}, {2, -2}]]} )
...> b = %Geo.LineString{coordinates: [{1, 3}, {2, -1}, {0, -1}, {1, 3}]}
...> Envelope.expand(a, b)
%Envelope{ min_x: 0, min_y: -2, max_x: 20, max_y: 11 }
"""
defstruct min_x: 0, min_y: 0, max_x: 0, max_y: 0
@type point :: {number, number}
@type points ::
point
| list
| %{coordinates: list}
| %Geo.Point{}
| %Geo.MultiPoint{}
| %Geo.LineString{}
| %Geo.MultiLineString{}
| %Geo.Polygon{}
| %Geo.MultiPolygon{}
alias Distance.GreatCircle
@doc ~S"""
Returns an `Envelope` that represents the extent of the geometry or
coordinates.
## Examples
iex> Envelope.from_geo %{coordinates: [{11, 10}, {4, 2.5}, {16, 2.5}, {11, 10}]}
%Envelope{ max_x: 16, max_y: 10, min_x: 4, min_y: 2.5 }
iex> Envelope.from_geo [{11, 10}, {4, 2.5}, {16, 2.5}, {11, 10}]
%Envelope{ max_x: 16, max_y: 10, min_x: 4, min_y: 2.5 }
iex> Envelope.from_geo %Geo.Polygon{coordinates: [[{1, 3}, {2, -1}, {0, -1}, {1, 3}]]}
%Envelope{ min_x: 0, min_y: -1, max_x: 2, max_y: 3 }
iex> Envelope.from_geo {1, 3}
%Envelope{ min_x: 1, min_y: 3, max_x: 1, max_y: 3 }
"""
@spec from_geo(points) :: %Envelope{}
def from_geo({x, y}), do: %Envelope{min_x: x, min_y: y, max_x: x, max_y: y}
def from_geo(%Geo.Point{coordinates: {x, y}}),
do: %Envelope{min_x: x, min_y: y, max_x: x, max_y: y}
def from_geo(%{coordinates: coordinates}), do: from_geo(coordinates)
def from_geo(coordinates) when is_list(coordinates) do
coordinates
|> List.flatten()
|> Enum.reduce(Envelope.empty(), &expand(&2, &1))
end
@doc ~S"""
Returns an `Envelope` that represents no extent at all. This is primarily
a convenience function for starting an expanding Envelope. Internally,
"empty" Envelopes are represented with `nil` values for all extents.
Note that there is a important distinction between an empty Envelope and
an Envelope around a single Point (where the min and max for each axis are
real numbers but may represent zero area).
## Examples
iex> Envelope.empty
%Envelope{max_x: nil, max_y: nil, min_x: nil, min_y: nil}
iex> Envelope.empty |> Envelope.empty?
true
"""
@spec empty() :: %Envelope{}
def empty, do: %Envelope{min_x: nil, min_y: nil, max_x: nil, max_y: nil}
@doc ~S"""
Returns `true` if the given envelope is empty (has non-existent extent),
otherwise `false`
## Examples
iex> Envelope.empty |> Envelope.empty?
true
iex> %Envelope{ min_x: 0, min_y: -1, max_x: 2, max_y: 3 } |> Envelope.empty?
false
"""
@spec empty?(%Envelope{}) :: boolean
def empty?(%Envelope{min_x: nil, min_y: nil, max_x: nil, max_y: nil}), do: true
def empty?(%Envelope{}), do: false
@doc ~S"""
Returns a new Envelope that is expanded to include an additional geometry.
## Examples
iex> a = Envelope.from_geo(%Geo.Polygon{coordinates: [[{2, -2}, {20, -2}, {11, 11}, {2, -2}]]})
...> b = %Geo.LineString{coordinates: [{1, 3}, {2, -1}, {0, -1}, {1, 3}]}
...> Envelope.expand(a, b)
%Envelope{ min_x: 0, min_y: -2, max_x: 20, max_y: 11 }
iex> a = %Envelope{ min_x: 0, min_y: -2, max_x: 20, max_y: 11 }
...> b = %Envelope{ min_x: 2, min_y: -3, max_x: 12, max_y: -2 }
...> Envelope.expand(a, b)
%Envelope{ min_x: 0, min_y: -3, max_x: 20, max_y: 11 }
iex> Envelope.empty
...> |> Envelope.expand(%Envelope{ min_x: 0, min_y: -2, max_x: 12, max_y: 11 })
...> |> Envelope.expand(%Geo.Polygon{coordinates: [[{2, -2}, {20, -2}, {11, 11}, {2, -2}]]})
...> |> Envelope.expand(%{type: "Point", coordinates: {-1, 3}})
%Envelope{ min_x: -1, min_y: -2, max_x: 20, max_y: 11 }
iex> Envelope.expand(Envelope.empty, Envelope.empty) |> Envelope.empty?
true
"""
@spec expand(%Envelope{}, point | %Envelope{} | points) :: %Envelope{}
def expand(%Envelope{} = env1, %Envelope{} = env2) do
cond do
Envelope.empty?(env1) ->
env2
Envelope.empty?(env2) ->
env1
true ->
%Envelope{
min_x: min(env1.min_x, env2.min_x),
min_y: min(env1.min_y, env2.min_y),
max_x: max(env1.max_x, env2.max_x),
max_y: max(env1.max_y, env2.max_y)
}
end
end
def expand(%Envelope{} = env, other), do: expand(env, from_geo(other))
@doc ~S"""
Returns a new Envelope that is expanded in positive and negative directions
in each axis by `radius`.
## Examples
iex> Envelope.expand_by(Envelope.from_geo(%Geo.Polygon{coordinates: [[{2, -2}, {20, -2}, {11, 11}, {2, -2}]]}), 3)
%Envelope{ min_x: -1, min_y: -5, max_x: 23, max_y: 14 }
iex> Envelope.expand_by(Envelope.empty, 4) |> Envelope.empty?
true
"""
@spec expand_by(%Envelope{}, number) :: %Envelope{}
def expand_by(%Envelope{} = env, radius) when is_number(radius) and radius >= 0 do
case Envelope.empty?(env) do
true ->
env
false ->
%Envelope{
min_x: env.min_x - radius,
min_y: env.min_y - radius,
max_x: env.max_x + radius,
max_y: env.max_y + radius
}
end
end
@doc ~S"""
Simple distance from the left bounadary to the right boundary of the Envelope.
## Examples
iex> Envelope.width(Envelope.from_geo(%Geo.Polygon{coordinates: [[{2, -2}, {20, -2}, {11, 11}, {2, -2}]]}))
18
"""
@spec width(%Envelope{}) :: number
def width(%Envelope{} = env) do
env.max_x - env.min_x
end
@doc ~S"""
When an Envelope's coordinates are in degress of longitude and latitude, calculates the
great circle distance between the center of the east and west extent in meters.
## Examples
iex> Envelope.width_gc(Envelope.from_geo(%Geo.Polygon{coordinates: [[{2, -2}, {20, -2}, {11, 11}, {2, -2}]]})) |> round
1982362
"""
@spec width_gc(%Envelope{}) :: number
def width_gc(%Envelope{} = env) do
bottom = GreatCircle.distance({env.min_x, env.min_y}, {env.max_x, env.min_y})
top = GreatCircle.distance({env.min_x, env.max_y}, {env.max_x, env.max_y})
(bottom + top) / 2.0
end
@doc ~S"""
Simple distance from the bottom bounadary to the top boundary of the Envelope.
## Examples
iex> Envelope.height(Envelope.from_geo(%Geo.Polygon{coordinates: [[{2, -2}, {20, -2}, {11, 11}, {2, -2}]]}))
13
"""
@spec height(%Envelope{}) :: number
def height(%Envelope{} = env) do
env.max_y - env.min_y
end
@doc ~S"""
When an Envelope's coordinates are in degress of longitude and latitude, calculates the
great circle distance between the center of the north and south extent in meters.
## Examples
iex> Envelope.height_gc(Envelope.from_geo(%Geo.Polygon{coordinates: [[{2, -2}, {20, -2}, {11, 11}, {2, -2}]]})) |> round
1445536
"""
@spec height_gc(%Envelope{}) :: number
def height_gc(%Envelope{} = env) do
GreatCircle.distance({env.min_x, env.min_y}, {env.min_x, env.max_y})
end
@doc ~S"""
Calculates the simple area of an Envelope.
## Examples
iex> Envelope.area(Envelope.from_geo(%Geo.Polygon{coordinates: [[{2, -2}, {20, -2}, {11, 11}, {2, -2}]]}))
234
"""
@spec area(%Envelope{}) :: number
def area(%Envelope{} = env) do
width(env) * height(env)
end
@doc ~S"""
Estimates the area of an Envelope in square meters when the Envelope's coordinates are in degress of longitude and latitude.
## Examples
iex> Envelope.area_gc(Envelope.from_geo(%Geo.Polygon{coordinates: [[{2, -2}, {20, -2}, {11, 11}, {2, -2}]]})) |> round
2865575088701
"""
@spec area_gc(%Envelope{}) :: number
def area_gc(%Envelope{} = env) do
width_gc(env) * height_gc(env)
end
@doc ~S"""
Returns whether one envelope fully contains another envelope or point.
## Examples
iex> Envelope.contains?(
...> %Envelope{ min_x: -1, min_y: -5, max_x: 23, max_y: 14 },
...> %Envelope{ min_x: 0, min_y: 3, max_x: 7, max_y: 4 })
true
iex> Envelope.contains?(
...> %Envelope{ min_x: -1, min_y: 5, max_x: 23, max_y: 14 },
...> %Envelope{ min_x: -2, min_y: 5, max_x: 7, max_y: 4 })
false
iex> Envelope.contains?(
...> %Geo.Polygon{ coordinates: [{-1, 3}, {-3, -1}, { 5, -3}, {4, 12}, {-2, 11}, {-1, 3}] },
...> {0, 11})
true
"""
@spec contains?(%Envelope{} | points, %Envelope{} | points) :: boolean
def contains?(%Envelope{} = env, {x, y}) do
env.min_x <= x && env.min_y <= y && env.max_x >= x && env.max_y >= y
end
def contains?(%Envelope{} = env, %{coordinates: {x, y}}), do: contains?(env, {x, y})
def contains?(%Envelope{} = env1, %Envelope{} = env2) do
env1.min_x <= env2.min_x && env1.min_y <= env2.min_y && env1.max_x >= env2.max_x &&
env1.max_y >= env2.max_y
end
def contains?(%Envelope{} = env1, other), do: contains?(env1, from_geo(other))
def contains?(a, b), do: contains?(from_geo(a), b)
@doc ~S"""
The inverse of the relationship tested by Envelope#contains?
## Examples
iex> Envelope.within?(
...> %Envelope{ min_x: 0, min_y: 3, max_x: 7, max_y: 4 },
...> %Envelope{ min_x: -1, min_y: -5, max_x: 23, max_y: 14 })
true
iex> Envelope.within?(
...> %Geo.Polygon{ coordinates: [{-1, 3}, {-3, -1}, { 5, -3}, {4, 12}, {-2, 11}, {-1, 3}] },
...> {0, 11})
false
"""
@spec within?(%Envelope{} | points, %Envelope{} | points) :: boolean
def within?(a, b), do: contains?(b, a)
@doc ~S"""
Returns whether two envelopes touch or intersect.
## Examples
iex> Envelope.intersects?(
...> %Envelope{ min_x: -1, min_y: -5, max_x: 23, max_y: 14 },
...> %Envelope{ min_x: 0, min_y: 3, max_x: 7, max_y: 4 })
true
iex> Envelope.intersects?(
...> %Envelope{ min_x: -1, min_y: 5, max_x: 23, max_y: 14 },
...> %Envelope{ min_x: 0, min_y: -3, max_x: 7, max_y: 4 })
false
"""
@spec intersects?(%Envelope{} | points, %Envelope{} | points) :: boolean
def intersects?(%Envelope{} = env1, %Envelope{} = env2) do
cond do
env1.min_x > env2.max_x -> false
env1.max_x < env2.min_x -> false
env1.min_y > env2.max_y -> false
env1.max_y < env2.min_y -> false
true -> true
end
end
def intersects?(a, b), do: intersects?(from_geo(a), from_geo(b))
end