122 lines
3.3 KiB
Elixir
122 lines
3.3 KiB
Elixir
defmodule MoundHunters.Boundary do
|
|
@moduledoc """
|
|
Ohio state boundary checking using point-in-polygon algorithm.
|
|
"""
|
|
|
|
# Simplified Ohio bounding box for fast preliminary check
|
|
# More precise polygon would be loaded from GeoJSON
|
|
@ohio_bbox %{
|
|
min_lat: 38.403,
|
|
max_lat: 42.327,
|
|
min_lng: -84.820,
|
|
max_lng: -80.519
|
|
}
|
|
|
|
# Ohio state boundary polygon (simplified)
|
|
# Source: Github -> PublicaMundi/MappingAPI us-states.json
|
|
# Coordinates are [lng, lat] pairs per GeoJSON spec
|
|
@ohio_polygon [
|
|
{-80.518598, 41.978802},
|
|
{-80.518598, 40.636951},
|
|
{-80.666475, 40.582182},
|
|
{-80.595275, 40.472643},
|
|
{-80.600752, 40.319289},
|
|
{-80.737675, 40.078303},
|
|
{-80.830783, 39.711348},
|
|
{-81.219646, 39.388209},
|
|
{-81.345616, 39.344393},
|
|
{-81.455155, 39.410117},
|
|
{-81.570170, 39.267716},
|
|
{-81.685186, 39.273193},
|
|
{-81.811156, 39.081500},
|
|
{-81.783771, 38.966484},
|
|
{-81.887833, 38.873376},
|
|
{-82.035710, 39.026731},
|
|
{-82.221926, 38.785745},
|
|
{-82.172634, 38.632391},
|
|
{-82.293127, 38.577622},
|
|
{-82.331465, 38.446175},
|
|
{-82.594358, 38.424267},
|
|
{-82.731282, 38.561191},
|
|
{-82.846298, 38.588575},
|
|
{-82.890113, 38.758361},
|
|
{-83.032514, 38.725499},
|
|
{-83.142052, 38.626914},
|
|
{-83.519961, 38.703591},
|
|
{-83.678792, 38.632391},
|
|
{-83.903347, 38.769315},
|
|
{-84.215533, 38.807653},
|
|
{-84.231963, 38.895284},
|
|
{-84.434610, 39.103408},
|
|
{-84.817996, 39.103408},
|
|
{-84.801565, 40.500028},
|
|
{-84.807042, 41.694001},
|
|
{-83.454238, 41.732339},
|
|
{-83.065375, 41.595416},
|
|
{-82.933929, 41.513262},
|
|
{-82.835344, 41.589939},
|
|
{-82.616266, 41.431108},
|
|
{-82.479343, 41.381815},
|
|
{-82.013803, 41.513262},
|
|
{-81.739956, 41.485877},
|
|
{-81.444201, 41.672093},
|
|
{-81.011523, 41.852832},
|
|
{-80.518598, 41.978802},
|
|
{-80.518598, 41.978802}
|
|
]
|
|
|
|
@doc """
|
|
Check if coordinates are within Ohio boundaries.
|
|
Returns :ok or {:error, reason}
|
|
"""
|
|
def check_bounds(lat, lng) when is_number(lat) and is_number(lng) do
|
|
cond do
|
|
not in_bounding_box?(lat, lng) ->
|
|
{:error, "Coordinates outside Ohio bounding box"}
|
|
|
|
not in_polygon?(lng, lat, @ohio_polygon) ->
|
|
{:error, "Coordinates outside Ohio boundary"}
|
|
|
|
true ->
|
|
:ok
|
|
end
|
|
end
|
|
|
|
def check_bounds(_lat, _lng) do
|
|
{:error, "Invalid coordinates"}
|
|
end
|
|
|
|
defp in_bounding_box?(lat, lng) do
|
|
lat >= @ohio_bbox.min_lat and lat <= @ohio_bbox.max_lat and
|
|
lng >= @ohio_bbox.min_lng and lng <= @ohio_bbox.max_lng
|
|
end
|
|
|
|
# Ray casting algorithm for point-in-polygon test
|
|
# Returns true if point (x, y) is inside the polygon
|
|
defp in_polygon?(x, y, polygon) do
|
|
n = length(polygon)
|
|
|
|
polygon
|
|
|> Enum.with_index()
|
|
|> Enum.reduce(false, fn {{xi, yi}, i}, inside ->
|
|
j = rem(i + n - 1, n)
|
|
{xj, yj} = Enum.at(polygon, j)
|
|
|
|
intersects =
|
|
yi > y != yj > y and
|
|
x < (xj - xi) * (y - yi) / (yj - yi) + xi
|
|
|
|
if intersects, do: not inside, else: inside
|
|
end)
|
|
end
|
|
|
|
@doc """
|
|
Format coordinates to 6 decimal places for consistent lookups.
|
|
"""
|
|
def format_lookup_id(lat, lng) do
|
|
lat_str = :erlang.float_to_binary(lat / 1.0, decimals: 6)
|
|
lng_str = :erlang.float_to_binary(lng / 1.0, decimals: 6)
|
|
"#{lat_str},#{lng_str}"
|
|
end
|
|
end
|