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