153 lines
5.1 KiB
Elixir
153 lines
5.1 KiB
Elixir
defmodule AprsmeWeb.Live.Shared.BoundsUtils do
|
|
@moduledoc """
|
|
Shared bounds validation, comparison, and filtering utilities.
|
|
Used across multiple LiveView modules for consistent bounds handling.
|
|
"""
|
|
|
|
alias AprsmeWeb.Live.Shared.CoordinateUtils
|
|
|
|
@doc """
|
|
Validate bounds to prevent invalid coordinates.
|
|
"""
|
|
@spec valid_bounds?(map()) :: boolean()
|
|
def valid_bounds?(%{north: north, south: south, east: east, west: west})
|
|
when is_number(north) and is_number(south) and is_number(east) and is_number(west) and north <= 90 and
|
|
south >= -90 and north > south and east >= -180 and west <= 180, do: true
|
|
|
|
def valid_bounds?(_invalid_bounds), do: false
|
|
|
|
@doc """
|
|
Compare two bounds maps for equality (with rounding for floating point comparison).
|
|
"""
|
|
@spec compare_bounds(map() | nil, map() | nil) :: boolean()
|
|
def compare_bounds(nil, nil), do: true
|
|
def compare_bounds(nil, _), do: false
|
|
def compare_bounds(_, nil), do: false
|
|
def compare_bounds(b1, b2), do: compare_bounds_maps(b1, b2)
|
|
|
|
@doc """
|
|
Check if a packet is within the specified bounds.
|
|
"""
|
|
@spec within_bounds?(map() | struct() | tuple(), map()) :: boolean()
|
|
def within_bounds?({lat, lon}, bounds) when is_number(lat) and is_number(lon) do
|
|
check_coordinate_bounds(lat, lon, bounds)
|
|
end
|
|
|
|
def within_bounds?(packet, bounds) do
|
|
{lat, lon, _data_extended} = CoordinateUtils.get_coordinates(packet)
|
|
check_coordinate_bounds(lat, lon, bounds)
|
|
end
|
|
|
|
@doc """
|
|
Filter packets by bounds - works with both maps and lists.
|
|
"""
|
|
@spec filter_packets_by_bounds(map() | list(), map()) :: map() | list()
|
|
def filter_packets_by_bounds(packets_map, bounds) when is_map(packets_map) do
|
|
packets_map
|
|
|> Enum.filter(fn {_k, packet} -> within_bounds?(packet, bounds) end)
|
|
|> Map.new()
|
|
end
|
|
|
|
def filter_packets_by_bounds(packets_list, bounds) when is_list(packets_list) do
|
|
Enum.filter(packets_list, &within_bounds?(&1, bounds))
|
|
end
|
|
|
|
@doc """
|
|
Get packet keys that are outside the specified bounds.
|
|
"""
|
|
@spec reject_packets_by_bounds(map(), map()) :: list()
|
|
def reject_packets_by_bounds(packets_map, bounds) when is_map(packets_map) do
|
|
packets_map
|
|
|> Enum.reject(fn {_k, packet} -> within_bounds?(packet, bounds) end)
|
|
|> Enum.map(fn {k, _} -> k end)
|
|
end
|
|
|
|
@doc """
|
|
Normalize map bounds from string keys to atom keys and convert values to floats.
|
|
"""
|
|
@spec normalize_bounds(map()) :: map() | nil
|
|
def normalize_bounds(%{"north" => n, "south" => s, "east" => e, "west" => w}) do
|
|
%{
|
|
north: to_float(n),
|
|
south: to_float(s),
|
|
east: to_float(e),
|
|
west: to_float(w)
|
|
}
|
|
end
|
|
|
|
def normalize_bounds(%{north: _, south: _, east: _, west: _} = bounds) do
|
|
# Already normalized with atom keys
|
|
bounds
|
|
end
|
|
|
|
def normalize_bounds(_), do: nil
|
|
|
|
@doc """
|
|
Calculate approximate bounds based on center point and zoom level.
|
|
"""
|
|
@spec calculate_bounds_from_center_and_zoom(map(), integer()) :: map()
|
|
def calculate_bounds_from_center_and_zoom(center, zoom) do
|
|
# Approximate degrees per pixel at different zoom levels
|
|
degrees_per_pixel = calculate_degrees_per_pixel(zoom)
|
|
|
|
# Assume viewport is roughly 800x600 pixels
|
|
# Half of 600px height
|
|
lat_offset = degrees_per_pixel * 300
|
|
# Half of 800px width
|
|
lng_offset = degrees_per_pixel * 400
|
|
|
|
%{
|
|
north: center.lat + lat_offset,
|
|
south: center.lat - lat_offset,
|
|
east: center.lng + lng_offset,
|
|
west: center.lng - lng_offset
|
|
}
|
|
end
|
|
|
|
# Private helper functions
|
|
|
|
defp compare_bounds_maps(b1, b2) do
|
|
Enum.all?([:north, :south, :east, :west], fn key ->
|
|
round_to_4_places(Map.get(b1, key)) == round_to_4_places(Map.get(b2, key))
|
|
end)
|
|
end
|
|
|
|
defp check_coordinate_bounds(nil, _, _), do: false
|
|
defp check_coordinate_bounds(_, nil, _), do: false
|
|
|
|
defp check_coordinate_bounds(lat, lon, bounds) do
|
|
# Check latitude bounds (straightforward)
|
|
lat_in_bounds = lat >= bounds.south && lat <= bounds.north
|
|
|
|
# Check longitude bounds (handle potential wrapping)
|
|
lng_in_bounds = check_longitude_bounds(lon, bounds.west, bounds.east)
|
|
|
|
lat_in_bounds && lng_in_bounds
|
|
end
|
|
|
|
defp check_longitude_bounds(lon, west, east) when west <= east do
|
|
# Normal case: bounds don't cross antimeridian
|
|
lon >= west && lon <= east
|
|
end
|
|
|
|
defp check_longitude_bounds(lon, west, east) do
|
|
# Bounds cross antimeridian (e.g., west=170, east=-170)
|
|
lon >= west || lon <= east
|
|
end
|
|
|
|
defp calculate_degrees_per_pixel(zoom) when zoom >= 15, do: 0.000005
|
|
defp calculate_degrees_per_pixel(zoom) when zoom >= 12, do: 0.00005
|
|
defp calculate_degrees_per_pixel(zoom) when zoom >= 10, do: 0.0002
|
|
defp calculate_degrees_per_pixel(zoom) when zoom >= 8, do: 0.001
|
|
defp calculate_degrees_per_pixel(zoom) when zoom >= 6, do: 0.005
|
|
defp calculate_degrees_per_pixel(zoom) when zoom >= 4, do: 0.02
|
|
defp calculate_degrees_per_pixel(_), do: 0.1
|
|
|
|
defp round_to_4_places(n) when is_float(n), do: Float.round(n, 4)
|
|
defp round_to_4_places(n) when is_integer(n), do: n * 1.0
|
|
defp round_to_4_places(x), do: x
|
|
|
|
defp to_float(value) do
|
|
Aprsme.EncodingUtils.to_float(value) || 0.0
|
|
end
|
|
end
|