account for gps drift
This commit is contained in:
parent
76c871ff39
commit
ad88e34ab6
4 changed files with 318 additions and 0 deletions
50
lib/aprsme/geo_utils.ex
Normal file
50
lib/aprsme/geo_utils.ex
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
defmodule Aprsme.GeoUtils do
|
||||
@moduledoc """
|
||||
Geographic utility functions for calculating distances and related operations.
|
||||
"""
|
||||
|
||||
@earth_radius_meters 6_371_000
|
||||
|
||||
@doc """
|
||||
Calculate the Haversine distance between two points in meters.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Aprsme.GeoUtils.haversine_distance(33.16961, -96.4921, 33.16962, -96.4921)
|
||||
1.11
|
||||
"""
|
||||
def haversine_distance(lat1, lon1, lat2, lon2)
|
||||
when is_number(lat1) and is_number(lon1) and is_number(lat2) and is_number(lon2) do
|
||||
# Convert to radians
|
||||
lat1_rad = lat1 * :math.pi() / 180
|
||||
lat2_rad = lat2 * :math.pi() / 180
|
||||
dlat_rad = (lat2 - lat1) * :math.pi() / 180
|
||||
dlon_rad = (lon2 - lon1) * :math.pi() / 180
|
||||
|
||||
# Haversine formula
|
||||
a =
|
||||
:math.sin(dlat_rad / 2) * :math.sin(dlat_rad / 2) +
|
||||
:math.cos(lat1_rad) * :math.cos(lat2_rad) *
|
||||
:math.sin(dlon_rad / 2) * :math.sin(dlon_rad / 2)
|
||||
|
||||
c = 2 * :math.atan2(:math.sqrt(a), :math.sqrt(1 - a))
|
||||
|
||||
# Distance in meters
|
||||
@earth_radius_meters * c
|
||||
end
|
||||
|
||||
def haversine_distance(_, _, _, _), do: nil
|
||||
|
||||
@doc """
|
||||
Check if the distance between two points exceeds a minimum threshold.
|
||||
This helps filter out GPS drift and insignificant movements.
|
||||
|
||||
Default threshold is 10 meters to account for typical GPS accuracy.
|
||||
"""
|
||||
def significant_movement?(lat1, lon1, lat2, lon2, threshold_meters \\ 10) do
|
||||
case haversine_distance(lat1, lon1, lat2, lon2) do
|
||||
nil -> false
|
||||
distance -> distance > threshold_meters
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -8,6 +8,7 @@ defmodule AprsmeWeb.MapLive.Index do
|
|||
import AprsmeWeb.TimeHelpers, only: [time_ago_in_words: 1]
|
||||
import Phoenix.LiveView, only: [connected?: 1, push_event: 3, push_navigate: 2, push_patch: 2]
|
||||
|
||||
alias Aprsme.GeoUtils
|
||||
alias Aprsme.Packets.Clustering
|
||||
alias AprsmeWeb.Endpoint
|
||||
alias AprsmeWeb.MapLive.MapHelpers
|
||||
|
|
@ -638,6 +639,23 @@ defmodule AprsmeWeb.MapLive.Index do
|
|||
should_add_marker?(lat, lon, callsign_key, socket) ->
|
||||
handle_valid_postgres_packet(packet, lat, lon, socket)
|
||||
|
||||
should_update_marker?(lat, lon, callsign_key, socket) ->
|
||||
# Marker exists and is within bounds - check if there's significant movement
|
||||
existing_packet = socket.assigns.visible_packets[callsign_key]
|
||||
{existing_lat, existing_lon, _} = MapHelpers.get_coordinates(existing_packet)
|
||||
|
||||
# Check if we have valid existing coordinates
|
||||
if is_number(existing_lat) and is_number(existing_lon) and
|
||||
GeoUtils.significant_movement?(existing_lat, existing_lon, lat, lon, 15) do
|
||||
# Significant movement detected (more than 15 meters), update the marker
|
||||
handle_valid_postgres_packet(packet, lat, lon, socket)
|
||||
else
|
||||
# Just GPS drift or invalid coordinates, update the packet data but don't send visual update
|
||||
new_visible_packets = Map.put(socket.assigns.visible_packets, callsign_key, packet)
|
||||
socket = assign(socket, visible_packets: new_visible_packets)
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
true ->
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
|
@ -655,6 +673,12 @@ defmodule AprsmeWeb.MapLive.Index do
|
|||
MapHelpers.within_bounds?(%{lat: lat, lon: lon}, socket.assigns.map_bounds)
|
||||
end
|
||||
|
||||
defp should_update_marker?(lat, lon, callsign_key, socket) do
|
||||
!is_nil(lat) and !is_nil(lon) and
|
||||
Map.has_key?(socket.assigns.visible_packets, callsign_key) and
|
||||
MapHelpers.within_bounds?(%{lat: lat, lon: lon}, socket.assigns.map_bounds)
|
||||
end
|
||||
|
||||
defp remove_marker_from_map(callsign_key, socket) do
|
||||
socket = push_event(socket, "remove_marker", %{id: callsign_key})
|
||||
new_visible_packets = Map.delete(socket.assigns.visible_packets, callsign_key)
|
||||
|
|
|
|||
81
test/aprsme/geo_utils_test.exs
Normal file
81
test/aprsme/geo_utils_test.exs
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
defmodule Aprsme.GeoUtilsTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias Aprsme.GeoUtils
|
||||
|
||||
describe "haversine_distance/4" do
|
||||
test "calculates zero distance for same point" do
|
||||
assert GeoUtils.haversine_distance(33.16961, -96.4921, 33.16961, -96.4921) == 0.0
|
||||
end
|
||||
|
||||
test "calculates small distances accurately" do
|
||||
# About 1.1 meters north
|
||||
distance = GeoUtils.haversine_distance(33.16961, -96.4921, 33.16962, -96.4921)
|
||||
assert_in_delta distance, 1.11, 0.1
|
||||
end
|
||||
|
||||
test "calculates GPS drift-like distances" do
|
||||
# Typical GPS drift of ~5 meters
|
||||
distance = GeoUtils.haversine_distance(33.16961, -96.4921, 33.169655, -96.4921)
|
||||
assert_in_delta distance, 5.0, 0.5
|
||||
end
|
||||
|
||||
test "calculates larger distances" do
|
||||
# About 100 meters
|
||||
distance = GeoUtils.haversine_distance(33.16961, -96.4921, 33.17061, -96.4921)
|
||||
assert_in_delta distance, 111.2, 1.0
|
||||
end
|
||||
|
||||
test "handles nil inputs" do
|
||||
assert GeoUtils.haversine_distance(nil, -96.4921, 33.16962, -96.4921) == nil
|
||||
assert GeoUtils.haversine_distance(33.16961, nil, 33.16962, -96.4921) == nil
|
||||
assert GeoUtils.haversine_distance(33.16961, -96.4921, nil, -96.4921) == nil
|
||||
assert GeoUtils.haversine_distance(33.16961, -96.4921, 33.16962, nil) == nil
|
||||
end
|
||||
|
||||
test "handles non-numeric inputs" do
|
||||
assert GeoUtils.haversine_distance("33.16961", -96.4921, 33.16962, -96.4921) == nil
|
||||
assert GeoUtils.haversine_distance(33.16961, "invalid", 33.16962, -96.4921) == nil
|
||||
end
|
||||
end
|
||||
|
||||
describe "significant_movement?/5" do
|
||||
test "returns false for no movement" do
|
||||
refute GeoUtils.significant_movement?(33.16961, -96.4921, 33.16961, -96.4921)
|
||||
end
|
||||
|
||||
test "returns false for GPS drift under default threshold" do
|
||||
# About 5 meters - typical GPS drift
|
||||
refute GeoUtils.significant_movement?(33.16961, -96.4921, 33.169655, -96.4921)
|
||||
end
|
||||
|
||||
test "returns true for movement over default threshold" do
|
||||
# About 11 meters
|
||||
assert GeoUtils.significant_movement?(33.16961, -96.4921, 33.1697, -96.4921)
|
||||
end
|
||||
|
||||
test "respects custom threshold" do
|
||||
# About 5 meters movement
|
||||
lat1 = 33.16961
|
||||
lon1 = -96.4921
|
||||
lat2 = 33.169655
|
||||
lon2 = -96.4921
|
||||
|
||||
# With 3 meter threshold, should be significant
|
||||
assert GeoUtils.significant_movement?(lat1, lon1, lat2, lon2, 3)
|
||||
|
||||
# With 10 meter threshold (default), should not be significant
|
||||
refute GeoUtils.significant_movement?(lat1, lon1, lat2, lon2, 10)
|
||||
|
||||
# With 20 meter threshold, definitely not significant
|
||||
refute GeoUtils.significant_movement?(lat1, lon1, lat2, lon2, 20)
|
||||
end
|
||||
|
||||
test "handles nil inputs" do
|
||||
refute GeoUtils.significant_movement?(nil, -96.4921, 33.16962, -96.4921)
|
||||
refute GeoUtils.significant_movement?(33.16961, nil, 33.16962, -96.4921)
|
||||
refute GeoUtils.significant_movement?(33.16961, -96.4921, nil, -96.4921)
|
||||
refute GeoUtils.significant_movement?(33.16961, -96.4921, 33.16962, nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
163
test/aprsme_web/live/map_live/movement_test.exs
Normal file
163
test/aprsme_web/live/map_live/movement_test.exs
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
defmodule AprsmeWeb.MapLive.MovementTest do
|
||||
use AprsmeWeb.ConnCase, async: false
|
||||
|
||||
import Mox
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
alias Aprsme.GeoUtils
|
||||
|
||||
setup :verify_on_exit!
|
||||
|
||||
describe "GPS drift filtering" do
|
||||
setup do
|
||||
# Mock the Packets module to return empty results for historical queries
|
||||
stub(Aprsme.PacketsMock, :get_recent_packets_optimized, fn _args -> [] end)
|
||||
:ok
|
||||
end
|
||||
|
||||
test "does not update marker for GPS drift", %{conn: conn} do
|
||||
# Start with initial location
|
||||
{:ok, view, _html} = live(conn, "/")
|
||||
|
||||
# First update the map state to set zoom level
|
||||
assert render_hook(view, "update_map_state", %{
|
||||
"center" => %{"lat" => 33.16961, "lng" => -96.4921},
|
||||
"zoom" => 9
|
||||
})
|
||||
|
||||
# Then update bounds
|
||||
bounds_params = %{
|
||||
"bounds" => %{
|
||||
"north" => 33.18,
|
||||
"south" => 33.16,
|
||||
"east" => -96.48,
|
||||
"west" => -96.50
|
||||
}
|
||||
}
|
||||
|
||||
assert render_hook(view, "bounds_changed", bounds_params)
|
||||
:timer.sleep(100)
|
||||
|
||||
# Simulate initial packet
|
||||
initial_packet = %{
|
||||
"id" => "TEST-1",
|
||||
"sender" => "TEST-1",
|
||||
"base_callsign" => "TEST",
|
||||
"lat" => 33.16961,
|
||||
"lon" => -96.4921,
|
||||
"has_position" => true,
|
||||
"received_at" => DateTime.utc_now()
|
||||
}
|
||||
|
||||
send(view.pid, {:postgres_packet, initial_packet})
|
||||
|
||||
# Wait for the initial packet to be processed
|
||||
:timer.sleep(100)
|
||||
|
||||
# Simulate GPS drift (5 meters movement)
|
||||
drift_packet = %{
|
||||
"id" => "TEST-1",
|
||||
"sender" => "TEST-1",
|
||||
"base_callsign" => "TEST",
|
||||
# About 5 meters north
|
||||
"lat" => 33.169655,
|
||||
"lon" => -96.4921,
|
||||
"has_position" => true,
|
||||
"received_at" => DateTime.utc_now()
|
||||
}
|
||||
|
||||
# Send the drift packet
|
||||
send(view.pid, {:postgres_packet, drift_packet})
|
||||
|
||||
# The view should not push a new_packet event for GPS drift
|
||||
refute_push_event(view, "new_packet", %{id: "TEST-1"}, 200)
|
||||
end
|
||||
|
||||
test "updates marker for significant movement", %{conn: conn} do
|
||||
# Start with initial location
|
||||
{:ok, view, _html} = live(conn, "/")
|
||||
|
||||
# First update the map state to set zoom level
|
||||
assert render_hook(view, "update_map_state", %{
|
||||
"center" => %{"lat" => 33.16961, "lng" => -96.4921},
|
||||
"zoom" => 9
|
||||
})
|
||||
|
||||
# Then update bounds
|
||||
bounds_params = %{
|
||||
"bounds" => %{
|
||||
"north" => 33.18,
|
||||
"south" => 33.16,
|
||||
"east" => -96.48,
|
||||
"west" => -96.50
|
||||
}
|
||||
}
|
||||
|
||||
assert render_hook(view, "bounds_changed", bounds_params)
|
||||
:timer.sleep(100)
|
||||
|
||||
# Simulate initial packet
|
||||
initial_packet = %{
|
||||
"id" => "TEST-2",
|
||||
"sender" => "TEST-2",
|
||||
"base_callsign" => "TEST",
|
||||
"lat" => 33.16961,
|
||||
"lon" => -96.4921,
|
||||
"has_position" => true,
|
||||
"received_at" => DateTime.utc_now()
|
||||
}
|
||||
|
||||
send(view.pid, {:postgres_packet, initial_packet})
|
||||
|
||||
# Wait for the initial packet to be processed
|
||||
:timer.sleep(100)
|
||||
|
||||
# Simulate significant movement (20+ meters)
|
||||
moved_packet = %{
|
||||
"id" => "TEST-2",
|
||||
"sender" => "TEST-2",
|
||||
"base_callsign" => "TEST",
|
||||
# About 20 meters north
|
||||
"lat" => 33.16980,
|
||||
"lon" => -96.4921,
|
||||
"has_position" => true,
|
||||
"received_at" => DateTime.utc_now()
|
||||
}
|
||||
|
||||
# Send the moved packet
|
||||
send(view.pid, {:postgres_packet, moved_packet})
|
||||
|
||||
# The view should push a new_packet event for significant movement
|
||||
assert_push_event(view, "new_packet", %{"lat" => 33.1698}, 500)
|
||||
end
|
||||
end
|
||||
|
||||
describe "distance calculations" do
|
||||
test "calculates correct distances for GPS drift scenarios" do
|
||||
# Example from the URL provided
|
||||
lat1 = 33.16961
|
||||
lon1 = -96.4921
|
||||
|
||||
# Tiny drift - should be filtered
|
||||
lat2 = 33.169615
|
||||
lon2 = -96.492095
|
||||
distance = GeoUtils.haversine_distance(lat1, lon1, lat2, lon2)
|
||||
# Less than 10 meters
|
||||
assert distance < 10
|
||||
|
||||
# Slightly larger drift
|
||||
lat3 = 33.16965
|
||||
lon3 = -96.4921
|
||||
distance2 = GeoUtils.haversine_distance(lat1, lon1, lat3, lon3)
|
||||
# Still GPS drift range
|
||||
assert distance2 < 10
|
||||
|
||||
# Actual movement
|
||||
lat4 = 33.17000
|
||||
lon4 = -96.4921
|
||||
distance3 = GeoUtils.haversine_distance(lat1, lon1, lat4, lon4)
|
||||
# Significant movement
|
||||
assert distance3 > 20
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Reference in a new issue