fix startup

This commit is contained in:
Graham McIntire 2025-06-16 11:16:56 -05:00
parent 6f54625dc8
commit a69a87873f
No known key found for this signature in database
5 changed files with 168 additions and 191 deletions

View file

@ -1,85 +0,0 @@
defmodule Aprs.GeometryType do
@moduledoc """
Custom Ecto type for PostGIS geometry fields.
This wraps the Geo.PostGIS.Geometry type to handle PostGIS geometry data.
"""
use Ecto.Type
def type, do: :geometry
def cast(%Geo.Point{} = point), do: {:ok, point}
def cast(%Geo.Polygon{} = polygon), do: {:ok, polygon}
def cast(%Geo.LineString{} = linestring), do: {:ok, linestring}
def cast(%Geo.MultiPoint{} = multipoint), do: {:ok, multipoint}
def cast(%Geo.MultiPolygon{} = multipolygon), do: {:ok, multipolygon}
def cast(%Geo.MultiLineString{} = multilinestring), do: {:ok, multilinestring}
def cast(%Geo.GeometryCollection{} = collection), do: {:ok, collection}
# Handle coordinate tuples and create Point geometry
def cast({lon, lat}) when is_number(lon) and is_number(lat) do
if lon >= -180 and lon <= 180 and lat >= -90 and lat <= 90 do
{:ok, %Geo.Point{coordinates: {lon, lat}, srid: 4326}}
else
:error
end
end
# Handle maps with lat/lon
def cast(%{lat: lat, lon: lon}) when is_number(lat) and is_number(lon) do
cast({lon, lat})
end
def cast(%{"lat" => lat, "lon" => lon}) when is_number(lat) and is_number(lon) do
cast({lon, lat})
end
def cast(nil), do: {:ok, nil}
def cast(_), do: :error
def load(data) when is_binary(data) do
# Handle WKB binary data directly
case Geo.WKB.decode(data) do
{:ok, geometry} -> {:ok, geometry}
_ -> :error
end
end
def load(%Geo.Point{} = point), do: {:ok, point}
def load(%Geo.Polygon{} = polygon), do: {:ok, polygon}
def load(%Geo.LineString{} = linestring), do: {:ok, linestring}
def load(%Geo.MultiPoint{} = multipoint), do: {:ok, multipoint}
def load(%Geo.MultiPolygon{} = multipolygon), do: {:ok, multipolygon}
def load(%Geo.MultiLineString{} = multilinestring), do: {:ok, multilinestring}
def load(%Geo.GeometryCollection{} = collection), do: {:ok, collection}
def load(nil), do: {:ok, nil}
def load(_), do: :error
def dump(geometry) when not is_nil(geometry) do
# Encode to WKB binary format
case Geo.WKB.encode(geometry) do
{:ok, data} -> {:ok, data}
_ -> :error
end
end
def dump(nil), do: {:ok, nil}
def dump(_), do: :error
@doc """
Helper function to create a Point geometry from lat/lon coordinates.
"""
def create_point(lat, lon) when is_number(lat) and is_number(lon) do
if lat >= -90 and lat <= 90 and lon >= -180 and lon <= 180 do
%Geo.Point{coordinates: {lon, lat}, srid: 4326}
end
end
def create_point(_, _), do: nil
@doc """
Extract lat/lon coordinates from a Point geometry.
"""
def extract_coordinates(%Geo.Point{coordinates: {lon, lat}}), do: {lat, lon}
def extract_coordinates(_), do: {nil, nil}
end

View file

