fix startup
This commit is contained in:
parent
6f54625dc8
commit
a69a87873f
5 changed files with 168 additions and 191 deletions
|
|
@ -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
|
|
||||||
|
|
@ -267,40 +267,50 @@ defmodule Aprs.Is do
|
||||||
# Use Task to avoid slowing down the main process
|
# Use Task to avoid slowing down the main process
|
||||||
Task.start(fn ->
|
Task.start(fn ->
|
||||||
require Logger
|
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
|
try do
|
||||||
attrs = struct_to_map(packet_data)
|
# 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
|
# Convert to map before storing to avoid struct conversion issues
|
||||||
attrs = Aprs.Packet.extract_additional_data(attrs, message)
|
attrs = struct_to_map(packet_data)
|
||||||
|
|
||||||
# Normalize data_type to string if it's an atom
|
# Extract additional data from the parsed packet including raw packet
|
||||||
attrs = normalize_data_type(attrs)
|
attrs = Aprs.Packet.extract_additional_data(attrs, message)
|
||||||
|
|
||||||
# Ensure SSID is never nil
|
# Normalize data_type to string if it's an atom
|
||||||
attrs =
|
attrs = normalize_data_type(attrs)
|
||||||
if Map.has_key?(attrs, :ssid) and is_nil(attrs.ssid) do
|
|
||||||
Map.put(attrs, :ssid, "0")
|
# Ensure SSID is never nil
|
||||||
else
|
attrs =
|
||||||
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
|
end
|
||||||
|
else
|
||||||
# Store in database through the Packets context
|
Logger.debug("Skipping packet without position data: #{inspect(parsed_message.sender)}")
|
||||||
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)}")
|
|
||||||
end
|
end
|
||||||
else
|
rescue
|
||||||
Logger.debug("Skipping packet without position data: #{inspect(parsed_message.sender)}")
|
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
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ defmodule Aprs.Packet do
|
||||||
field(:region, :string)
|
field(:region, :string)
|
||||||
field(:lat, :float, virtual: true)
|
field(:lat, :float, virtual: true)
|
||||||
field(:lon, :float, virtual: true)
|
field(:lon, :float, virtual: true)
|
||||||
field(:location, Aprs.GeometryType)
|
field(:location, Geo.PostGIS.Geometry)
|
||||||
field(:has_position, :boolean, default: false)
|
field(:has_position, :boolean, default: false)
|
||||||
|
|
||||||
# Original raw packet and symbol information
|
# Original raw packet and symbol information
|
||||||
|
|
@ -144,8 +144,19 @@ defmodule Aprs.Packet do
|
||||||
end
|
end
|
||||||
|
|
||||||
if is_valid_coordinates?(lat, lon) do
|
if is_valid_coordinates?(lat, lon) do
|
||||||
location = Aprs.GeometryType.create_point(lat, lon)
|
try do
|
||||||
put_change(changeset, :location, location)
|
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
|
else
|
||||||
changeset
|
changeset
|
||||||
end
|
end
|
||||||
|
|
@ -418,12 +429,19 @@ defmodule Aprs.Packet do
|
||||||
@doc """
|
@doc """
|
||||||
Create a geometry point from lat/lon coordinates.
|
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 """
|
@doc """
|
||||||
Extract lat/lon from a PostGIS geometry point.
|
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 """
|
@doc """
|
||||||
Get latitude from a packet's location geometry.
|
Get latitude from a packet's location geometry.
|
||||||
|
|
|
||||||
|
|
@ -18,53 +18,68 @@ defmodule Aprs.Packets do
|
||||||
def store_packet(packet_data) do
|
def store_packet(packet_data) do
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
# Convert to map if it's a struct, or use as is if already a map
|
try do
|
||||||
packet_attrs =
|
# Convert to map if it's a struct, or use as is if already a map
|
||||||
case packet_data do
|
packet_attrs =
|
||||||
%Packet{} = packet ->
|
case packet_data do
|
||||||
packet
|
%Packet{} = packet ->
|
||||||
|> Map.from_struct()
|
packet
|
||||||
|> Map.delete(:__meta__)
|
|> Map.from_struct()
|
||||||
|
|> Map.delete(:__meta__)
|
||||||
|
|
||||||
%{} ->
|
%{} ->
|
||||||
packet_data
|
packet_data
|
||||||
end
|
end
|
||||||
|
|
||||||
# Convert data_type to string if it's an atom
|
# Convert data_type to string if it's an atom
|
||||||
packet_attrs =
|
packet_attrs =
|
||||||
case packet_attrs do
|
case packet_attrs do
|
||||||
%{data_type: data_type} when is_atom(data_type) ->
|
%{data_type: data_type} when is_atom(data_type) ->
|
||||||
Map.put(packet_attrs, :data_type, to_string(data_type))
|
Map.put(packet_attrs, :data_type, to_string(data_type))
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
packet_attrs
|
packet_attrs
|
||||||
end
|
end
|
||||||
|
|
||||||
# Make sure received_at is set with explicit UTC DateTime
|
# Make sure received_at is set with explicit UTC DateTime
|
||||||
current_time = DateTime.truncate(DateTime.utc_now(), :microsecond)
|
current_time = DateTime.truncate(DateTime.utc_now(), :microsecond)
|
||||||
packet_attrs = Map.put(packet_attrs, :received_at, current_time)
|
packet_attrs = Map.put(packet_attrs, :received_at, current_time)
|
||||||
|
|
||||||
# Extract position data
|
# Extract position data with error handling
|
||||||
{lat, lon} = extract_position(packet_attrs)
|
{lat, lon} = extract_position(packet_attrs)
|
||||||
|
|
||||||
# Set position fields if found
|
# Set position fields if found
|
||||||
packet_attrs =
|
packet_attrs =
|
||||||
if lat && lon do
|
if lat && lon do
|
||||||
packet_attrs
|
# Validate coordinates before creating geometry
|
||||||
|> Map.put(:lat, lat)
|
if are_valid_coordinates?(lat, lon) do
|
||||||
|> Map.put(:lon, lon)
|
packet_attrs
|
||||||
|> Map.put(:has_position, true)
|
|> Map.put(:lat, lat)
|
||||||
|> Map.put(:region, "#{Float.round(lat, 1)},#{Float.round(lon, 1)}")
|
|> Map.put(:lon, lon)
|
||||||
else
|
|> Map.put(:has_position, true)
|
||||||
# Set region based on callsign if no position
|
|> Map.put(:region, "#{Float.round(lat, 1)},#{Float.round(lon, 1)}")
|
||||||
sender_region = if packet_attrs[:sender], do: String.slice(packet_attrs.sender || "", 0, 3), else: "unknown"
|
else
|
||||||
Map.put(packet_attrs, :region, "call:#{sender_region}")
|
Logger.warning("Invalid coordinates for packet from #{packet_attrs[:sender]}: lat=#{lat}, lon=#{lon}")
|
||||||
end
|
# 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
|
# Insert the packet
|
||||||
%Packet{}
|
%Packet{}
|
||||||
|> Packet.changeset(packet_attrs)
|
|> Packet.changeset(packet_attrs)
|
||||||
|> Repo.insert()
|
|> Repo.insert()
|
||||||
|
|
||||||
|
rescue
|
||||||
|
error ->
|
||||||
|
Logger.error("Exception in store_packet for #{inspect(packet_data[:sender])}: #{inspect(error)}")
|
||||||
|
{:error, :storage_exception}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Extracts position data from packet, checking various possible locations
|
# Extracts position data from packet, checking various possible locations
|
||||||
|
|
@ -329,6 +344,12 @@ defmodule Aprs.Packets do
|
||||||
|
|
||||||
defp to_float(_), do: nil
|
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
|
# Get packets from last hour only - used to initialize the map
|
||||||
def get_last_hour_packets do
|
def get_last_hour_packets do
|
||||||
one_hour_ago = DateTime.add(DateTime.utc_now(), -3600, :second)
|
one_hour_ago = DateTime.add(DateTime.utc_now(), -3600, :second)
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,7 @@
|
||||||
#!/usr/bin/env elixir
|
#!/usr/bin/env elixir
|
||||||
|
|
||||||
# Test script to verify PostGIS functionality in the APRS application
|
# Test script to verify PostGIS functionality in the APRS application
|
||||||
|
# This script must be run from within the Mix project using: mix run scripts/test_postgis.exs
|
||||||
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()
|
|
||||||
|
|
||||||
IO.puts("🗺️ Testing PostGIS functionality...")
|
IO.puts("🗺️ Testing PostGIS functionality...")
|
||||||
|
|
||||||
|
|
@ -64,9 +38,13 @@ try do
|
||||||
point = %Geo.Point{coordinates: {-96.7969, 32.7767}, srid: 4326} # Dallas, TX
|
point = %Geo.Point{coordinates: {-96.7969, 32.7767}, srid: 4326} # Dallas, TX
|
||||||
IO.puts("✅ Created point: #{inspect(point)}")
|
IO.puts("✅ Created point: #{inspect(point)}")
|
||||||
|
|
||||||
# Test the custom GeometryType
|
# Test the PostGIS Geometry type directly
|
||||||
{:ok, cast_result} = Aprs.GeometryType.cast(point)
|
{:ok, cast_result} = Geo.PostGIS.Geometry.cast(point)
|
||||||
IO.puts("✅ GeometryType cast successful: #{inspect(cast_result)}")
|
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
|
rescue
|
||||||
e ->
|
e ->
|
||||||
|
|
@ -157,14 +135,49 @@ rescue
|
||||||
IO.puts("❌ Error in spatial query: #{inspect(e)}")
|
IO.puts("❌ Error in spatial query: #{inspect(e)}")
|
||||||
end
|
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🎉 PostGIS testing completed!")
|
||||||
IO.puts("\n📊 Summary:")
|
IO.puts("\n📊 Summary:")
|
||||||
IO.puts(" - PostGIS extension is enabled")
|
IO.puts(" - PostGIS extension is enabled")
|
||||||
IO.puts(" - Location column with geometry type exists")
|
IO.puts(" - Location column with geometry type exists")
|
||||||
IO.puts(" - Spatial indexes are created")
|
IO.puts(" - Spatial indexes are created")
|
||||||
IO.puts(" - Basic spatial functions are working")
|
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(" - Data migration from lat/lon to PostGIS geometry completed")
|
||||||
IO.puts("\n🚀 Your APRS application is now ready for efficient spatial queries!")
|
IO.puts("\n🚀 Your APRS application is now ready for efficient spatial queries!")
|
||||||
|
|
||||||
# Clean up
|
|
||||||
Aprs.Repo.stop()
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue