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 AprsmeWeb.TimeHelpers, only: [time_ago_in_words: 1]
|
||||||
import Phoenix.LiveView, only: [connected?: 1, push_event: 3, push_navigate: 2, push_patch: 2]
|
import Phoenix.LiveView, only: [connected?: 1, push_event: 3, push_navigate: 2, push_patch: 2]
|
||||||
|
|
||||||
|
alias Aprsme.GeoUtils
|
||||||
alias Aprsme.Packets.Clustering
|
alias Aprsme.Packets.Clustering
|
||||||
alias AprsmeWeb.Endpoint
|
alias AprsmeWeb.Endpoint
|
||||||
alias AprsmeWeb.MapLive.MapHelpers
|
alias AprsmeWeb.MapLive.MapHelpers
|
||||||
|
|
@ -638,6 +639,23 @@ defmodule AprsmeWeb.MapLive.Index do
|
||||||
should_add_marker?(lat, lon, callsign_key, socket) ->
|
should_add_marker?(lat, lon, callsign_key, socket) ->
|
||||||
handle_valid_postgres_packet(packet, lat, lon, 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 ->
|
true ->
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
|
|
@ -655,6 +673,12 @@ defmodule AprsmeWeb.MapLive.Index do
|
||||||
MapHelpers.within_bounds?(%{lat: lat, lon: lon}, socket.assigns.map_bounds)
|
MapHelpers.within_bounds?(%{lat: lat, lon: lon}, socket.assigns.map_bounds)
|
||||||
end
|
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
|
defp remove_marker_from_map(callsign_key, socket) do
|
||||||
socket = push_event(socket, "remove_marker", %{id: callsign_key})
|
socket = push_event(socket, "remove_marker", %{id: callsign_key})
|
||||||
new_visible_packets = Map.delete(socket.assigns.visible_packets, 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