fix bounds error

This commit is contained in:
Graham McIntire 2025-06-16 11:36:49 -05:00
parent 86f874194e
commit 445c9899cf
No known key found for this signature in database
10 changed files with 531 additions and 161 deletions

View file

@ -19,6 +19,7 @@ import Config
# Always start the server in production/docker environments
if System.get_env("PHX_SERVER") || config_env() == :prod do
config :aprs, AprsWeb.Endpoint, server: true
IO.puts("Phoenix server enabled - will start HTTP listener")
else
IO.puts("Phoenix server disabled - no HTTP listener will start")

View file

@ -299,7 +299,10 @@ defmodule Aprs.Is do
Logger.info("Successfully stored packet from #{packet.sender}")
{:error, changeset} ->
Logger.error("Failed to store packet from #{inspect(parsed_message.sender)}: #{inspect(changeset.errors)}")
Logger.error(
"Failed to store packet from #{inspect(parsed_message.sender)}: #{inspect(changeset.errors)}"
)
# Log the problematic attributes for debugging
Logger.debug("Packet attributes that failed: #{inspect(attrs)}")
end

View file

@ -146,6 +146,7 @@ defmodule Aprs.Packet do
if is_valid_coordinates?(lat, lon) do
try do
location = create_point(lat, lon)
if location do
put_change(changeset, :location, location)
else
@ -154,6 +155,7 @@ defmodule Aprs.Packet do
rescue
error ->
require Logger
Logger.error("Failed to create geometry for lat=#{lat}, lon=#{lon}: #{inspect(error)}")
changeset
end

View file

@ -74,7 +74,6 @@ defmodule Aprs.Packets do
%Packet{}
|> Packet.changeset(packet_attrs)
|> Repo.insert()
rescue
error ->
Logger.error("Exception in store_packet for #{inspect(packet_data[:sender])}: #{inspect(error)}")

View file

@ -200,6 +200,7 @@ defmodule AprsWeb.MapLive.CallsignView do
# Helper function to convert string or float to float
defp to_float(value) when is_float(value), do: value
defp to_float(value) when is_integer(value), do: value * 1.0
defp to_float(value) when is_binary(value) do
case Float.parse(value) do
{float_val, _} -> float_val

View file

