From 50544c8354ad6db4dd0411b782e02e75ae98801d Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Fri, 20 Feb 2026 13:38:47 -0600 Subject: [PATCH] Remove dead PostGIS code, deduplicate utilities, and simplify module hierarchy - Delete ~170 lines of commented-out PostGIS spatial query functions - Consolidate coordinate validation into CoordinateUtils - Replace private normalize_data_type with EncodingUtils delegation - Inline to_float/to_decimal wrappers at call sites - Fix has_weather_data? missing wind_gust field - Remove BoundsManager passthrough delegation module - Simplify MapLive.PacketUtils by removing pure-passthrough delegations --- lib/aprsme/packet.ex | 127 +----------------- lib/aprsme/packet_consumer.ex | 21 ++- lib/aprsme/packets.ex | 88 +----------- lib/aprsme_web/controllers/info_controller.ex | 2 +- lib/aprsme_web/live/info_live/show.ex | 12 +- .../live/map_live/bounds_manager.ex | 25 ---- .../live/map_live/display_manager.ex | 4 +- lib/aprsme_web/live/map_live/index.ex | 3 +- lib/aprsme_web/live/map_live/packet_utils.ex | 66 ++------- .../live/shared/coordinate_utils.ex | 21 +++ lib/aprsme_web/live/shared/packet_utils.ex | 14 +- .../live/weather_live/callsign_view.html.heex | 2 +- .../live/map_live/packet_utils_test.exs | 13 +- 13 files changed, 64 insertions(+), 334 deletions(-) delete mode 100644 lib/aprsme_web/live/map_live/bounds_manager.ex diff --git a/lib/aprsme/packet.ex b/lib/aprsme/packet.ex index bbfe8c2..b8eb6d6 100644 --- a/lib/aprsme/packet.ex +++ b/lib/aprsme/packet.ex @@ -6,6 +6,7 @@ defmodule Aprsme.Packet do alias Aprs.Types.MicE alias Aprsme.DataExtended + alias AprsmeWeb.Live.Shared.CoordinateUtils schema "packets" do field(:base_callsign, :string) @@ -217,37 +218,14 @@ defmodule Aprsme.Packet do end defp valid_coordinates?(lat, lon) do - lat = normalize_coordinate(lat) - lon = normalize_coordinate(lon) - - is_number(lat) && is_number(lon) && - lat >= -90 && lat <= 90 && - lon >= -180 && lon <= 180 + CoordinateUtils.valid_coordinates_any_type?(lat, lon) end - defp normalize_coordinate(%Decimal{} = decimal), do: Decimal.to_float(decimal) - defp normalize_coordinate(coord), do: coord - - # Convert atom data_type to string for storage - defp normalize_data_type(%{data_type: data_type} = attrs) when is_atom(data_type) do - %{attrs | data_type: to_string(data_type)} + defp normalize_coordinate(coord) do + CoordinateUtils.normalize_coordinate(coord) end - defp normalize_data_type(%{"data_type" => data_type} = attrs) when is_atom(data_type) do - %{attrs | "data_type" => to_string(data_type)} - end - - defp normalize_data_type(attrs) when is_map(attrs) do - case {Map.has_key?(attrs, :data_type), Map.get(attrs, :data_type)} do - {true, data_type} when is_atom(data_type) -> - %{attrs | data_type: to_string(data_type)} - - _ -> - attrs - end - end - - defp normalize_data_type(attrs), do: attrs + defp normalize_data_type(attrs), do: Aprsme.EncodingUtils.normalize_data_type(attrs) @doc """ Extracts additional data from the raw packet and data_extended structure @@ -613,88 +591,6 @@ defmodule Aprsme.Packet do defp maybe_put(map, _key, ""), do: map defp maybe_put(map, key, value), do: Map.put(map, key, value) - # @doc """ - # Spatial query functions for efficient location-based searches - # """ - # Temporarily commented out until PostGIS is properly configured - - # @doc """ - # Find packets within a given radius (in meters) of a point. - # """ - # def within_radius(query \\ __MODULE__, lat, lon, radius_meters) do - # point = %Geo.Point{coordinates: {lon, lat}, srid: 4326} - - # from p in query, - # where: st_dwithin_in_meters(p.location, ^point, ^radius_meters), - # where: not is_nil(p.location) - # end - - # @doc """ - # Find packets within a bounding box defined by southwest and northeast corners. - # """ - # def within_bbox(query \\ __MODULE__, sw_lat, sw_lon, ne_lat, ne_lon) do - # # Create a polygon representing the bounding box - # bbox = %Geo.Polygon{ - # coordinates: [[ - # {sw_lon, sw_lat}, - # {ne_lon, sw_lat}, - # {ne_lon, ne_lat}, - # {sw_lon, ne_lat}, - # {sw_lon, sw_lat} - # ]], - # srid: 4326 - # } - - # from p in query, - # where: st_within(p.location, ^bbox), - # where: not is_nil(p.location) - # end - - # @doc """ - # Find packets ordered by distance from a given point. - # """ - # def nearest_to(query \\ __MODULE__, lat, lon, limit \\ 100) do - # point = %Geo.Point{coordinates: {lon, lat}, srid: 4326} - - # from p in query, - # where: not is_nil(p.location), - # order_by: st_distance(p.location, ^point), - # limit: ^limit, - # select: %{p | distance: st_distance_in_meters(p.location, ^point)} - # end - - # @doc """ - # Find recent packets with location data within the last N hours. - # """ - # def recent_with_location(query \\ __MODULE__, hours_back \\ 24) do - # cutoff_time = DateTime.utc_now() |> DateTime.add(-hours_back, :hour) - - # from p in query, - # where: p.has_position == true, - # where: not is_nil(p.location), - # where: p.received_at > ^cutoff_time, - # order_by: [desc: p.received_at] - # end - - # @doc """ - # Get statistics for packets in a geographic area. - # """ - # def location_stats(query \\ __MODULE__, lat, lon, radius_meters) do - # point = %Geo.Point{coordinates: {lon, lat}, srid: 4326} - - # from p in query, - # where: st_dwithin_in_meters(p.location, ^point, ^radius_meters), - # where: not is_nil(p.location), - # group_by: p.base_callsign, - # select: %{ - # callsign: p.base_callsign, - # packet_count: count(p.id), - # latest_position: max(p.received_at), - # avg_lat: avg(fragment("ST_Y(?)", p.location)), - # avg_lon: avg(fragment("ST_X(?)", p.location)) - # } - # end - @doc """ Create a geometry point from lat/lon coordinates. """ @@ -732,19 +628,6 @@ defmodule Aprsme.Packet do def lon(%__MODULE__{location: %Geo.Point{coordinates: {lon, _lat}}}), do: lon def lon(_), do: nil - # @doc """ - # Calculate distance between two packets in meters. - # """ - # def distance_between(%__MODULE__{location: %Geo.Point{} = p1}, %__MODULE__{location: %Geo.Point{} = p2}) do - # Repo.one( - # from p in "packets", - # select: fragment("ST_Distance_Sphere(?, ?)", ^p1, ^p2), - # limit: 1 - # ) - # end - - # def distance_between(_, _), do: nil - # Clean comment by removing altitude and PHG data defp clean_comment(nil), do: nil diff --git a/lib/aprsme/packet_consumer.ex b/lib/aprsme/packet_consumer.ex index 03862c3..717ce63 100644 --- a/lib/aprsme/packet_consumer.ex +++ b/lib/aprsme/packet_consumer.ex @@ -9,6 +9,7 @@ defmodule Aprsme.PacketConsumer do alias Aprsme.Cluster.PacketDistributor alias Aprsme.LogSanitizer alias Aprsme.Repo + alias AprsmeWeb.Live.Shared.CoordinateUtils require Logger @@ -449,16 +450,12 @@ defmodule Aprsme.PacketConsumer do # Helper functions for coordinate validation and point creation defp valid_coordinates?(lat, lon) do - lat = normalize_coordinate(lat) - lon = normalize_coordinate(lon) - - is_number(lat) && is_number(lon) && - lat >= -90 && lat <= 90 && - lon >= -180 && lon <= 180 + CoordinateUtils.valid_coordinates_any_type?(lat, lon) end - defp normalize_coordinate(%Decimal{} = decimal), do: Decimal.to_float(decimal) - defp normalize_coordinate(coord), do: coord + defp normalize_coordinate(coord) do + CoordinateUtils.normalize_coordinate(coord) + end defp create_point(lat, lon) when (is_number(lat) or is_struct(lat, Decimal)) and (is_number(lon) or is_struct(lon, Decimal)) do @@ -615,7 +612,7 @@ defmodule Aprsme.PacketConsumer do defp extract_position(packet_data) do if not is_nil(packet_data[:lat]) and not is_nil(packet_data[:lon]) do - {to_float(packet_data.lat), to_float(packet_data.lon)} + {Aprsme.EncodingUtils.to_float(packet_data.lat), Aprsme.EncodingUtils.to_float(packet_data.lon)} else extract_position_from_data_extended(packet_data[:data_extended]) end @@ -640,7 +637,7 @@ defmodule Aprsme.PacketConsumer do defp has_standard_position?(_), do: false defp extract_standard_position(data_extended) when is_map(data_extended) and not is_struct(data_extended) do - {to_float(data_extended[:latitude]), to_float(data_extended[:longitude])} + {Aprsme.EncodingUtils.to_float(data_extended[:latitude]), Aprsme.EncodingUtils.to_float(data_extended[:longitude])} end defp extract_standard_position(_), do: {nil, nil} @@ -648,7 +645,7 @@ defmodule Aprsme.PacketConsumer do defp extract_position_from_data_extended_case(data_extended) do lat = extract_lat_from_ext_map(data_extended) lon = extract_lon_from_ext_map(data_extended) - {to_float(lat), to_float(lon)} + {Aprsme.EncodingUtils.to_float(lat), Aprsme.EncodingUtils.to_float(lon)} end defp extract_lat_from_ext_map(ext_map) do @@ -691,8 +688,6 @@ defmodule Aprsme.PacketConsumer do defp sanitize_packet_strings(value), do: Aprsme.EncodingUtils.sanitize_packet_strings(value) - defp to_float(value), do: Aprsme.EncodingUtils.to_float(value) - defp normalize_data_type(attrs), do: Aprsme.EncodingUtils.normalize_data_type(attrs) defp struct_to_map(%{__struct__: ParseError} = error) do diff --git a/lib/aprsme/packets.ex b/lib/aprsme/packets.ex index 644c4db..617fc64 100644 --- a/lib/aprsme/packets.ex +++ b/lib/aprsme/packets.ex @@ -151,7 +151,7 @@ defmodule Aprsme.Packets do defp to_decimal_safe(nil), do: {:error, :nil_value} defp to_decimal_safe(value) do - case to_decimal(value) do + case Aprsme.EncodingUtils.to_decimal(value) do nil -> {:error, :conversion_failed} decimal -> {:ok, decimal} end @@ -329,7 +329,7 @@ defmodule Aprsme.Packets do defp extract_position(packet_data) do # Check for lat/lon at top level if not is_nil(packet_data[:lat]) and not is_nil(packet_data[:lon]) do - {to_float(packet_data.lat), to_float(packet_data.lon)} + {Aprsme.EncodingUtils.to_float(packet_data.lat), Aprsme.EncodingUtils.to_float(packet_data.lon)} else extract_position_from_data_extended(packet_data[:data_extended]) end @@ -354,7 +354,7 @@ defmodule Aprsme.Packets do defp has_standard_position?(_), do: false defp extract_standard_position(data_extended) when is_map(data_extended) and not is_struct(data_extended) do - {to_float(data_extended[:latitude]), to_float(data_extended[:longitude])} + {Aprsme.EncodingUtils.to_float(data_extended[:latitude]), Aprsme.EncodingUtils.to_float(data_extended[:longitude])} end defp extract_standard_position(_), do: {nil, nil} @@ -761,12 +761,6 @@ defmodule Aprsme.Packets do {:ok, deleted_count} end - # Helper to convert various types to float - defp to_float(value), do: Aprsme.EncodingUtils.to_float(value) - - # Helper to convert various types to Decimal - defp to_decimal(value), do: Aprsme.EncodingUtils.to_decimal(value) - # Get packets from last hour only - used to initialize the map @spec get_last_hour_packets() :: [struct()] def get_last_hour_packets do @@ -780,82 +774,6 @@ defmodule Aprsme.Packets do [] end - # @doc """ - # Spatial query functions for efficient location-based searches using PostGIS - # """ - - # Temporarily commented out PostGIS functions until PostGIS is properly configured - - # @doc """ - # Find packets within a given radius (in meters) of a point. - # Uses PostGIS spatial indexes for efficient querying. - # """ - # def find_packets_within_radius(lat, lon, radius_meters, opts \\ %{}) do - # point = %Geo.Point{coordinates: {lon, lat}, srid: 4326} - - # base_query = - # Packet - # |> where([p], not is_nil(p.location)) - # |> where([p], st_dwithin_in_meters(p.location, ^point, ^radius_meters)) - # |> order_by([p], st_distance(p.location, ^point)) - - # base_query - # |> apply_common_filters(opts) - # |> maybe_limit(opts) - # |> Repo.all() - # end - - # Additional PostGIS functions commented out for now... - - # Helper functions for spatial queries - commented out for now - - # defp apply_common_filters(query, opts) do - # query - # |> filter_by_time_range(opts) - # |> filter_by_callsign(opts) - # |> filter_by_data_type(opts) - # end - - # defp filter_by_time_range(query, %{start_time: start_time, end_time: end_time}) do - # from p in query, - # where: p.received_at >= ^start_time and p.received_at <= ^end_time - # end - - # defp filter_by_time_range(query, %{start_time: start_time}) do - # from p in query, where: p.received_at >= ^start_time - # end - - # defp filter_by_time_range(query, %{end_time: end_time}) do - # from p in query, where: p.received_at <= ^end_time - # end - - # defp filter_by_time_range(query, %{hours_back: hours}) do - # cutoff_time = DateTime.utc_now() |> DateTime.add(-hours, :hour) - # from p in query, where: p.received_at >= ^cutoff_time - # end - - # defp filter_by_time_range(query, _), do: query - - # defp filter_by_data_type(query, %{data_type: data_type}) do - # from p in query, where: p.data_type == ^data_type - # end - - # defp filter_by_data_type(query, _), do: query - - # defp maybe_limit(query, %{limit: limit}) when is_integer(limit) and limit > 0 do - # from p in query, limit: ^limit - # end - - # defp maybe_limit(query, _), do: query - - # Calculate clustering distance based on zoom level - # Higher zoom levels need smaller clustering distances - # defp calculate_cluster_distance(zoom_level) when zoom_level >= 15, do: 100 # 100m - # defp calculate_cluster_distance(zoom_level) when zoom_level >= 12, do: 500 # 500m - # defp calculate_cluster_distance(zoom_level) when zoom_level >= 9, do: 2000 # 2km - # defp calculate_cluster_distance(zoom_level) when zoom_level >= 6, do: 10000 # 10km - # defp calculate_cluster_distance(_), do: 50000 # 50km - @doc """ Gets the most recent packet for a callsign regardless of type or age. This is used for API endpoints that need the latest packet from a source. diff --git a/lib/aprsme_web/controllers/info_controller.ex b/lib/aprsme_web/controllers/info_controller.ex index 5339246..c087506 100644 --- a/lib/aprsme_web/controllers/info_controller.ex +++ b/lib/aprsme_web/controllers/info_controller.ex @@ -2,7 +2,7 @@ defmodule AprsmeWeb.InfoController do use AprsmeWeb, :controller alias Aprsme.Packets - alias AprsmeWeb.MapLive.PacketUtils + alias AprsmeWeb.Live.Shared.PacketUtils @neighbor_radius_km 10 @neighbor_limit 10 diff --git a/lib/aprsme_web/live/info_live/show.ex b/lib/aprsme_web/live/info_live/show.ex index 47297a4..d617fc9 100644 --- a/lib/aprsme_web/live/info_live/show.ex +++ b/lib/aprsme_web/live/info_live/show.ex @@ -143,10 +143,10 @@ defmodule AprsmeWeb.InfoLive.Show do defp position_changed?(current_packet, new_packet) do # Compare lat/lon to see if position actually changed - current_lat = to_float_safe(current_packet.lat) - current_lon = to_float_safe(current_packet.lon) - new_lat = to_float_safe(new_packet.lat) - new_lon = to_float_safe(new_packet.lon) + current_lat = EncodingUtils.to_float(current_packet.lat) + current_lon = EncodingUtils.to_float(current_packet.lon) + new_lat = EncodingUtils.to_float(new_packet.lat) + new_lon = EncodingUtils.to_float(new_packet.lon) # If any coordinate is invalid, consider it a change case {current_lat, current_lon, new_lat, new_lon} do @@ -274,10 +274,6 @@ defmodule AprsmeWeb.InfoLive.Show do r * c end - # Safe float conversion that preserves nil for invalid coordinates - defp to_float_safe(value), do: EncodingUtils.to_float(value) - - # Legacy float conversion that defaults to 0.0 for backward compatibility defp to_float(value), do: EncodingUtils.to_float(value) || 0.0 defp format_timestamp_for_display(packet) do diff --git a/lib/aprsme_web/live/map_live/bounds_manager.ex b/lib/aprsme_web/live/map_live/bounds_manager.ex deleted file mode 100644 index 05ff15a..0000000 --- a/lib/aprsme_web/live/map_live/bounds_manager.ex +++ /dev/null @@ -1,25 +0,0 @@ -defmodule AprsmeWeb.MapLive.BoundsManager do - @moduledoc """ - Handles map bounds calculations, validation, and filtering operations. - """ - - alias AprsmeWeb.Live.Shared.BoundsUtils - alias AprsmeWeb.Live.Shared.PacketUtils - - # Delegate to shared utilities - defdelegate calculate_bounds_from_center_and_zoom(center, zoom), to: BoundsUtils - defdelegate valid_bounds?(map_bounds), to: BoundsUtils - defdelegate compare_bounds(b1, b2), to: BoundsUtils - defdelegate within_bounds?(packet, bounds), to: BoundsUtils - defdelegate filter_packets_by_bounds(packets, bounds), to: BoundsUtils - defdelegate reject_packets_by_bounds(packets_map, bounds), to: BoundsUtils - - @doc """ - Filter packets by both time threshold and bounds. - """ - @spec filter_packets_by_time_and_bounds(map(), map(), DateTime.t()) :: map() - def filter_packets_by_time_and_bounds(packets, bounds, time_threshold) do - # Use shared packet utils for this functionality - PacketUtils.filter_packets_by_time_and_bounds(packets, bounds, time_threshold) - end -end diff --git a/lib/aprsme_web/live/map_live/display_manager.ex b/lib/aprsme_web/live/map_live/display_manager.ex index 08f8e88..482a8b5 100644 --- a/lib/aprsme_web/live/map_live/display_manager.ex +++ b/lib/aprsme_web/live/map_live/display_manager.ex @@ -4,7 +4,7 @@ defmodule AprsmeWeb.MapLive.DisplayManager do """ alias Aprsme.Packets.Clustering - alias AprsmeWeb.MapLive.BoundsManager + alias AprsmeWeb.Live.Shared.BoundsUtils alias AprsmeWeb.MapLive.DataBuilder alias Phoenix.LiveView alias Phoenix.LiveView.Socket @@ -50,7 +50,7 @@ defmodule AprsmeWeb.MapLive.DisplayManager do # Filter by bounds filtered_packets = all_packets - |> BoundsManager.filter_packets_by_bounds(socket.assigns.map_bounds) + |> BoundsUtils.filter_packets_by_bounds(socket.assigns.map_bounds) |> Enum.uniq_by(fn packet -> Map.get(packet, :id) || Map.get(packet, "id") end) diff --git a/lib/aprsme_web/live/map_live/index.ex b/lib/aprsme_web/live/map_live/index.ex index c4a5da5..e967a1a 100644 --- a/lib/aprsme_web/live/map_live/index.ex +++ b/lib/aprsme_web/live/map_live/index.ex @@ -18,7 +18,6 @@ defmodule AprsmeWeb.MapLive.Index do alias AprsmeWeb.Live.Shared.CoordinateUtils alias AprsmeWeb.Live.Shared.PacketUtils, as: SharedPacketUtils alias AprsmeWeb.Live.Shared.ParamUtils - alias AprsmeWeb.MapLive.BoundsManager alias AprsmeWeb.MapLive.DataBuilder alias AprsmeWeb.MapLive.DisplayManager alias AprsmeWeb.MapLive.HistoricalLoader @@ -83,7 +82,7 @@ defmodule AprsmeWeb.MapLive.Index do ) # Calculate initial bounds - initial_bounds = BoundsManager.calculate_bounds_from_center_and_zoom(final_map_center, final_map_zoom) + initial_bounds = BoundsUtils.calculate_bounds_from_center_and_zoom(final_map_center, final_map_zoom) # Final socket assignment {:ok, diff --git a/lib/aprsme_web/live/map_live/packet_utils.ex b/lib/aprsme_web/live/map_live/packet_utils.ex index a375410..fd88851 100644 --- a/lib/aprsme_web/live/map_live/packet_utils.ex +++ b/lib/aprsme_web/live/map_live/packet_utils.ex @@ -1,66 +1,22 @@ defmodule AprsmeWeb.MapLive.PacketUtils do @moduledoc """ - Shared utilities for extracting and processing packet data in map views. + Map-specific packet utilities. Provides weather-check queries against the + database and a convenience wrapper for weather field display. + + For general packet field access, symbol info, timestamps, callsign + generation, and weather-packet detection, use + `AprsmeWeb.Live.Shared.PacketUtils` directly. + + For building marker/popup data, use `AprsmeWeb.MapLive.DataBuilder` directly. """ alias AprsmeWeb.Live.Shared.PacketUtils, as: SharedPacketUtils - alias AprsmeWeb.Live.Shared.ParamUtils alias AprsmeWeb.MapLive.DataBuilder - @doc """ - Safely extracts a value from a packet or data_extended map with fallback support. - Checks both atom and string keys, and provides a default value. - """ - @spec get_packet_field(map(), atom() | String.t(), any()) :: any() - def get_packet_field(packet, field, default \\ nil) do - SharedPacketUtils.get_packet_field(packet, field, default) - end - - @doc """ - Extracts symbol information from a packet with fallbacks. - """ - @spec get_symbol_info(map()) :: {String.t(), String.t()} - def get_symbol_info(packet) do - SharedPacketUtils.get_symbol_info(packet) - end - - @doc """ - Extracts timestamp from a packet in ISO8601 format. - """ - @spec get_timestamp(map()) :: String.t() - def get_timestamp(packet) do - SharedPacketUtils.get_timestamp(packet) - end - - @doc """ - Converts various numeric types to float for consistent display. - """ - @spec to_float(any()) :: float() - def to_float(value) do - ParamUtils.to_float(value) - end - - @doc """ - Generates a callsign string with SSID if present. - """ - @spec generate_callsign(map()) :: String.t() - def generate_callsign(packet) do - SharedPacketUtils.generate_callsign(packet) - end - - @doc """ - Determines if a packet is a weather packet. - """ - @spec weather_packet?(map()) :: boolean() - def weather_packet?(packet) do - SharedPacketUtils.weather_packet?(packet) - end - @doc """ Checks if a station has sent weather packets by looking at recent packets. """ @spec has_weather_packets?(String.t()) :: boolean() - # Get recent packets for this callsign and check if any are weather packets def has_weather_packets?(callsign) when is_binary(callsign) do query = build_weather_check_query(callsign) Aprsme.Repo.exists?(query) @@ -95,7 +51,7 @@ defmodule AprsmeWeb.MapLive.PacketUtils do def convert_tuples_to_strings(other), do: other @doc """ - Extracts weather field data with fallback support. + Extracts weather field data with "N/A" fallback for display. """ @spec get_weather_field(map(), atom()) :: String.t() def get_weather_field(packet, key) do @@ -105,13 +61,11 @@ defmodule AprsmeWeb.MapLive.PacketUtils do end end - # These functions have been moved to AprsmeWeb.MapLive.DataBuilder + # Keep build_packet_data delegations for backward compatibility with tests defdelegate build_packet_data(packet, is_most_recent_for_callsign, locale), to: DataBuilder defdelegate build_packet_data(packet, is_most_recent_for_callsign), to: DataBuilder defdelegate build_packet_data(packet), to: DataBuilder - # All packet data building functions have been moved to AprsmeWeb.MapLive.DataBuilder - defp build_weather_check_query(callsign) do import Ecto.Query diff --git a/lib/aprsme_web/live/shared/coordinate_utils.ex b/lib/aprsme_web/live/shared/coordinate_utils.ex index 19f3fd1..57aa710 100644 --- a/lib/aprsme_web/live/shared/coordinate_utils.ex +++ b/lib/aprsme_web/live/shared/coordinate_utils.ex @@ -65,6 +65,27 @@ defmodule AprsmeWeb.Live.Shared.CoordinateUtils do def valid_coordinates?(_, _), do: false + @doc """ + Validate coordinates that may be Decimals, floats, or integers. + Normalizes Decimals to floats before checking range. + """ + @spec valid_coordinates_any_type?(any(), any()) :: boolean() + def valid_coordinates_any_type?(lat, lon) do + lat = normalize_coordinate(lat) + lon = normalize_coordinate(lon) + + is_number(lat) && is_number(lon) && + lat >= -90 && lat <= 90 && + lon >= -180 && lon <= 180 + end + + @doc """ + Normalize a coordinate value, converting Decimals to floats. + """ + @spec normalize_coordinate(any()) :: number() | any() + def normalize_coordinate(%Decimal{} = decimal), do: Decimal.to_float(decimal) + def normalize_coordinate(coord), do: coord + @doc """ Calculate distance between two lat/lon points in meters using Haversine formula. """ diff --git a/lib/aprsme_web/live/shared/packet_utils.ex b/lib/aprsme_web/live/shared/packet_utils.ex index 3dbd082..a5f0031 100644 --- a/lib/aprsme_web/live/shared/packet_utils.ex +++ b/lib/aprsme_web/live/shared/packet_utils.ex @@ -94,19 +94,7 @@ defmodule AprsmeWeb.Live.Shared.PacketUtils do """ @spec has_weather_data?(map()) :: boolean() def has_weather_data?(packet) do - weather_fields = [ - :temperature, - :humidity, - :pressure, - :wind_speed, - :wind_direction, - :rain_1h, - :rain_24h, - :rain_since_midnight, - :snow - ] - - Enum.any?(weather_fields, fn field -> + Enum.any?(Aprsme.EncodingUtils.weather_fields(), fn field -> value = get_packet_field(packet, field, nil) not is_nil(value) end) diff --git a/lib/aprsme_web/live/weather_live/callsign_view.html.heex b/lib/aprsme_web/live/weather_live/callsign_view.html.heex index 986e985..3d2432d 100644 --- a/lib/aprsme_web/live/weather_live/callsign_view.html.heex +++ b/lib/aprsme_web/live/weather_live/callsign_view.html.heex @@ -12,7 +12,7 @@ {@callsign} <% {symbol_table_id, symbol_code} = - AprsmeWeb.MapLive.PacketUtils.get_symbol_info(@weather_packet) %> + AprsmeWeb.Live.Shared.PacketUtils.get_symbol_info(@weather_packet) %> <% symbol_table = if symbol_table_id in ["/", "\\", "]"], do: symbol_table_id, else: "/" %> <% symbol_code = (symbol_code || ">") |> to_string() %> diff --git a/test/aprsme_web/live/map_live/packet_utils_test.exs b/test/aprsme_web/live/map_live/packet_utils_test.exs index b34251c..08149a7 100644 --- a/test/aprsme_web/live/map_live/packet_utils_test.exs +++ b/test/aprsme_web/live/map_live/packet_utils_test.exs @@ -1,6 +1,7 @@ defmodule AprsmeWeb.MapLive.PacketUtilsTest do use ExUnit.Case, async: true + alias AprsmeWeb.Live.Shared.PacketUtils, as: SharedPacketUtils alias AprsmeWeb.MapLive.PacketUtils describe "weather popup generation" do @@ -282,7 +283,7 @@ defmodule AprsmeWeb.MapLive.PacketUtilsTest do temperature: 75 } - assert PacketUtils.weather_packet?(weather_packet) + assert SharedPacketUtils.weather_packet?(weather_packet) end test "identifies weather packets with humidity" do @@ -293,7 +294,7 @@ defmodule AprsmeWeb.MapLive.PacketUtilsTest do humidity: 80 } - assert PacketUtils.weather_packet?(weather_packet) + assert SharedPacketUtils.weather_packet?(weather_packet) end test "does not identify non-weather packets" do @@ -303,7 +304,7 @@ defmodule AprsmeWeb.MapLive.PacketUtilsTest do symbol_code: ">" } - refute PacketUtils.weather_packet?(regular_packet) + refute SharedPacketUtils.weather_packet?(regular_packet) end test "has_weather_packets? returns false for non-existent callsign" do @@ -383,7 +384,7 @@ defmodule AprsmeWeb.MapLive.PacketUtilsTest do ssid: "1" } - assert PacketUtils.generate_callsign(packet) == "TEST-1" + assert SharedPacketUtils.generate_callsign(packet) == "TEST-1" end test "generates callsign without SSID" do @@ -392,7 +393,7 @@ defmodule AprsmeWeb.MapLive.PacketUtilsTest do ssid: nil } - assert PacketUtils.generate_callsign(packet) == "TEST" + assert SharedPacketUtils.generate_callsign(packet) == "TEST" end test "generates callsign with empty SSID" do @@ -401,7 +402,7 @@ defmodule AprsmeWeb.MapLive.PacketUtilsTest do ssid: "" } - assert PacketUtils.generate_callsign(packet) == "TEST" + assert SharedPacketUtils.generate_callsign(packet) == "TEST" end end end