add more info on info page
This commit is contained in:
parent
184ca52a8c
commit
93b298ab9f
2 changed files with 447 additions and 0 deletions
|
|
@ -30,6 +30,9 @@ defmodule AprsmeWeb.InfoLive.Show do
|
|||
has_weather_packets = PacketUtils.has_weather_packets?(normalized_callsign)
|
||||
other_ssids = get_other_ssids(normalized_callsign)
|
||||
|
||||
heard_by_stations = get_heard_by_stations(normalized_callsign, locale)
|
||||
stations_heard_by = get_stations_heard_by(normalized_callsign, locale)
|
||||
|
||||
socket =
|
||||
socket
|
||||
|> assign(:callsign, normalized_callsign)
|
||||
|
|
@ -38,6 +41,8 @@ defmodule AprsmeWeb.InfoLive.Show do
|
|||
|> assign(:page_title, "APRS station #{normalized_callsign}")
|
||||
|> 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)
|
||||
|
||||
{:ok, socket}
|
||||
end
|
||||
|
|
@ -62,6 +67,8 @@ defmodule AprsmeWeb.InfoLive.Show do
|
|||
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)
|
||||
|
||||
socket =
|
||||
socket
|
||||
|
|
@ -69,6 +76,8 @@ defmodule AprsmeWeb.InfoLive.Show do
|
|||
|> 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)
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
|
@ -275,6 +284,237 @@ defmodule AprsmeWeb.InfoLive.Show do
|
|||
Callsign.extract_base(callsign)
|
||||
end
|
||||
|
||||
defp get_heard_by_stations(callsign, locale) do
|
||||
alias Aprsme.Repo
|
||||
|
||||
# Get packets from the last month where this callsign was heard on RF
|
||||
one_month_ago = DateTime.add(DateTime.utc_now(), -30, :day)
|
||||
|
||||
# Query to find stations that heard this callsign directly
|
||||
# In APRS, the first station with an asterisk (*) in the path is the one that heard the packet directly
|
||||
query = """
|
||||
WITH parsed_paths AS (
|
||||
SELECT
|
||||
id,
|
||||
sender,
|
||||
path,
|
||||
received_at,
|
||||
lat,
|
||||
lon,
|
||||
location,
|
||||
-- Extract the first digipeater from the path
|
||||
CASE
|
||||
WHEN path ~ '[A-Z0-9]+-?[0-9]*\\*' THEN
|
||||
substring(path from '([A-Z0-9]+-?[0-9]*)\\*')
|
||||
ELSE NULL
|
||||
END as first_digipeater
|
||||
FROM packets
|
||||
WHERE sender = $1
|
||||
AND received_at >= $2
|
||||
AND path IS NOT NULL
|
||||
AND path != ''
|
||||
AND path !~ '^TCPIP'
|
||||
AND path !~ ',TCPIP'
|
||||
),
|
||||
digipeater_stats AS (
|
||||
SELECT
|
||||
first_digipeater as digipeater,
|
||||
MIN(received_at) as first_heard,
|
||||
MAX(received_at) as last_heard,
|
||||
COUNT(*) as packet_count,
|
||||
-- Find the packet with maximum distance for this digipeater
|
||||
(SELECT pp2.id
|
||||
FROM parsed_paths pp2
|
||||
WHERE pp2.first_digipeater = pp.first_digipeater
|
||||
AND pp2.lat IS NOT NULL
|
||||
AND pp2.lon IS NOT NULL
|
||||
ORDER BY
|
||||
ST_Distance(
|
||||
pp2.location::geography,
|
||||
(SELECT location::geography
|
||||
FROM packets
|
||||
WHERE sender = pp2.first_digipeater
|
||||
AND location IS NOT NULL
|
||||
ORDER BY received_at DESC
|
||||
LIMIT 1)
|
||||
) DESC NULLS LAST
|
||||
LIMIT 1
|
||||
) as longest_path_packet_id
|
||||
FROM parsed_paths pp
|
||||
WHERE first_digipeater IS NOT NULL
|
||||
GROUP BY first_digipeater
|
||||
)
|
||||
SELECT
|
||||
ds.digipeater,
|
||||
ds.first_heard,
|
||||
ds.last_heard,
|
||||
ds.packet_count,
|
||||
p.received_at as longest_path_time,
|
||||
CASE
|
||||
WHEN p.location IS NOT NULL AND dig_loc.location IS NOT NULL THEN
|
||||
ST_Distance(p.location::geography, dig_loc.location::geography) / 1000.0
|
||||
ELSE NULL
|
||||
END as longest_distance_km
|
||||
FROM digipeater_stats ds
|
||||
LEFT JOIN packets p ON p.id = ds.longest_path_packet_id
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT location
|
||||
FROM packets
|
||||
WHERE sender = ds.digipeater
|
||||
AND location IS NOT NULL
|
||||
ORDER BY received_at DESC
|
||||
LIMIT 1
|
||||
) dig_loc ON true
|
||||
ORDER BY ds.last_heard DESC
|
||||
LIMIT 50
|
||||
"""
|
||||
|
||||
case Repo.query(query, [callsign, one_month_ago]) do
|
||||
{:ok, result} ->
|
||||
result.rows
|
||||
|> Enum.map(fn row ->
|
||||
case row do
|
||||
[digipeater, first_heard, last_heard, packet_count, longest_path_time, longest_distance_km] ->
|
||||
%{
|
||||
digipeater: digipeater || "",
|
||||
first_heard: first_heard,
|
||||
last_heard: last_heard,
|
||||
packet_count: packet_count || 0,
|
||||
longest_distance_km: longest_distance_km,
|
||||
longest_distance: if(longest_distance_km, do: format_distance(longest_distance_km, locale)),
|
||||
longest_path_time: longest_path_time
|
||||
}
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end)
|
||||
|> Enum.reject(&is_nil/1)
|
||||
|
||||
{:error, error} ->
|
||||
require Logger
|
||||
|
||||
Logger.error("Error in get_heard_by_stations: #{inspect(error)}")
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
defp get_stations_heard_by(callsign, locale) do
|
||||
alias Aprsme.Repo
|
||||
|
||||
# Get packets from the last month where this callsign heard other stations on RF
|
||||
one_month_ago = DateTime.add(DateTime.utc_now(), -30, :day)
|
||||
|
||||
# Query to find stations that were heard directly by this callsign
|
||||
# We look for packets where this callsign appears as the first digipeater with an asterisk
|
||||
query = """
|
||||
WITH parsed_paths AS (
|
||||
SELECT
|
||||
id,
|
||||
sender,
|
||||
path,
|
||||
received_at,
|
||||
lat,
|
||||
lon,
|
||||
location,
|
||||
-- Extract the first digipeater from the path
|
||||
CASE
|
||||
WHEN path ~ '[A-Z0-9]+-?[0-9]*\\*' THEN
|
||||
substring(path from '([A-Z0-9]+-?[0-9]*)\\*')
|
||||
ELSE NULL
|
||||
END as first_digipeater
|
||||
FROM packets
|
||||
WHERE path ~ ($1 || '\\*')
|
||||
AND received_at >= $2
|
||||
AND path IS NOT NULL
|
||||
AND path != ''
|
||||
AND path !~ '^TCPIP'
|
||||
AND path !~ ',TCPIP'
|
||||
),
|
||||
station_stats AS (
|
||||
SELECT
|
||||
sender as station,
|
||||
MIN(received_at) as first_heard,
|
||||
MAX(received_at) as last_heard,
|
||||
COUNT(*) as packet_count,
|
||||
-- Find the packet with maximum distance for this station
|
||||
(SELECT pp2.id
|
||||
FROM parsed_paths pp2
|
||||
WHERE pp2.sender = pp.sender
|
||||
AND pp2.first_digipeater = $1
|
||||
AND pp2.lat IS NOT NULL
|
||||
AND pp2.lon IS NOT NULL
|
||||
ORDER BY
|
||||
ST_Distance(
|
||||
pp2.location::geography,
|
||||
(SELECT location::geography
|
||||
FROM packets
|
||||
WHERE sender = $1
|
||||
AND location IS NOT NULL
|
||||
ORDER BY received_at DESC
|
||||
LIMIT 1)
|
||||
) DESC NULLS LAST
|
||||
LIMIT 1
|
||||
) as longest_path_packet_id
|
||||
FROM parsed_paths pp
|
||||
WHERE first_digipeater = $1
|
||||
GROUP BY sender
|
||||
)
|
||||
SELECT
|
||||
ss.station,
|
||||
ss.first_heard,
|
||||
ss.last_heard,
|
||||
ss.packet_count,
|
||||
p.received_at as longest_path_time,
|
||||
CASE
|
||||
WHEN p.location IS NOT NULL AND dig_loc.location IS NOT NULL THEN
|
||||
ST_Distance(p.location::geography, dig_loc.location::geography) / 1000.0
|
||||
ELSE NULL
|
||||
END as longest_distance_km
|
||||
FROM station_stats ss
|
||||
LEFT JOIN packets p ON p.id = ss.longest_path_packet_id
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT location
|
||||
FROM packets
|
||||
WHERE sender = $1
|
||||
AND location IS NOT NULL
|
||||
ORDER BY received_at DESC
|
||||
LIMIT 1
|
||||
) dig_loc ON true
|
||||
ORDER BY ss.last_heard DESC
|
||||
LIMIT 50
|
||||
"""
|
||||
|
||||
case Repo.query(query, [callsign, one_month_ago]) do
|
||||
{:ok, result} ->
|
||||
result.rows
|
||||
|> Enum.map(fn row ->
|
||||
case row do
|
||||
[station, first_heard, last_heard, packet_count, longest_path_time, longest_distance_km] ->
|
||||
%{
|
||||
station: station || "",
|
||||
first_heard: first_heard,
|
||||
last_heard: last_heard,
|
||||
packet_count: packet_count || 0,
|
||||
longest_distance_km: longest_distance_km,
|
||||
longest_distance: if(longest_distance_km, do: format_distance(longest_distance_km, locale)),
|
||||
longest_path_time: longest_path_time
|
||||
}
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end)
|
||||
|> Enum.reject(&is_nil/1)
|
||||
|
||||
{:error, error} ->
|
||||
require Logger
|
||||
|
||||
Logger.error("Error in get_stations_heard_by: #{inspect(error)}")
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders an APRS symbol style for use in templates.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -466,6 +466,213 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stations that heard this callsign on RF -->
|
||||
<%= if length(@heard_by_stations) > 0 do %>
|
||||
<div class="card bg-base-100 shadow-xl mt-6">
|
||||
<div class="card-body">
|
||||
<div class="flex items-center mb-3">
|
||||
<div class="flex-shrink-0">
|
||||
<svg
|
||||
class="h-4 w-4 opacity-70"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M9.348 14.651a3.75 3.75 0 010-5.303m5.304 0a3.75 3.75 0 010 5.303m-7.425 2.122a6.75 6.75 0 010-9.546m9.546 0a6.75 6.75 0 010 9.546M5.106 18.894c-3.808-3.808-3.808-9.98 0-13.789m13.788 0c3.808 3.808 3.808 9.981 0 13.79M12 12h.008v.007H12V12zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-2">
|
||||
<h3 class="text-sm font-medium">
|
||||
{gettext("Stations which heard %{callsign} directly on radio",
|
||||
callsign: @callsign
|
||||
)}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-zebra">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-xs font-medium uppercase tracking-wider">
|
||||
{gettext("Callsign")}
|
||||
</th>
|
||||
<th class="text-xs font-medium uppercase tracking-wider">
|
||||
{gettext("First Heard")}
|
||||
</th>
|
||||
<th class="text-xs font-medium uppercase tracking-wider">
|
||||
{gettext("Last Heard")}
|
||||
</th>
|
||||
<th class="text-xs font-medium uppercase tracking-wider">
|
||||
{gettext("Packets")}
|
||||
</th>
|
||||
<th class="text-xs font-medium uppercase tracking-wider">
|
||||
{gettext("Longest Path")}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<%= for station <- @heard_by_stations do %>
|
||||
<tr>
|
||||
<td class="font-medium">
|
||||
<.link
|
||||
navigate={~p"/info/#{station.digipeater}"}
|
||||
class="link link-primary"
|
||||
>
|
||||
{station.digipeater}
|
||||
</.link>
|
||||
</td>
|
||||
<td class="opacity-70">
|
||||
<%= if station.first_heard do %>
|
||||
<div class="text-xs font-mono">
|
||||
{Calendar.strftime(station.first_heard, "%Y-%m-%d %H:%M")}
|
||||
</div>
|
||||
<% else %>
|
||||
-
|
||||
<% end %>
|
||||
</td>
|
||||
<td class="opacity-70">
|
||||
<%= if station.last_heard do %>
|
||||
<div class="font-semibold">
|
||||
{AprsmeWeb.TimeHelpers.time_ago_in_words(station.last_heard)}
|
||||
</div>
|
||||
<div class="text-xs opacity-70 font-mono">
|
||||
{Calendar.strftime(station.last_heard, "%Y-%m-%d %H:%M")}
|
||||
</div>
|
||||
<% else %>
|
||||
-
|
||||
<% end %>
|
||||
</td>
|
||||
<td>
|
||||
{station.packet_count}
|
||||
</td>
|
||||
<td class="opacity-70">
|
||||
<%= if station.longest_distance do %>
|
||||
{station.longest_distance}
|
||||
<%= if station.longest_path_time do %>
|
||||
<div class="text-xs opacity-70 font-mono">
|
||||
{Calendar.strftime(station.longest_path_time, "%Y-%m-%d %H:%M")}
|
||||
</div>
|
||||
<% end %>
|
||||
<% else %>
|
||||
-
|
||||
<% end %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- Stations heard directly by this callsign -->
|
||||
<%= if length(@stations_heard_by) > 0 do %>
|
||||
<div class="card bg-base-100 shadow-xl mt-6">
|
||||
<div class="card-body">
|
||||
<div class="flex items-center mb-3">
|
||||
<div class="flex-shrink-0">
|
||||
<svg
|
||||
class="h-4 w-4 opacity-70"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M9.348 14.651a3.75 3.75 0 010-5.303m5.304 0a3.75 3.75 0 010 5.303m-7.425 2.122a6.75 6.75 0 010-9.546m9.546 0a6.75 6.75 0 010 9.546M5.106 18.894c-3.808-3.808-3.808-9.98 0-13.789m13.788 0c3.808 3.808 3.808 9.981 0 13.79M12 12h.008v.007H12V12zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-2">
|
||||
<h3 class="text-sm font-medium">
|
||||
{gettext("Stations heard directly by %{callsign}",
|
||||
callsign: @callsign
|
||||
)}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-zebra">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-xs font-medium uppercase tracking-wider">
|
||||
{gettext("Callsign")}
|
||||
</th>
|
||||
<th class="text-xs font-medium uppercase tracking-wider">
|
||||
{gettext("First Heard")}
|
||||
</th>
|
||||
<th class="text-xs font-medium uppercase tracking-wider">
|
||||
{gettext("Last Heard")}
|
||||
</th>
|
||||
<th class="text-xs font-medium uppercase tracking-wider">
|
||||
{gettext("Packets")}
|
||||
</th>
|
||||
<th class="text-xs font-medium uppercase tracking-wider">
|
||||
{gettext("Longest Path")}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<%= for station <- @stations_heard_by do %>
|
||||
<tr>
|
||||
<td class="font-medium">
|
||||
<.link navigate={~p"/info/#{station.station}"} class="link link-primary">
|
||||
{station.station}
|
||||
</.link>
|
||||
</td>
|
||||
<td class="opacity-70">
|
||||
<%= if station.first_heard do %>
|
||||
<div class="text-xs font-mono">
|
||||
{Calendar.strftime(station.first_heard, "%Y-%m-%d %H:%M")}
|
||||
</div>
|
||||
<% else %>
|
||||
-
|
||||
<% end %>
|
||||
</td>
|
||||
<td class="opacity-70">
|
||||
<%= if station.last_heard do %>
|
||||
<div class="font-semibold">
|
||||
{AprsmeWeb.TimeHelpers.time_ago_in_words(station.last_heard)}
|
||||
</div>
|
||||
<div class="text-xs opacity-70 font-mono">
|
||||
{Calendar.strftime(station.last_heard, "%Y-%m-%d %H:%M")}
|
||||
</div>
|
||||
<% else %>
|
||||
-
|
||||
<% end %>
|
||||
</td>
|
||||
<td>
|
||||
{station.packet_count}
|
||||
</td>
|
||||
<td class="opacity-70">
|
||||
<%= if station.longest_distance do %>
|
||||
{station.longest_distance}
|
||||
<%= if station.longest_path_time do %>
|
||||
<div class="text-xs opacity-70 font-mono">
|
||||
{Calendar.strftime(station.longest_path_time, "%Y-%m-%d %H:%M")}
|
||||
</div>
|
||||
<% end %>
|
||||
<% else %>
|
||||
-
|
||||
<% end %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<div class="text-center py-8">
|
||||
<svg
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue