main map improvements
This commit is contained in:
parent
532a81c0b4
commit
fa89712587
8 changed files with 590 additions and 21 deletions
112
assets/js/map.ts
112
assets/js/map.ts
|
|
@ -271,6 +271,9 @@ let MapAPRSMap = {
|
|||
|
||||
// Track marker states to prevent unnecessary operations
|
||||
self.markerStates = new Map<string, MarkerState>();
|
||||
|
||||
// Store RF path lines for hover visualization
|
||||
self.rfPathLines = [];
|
||||
|
||||
// Force initial size calculation
|
||||
try {
|
||||
|
|
@ -897,6 +900,76 @@ let MapAPRSMap = {
|
|||
}
|
||||
});
|
||||
|
||||
// Handle drawing RF path lines when hovering over a station
|
||||
self.handleEvent("draw_rf_path", (data: {
|
||||
station_lat: number,
|
||||
station_lng: number,
|
||||
path_stations: Array<{callsign: string, lat: number, lng: number}>
|
||||
}) => {
|
||||
if (!self.map || !data.path_stations || data.path_stations.length === 0) return;
|
||||
|
||||
// Clear any existing path lines
|
||||
if (self.rfPathLines) {
|
||||
self.rfPathLines.forEach(line => self.map!.removeLayer(line));
|
||||
}
|
||||
self.rfPathLines = [];
|
||||
|
||||
// Draw lines from station to each digipeater/igate in sequence
|
||||
let prevLat = data.station_lat;
|
||||
let prevLng = data.station_lng;
|
||||
|
||||
data.path_stations.forEach((station, index) => {
|
||||
// Draw line from previous position to this station
|
||||
const line = L.polyline(
|
||||
[[prevLat, prevLng], [station.lat, station.lng]],
|
||||
{
|
||||
color: '#FF6B6B',
|
||||
weight: 3,
|
||||
opacity: 0.8,
|
||||
dashArray: index === 0 ? null : '5, 10', // Solid line for first hop, dashed for subsequent
|
||||
}
|
||||
);
|
||||
|
||||
line.addTo(self.map!);
|
||||
self.rfPathLines.push(line);
|
||||
|
||||
// Add a small circle marker at the digipeater/igate location
|
||||
const circle = L.circleMarker([station.lat, station.lng], {
|
||||
radius: 6,
|
||||
fillColor: '#2563eb',
|
||||
color: '#fff',
|
||||
weight: 2,
|
||||
opacity: 1,
|
||||
fillOpacity: 0.8
|
||||
});
|
||||
|
||||
circle.bindTooltip(station.callsign, {
|
||||
permanent: false,
|
||||
direction: 'top',
|
||||
offset: [0, -10]
|
||||
});
|
||||
|
||||
circle.addTo(self.map!);
|
||||
self.rfPathLines.push(circle);
|
||||
|
||||
// Update prev position for next line
|
||||
prevLat = station.lat;
|
||||
prevLng = station.lng;
|
||||
});
|
||||
});
|
||||
|
||||
// Handle clearing RF path lines
|
||||
self.handleEvent("clear_rf_path", () => {
|
||||
if (self.rfPathLines) {
|
||||
self.rfPathLines.forEach(line => {
|
||||
if (self.map && self.map.hasLayer(line)) {
|
||||
self.map.removeLayer(line);
|
||||
}
|
||||
});
|
||||
self.rfPathLines = [];
|
||||
}
|
||||
});
|
||||
|
||||
// Handle bounds-based marker filtering
|
||||
self.handleEvent("filter_markers_by_bounds", (data: { bounds: BoundsData }) => {
|
||||
if (data.bounds) {
|
||||
|
|
@ -1156,6 +1229,45 @@ let MapAPRSMap = {
|
|||
lng: lng,
|
||||
});
|
||||
});
|
||||
|
||||
// Handle marker hover for RF path visualization
|
||||
// Check for non-empty RF path (not TCPIP and not empty string)
|
||||
if (data.path && data.path.trim() !== "" && !data.path.includes("TCPIP")) {
|
||||
console.log("Adding hover handlers for RF packet:", data.callsign, "path:", data.path);
|
||||
marker.on("mouseover", () => {
|
||||
console.log("Marker hover start for:", data.callsign);
|
||||
// Check if LiveView is still connected before sending event
|
||||
if (self.pushEvent && !self.isDestroyed) {
|
||||
try {
|
||||
self.pushEvent("marker_hover_start", {
|
||||
id: data.id,
|
||||
callsign: data.callsign,
|
||||
path: data.path,
|
||||
lat: lat,
|
||||
lng: lng,
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn("Failed to send marker_hover_start event:", error);
|
||||
}
|
||||
} else {
|
||||
console.warn("LiveView not connected, cannot send hover event");
|
||||
}
|
||||
});
|
||||
|
||||
marker.on("mouseout", () => {
|
||||
console.log("Marker hover end for:", data.callsign);
|
||||
// Check if LiveView is still connected before sending event
|
||||
if (self.pushEvent && !self.isDestroyed) {
|
||||
try {
|
||||
self.pushEvent("marker_hover_end", {
|
||||
id: data.id,
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn("Failed to send marker_hover_end event:", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Mark historical markers for identification
|
||||
|
|
|
|||
4
assets/js/types/map.d.ts
vendored
4
assets/js/types/map.d.ts
vendored
|
|
@ -28,6 +28,9 @@ export interface LiveViewHookContext {
|
|||
popupNavigationHandler?: (e: Event) => void;
|
||||
moveEndHandler?: () => void;
|
||||
zoomEndHandler?: () => void;
|
||||
rfPathLines?: any[]; // Array of Leaflet polylines and markers for RF path visualization
|
||||
trailLayer?: LayerGroup;
|
||||
markerClusterGroup?: any;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
|
|
@ -48,6 +51,7 @@ export interface MarkerData {
|
|||
callsign_group?: string;
|
||||
symbol_html?: string;
|
||||
openPopup?: boolean;
|
||||
path?: string;
|
||||
}
|
||||
|
||||
export interface BoundsData {
|
||||
|
|
|
|||
|
|
@ -424,8 +424,13 @@ defmodule Aprsme.Packets do
|
|||
@impl true
|
||||
@spec get_recent_packets(map()) :: [struct()]
|
||||
def get_recent_packets(opts \\ %{}) do
|
||||
# Always limit to the last 24 hours for more symbol variety
|
||||
opts_with_time = Map.put(opts, :hours_back, 24)
|
||||
# Use provided hours_back or default to 24 hours
|
||||
opts_with_time =
|
||||
if Map.has_key?(opts, :hours_back) do
|
||||
opts
|
||||
else
|
||||
Map.put(opts, :hours_back, 24)
|
||||
end
|
||||
|
||||
opts_with_time
|
||||
|> QueryBuilder.recent_position_packets()
|
||||
|
|
@ -439,8 +444,9 @@ defmodule Aprsme.Packets do
|
|||
@impl true
|
||||
@spec get_recent_packets_optimized(map()) :: [struct()]
|
||||
def get_recent_packets_optimized(opts \\ %{}) do
|
||||
# Always limit to the last 24 hours for more symbol variety
|
||||
one_hour_ago = TimeUtils.one_day_ago()
|
||||
# Use hours_back from opts if provided, otherwise default to 24 hours
|
||||
hours_back = Map.get(opts, :hours_back, 24)
|
||||
time_ago = DateTime.add(DateTime.utc_now(), -hours_back * 3600, :second)
|
||||
limit = Map.get(opts, :limit, 200)
|
||||
offset = Map.get(opts, :offset, 0)
|
||||
|
||||
|
|
@ -448,7 +454,7 @@ defmodule Aprsme.Packets do
|
|||
base_query =
|
||||
from(p in Packet,
|
||||
where: p.has_position == true,
|
||||
where: p.received_at >= ^one_hour_ago
|
||||
where: p.received_at >= ^time_ago
|
||||
)
|
||||
|
||||
# Apply spatial and other filters BEFORE limiting
|
||||
|
|
|
|||
|
|
@ -752,13 +752,33 @@ defmodule AprsmeWeb.CoreComponents do
|
|||
attr :class, :string, default: ""
|
||||
attr :variant, :atom, values: [:horizontal, :vertical], default: :horizontal
|
||||
attr :current_user, :any, default: nil
|
||||
attr :map_state, :map, default: nil
|
||||
attr :tracked_callsign, :string, default: ""
|
||||
|
||||
def navigation(assigns) do
|
||||
# Build home URL with map state if available
|
||||
home_url =
|
||||
if assigns[:map_state] do
|
||||
base_url = "/?lat=#{assigns.map_state.lat}&lng=#{assigns.map_state.lng}&z=#{assigns.map_state.zoom}"
|
||||
|
||||
if assigns[:tracked_callsign] && assigns.tracked_callsign != "" do
|
||||
base_url <> "&call=#{assigns.tracked_callsign}"
|
||||
else
|
||||
base_url
|
||||
end
|
||||
else
|
||||
"/"
|
||||
end
|
||||
|
||||
assigns = assign(assigns, :home_url, home_url)
|
||||
|
||||
~H"""
|
||||
<%= if @variant == :horizontal do %>
|
||||
<ul class="menu menu-horizontal px-1">
|
||||
<li>
|
||||
<.link navigate="/" class="text-gray-900 hover:text-gray-700">{gettext("Home")}</.link>
|
||||
<.link navigate={@home_url} class="text-gray-900 hover:text-gray-700">
|
||||
{gettext("Home")}
|
||||
</.link>
|
||||
</li>
|
||||
<li><.link navigate="/api" class="text-gray-900 hover:text-gray-700">API</.link></li>
|
||||
<li>
|
||||
|
|
@ -791,7 +811,11 @@ defmodule AprsmeWeb.CoreComponents do
|
|||
<% end %>
|
||||
</ul>
|
||||
<% else %>
|
||||
<li><.link navigate="/" class="text-gray-900 hover:text-gray-700">{gettext("Home")}</.link></li>
|
||||
<li>
|
||||
<.link navigate={@home_url} class="text-gray-900 hover:text-gray-700">
|
||||
{gettext("Home")}
|
||||
</.link>
|
||||
</li>
|
||||
<li><.link navigate="/api" class="text-gray-900 hover:text-gray-700">API</.link></li>
|
||||
<li>
|
||||
<.link navigate="/about" class="text-gray-900 hover:text-gray-700">{gettext("About")}</.link>
|
||||
|
|
|
|||
|
|
@ -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.CachedQueries
|
||||
alias Aprsme.GeoUtils
|
||||
alias Aprsme.Packets.Clustering
|
||||
alias AprsmeWeb.Endpoint
|
||||
|
|
@ -92,7 +93,7 @@ defmodule AprsmeWeb.MapLive.Index do
|
|||
# Check for IP geolocation in session
|
||||
|
||||
# Check if URL params were explicitly provided (not just defaults)
|
||||
has_explicit_url_params = params["lat"] || params["lng"] || params["z"]
|
||||
has_explicit_url_params = !!(params["lat"] || params["lng"] || params["z"])
|
||||
|
||||
{map_center, map_zoom, should_skip_initial_url_update} =
|
||||
case session["ip_geolocation"] do
|
||||
|
|
@ -118,9 +119,6 @@ defmodule AprsmeWeb.MapLive.Index do
|
|||
# Initialize the flag to track if initial historical load is completed
|
||||
socket = assign(socket, initial_historical_completed: false)
|
||||
|
||||
# Calculate initial bounds based on center and zoom level
|
||||
initial_bounds = calculate_bounds_from_center_and_zoom(map_center, map_zoom)
|
||||
|
||||
if connected?(socket) do
|
||||
Endpoint.subscribe("aprs_messages")
|
||||
Phoenix.PubSub.subscribe(Aprsme.PubSub, "postgres:aprsme_packets")
|
||||
|
|
@ -129,12 +127,29 @@ defmodule AprsmeWeb.MapLive.Index do
|
|||
# Check for callsign parameter
|
||||
tracked_callsign = Map.get(params, "call", "")
|
||||
|
||||
# If tracking a callsign and no explicit map parameters, center on that callsign
|
||||
{final_map_center, final_map_zoom} =
|
||||
if tracked_callsign != "" and not has_explicit_url_params do
|
||||
case CachedQueries.get_latest_packet_for_callsign_cached(tracked_callsign) do
|
||||
%{lat: lat, lon: lon} when is_number(lat) and is_number(lon) ->
|
||||
{%{lat: lat, lng: lon}, 12}
|
||||
|
||||
_ ->
|
||||
{map_center, map_zoom}
|
||||
end
|
||||
else
|
||||
{map_center, map_zoom}
|
||||
end
|
||||
|
||||
# Calculate initial bounds based on final center and zoom level
|
||||
initial_bounds = calculate_bounds_from_center_and_zoom(final_map_center, final_map_zoom)
|
||||
|
||||
{:ok,
|
||||
assign(socket,
|
||||
map_ready: false,
|
||||
map_bounds: initial_bounds,
|
||||
map_center: map_center,
|
||||
map_zoom: map_zoom,
|
||||
map_center: final_map_center,
|
||||
map_zoom: final_map_zoom,
|
||||
should_skip_initial_url_update: should_skip_initial_url_update,
|
||||
visible_packets: %{},
|
||||
historical_packets: %{},
|
||||
|
|
@ -151,7 +166,7 @@ defmodule AprsmeWeb.MapLive.Index do
|
|||
all_packets: %{},
|
||||
station_popup_open: false,
|
||||
initial_bounds_loaded: false,
|
||||
needs_initial_historical_load: false
|
||||
needs_initial_historical_load: tracked_callsign != ""
|
||||
)}
|
||||
end
|
||||
|
||||
|
|
@ -302,11 +317,9 @@ defmodule AprsmeWeb.MapLive.Index do
|
|||
def handle_event("map_ready", _params, socket) do
|
||||
require Logger
|
||||
|
||||
# Mark map as ready and that we need to load historical packets
|
||||
socket =
|
||||
socket
|
||||
|> assign(map_ready: true)
|
||||
|> assign(needs_initial_historical_load: true)
|
||||
# Mark map as ready - preserve existing needs_initial_historical_load state
|
||||
# (it's already set correctly in mount based on whether we're tracking a callsign)
|
||||
socket = assign(socket, map_ready: true)
|
||||
|
||||
# If we have non-default center coordinates (e.g., from geolocation), apply them now
|
||||
socket =
|
||||
|
|
@ -334,6 +347,41 @@ defmodule AprsmeWeb.MapLive.Index do
|
|||
{:noreply, assign(socket, station_popup_open: true)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("marker_hover_start", %{"id" => _id, "path" => path, "lat" => lat, "lng" => lng}, socket) do
|
||||
require Logger
|
||||
|
||||
Logger.info("RF path hover start - path: #{inspect(path)}")
|
||||
|
||||
# Parse the path to find digipeater/igate stations
|
||||
path_stations = parse_rf_path(path)
|
||||
Logger.info("Parsed path stations: #{inspect(path_stations)}")
|
||||
|
||||
# Query for positions of path stations
|
||||
path_station_positions = get_path_station_positions(path_stations, socket)
|
||||
Logger.info("Found #{length(path_station_positions)} station positions")
|
||||
|
||||
# Send event to draw the RF path lines
|
||||
socket =
|
||||
if length(path_station_positions) > 0 do
|
||||
push_event(socket, "draw_rf_path", %{
|
||||
station_lat: lat,
|
||||
station_lng: lng,
|
||||
path_stations: path_station_positions
|
||||
})
|
||||
else
|
||||
socket
|
||||
end
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("marker_hover_end", _params, socket) do
|
||||
# Clear the RF path lines
|
||||
{:noreply, push_event(socket, "clear_rf_path", %{})}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("update_callsign", %{"callsign" => callsign}, socket) do
|
||||
{:noreply, assign(socket, overlay_callsign: callsign)}
|
||||
|
|
@ -1222,7 +1270,13 @@ defmodule AprsmeWeb.MapLive.Index do
|
|||
</svg>
|
||||
<span class="font-medium">{gettext("Navigation")}</span>
|
||||
</div>
|
||||
<.navigation variant={:vertical} class="text-sm" current_user={@current_user} />
|
||||
<.navigation
|
||||
variant={:vertical}
|
||||
class="text-sm"
|
||||
current_user={@current_user}
|
||||
map_state={%{lat: @map_center.lat, lng: @map_center.lng, zoom: @map_zoom}}
|
||||
tracked_callsign={@tracked_callsign}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Deployment Information -->
|
||||
|
|
@ -1461,6 +1515,7 @@ defmodule AprsmeWeb.MapLive.Index do
|
|||
"timestamp" => DateTime.to_unix(packet.received_at || DateTime.utc_now(), :millisecond),
|
||||
"historical" => !is_most_recent,
|
||||
"is_most_recent_for_callsign" => is_most_recent,
|
||||
"path" => packet.path || "",
|
||||
"popup" => build_simple_popup(packet, has_weather)
|
||||
}
|
||||
end
|
||||
|
|
@ -1671,6 +1726,10 @@ defmodule AprsmeWeb.MapLive.Index do
|
|||
Map.put(params, :callsign, socket.assigns.tracked_callsign)
|
||||
end
|
||||
|
||||
# Use the historical_hours setting from the UI dropdown
|
||||
historical_hours = String.to_integer(socket.assigns.historical_hours || "1")
|
||||
params = Map.put(params, :hours_back, historical_hours)
|
||||
|
||||
Aprsme.CachedQueries.get_recent_packets_cached(params)
|
||||
else
|
||||
# Fallback for testing
|
||||
|
|
@ -1993,4 +2052,77 @@ defmodule AprsmeWeb.MapLive.Index do
|
|||
socket
|
||||
end
|
||||
end
|
||||
|
||||
# Parse RF path string to extract digipeater/igate callsigns
|
||||
defp parse_rf_path(path) when is_binary(path) do
|
||||
# Split by comma and filter out empty strings
|
||||
path
|
||||
|> String.split(",")
|
||||
|> Enum.map(&String.trim/1)
|
||||
|> Enum.reject(&(&1 == ""))
|
||||
# Skip TCPIP entries and qA* entries (but preserve the actual digipeater/igate callsigns)
|
||||
|> Enum.reject(&String.contains?(&1, "TCPIP"))
|
||||
|> Enum.reject(&String.starts_with?(&1, "qA"))
|
||||
|> Enum.map(fn callsign ->
|
||||
# Remove any asterisk (used to mark heard stations) and WIDE patterns
|
||||
callsign
|
||||
|> String.replace("*", "")
|
||||
|> String.trim()
|
||||
end)
|
||||
# Filter out WIDE patterns (both WIDE1-1 and WIDE1 forms)
|
||||
|> Enum.reject(fn callsign ->
|
||||
String.starts_with?(callsign, "WIDE")
|
||||
end)
|
||||
|> Enum.reject(&(&1 == ""))
|
||||
|> Enum.uniq()
|
||||
end
|
||||
|
||||
defp parse_rf_path(_), do: []
|
||||
|
||||
# Get positions of path stations from database
|
||||
defp get_path_station_positions(callsigns, socket) when is_list(callsigns) do
|
||||
# Query for the most recent position of each station
|
||||
# Show complete RF path regardless of map bounds - only the originating station needs to be visible
|
||||
_bounds = socket.assigns.map_bounds
|
||||
|
||||
query = """
|
||||
WITH latest_positions AS (
|
||||
SELECT DISTINCT ON (sender)
|
||||
sender,
|
||||
lat,
|
||||
lon,
|
||||
received_at
|
||||
FROM packets
|
||||
WHERE
|
||||
sender = ANY($1::text[])
|
||||
AND lat IS NOT NULL
|
||||
AND lon IS NOT NULL
|
||||
AND received_at > NOW() - INTERVAL '7 days'
|
||||
ORDER BY sender, received_at DESC
|
||||
)
|
||||
SELECT sender, lat, lon
|
||||
FROM latest_positions
|
||||
ORDER BY array_position($1::text[], sender)
|
||||
"""
|
||||
|
||||
case Ecto.Adapters.SQL.query(
|
||||
Aprsme.Repo,
|
||||
query,
|
||||
[callsigns]
|
||||
) do
|
||||
{:ok, %{rows: rows}} ->
|
||||
Enum.map(rows, fn [callsign, lat, lon] ->
|
||||
%{
|
||||
callsign: callsign,
|
||||
lat: Decimal.to_float(lat),
|
||||
lng: Decimal.to_float(lon)
|
||||
}
|
||||
end)
|
||||
|
||||
{:error, _} ->
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
defp get_path_station_positions(_, _), do: []
|
||||
end
|
||||
|
|
|
|||
|
|
@ -268,6 +268,8 @@ defmodule AprsmeWeb.MapLive.PacketUtils do
|
|||
32
|
||||
)
|
||||
|
||||
path_value = get_packet_field(packet, :path, "")
|
||||
|
||||
%{
|
||||
"id" => packet_id,
|
||||
"callsign" => packet_info.callsign,
|
||||
|
|
@ -276,7 +278,7 @@ defmodule AprsmeWeb.MapLive.PacketUtils do
|
|||
"lat" => to_float(lat),
|
||||
"lng" => to_float(lon),
|
||||
"data_type" => to_string(get_packet_field(packet, :data_type, "unknown")),
|
||||
"path" => get_packet_field(packet, :path, ""),
|
||||
"path" => path_value,
|
||||
"comment" => packet_info.comment,
|
||||
"data_extended" => packet_info.safe_data_extended || %{},
|
||||
"symbol_table_id" => packet_info.symbol_table_id,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,14 @@ defmodule AprsmeWeb.TimeHelpers do
|
|||
@doc """
|
||||
Returns a human-readable string for how long ago the given DateTime was.
|
||||
"""
|
||||
def time_ago_in_words(nil), do: gettext("unknown time") <> " " <> gettext("ago")
|
||||
|
||||
def time_ago_in_words(%NaiveDateTime{} = naive_datetime) do
|
||||
# Convert NaiveDateTime to DateTime assuming UTC
|
||||
datetime = DateTime.from_naive!(naive_datetime, "Etc/UTC")
|
||||
time_ago_in_words(datetime)
|
||||
end
|
||||
|
||||
def time_ago_in_words(datetime) do
|
||||
now = DateTime.utc_now()
|
||||
diff_seconds = DateTime.diff(now, datetime, :second)
|
||||
|
|
|
|||
281
test/aprsme_web/live/map_live/rf_path_test.exs
Normal file
281
test/aprsme_web/live/map_live/rf_path_test.exs
Normal file
|
|
@ -0,0 +1,281 @@
|
|||
defmodule AprsmeWeb.MapLive.RfPathTest do
|
||||
use AprsmeWeb.ConnCase
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
alias Aprsme.Packet
|
||||
alias Aprsme.Repo
|
||||
alias AprsmeWeb.MapLive.Index
|
||||
|
||||
describe "RF path parsing" do
|
||||
test "parses RF paths correctly", %{conn: conn} do
|
||||
# Test the parse_rf_path function indirectly through module
|
||||
# Since it's a private function, we test the behavior
|
||||
|
||||
{:ok, _digi1} =
|
||||
Repo.insert(%Packet{
|
||||
sender: "K5GVL-10",
|
||||
base_callsign: "K5GVL",
|
||||
ssid: "10",
|
||||
lat: Decimal.new("33.1000"),
|
||||
lon: Decimal.new("-96.6000"),
|
||||
has_position: true,
|
||||
received_at: DateTime.truncate(DateTime.utc_now(), :second),
|
||||
data_type: "position",
|
||||
symbol_table_id: "#",
|
||||
symbol_code: "r"
|
||||
})
|
||||
|
||||
{:ok, view, _html} = live(conn, "/")
|
||||
|
||||
# Send a marker_hover_start event
|
||||
render_hook(view, "marker_hover_start", %{
|
||||
"id" => "test-1",
|
||||
"path" => "K5GVL-10*,WIDE1-1,WIDE2-1",
|
||||
"lat" => 33.2837,
|
||||
"lng" => -96.5728
|
||||
})
|
||||
|
||||
# The function should parse the path and try to find positions
|
||||
# We can't directly test the private function, but we can verify no errors occur
|
||||
assert view.module == Index
|
||||
end
|
||||
|
||||
test "parses complex RF paths with multiple stations", %{conn: conn} do
|
||||
# Create multiple stations that could be in the path
|
||||
{:ok, _station1} =
|
||||
Repo.insert(%Packet{
|
||||
sender: "N5ABC",
|
||||
base_callsign: "N5ABC",
|
||||
ssid: nil,
|
||||
lat: Decimal.new("33.2000"),
|
||||
lon: Decimal.new("-96.6000"),
|
||||
has_position: true,
|
||||
received_at: DateTime.truncate(DateTime.utc_now(), :second),
|
||||
data_type: "position"
|
||||
})
|
||||
|
||||
{:ok, _station2} =
|
||||
Repo.insert(%Packet{
|
||||
sender: "WB5DEF-1",
|
||||
base_callsign: "WB5DEF",
|
||||
ssid: "1",
|
||||
lat: Decimal.new("33.3000"),
|
||||
lon: Decimal.new("-96.7000"),
|
||||
has_position: true,
|
||||
received_at: DateTime.truncate(DateTime.utc_now(), :second),
|
||||
data_type: "position"
|
||||
})
|
||||
|
||||
{:ok, view, _html} = live(conn, "/")
|
||||
|
||||
# Test path with multiple digipeaters
|
||||
render_hook(view, "marker_hover_start", %{
|
||||
"id" => "test-complex",
|
||||
"path" => "N5ABC,WB5DEF-1,WIDE2-1,qAR,K5VOM-10",
|
||||
"lat" => 33.2837,
|
||||
"lng" => -96.5728
|
||||
})
|
||||
|
||||
# Should handle complex paths without errors
|
||||
assert view.module == Index
|
||||
end
|
||||
|
||||
test "handles paths with asterisks correctly", %{conn: conn} do
|
||||
{:ok, _station} =
|
||||
Repo.insert(%Packet{
|
||||
sender: "WA5VHU-8",
|
||||
base_callsign: "WA5VHU",
|
||||
ssid: "8",
|
||||
lat: Decimal.new("33.1500"),
|
||||
lon: Decimal.new("-96.5500"),
|
||||
has_position: true,
|
||||
received_at: DateTime.truncate(DateTime.utc_now(), :second),
|
||||
data_type: "position"
|
||||
})
|
||||
|
||||
{:ok, view, _html} = live(conn, "/")
|
||||
|
||||
# Test path with asterisks (used digipeaters)
|
||||
render_hook(view, "marker_hover_start", %{
|
||||
"id" => "test-asterisk",
|
||||
"path" => "WA5VHU-8,WIDE1*,WIDE2-1,qAR,K5VOM-10",
|
||||
"lat" => 33.2837,
|
||||
"lng" => -96.5728
|
||||
})
|
||||
|
||||
# Should handle asterisks without errors
|
||||
assert view.module == Index
|
||||
end
|
||||
|
||||
test "filters out TCPIP from paths", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, "/")
|
||||
|
||||
# Send a hover event with TCPIP in path
|
||||
render_hook(view, "marker_hover_start", %{
|
||||
"id" => "test-2",
|
||||
"path" => "TCPIP*,qAC,T2TEXAS",
|
||||
"lat" => 33.2837,
|
||||
"lng" => -96.5728
|
||||
})
|
||||
|
||||
# Should not crash and should filter out TCPIP
|
||||
assert view.module == Index
|
||||
end
|
||||
|
||||
test "handles empty paths gracefully", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, "/")
|
||||
|
||||
# Send hover event with empty path
|
||||
render_hook(view, "marker_hover_start", %{"id" => "test-3", "path" => "", "lat" => 33.2837, "lng" => -96.5728})
|
||||
|
||||
# Should not crash
|
||||
assert view.module == Index
|
||||
end
|
||||
|
||||
test "marker hover end event works", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, "/")
|
||||
|
||||
# Send hover end event
|
||||
render_hook(view, "marker_hover_end", %{"id" => "test-1"})
|
||||
|
||||
# Should not crash
|
||||
assert view.module == Index
|
||||
end
|
||||
end
|
||||
|
||||
describe "path station position queries" do
|
||||
setup do
|
||||
# Create test packets with positions
|
||||
{:ok, _digi1} =
|
||||
Repo.insert(%Packet{
|
||||
sender: "K5GVL-10",
|
||||
base_callsign: "K5GVL",
|
||||
ssid: "10",
|
||||
lat: Decimal.new("33.1000"),
|
||||
lon: Decimal.new("-96.6000"),
|
||||
has_position: true,
|
||||
received_at: DateTime.truncate(DateTime.utc_now(), :second),
|
||||
data_type: "position"
|
||||
})
|
||||
|
||||
{:ok, _digi2} =
|
||||
Repo.insert(%Packet{
|
||||
sender: "N5TXZ-10",
|
||||
base_callsign: "N5TXZ",
|
||||
ssid: "10",
|
||||
lat: Decimal.new("33.2000"),
|
||||
lon: Decimal.new("-96.5000"),
|
||||
has_position: true,
|
||||
received_at: DateTime.truncate(DateTime.utc_now(), :second),
|
||||
data_type: "position"
|
||||
})
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
test "finds positions for digipeaters in path", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, "/")
|
||||
|
||||
# Send hover event with known digipeaters
|
||||
render_hook(view, "marker_hover_start", %{
|
||||
"id" => "test-4",
|
||||
"path" => "K5GVL-10*,N5TXZ-10",
|
||||
"lat" => 33.2837,
|
||||
"lng" => -96.5728
|
||||
})
|
||||
|
||||
# The function should find these stations
|
||||
assert view.module == Index
|
||||
end
|
||||
end
|
||||
|
||||
describe "RF path with stations outside bounds" do
|
||||
setup do
|
||||
# Create test packets - one inside a small bounds, one outside
|
||||
# Station inside typical Texas bounds
|
||||
{:ok, _inside_station} =
|
||||
Repo.insert(%Packet{
|
||||
sender: "K5INSIDE-10",
|
||||
base_callsign: "K5INSIDE",
|
||||
ssid: "10",
|
||||
# Inside Texas area
|
||||
lat: Decimal.new("32.5000"),
|
||||
# Inside Texas area
|
||||
lon: Decimal.new("-96.5000"),
|
||||
has_position: true,
|
||||
received_at: DateTime.truncate(DateTime.utc_now(), :second),
|
||||
data_type: "position"
|
||||
})
|
||||
|
||||
# Station outside bounds (way outside in Boston area)
|
||||
{:ok, _outside_station} =
|
||||
Repo.insert(%Packet{
|
||||
sender: "W1OUTSIDE-10",
|
||||
base_callsign: "W1OUTSIDE",
|
||||
ssid: "10",
|
||||
# Boston area - far from Texas
|
||||
lat: Decimal.new("42.0000"),
|
||||
# Boston area - far from Texas
|
||||
lon: Decimal.new("-71.0000"),
|
||||
has_position: true,
|
||||
received_at: DateTime.truncate(DateTime.utc_now(), :second),
|
||||
data_type: "position"
|
||||
})
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
test "includes path stations outside map bounds", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, "/")
|
||||
|
||||
# Set map bounds to a small area around Texas that excludes the outside station
|
||||
bounds = %{
|
||||
"north" => "33.0",
|
||||
"south" => "32.0",
|
||||
"east" => "-96.0",
|
||||
"west" => "-97.0"
|
||||
}
|
||||
|
||||
render_hook(view, "bounds_changed", %{"bounds" => bounds})
|
||||
|
||||
# Now hover over a marker with a path that includes both inside and outside stations
|
||||
render_hook(view, "marker_hover_start", %{
|
||||
"id" => "test-bounds",
|
||||
# Both inside and outside stations
|
||||
"path" => "K5INSIDE-10*,W1OUTSIDE-10",
|
||||
"lat" => 32.5,
|
||||
"lng" => -96.5
|
||||
})
|
||||
|
||||
# With the fix, both stations should be found regardless of bounds
|
||||
# The test passes if no errors occur during the hover event
|
||||
assert view.module == Index
|
||||
end
|
||||
|
||||
test "finds stations regardless of bounds constraints", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, "/")
|
||||
|
||||
# Set very restrictive bounds that exclude both stations
|
||||
bounds = %{
|
||||
"north" => "31.0",
|
||||
"south" => "30.0",
|
||||
"east" => "-95.0",
|
||||
"west" => "-96.0"
|
||||
}
|
||||
|
||||
render_hook(view, "bounds_changed", %{"bounds" => bounds})
|
||||
|
||||
# Hover with path containing stations outside these bounds
|
||||
render_hook(view, "marker_hover_start", %{
|
||||
"id" => "test-no-bounds",
|
||||
"path" => "K5INSIDE-10,W1OUTSIDE-10",
|
||||
"lat" => 32.5,
|
||||
"lng" => -96.5
|
||||
})
|
||||
|
||||
# Should work without errors - stations found regardless of bounds
|
||||
assert view.module == Index
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Reference in a new issue