@ -267,40 +267,50 @@ defmodule Aprs.Is do
# Use Task to avoid slowing down the main process
Task.start(fn ->
require Logger
# Store the packet if it has position data
if has_position_data?(parsed_message) do
Logger.info("Storing packet with position data: #{inspect(parsed_message.sender)}")
# Always set received_at timestamp to ensure consistency
current_time = DateTime.truncate(DateTime.utc_now(), :microsecond)
packet_data = Map.put(parsed_message, :received_at, current_time)
# Convert to map before storing to avoid struct conversion issues
attrs = struct_to_map(packet_data)
try do
# Store the packet if it has position data
if has_position_data?(parsed_message) do
Logger.info("Storing packet with position data: #{inspect(parsed_message.sender)}")
# Always set received_at timestamp to ensure consistency
current_time = DateTime.truncate(DateTime.utc_now(), :microsecond)
packet_data = Map.put(parsed_message, :received_at, current_time)
# Extract additional data from the parsed packet including raw packet
attrs = Aprs.Packet.extract_additional_data(attrs, message)
# Convert to map before storing to avoid struct conversion issues
attrs = struct_to_map(packet_data)
# Normalize data_type to string if it's an atom
attrs = normalize_data_type(attrs)
# Extract additional data from the parsed packet including raw packet
attrs = Aprs.Packet.extract_additional_data(attrs, message)
# Ensure SSID is never nil
attrs =
if Map.has_key?(attrs, :ssid) and is_nil(attrs.ssid) do
Map.put(attrs, :ssid, "0")
else
attrs
# Normalize data_type to string if it's an atom
attrs = normalize_data_type(attrs)
# Ensure SSID is never nil
attrs =
if Map.has_key?(attrs, :ssid) and is_nil(attrs.ssid) do
Map.put(attrs, :ssid, "0")
else
attrs
end
# Store in database through the Packets context
case Aprs.Packets.store_packet(attrs) do
{:ok, packet} ->
Logger.info("Successfully stored packet from #{packet.sender}")
{:error, changeset} ->
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
# Store in database through the Packets context
case Aprs.Packets.store_packet(attrs) do
{:ok, packet} ->
Logger.info("Successfully stored packet from #{packet.sender}")
{:error, changeset} ->
Logger.error("Failed to store packet: #{inspect(changeset.errors)}")
else
Logger.debug("Skipping packet without position data: #{inspect(parsed_message.sender)}")
end
else
Logger.debug("Skipping packet without position data: #{inspect(parsed_message.sender)}")
rescue
error ->
Logger.error("Exception while storing packet from #{inspect(parsed_message.sender)}: #{inspect(error)}")
Logger.debug("Raw message: #{inspect(message)}")
Logger.debug("Parsed message: #{inspect(parsed_message)}")
end
end)

View file

