Current section
Files
Jump to
Current section
Files
lib/geo_stream_data.ex
defmodule GeoStreamData do
@moduledoc """
A generator for property-based testing of geospatial data.
"""
# use ExUnitProperties
@type geometry_type() ::
Geo.Point.t()
| Geo.LineString.t()
| Geo.Polygon.t()
| Geo.MultiPoint.t()
| Geo.MultiLineString.t()
| Geo.MultiPolygon.t()
@geometry_types ~w(point line_string polygon multi_point multi_line_string multi_polygon)a
@spec geometry() :: StreamData.t(geometry_type())
def geometry() do
StreamData.bind(StreamData.member_of(@geometry_types), fn
:point -> point()
:line_string -> line_string()
:polygon -> polygon()
:multi_point -> multi_point()
:multi_line_string -> multi_line_string()
:multi_polygon -> multi_polygon()
end)
end
@spec point_tuple() :: StreamData.t({number(), number()})
def point_tuple() do
StreamData.bind(StreamData.float(min: -180, max: 180), fn x ->
StreamData.bind(StreamData.float(min: -90, max: 90), fn y ->
StreamData.constant({x, y})
end)
end)
end
@spec point() :: StreamData.t(Geo.Point.t())
def point() do
StreamData.bind(point_tuple(), fn point ->
%Geo.Point{coordinates: point} |> StreamData.constant()
end)
end
@spec line_string() :: StreamData.t(Geo.LineString.t())
def line_string() do
StreamData.uniq_list_of(point_tuple(), min_length: 3)
|> StreamData.bind_filter(fn points ->
sorted = points |> sort_radially() |> shift_random() |> reverse_sometimes()
line_string = %Geo.LineString{coordinates: sorted}
if __MODULE__.Utils.simple?(line_string) do
{:cont, StreamData.constant(line_string)}
else
:skip
end
end)
end
@spec polygon() :: StreamData.t(Geo.Polygon.t())
def polygon() do
StreamData.uniq_list_of(point_tuple(), min_length: 3)
|> StreamData.bind_filter(fn points ->
sorted = points |> sort_radially() |> shift_random()
polygon = %Geo.Polygon{coordinates: [sorted ++ Enum.take(sorted, 1)]}
if __MODULE__.Utils.simple?(polygon) do
{:cont, StreamData.constant(polygon)}
else
:skip
end
end)
end
@spec multi_point() :: StreamData.t(Geo.MultiPoint.t())
def multi_point() do
StreamData.list_of(point_tuple(), min_length: 1)
|> StreamData.bind(fn points ->
%Geo.MultiPoint{coordinates: points}
|> StreamData.constant()
end)
end
@spec multi_line_string() :: StreamData.t(Geo.MultiLineString.t())
def multi_line_string() do
StreamData.list_of(line_string(), min_length: 1)
|> StreamData.bind(fn line_strings ->
%Geo.MultiLineString{coordinates: Enum.map(line_strings, & &1.coordinates)}
|> StreamData.constant()
end)
end
@spec multi_polygon() :: StreamData.t(Geo.MultiPolygon.t())
def multi_polygon() do
StreamData.list_of(polygon(), min_length: 1)
|> StreamData.bind(fn polygons ->
%Geo.MultiPolygon{coordinates: Enum.map(polygons, & &1.coordinates)}
|> StreamData.constant()
end)
end
defp sort_radially(points) do
{min_x, max_x} = Enum.map(points, &elem(&1, 0)) |> Enum.min_max()
{min_y, max_y} = Enum.map(points, &elem(&1, 1)) |> Enum.min_max()
center = {(min_x + max_x) / 2, (min_y + max_y) / 2}
points |> Enum.sort_by(&angle(&1, center))
end
def angle({x, y}, {cx, cy}), do: {do_angle(cx - x, cy - y), cy - y}
defp do_angle(dx, dy) when dx == 0 and dy < 0, do: 3 * :math.pi() / 2.0
defp do_angle(dx, _) when dx == 0, do: :math.pi() / 2.0
defp do_angle(dx, dy) when dy == 0 and dx < 0, do: :math.pi()
defp do_angle(_, dy) when dy == 0, do: 0.0
defp do_angle(dx, dy) when dx < 0, do: :math.pi() + :math.atan(dy / dx)
defp do_angle(dx, dy), do: :math.atan(dy / dx)
defp reverse_sometimes(points) do
if :rand.uniform() > 0.5 do
points
else
points |> Enum.reverse()
end
end
defp shift_random(points) do
points
|> Enum.split(:rand.uniform(length(points)))
|> Tuple.to_list()
|> Enum.reverse()
|> List.flatten()
end
end