@ -57,42 +57,45 @@ defmodule AprsWeb.MapLive.Index do
)
if connected?(socket) do
IO.puts("Socket is connected, attempting to get IP location")
Endpoint.subscribe("aprs_messages")
# Get IP-based location on initial load
IO.puts("Connect info: #{inspect(socket.private[:connect_info])}")
IO.puts("Peer data: #{inspect(socket.private[:connect_info][:peer_data])}")
ip =
case socket.private[:connect_info][:peer_data][:address] do
{a, b, c, d} -> "#{a}.#{b}.#{c}.#{d}"
{a, b, c, d, e, f, g, h} -> "#{a}:#{b}:#{c}:#{d}:#{e}:#{f}:#{g}:#{h}"
_ -> nil
end
# Only do IP geolocation in non-test environments
if Mix.env() != :test do
IO.puts("Socket is connected, attempting to get IP location")
# Get IP-based location on initial load
IO.puts("Connect info: #{inspect(socket.private[:connect_info])}")
IO.puts("Peer data: #{inspect(socket.private[:connect_info][:peer_data])}")
IO.puts("Extracted IP address: #{inspect(ip)}")
if ip do
IO.puts("Starting IP geolocation task for IP: #{ip}")
# Start as a separate task and await the result
Task.start(fn ->
try do
get_ip_location(ip)
rescue
error ->
IO.puts("Error in IP geolocation task: #{inspect(error)}")
IO.puts("Stacktrace: #{inspect(__STACKTRACE__)}")
send(self(), {:ip_location, @default_center})
ip =
case socket.private[:connect_info][:peer_data][:address] do
{a, b, c, d} -> "#{a}.#{b}.#{c}.#{d}"
{a, b, c, d, e, f, g, h} -> "#{a}:#{b}:#{c}:#{d}:#{e}:#{f}:#{g}:#{h}"
_ -> nil
end
end)
else
IO.puts("No IP address found, skipping geolocation")
# Only attempt IP geolocation if not localhost
if ip && !String.starts_with?(ip, "127.") && !String.starts_with?(ip, "::1") do
IO.puts("Starting IP geolocation task for IP: #{ip}")
# Start as a separate task and await the result
Task.start(fn ->
try do
get_ip_location(ip)
rescue
error ->
IO.puts("Error in IP geolocation task: #{inspect(error)}")
IO.puts("Stacktrace: #{inspect(__STACKTRACE__)}")
send(self(), {:ip_location, @default_center})
end
end)
else
IO.puts("No IP address found, skipping geolocation")
end
end
# Schedule regular cleanup of old packets from the map
if connected?(socket), do: Process.send_after(self(), :cleanup_old_packets, 60_000)
Process.send_after(self(), :cleanup_old_packets, 60_000)
# Schedule initialization of replay after a short delay
if connected?(socket), do: Process.send_after(self(), :initialize_replay, 2000)
Process.send_after(self(), :initialize_replay, 2000)
end
{:ok, socket}
@ -898,6 +901,12 @@ defmodule AprsWeb.MapLive.Index do
# Helper function to convert string or float to float
defp to_float(value) when is_float(value), do: value
defp to_float(value) when is_binary(value), do: String.to_float(value)
defp to_float(value) when is_integer(value), do: value * 1.0
defp to_float(value) when is_binary(value) do
case Float.parse(value) do
{float_val, _} -> float_val
:error -> raise ArgumentError, "Invalid float string: #{value}"
end
end
end

View file

@ -1,130 +0,0 @@
#!/usr/bin/env elixir
# Test script to verify bounds parsing fix in LiveView components
# This script tests the to_float helper function to ensure it handles both string and float inputs
IO.puts("🧪 Testing bounds parsing fix...")
# Simulate the helper function from the LiveView modules
defmodule BoundsParsingTest do
# Helper function to convert string or float to float
defp to_float(value) when is_float(value), do: value
defp to_float(value) when is_binary(value), do: String.to_float(value)
defp to_float(value) when is_integer(value), do: value * 1.0
def test_bounds_parsing do
# Test case 1: Float values (the problematic case from the error)
float_bounds = %{
"north" => 61.897577621605016,
"south" => 5.441022303717974,
"east" => -34.45312500000001,
"west" => -161.54296875000003
}
# Test case 2: String values (the expected case)
string_bounds = %{
"north" => "61.897577621605016",
"south" => "5.441022303717974",
"east" => "-34.45312500000001",
"west" => "-161.54296875000003"
}
# Test case 3: Integer values
integer_bounds = %{
"north" => 62,
"south" => 5,
"east" => -34,
"west" => -162
}
# Test case 4: Mixed types
mixed_bounds = %{
"north" => 61.897577621605016,
"south" => "5.441022303717974",
"east" => -34,
"west" => "-161.54296875000003"
}
test_cases = [
{"Float bounds", float_bounds},
{"String bounds", string_bounds},
{"Integer bounds", integer_bounds},
{"Mixed bounds", mixed_bounds}
]
Enum.each(test_cases, fn {name, bounds} ->
IO.puts("\n🔍 Testing #{name}:")
try do
normalized_bounds = %{
north: to_float(bounds["north"]),
south: to_float(bounds["south"]),
east: to_float(bounds["east"]),
west: to_float(bounds["west"])
}
IO.puts("✅ Success: #{inspect(normalized_bounds)}")
rescue
error ->
IO.puts("❌ Error: #{inspect(error)}")
end
end)
end
def test_replay_speed_parsing do
IO.puts("\n🔍 Testing replay speed parsing:")
test_speeds = [
{"Float speed", 1.5},
{"String speed", "2.0"},
{"Integer speed", 3},
{"String integer", "1"}
]
Enum.each(test_speeds, fn {name, speed} ->
try do
parsed_speed = to_float(speed)
IO.puts("#{name}: #{inspect(speed)} -> #{parsed_speed}")
rescue
error ->
IO.puts("#{name} error: #{inspect(error)}")
end
end)
end
# Test edge cases
def test_edge_cases do
IO.puts("\n🔍 Testing edge cases:")
edge_cases = [
{"Zero float", 0.0},
{"Zero string", "0.0"},
{"Zero integer", 0},
{"Negative float", -123.456},
{"Negative string", "-123.456"},
{"Large number", 999999.999999}
]
Enum.each(edge_cases, fn {name, value} ->
try do
result = to_float(value)
IO.puts("#{name}: #{inspect(value)} -> #{result}")
rescue
error ->
IO.puts("#{name} error: #{inspect(error)}")
end
end)
end
end
# Run the tests
BoundsParsingTest.test_bounds_parsing()
BoundsParsingTest.test_replay_speed_parsing()
BoundsParsingTest.test_edge_cases()
IO.puts("\n🎉 Bounds parsing test completed!")
IO.puts("\n📊 Summary:")
IO.puts(" - The to_float helper function handles float, string, and integer inputs")
IO.puts(" - This prevents the ArgumentError: not a binary when JavaScript sends float values")
IO.puts(" - The LiveView bounds_changed event should now work correctly")
IO.puts("\n🚀 The bounds parsing fix is working correctly!")

296
test/aprs/geometry_test.exs Normal file
View file

@ -0,0 +1,296 @@
defmodule Aprs.GeometryTest do
use Aprs.DataCase
alias Aprs.Packet
alias Aprs.Packets
alias Geo.PostGIS.Geometry
describe "PostGIS geometry functionality" do
test "create_point/2 creates valid Point geometry" do
lat = 32.7767
lon = -96.7970
point = Packet.create_point(lat, lon)
assert %Geo.Point{} = point
assert point.coordinates == {lon, lat}
assert point.srid == 4326
end
test "create_point/2 returns nil for invalid coordinates" do
# Test invalid latitude
assert Packet.create_point(91.0, -96.7970) == nil
assert Packet.create_point(-91.0, -96.7970) == nil
# Test invalid longitude
assert Packet.create_point(32.7767, 181.0) == nil
assert Packet.create_point(32.7767, -181.0) == nil
# Test nil values
assert Packet.create_point(nil, -96.7970) == nil
assert Packet.create_point(32.7767, nil) == nil
end
test "extract_coordinates/1 extracts lat/lon from Point geometry" do
point = %Geo.Point{coordinates: {-96.7970, 32.7767}, srid: 4326}
{lat, lon} = Packet.extract_coordinates(point)
assert lat == 32.7767
assert lon == -96.7970
end
test "extract_coordinates/1 returns {nil, nil} for invalid geometry" do
assert Packet.extract_coordinates(nil) == {nil, nil}
assert Packet.extract_coordinates("invalid") == {nil, nil}
end
test "packet changeset creates geometry from lat/lon" do
attrs = %{
base_callsign: "TEST",
data_type: "position",
destination: "APRS",
information_field: "test",
path: "WIDE1-1",
sender: "TEST-1",
ssid: "1",
received_at: DateTime.utc_now(),
lat: 32.7767,
lon: -96.7970
}
changeset = Packet.changeset(%Packet{}, attrs)
assert changeset.valid?
assert get_change(changeset, :location) != nil
assert get_change(changeset, :has_position) == true
location = get_change(changeset, :location)
assert %Geo.Point{} = location
assert location.coordinates == {-96.7970, 32.7767}
end
test "packet changeset handles invalid coordinates gracefully" do
attrs = %{
base_callsign: "TEST",
data_type: "position",
destination: "APRS",
information_field: "test",
path: "WIDE1-1",
sender: "TEST-1",
ssid: "1",
received_at: DateTime.utc_now(),
# Invalid latitude
lat: 91.0,
lon: -96.7970
}
changeset = Packet.changeset(%Packet{}, attrs)
# Should still be valid, just without location
assert changeset.valid?
assert get_change(changeset, :location) == nil
assert get_change(changeset, :has_position) != true
end
test "store_packet/1 successfully stores packet with geometry" do
attrs = %{
base_callsign: "TEST",
data_type: "position",
destination: "APRS",
information_field: "!3216.46N/09647.82W>Test packet",
path: "WIDE1-1,WIDE2-1",
sender: "TEST-1",
ssid: "1",
received_at: DateTime.utc_now(),
lat: 32.2743,
lon: -96.7970,
has_position: true,
region: "32.3,-96.8",
raw_packet: "TEST-1>APRS,WIDE1-1,WIDE2-1:!3216.46N/09647.82W>Test packet"
}
assert {:ok, packet} = Packets.store_packet(attrs)
assert packet.sender == "TEST-1"
assert packet.has_position == true
assert packet.location != nil
assert %Geo.Point{} = packet.location
assert packet.location.coordinates == {-96.7970, 32.2743}
end
test "store_packet/1 handles packet without position data" do
attrs = %{
base_callsign: "TEST",
data_type: "message",
destination: "APRS",
information_field: ":TEST-2 :Hello world",
path: "WIDE1-1",
sender: "TEST-1",
ssid: "1",
received_at: DateTime.utc_now(),
raw_packet: "TEST-1>APRS,WIDE1-1::TEST-2 :Hello world"
}
assert {:ok, packet} = Packets.store_packet(attrs)
assert packet.sender == "TEST-1"
assert packet.has_position == false
assert packet.location == nil
end
test "store_packet/1 handles coordinate validation errors gracefully" do
attrs = %{
base_callsign: "TEST",
data_type: "position",
destination: "APRS",
information_field: "test",
path: "WIDE1-1",
sender: "TEST-1",
ssid: "1",
received_at: DateTime.utc_now(),
# Invalid latitude
lat: 200.0,
lon: -96.7970,
raw_packet: "TEST-1>APRS,WIDE1-1:test"
}
# Should not crash, but should handle gracefully
assert {:ok, packet} = Packets.store_packet(attrs)
assert packet.sender == "TEST-1"
assert packet.has_position == false
assert packet.location == nil
end
test "Geo.PostGIS.Geometry cast works with Point" do
point = %Geo.Point{coordinates: {-96.7970, 32.7767}, srid: 4326}
assert {:ok, ^point} = Geometry.cast(point)
end
test "Geo.PostGIS.Geometry cast works with coordinate tuple" do
coords = {-96.7970, 32.7767}
# PostGIS geometry casting may not support tuples directly
# Instead test creating a point manually
point = %Geo.Point{coordinates: coords, srid: 4326}
assert {:ok, ^point} = Geometry.cast(point)
end
test "packet lat/lon helper functions work correctly" do
packet = %Packet{
location: %Geo.Point{coordinates: {-96.7970, 32.7767}, srid: 4326}
}
assert Packet.lat(packet) == 32.7767
assert Packet.lon(packet) == -96.7970
end
test "packet lat/lon helpers return nil for packets without location" do
packet = %Packet{location: nil}
assert Packet.lat(packet) == nil
assert Packet.lon(packet) == nil
end
end
describe "coordinate conversion and validation" do
test "handles various coordinate formats" do
# Test float coordinates
assert Packet.create_point(32.7767, -96.7970) != nil
# Test integer coordinates
assert Packet.create_point(33, -97) != nil
# Test boundary values
assert Packet.create_point(90.0, 180.0) != nil
assert Packet.create_point(-90.0, -180.0) != nil
# Test just inside boundaries
assert Packet.create_point(89.9999, 179.9999) != nil
assert Packet.create_point(-89.9999, -179.9999) != nil
end
test "rejects coordinates outside valid ranges" do
# Test latitude boundaries
assert Packet.create_point(90.0001, 0.0) == nil
assert Packet.create_point(-90.0001, 0.0) == nil
# Test longitude boundaries
assert Packet.create_point(0.0, 180.0001) == nil
assert Packet.create_point(0.0, -180.0001) == nil
end
test "handles edge case coordinates" do
# Test zero coordinates
assert Packet.create_point(0.0, 0.0) != nil
# Test very small coordinates
assert Packet.create_point(0.000001, 0.000001) != nil
# Test coordinates near boundaries
assert Packet.create_point(89.999999, 179.999999) != nil
end
end
describe "packet storage with different position formats" do
test "stores packet with MicE-like data structure" do
# Test packet storage that would involve MicE conversion
attrs = %{
base_callsign: "TEST",
data_type: "position",
destination: "APRS",
information_field: "test",
path: "WIDE1-1",
sender: "TEST-1",
ssid: "1",
received_at: DateTime.utc_now(),
# Pre-converted coordinates
lat: 32.7767,
lon: -96.7970,
raw_packet: "TEST-1>APRS,WIDE1-1:test"
}
assert {:ok, packet} = Packets.store_packet(attrs)
assert packet.sender == "TEST-1"
assert packet.has_position == true
assert packet.location != nil
end
test "stores packet with standard position data" do
attrs = %{
base_callsign: "TEST",
data_type: "position",
destination: "APRS",
information_field: "test",
path: "WIDE1-1",
sender: "TEST-2",
ssid: "2",
received_at: DateTime.utc_now(),
lat: 32.7767,
lon: -96.7970,
raw_packet: "TEST-2>APRS,WIDE1-1:test"
}
assert {:ok, packet} = Packets.store_packet(attrs)
assert packet.has_position == true
assert %Geo.Point{} = packet.location
end
test "stores packet without position data" do
attrs = %{
base_callsign: "TEST",
data_type: "message",
destination: "APRS",
information_field: ":TEST-2 :Hello",
path: "WIDE1-1",
sender: "TEST-3",
ssid: "3",
received_at: DateTime.utc_now(),
raw_packet: "TEST-3>APRS,WIDE1-1::TEST-2 :Hello"
}
assert {:ok, packet} = Packets.store_packet(attrs)
assert packet.has_position == false
assert packet.location == nil
end
end
end

View file

@ -66,5 +66,82 @@ defmodule AprsWeb.MapLive.CallsignViewTest do
assert page_title(view) =~ "APRS Map - W5ISP-9"
end
test "handles bounds_changed event with float values", %{conn: conn} do
{:ok, view, _html} = live(conn, "/W5ISP-9")
# Simulate JavaScript sending float values (the problematic case)
bounds_params = %{
"bounds" => %{
"north" => 61.897577621605016,
"south" => 5.441022303717974,
"east" => -34.45312500000001,
"west" => -161.54296875000003
}
}
# This should not crash with ArgumentError: not a binary
assert render_hook(view, "bounds_changed", bounds_params)
end
test "handles bounds_changed event with string values", %{conn: conn} do
{:ok, view, _html} = live(conn, "/W5ISP-9")
# Simulate JavaScript sending string values (expected case)
bounds_params = %{
"bounds" => %{
"north" => "61.897577621605016",
"south" => "5.441022303717974",
"east" => "-34.45312500000001",
"west" => "-161.54296875000003"
}
}
assert render_hook(view, "bounds_changed", bounds_params)
end
test "handles bounds_changed event with mixed value types", %{conn: conn} do
{:ok, view, _html} = live(conn, "/W5ISP-9")
# Test mixed types (float, string, integer)
bounds_params = %{
"bounds" => %{
# float
"north" => 61.897577621605016,
# string
"south" => "5.441022303717974",
# integer
"east" => -34,
# string
"west" => "-161.54296875000003"
}
}
assert render_hook(view, "bounds_changed", bounds_params)
end
test "handles adjust_replay_speed event with float values", %{conn: conn} do
{:ok, view, _html} = live(conn, "/W5ISP-9")
# Test with float speed value
speed_params = %{"speed" => 1.5}
assert render_hook(view, "adjust_replay_speed", speed_params)
end
test "handles adjust_replay_speed event with string values", %{conn: conn} do
{:ok, view, _html} = live(conn, "/W5ISP-9")
# Test with string speed value
speed_params = %{"speed" => "2.0"}
assert render_hook(view, "adjust_replay_speed", speed_params)
end
test "handles adjust_replay_speed event with integer values", %{conn: conn} do
{:ok, view, _html} = live(conn, "/W5ISP-9")
# Test with integer speed value
speed_params = %{"speed" => 3}
assert render_hook(view, "adjust_replay_speed", speed_params)
end
end
end

