diff --git a/assets/js/hooks/info_map.js b/assets/js/hooks/info_map.js
index 9fdfba9..285877d 100644
--- a/assets/js/hooks/info_map.js
+++ b/assets/js/hooks/info_map.js
@@ -11,7 +11,9 @@ export const InfoMap = {
const symbolHtml = this.el.dataset.symbolHtml;
// Validate coordinates
+ const callsign = this.el.dataset.callsign;
if (isNaN(lat) || isNaN(lon)) {
+ console.warn(`InfoMap: Invalid coordinates lat=${lat}, lon=${lon} for ${callsign} during update`);
return;
}
@@ -61,10 +63,16 @@ export const InfoMap = {
// Validate coordinates
if (isNaN(lat) || isNaN(lon)) {
- console.error("Invalid coordinates for InfoMap");
+ console.warn(`InfoMap: Invalid coordinates lat=${lat}, lon=${lon} for ${callsign}`);
return;
}
+ // Hide loading spinner
+ const loadingEl = this.el.querySelector(`#${this.el.id}-loading`);
+ if (loadingEl) {
+ loadingEl.style.display = 'none';
+ }
+
// Initialize the map
try {
this.map = L.map(this.el, {
diff --git a/config/config.exs b/config/config.exs
index e705f8e..250d640 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -37,6 +37,11 @@ config :aprsme, :cleanup_scheduler,
# 6 hours in milliseconds
interval: 6 * 60 * 60 * 1000
+# Configure position tracking sensitivity
+config :aprsme, :position_tracking,
+ # Position change threshold in degrees (~100 meters at equator)
+ change_threshold: 0.001
+
config :aprsme,
ecto_repos: [Aprsme.Repo]
diff --git a/lib/aprsme_web/components/info_map_component.ex b/lib/aprsme_web/components/info_map_component.ex
index 15f9406..8b61f05 100644
--- a/lib/aprsme_web/components/info_map_component.ex
+++ b/lib/aprsme_web/components/info_map_component.ex
@@ -28,7 +28,7 @@ defmodule AprsmeWeb.Components.InfoMapComponent do
data-callsign={@callsign}
data-symbol-html={@symbol_html}
>
-
diff --git a/lib/aprsme_web/live/info_live/show.ex b/lib/aprsme_web/live/info_live/show.ex
index e52c52b..ff08797 100644
--- a/lib/aprsme_web/live/info_live/show.ex
+++ b/lib/aprsme_web/live/info_live/show.ex
@@ -67,27 +67,71 @@ defmodule AprsmeWeb.InfoLive.Show do
"InfoLive received packet update for #{socket.assigns.callsign}: #{inspect(Map.get(incoming_packet, "raw_packet"))}"
)
- # Refresh all data when new packet arrives
- packet = get_latest_packet(socket.assigns.callsign)
- packet = if packet, do: SharedPacketHandler.enrich_with_device_info(packet)
- # Get locale from socket assigns
- locale = Map.get(socket.assigns, :locale, "en")
- neighbors = get_neighbors(packet, socket.assigns.callsign, locale)
- has_weather_packets = PacketUtils.has_weather_packets?(socket.assigns.callsign)
- other_ssids = get_other_ssids(socket.assigns.callsign)
- heard_by_stations = get_heard_by_stations(socket.assigns.callsign, locale)
- stations_heard_by = get_stations_heard_by(socket.assigns.callsign, locale)
+ # Get the new packet data
+ new_packet = get_latest_packet(socket.assigns.callsign)
+ new_packet = if new_packet, do: SharedPacketHandler.enrich_with_device_info(new_packet)
- socket =
- socket
- |> assign(:packet, packet)
- |> assign(:neighbors, neighbors)
- |> assign(:has_weather_packets, has_weather_packets)
- |> assign(:other_ssids, other_ssids)
- |> assign(:heard_by_stations, heard_by_stations)
- |> assign(:stations_heard_by, stations_heard_by)
+ current_packet = socket.assigns.packet
- {:noreply, socket}
+ # Check if this is a position update by comparing location and other key fields
+ position_changed = position_changed?(current_packet, new_packet)
+
+ if position_changed do
+ # Only update position-related data if position changed
+ # Get locale from socket assigns
+ locale = Map.get(socket.assigns, :locale, "en")
+ neighbors = get_neighbors(new_packet, socket.assigns.callsign, locale)
+
+ socket =
+ socket
+ |> assign(:packet, new_packet)
+ |> assign(:neighbors, neighbors)
+
+ {:noreply, socket}
+ else
+ # Just update the packet data (for timestamp, comment, etc.) without affecting neighbors
+ socket = assign(socket, :packet, new_packet)
+
+ {:noreply, socket}
+ end
+ end
+
+ defp position_changed?(nil, _new_packet), do: true
+ defp position_changed?(_current_packet, nil), do: false
+
+ defp position_changed?(current_packet, new_packet) do
+ # Compare lat/lon to see if position actually changed
+ current_lat = to_float_safe(current_packet.lat)
+ current_lon = to_float_safe(current_packet.lon)
+ new_lat = to_float_safe(new_packet.lat)
+ new_lon = to_float_safe(new_packet.lon)
+
+ # If any coordinate is invalid, consider it a change
+ case {current_lat, current_lon, new_lat, new_lon} do
+ {nil, _, _, _} ->
+ true
+
+ {_, nil, _, _} ->
+ true
+
+ {_, _, nil, _} ->
+ true
+
+ {_, _, _, nil} ->
+ true
+
+ {curr_lat, curr_lon, new_lat, new_lon} ->
+ # Consider position changed if coordinates differ by more than the configured threshold
+ threshold = Application.get_env(:aprsme, :position_tracking, [])[:change_threshold] || 0.001
+ lat_diff = abs(curr_lat - new_lat)
+ lon_diff = abs(curr_lon - new_lon)
+ lat_diff > threshold or lon_diff > threshold
+ end
+ end
+
+ # Expose for testing
+ if Mix.env() == :test do
+ def position_changed_for_test(current, new), do: position_changed?(current, new)
end
defp get_latest_packet(callsign) do
@@ -158,6 +202,10 @@ defmodule AprsmeWeb.InfoLive.Show do
r * c
end
+ # Safe float conversion that preserves nil for invalid coordinates
+ defp to_float_safe(value), do: EncodingUtils.to_float(value)
+
+ # Legacy float conversion that defaults to 0.0 for backward compatibility
defp to_float(value), do: EncodingUtils.to_float(value) || 0.0
defp format_timestamp_for_display(packet) do
diff --git a/test/aprsme_web/live/info_live/position_change_test.exs b/test/aprsme_web/live/info_live/position_change_test.exs
new file mode 100644
index 0000000..29d6cce
--- /dev/null
+++ b/test/aprsme_web/live/info_live/position_change_test.exs
@@ -0,0 +1,72 @@
+defmodule AprsmeWeb.InfoLive.PositionChangeTest do
+ @moduledoc """
+ Tests for position change detection in InfoLive
+ """
+ use ExUnit.Case, async: true
+
+ alias AprsmeWeb.InfoLive.Show
+
+ describe "position_changed?/2" do
+ test "returns true when current packet is nil" do
+ new_packet = %{lat: 40.7128, lon: -74.0060}
+ assert Show.position_changed_for_test(nil, new_packet)
+ end
+
+ test "returns false when new packet is nil" do
+ current_packet = %{lat: 40.7128, lon: -74.0060}
+ refute Show.position_changed_for_test(current_packet, nil)
+ end
+
+ test "returns true when position changed significantly" do
+ current_packet = %{lat: 40.7128, lon: -74.0060}
+ # ~0.8km difference
+ new_packet = %{lat: 40.7200, lon: -74.0060}
+ assert Show.position_changed_for_test(current_packet, new_packet)
+ end
+
+ test "returns false when position changed minimally" do
+ current_packet = %{lat: 40.7128, lon: -74.0060}
+ # ~10m difference
+ new_packet = %{lat: 40.7129, lon: -74.0061}
+ refute Show.position_changed_for_test(current_packet, new_packet)
+ end
+
+ test "returns true when longitude changed significantly" do
+ current_packet = %{lat: 40.7128, lon: -74.0060}
+ # longitude changed
+ new_packet = %{lat: 40.7128, lon: -74.0200}
+ assert Show.position_changed_for_test(current_packet, new_packet)
+ end
+
+ test "handles Decimal values" do
+ current_packet = %{lat: Decimal.new("40.7128"), lon: Decimal.new("-74.0060")}
+ new_packet = %{lat: Decimal.new("40.7200"), lon: Decimal.new("-74.0060")}
+ assert Show.position_changed_for_test(current_packet, new_packet)
+ end
+
+ test "handles coordinates at equator/prime meridian (0.0 is valid)" do
+ current_packet = %{lat: 0.0, lon: 0.0}
+ # Small change from valid 0.0
+ new_packet = %{lat: 0.0005, lon: 0.0}
+ refute Show.position_changed_for_test(current_packet, new_packet)
+ end
+
+ test "detects change when coordinates become invalid (nil)" do
+ current_packet = %{lat: 40.7128, lon: -74.0060}
+ new_packet = %{lat: nil, lon: -74.0060}
+ assert Show.position_changed_for_test(current_packet, new_packet)
+ end
+
+ test "detects change when coordinates become valid from invalid" do
+ current_packet = %{lat: nil, lon: -74.0060}
+ new_packet = %{lat: 40.7128, lon: -74.0060}
+ assert Show.position_changed_for_test(current_packet, new_packet)
+ end
+
+ test "handles both coordinates being invalid" do
+ current_packet = %{lat: nil, lon: nil}
+ new_packet = %{lat: nil, lon: nil}
+ assert Show.position_changed_for_test(current_packet, new_packet)
+ end
+ end
+end