@ -19,7 +19,7 @@ defmodule Aprs.Packet do
field(:region, :string)
field(:lat, :float, virtual: true)
field(:lon, :float, virtual: true)
field(:location, Aprs.GeometryType)
field(:location, Geo.PostGIS.Geometry)
field(:has_position, :boolean, default: false)
# Original raw packet and symbol information
@ -144,8 +144,19 @@ defmodule Aprs.Packet do
end
if is_valid_coordinates?(lat, lon) do
location = Aprs.GeometryType.create_point(lat, lon)
put_change(changeset, :location, location)
try do
location = create_point(lat, lon)
if location do
put_change(changeset, :location, location)
else
changeset
end
rescue
error ->
require Logger
Logger.error("Failed to create geometry for lat=#{lat}, lon=#{lon}: #{inspect(error)}")
changeset
end
else
changeset
end
@ -418,12 +429,19 @@ defmodule Aprs.Packet do
@doc """
Create a geometry point from lat/lon coordinates.
"""
def create_point(lat, lon), do: Aprs.GeometryType.create_point(lat, lon)
def create_point(lat, lon) when is_number(lat) and is_number(lon) do
if lat >= -90 and lat <= 90 and lon >= -180 and lon <= 180 do
%Geo.Point{coordinates: {lon, lat}, srid: 4326}
end
end
def create_point(_, _), do: nil
@doc """
Extract lat/lon from a PostGIS geometry point.
"""
def extract_coordinates(geometry), do: Aprs.GeometryType.extract_coordinates(geometry)
def extract_coordinates(%Geo.Point{coordinates: {lon, lat}}), do: {lat, lon}
def extract_coordinates(_), do: {nil, nil}
@doc """
Get latitude from a packet's location geometry.

View file

@ -18,53 +18,68 @@ defmodule Aprs.Packets do
def store_packet(packet_data) do
require Logger
# Convert to map if it's a struct, or use as is if already a map
packet_attrs =
case packet_data do
%Packet{} = packet ->
packet
|> Map.from_struct()
|> Map.delete(:__meta__)
try do
# Convert to map if it's a struct, or use as is if already a map
packet_attrs =
case packet_data do
%Packet{} = packet ->
packet
|> Map.from_struct()
|> Map.delete(:__meta__)
%{} ->
packet_data
end
%{} ->
packet_data
end
# Convert data_type to string if it's an atom
packet_attrs =
case packet_attrs do
%{data_type: data_type} when is_atom(data_type) ->
Map.put(packet_attrs, :data_type, to_string(data_type))
# Convert data_type to string if it's an atom
packet_attrs =
case packet_attrs do
%{data_type: data_type} when is_atom(data_type) ->
Map.put(packet_attrs, :data_type, to_string(data_type))
_ ->
packet_attrs
end
_ ->
packet_attrs
end
# Make sure received_at is set with explicit UTC DateTime
current_time = DateTime.truncate(DateTime.utc_now(), :microsecond)
packet_attrs = Map.put(packet_attrs, :received_at, current_time)
# Make sure received_at is set with explicit UTC DateTime
current_time = DateTime.truncate(DateTime.utc_now(), :microsecond)
packet_attrs = Map.put(packet_attrs, :received_at, current_time)
# Extract position data
{lat, lon} = extract_position(packet_attrs)
# Extract position data with error handling
{lat, lon} = extract_position(packet_attrs)
# Set position fields if found
packet_attrs =
if lat && lon do
packet_attrs
|> Map.put(:lat, lat)
|> Map.put(:lon, lon)
|> Map.put(:has_position, true)
|> Map.put(:region, "#{Float.round(lat, 1)},#{Float.round(lon, 1)}")
else
# Set region based on callsign if no position
sender_region = if packet_attrs[:sender], do: String.slice(packet_attrs.sender || "", 0, 3), else: "unknown"
Map.put(packet_attrs, :region, "call:#{sender_region}")
end
# Set position fields if found
packet_attrs =
if lat && lon do
# Validate coordinates before creating geometry
if are_valid_coordinates?(lat, lon) do
packet_attrs
|> Map.put(:lat, lat)
|> Map.put(:lon, lon)
|> Map.put(:has_position, true)
|> Map.put(:region, "#{Float.round(lat, 1)},#{Float.round(lon, 1)}")
else
Logger.warning("Invalid coordinates for packet from #{packet_attrs[:sender]}: lat=#{lat}, lon=#{lon}")
# Set region based on callsign if coordinates are invalid
sender_region = if packet_attrs[:sender], do: String.slice(packet_attrs.sender || "", 0, 3), else: "unknown"
Map.put(packet_attrs, :region, "call:#{sender_region}")
end
else
# Set region based on callsign if no position
sender_region = if packet_attrs[:sender], do: String.slice(packet_attrs.sender || "", 0, 3), else: "unknown"
Map.put(packet_attrs, :region, "call:#{sender_region}")
end
# Insert the packet
%Packet{}
|> Packet.changeset(packet_attrs)
|> Repo.insert()
# Insert the packet
%Packet{}
|> Packet.changeset(packet_attrs)
|> Repo.insert()
rescue
error ->
Logger.error("Exception in store_packet for #{inspect(packet_data[:sender])}: #{inspect(error)}")
{:error, :storage_exception}
end
end
# Extracts position data from packet, checking various possible locations
@ -329,6 +344,12 @@ defmodule Aprs.Packets do
defp to_float(_), do: nil
# Helper to validate coordinate values
defp are_valid_coordinates?(lat, lon) do
is_number(lat) and is_number(lon) and
lat >= -90 and lat <= 90 and lon >= -180 and lon <= 180
end
# Get packets from last hour only - used to initialize the map
def get_last_hour_packets do
one_hour_ago = DateTime.add(DateTime.utc_now(), -3600, :second)

View file

@ -1,33 +1,7 @@
#!/usr/bin/env elixir
# Test script to verify PostGIS functionality in the APRS application
Mix.install([])
# Add the project to the code path
Code.append_path("_build/dev/lib/aprs/ebin")
Code.append_path("_build/dev/lib/ecto/ebin")
Code.append_path("_build/dev/lib/ecto_sql/ebin")
Code.append_path("_build/dev/lib/postgrex/ebin")
Code.append_path("_build/dev/lib/geo/ebin")
Code.append_path("_build/dev/lib/geo_postgis/ebin")
# Load the application configuration
Application.put_env(:aprs, Aprs.Repo,
username: "postgres",
password: "postgres",
hostname: "localhost",
database: "aprs_dev",
types: Aprs.PostgresTypes
)
# Start necessary applications
Application.ensure_all_started(:postgrex)
Application.ensure_all_started(:ecto)
Application.ensure_all_started(:ecto_sql)
# Start the repo
{:ok, _} = Aprs.Repo.start_link()
# This script must be run from within the Mix project using: mix run scripts/test_postgis.exs
IO.puts("🗺️ Testing PostGIS functionality...")
@ -64,9 +38,13 @@ try do
point = %Geo.Point{coordinates: {-96.7969, 32.7767}, srid: 4326} # Dallas, TX
IO.puts("✅ Created point: #{inspect(point)}")
# Test the custom GeometryType
{:ok, cast_result} = Aprs.GeometryType.cast(point)
IO.puts("✅ GeometryType cast successful: #{inspect(cast_result)}")
# Test the PostGIS Geometry type directly
{:ok, cast_result} = Geo.PostGIS.Geometry.cast(point)
IO.puts("✅ Geo.PostGIS.Geometry cast successful: #{inspect(cast_result)}")
# Test creating a point using the Packet helper
created_point = Aprs.Packet.create_point(32.7767, -96.7969)
IO.puts("✅ Packet.create_point successful: #{inspect(created_point)}")
rescue
e ->
@ -157,14 +135,49 @@ rescue
IO.puts("❌ Error in spatial query: #{inspect(e)}")
end
# Test 8: Test inserting a packet with geometry
IO.puts("\n8. Testing packet insertion with geometry...")
try do
# Create test packet data
test_packet_attrs = %{
base_callsign: "TEST",
data_type: "position",
destination: "APRS",
information_field: "!3216.50N/09647.00W>Test packet",
path: "WIDE1-1,WIDE2-1",
sender: "TEST-1",
ssid: "1",
received_at: DateTime.utc_now(),
lat: 32.275,
lon: -96.783,
has_position: true,
region: "32.3,-96.8",
raw_packet: "TEST-1>APRS,WIDE1-1,WIDE2-1:!3216.50N/09647.00W>Test packet"
}
case Aprs.Packets.store_packet(test_packet_attrs) do
{:ok, packet} ->
IO.puts("✅ Successfully inserted test packet: #{packet.sender}")
# Clean up test packet
Aprs.Repo.delete(packet)
IO.puts("✅ Test packet cleaned up")
{:error, changeset} ->
IO.puts("❌ Failed to insert test packet: #{inspect(changeset.errors)}")
end
rescue
e ->
IO.puts("❌ Error in packet insertion test: #{inspect(e)}")
end
IO.puts("\n🎉 PostGIS testing completed!")
IO.puts("\n📊 Summary:")
IO.puts(" - PostGIS extension is enabled")
IO.puts(" - Location column with geometry type exists")
IO.puts(" - Spatial indexes are created")
IO.puts(" - Basic spatial functions are working")
IO.puts(" - Packet insertion with geometry works")
IO.puts(" - Data migration from lat/lon to PostGIS geometry completed")
IO.puts("\n🚀 Your APRS application is now ready for efficient spatial queries!")
# Clean up
Aprs.Repo.stop()