View file

@ -0,0 +1,112 @@
defmodule AprsWeb.MapLive.IndexTest do
use AprsWeb.ConnCase
import Phoenix.LiveViewTest
describe "Index" do
test "renders map view", %{conn: conn} do
{:ok, view, html} = live(conn, "/")
assert html =~ "APRS Map"
assert has_element?(view, "#aprs-map")
end
test "handles bounds_changed event with float values", %{conn: conn} do
{:ok, view, _html} = live(conn, "/")
# Simulate JavaScript sending float values (the problematic case)
bounds_params = %{
"bounds" => %{
"north" => 61.897577621605016,
"south" => 5.441022303717974,
"east" => -34.45312500000001,
"west" => -161.54296875000003
}
}
# This should not crash with ArgumentError: not a binary
assert render_hook(view, "bounds_changed", bounds_params)
end
test "handles bounds_changed event with string values", %{conn: conn} do
{:ok, view, _html} = live(conn, "/")
# Simulate JavaScript sending string values (expected case)
bounds_params = %{
"bounds" => %{
"north" => "61.897577621605016",
"south" => "5.441022303717974",
"east" => "-34.45312500000001",
"west" => "-161.54296875000003"
}
}
assert render_hook(view, "bounds_changed", bounds_params)
end
test "handles bounds_changed event with mixed value types", %{conn: conn} do
{:ok, view, _html} = live(conn, "/")
# Test mixed types (float, string, integer)
bounds_params = %{
"bounds" => %{
# float
"north" => 61.897577621605016,
# string
"south" => "5.441022303717974",
# integer
"east" => -34,
# string
"west" => "-161.54296875000003"
}
}
assert render_hook(view, "bounds_changed", bounds_params)
end
test "handles adjust_replay_speed event with float values", %{conn: conn} do
{:ok, view, _html} = live(conn, "/")
# Test with float speed value
speed_params = %{"speed" => 1.5}
assert render_hook(view, "adjust_replay_speed", speed_params)
end
test "handles adjust_replay_speed event with string values", %{conn: conn} do
{:ok, view, _html} = live(conn, "/")
# Test with string speed value
speed_params = %{"speed" => "2.0"}
assert render_hook(view, "adjust_replay_speed", speed_params)
end
test "handles adjust_replay_speed event with integer values", %{conn: conn} do
{:ok, view, _html} = live(conn, "/")
# Test with integer speed value
speed_params = %{"speed" => 3}
assert render_hook(view, "adjust_replay_speed", speed_params)
end
test "handles adjust_replay_speed event with integer string values", %{conn: conn} do
{:ok, view, _html} = live(conn, "/")
# Test with integer string value (should work with improved Float.parse)
speed_params = %{"speed" => "1"}
assert render_hook(view, "adjust_replay_speed", speed_params)
end
test "sets correct page title", %{conn: conn} do
{:ok, view, _html} = live(conn, "/")
assert page_title(view) =~ "APRS Map"
end
test "handles map_ready event", %{conn: conn} do
{:ok, view, _html} = live(conn, "/")
# Simulate map initialization
assert render_hook(view, "map_ready", %{})
end
end
end