add info page
This commit is contained in:
parent
6384aa80e4
commit
3b03a38bd0
3 changed files with 236 additions and 0 deletions
108
lib/aprs_web/live/info_live/show.ex
Normal file
108
lib/aprs_web/live/info_live/show.ex
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
defmodule AprsWeb.InfoLive.Show do
|
||||
@moduledoc false
|
||||
use AprsWeb, :live_view
|
||||
|
||||
alias Aprs.Packets
|
||||
alias AprsWeb.MapLive.PacketUtils
|
||||
|
||||
@neighbor_radius_km 10
|
||||
@neighbor_limit 10
|
||||
|
||||
@impl true
|
||||
def mount(%{"callsign" => callsign}, _session, socket) do
|
||||
normalized_callsign = String.upcase(String.trim(callsign))
|
||||
packet = get_latest_packet(normalized_callsign)
|
||||
neighbors = get_neighbors(packet, normalized_callsign)
|
||||
|
||||
socket =
|
||||
socket
|
||||
|> assign(:callsign, normalized_callsign)
|
||||
|> assign(:packet, packet)
|
||||
|> assign(:neighbors, neighbors)
|
||||
|> assign(:page_title, "APRS station #{normalized_callsign}")
|
||||
|
||||
{:ok, socket}
|
||||
end
|
||||
|
||||
defp get_latest_packet(callsign) do
|
||||
%{callsign: callsign, limit: 1}
|
||||
|> Packets.get_recent_packets()
|
||||
|> List.first()
|
||||
end
|
||||
|
||||
defp get_neighbors(nil, _callsign), do: []
|
||||
|
||||
defp get_neighbors(packet, callsign) do
|
||||
lat = packet.lat
|
||||
lon = packet.lon
|
||||
|
||||
if is_nil(lat) or is_nil(lon) do
|
||||
[]
|
||||
else
|
||||
# Simple bounding box for ~10km radius
|
||||
delta = @neighbor_radius_km / 111.0
|
||||
min_lat = lat - delta
|
||||
max_lat = lat + delta
|
||||
min_lon = lon - delta
|
||||
max_lon = lon + delta
|
||||
opts = %{bounds: [min_lon, min_lat, max_lon, max_lat], limit: 50}
|
||||
|
||||
opts
|
||||
|> Packets.get_recent_packets()
|
||||
|> Enum.filter(fn p ->
|
||||
(p.sender != callsign and p.lat) && p.lon
|
||||
end)
|
||||
|> uniq_by(& &1.sender)
|
||||
|> Enum.map(fn p ->
|
||||
dist = haversine(lat, lon, p.lat, p.lon)
|
||||
|
||||
%{
|
||||
callsign: p.sender,
|
||||
distance: format_distance(dist),
|
||||
last_heard: PacketUtils.get_timestamp(p),
|
||||
packet: p
|
||||
}
|
||||
end)
|
||||
|> Enum.sort_by(& &1.distance)
|
||||
|> Enum.take(@neighbor_limit)
|
||||
end
|
||||
end
|
||||
|
||||
defp uniq_by(list, fun) do
|
||||
list
|
||||
|> Enum.reduce({MapSet.new(), []}, fn item, {set, acc} ->
|
||||
key = fun.(item)
|
||||
|
||||
if MapSet.member?(set, key) do
|
||||
{set, acc}
|
||||
else
|
||||
{MapSet.put(set, key), [item | acc]}
|
||||
end
|
||||
end)
|
||||
|> elem(1)
|
||||
|> Enum.reverse()
|
||||
end
|
||||
|
||||
defp haversine(lat1, lon1, lat2, lon2) do
|
||||
# Returns distance in km
|
||||
r = 6371
|
||||
dlat = :math.pi() / 180 * (lat2 - lat1)
|
||||
dlon = :math.pi() / 180 * (lon2 - lon1)
|
||||
|
||||
a =
|
||||
:math.sin(dlat / 2) * :math.sin(dlat / 2) +
|
||||
:math.cos(:math.pi() / 180 * lat1) * :math.cos(:math.pi() / 180 * lat2) *
|
||||
:math.sin(dlon / 2) * :math.sin(dlon / 2)
|
||||
|
||||
c = 2 * :math.atan2(:math.sqrt(a), :math.sqrt(1 - a))
|
||||
r * c
|
||||
end
|
||||
|
||||
defp format_distance(km) when km < 1.0 do
|
||||
"#{Float.round(km * 1000, 0)} m"
|
||||
end
|
||||
|
||||
defp format_distance(km) do
|
||||
"#{Float.round(km, 2)} km"
|
||||
end
|
||||
end
|
||||
127
lib/aprs_web/live/info_live/show.html.heex
Normal file
127
lib/aprs_web/live/info_live/show.html.heex
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
<% {symbol_table_id, symbol_code} = AprsWeb.MapLive.PacketUtils.get_symbol_info(@packet || %{}) %>
|
||||
<% symbol_table = if symbol_table_id in ["/", "\\", "]"], do: symbol_table_id, else: "/" %>
|
||||
<% symbol_code = symbol_code || ">" %>
|
||||
<% symbol_table_num =
|
||||
case symbol_table do
|
||||
"/" -> 0
|
||||
"\\" -> 1
|
||||
"]" -> 2
|
||||
_ -> 0
|
||||
end %>
|
||||
<% symbol_code_ord =
|
||||
symbol_code
|
||||
|> String.to_charlist()
|
||||
|> List.first()
|
||||
|> (fn c -> if is_integer(c), do: c, else: 63 end).() %>
|
||||
<% symbol_img = "/aprs-symbols/aprs-symbols-24-#{symbol_table_num}@2x.png" %>
|
||||
<% symbol_index = symbol_code_ord - 33 %>
|
||||
|
||||
<div class="max-w-3xl mx-auto mt-10 p-8 bg-white rounded-xl shadow-lg">
|
||||
<h1 class="text-3xl font-bold mb-2 flex items-center gap-2">
|
||||
APRS station <span class="text-blue-700">{@callsign}</span>
|
||||
<%= if @packet do %>
|
||||
<% {symbol_table_id, symbol_code} = AprsWeb.MapLive.PacketUtils.get_symbol_info(@packet) %>
|
||||
<% symbol_table = if symbol_table_id in ["/", "\\", "]"], do: symbol_table_id, else: "/" %>
|
||||
<% symbol_code = symbol_code || ">" %>
|
||||
<% symbol_table_num =
|
||||
case symbol_table do
|
||||
"/" -> 0
|
||||
"\\" -> 1
|
||||
"]" -> 2
|
||||
_ -> 0
|
||||
end %>
|
||||
<% symbol_code_ord =
|
||||
symbol_code
|
||||
|> String.to_charlist()
|
||||
|> List.first()
|
||||
|> (fn c -> if is_integer(c), do: c, else: 63 end).() %>
|
||||
<% symbol_img = "/aprs-symbols/aprs-symbols-24-#{symbol_table_num}@2x.png" %>
|
||||
<% symbol_index = symbol_code_ord - 33 %>
|
||||
<img
|
||||
src={symbol_img}
|
||||
style={"object-fit: none; object-position: -#{symbol_index * 48}px 0; width: 48px; height: 48px; transform: scale(0.5); transform-origin: top left; display: inline-block; vertical-align: middle; margin-bottom: -6px; image-rendering: pixelated;"}
|
||||
alt="APRS symbol"
|
||||
/>
|
||||
<% end %>
|
||||
</h1>
|
||||
<%= if @packet do %>
|
||||
<div class="text-sm text-gray-500 mb-6">
|
||||
{@packet.comment}
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
||||
<div>
|
||||
<div class="mb-2">
|
||||
<span class="font-semibold">Location:</span> {@packet.lat}, {@packet.lon}
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<span class="font-semibold">Last position:</span> {AprsWeb.MapLive.PacketUtils.get_timestamp(
|
||||
@packet
|
||||
)}
|
||||
</div>
|
||||
<div class="mb-2"><span class="font-semibold">Altitude:</span> {@packet.altitude} ft</div>
|
||||
<div class="mb-2"><span class="font-semibold">Course:</span> {@packet.course}°</div>
|
||||
<div class="mb-2"><span class="font-semibold">Speed:</span> {@packet.speed} MPH</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="mb-2">
|
||||
<span class="font-semibold">Device:</span> {@packet.manufacturer} {@packet.equipment_type}
|
||||
</div>
|
||||
<div class="mb-2"><span class="font-semibold">Path:</span> {@packet.path}</div>
|
||||
<div class="mb-2"><span class="font-semibold">Raw:</span> {@packet.raw_packet}</div>
|
||||
</div>
|
||||
</div>
|
||||
<h2 class="text-xl font-semibold mt-8 mb-2">Stations near current position</h2>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full text-sm border rounded-lg">
|
||||
<thead>
|
||||
<tr class="bg-gray-100">
|
||||
<th class="px-4 py-2 text-left">Callsign</th>
|
||||
<th class="px-4 py-2 text-left">Distance</th>
|
||||
<th class="px-4 py-2 text-left">Last heard</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<%= for neighbor <- @neighbors do %>
|
||||
<tr class="border-t">
|
||||
<td class="px-4 py-2 font-mono text-blue-700 flex items-center gap-2">
|
||||
<.link navigate={~p"/info/#{neighbor.callsign}"} class="hover:underline">
|
||||
{neighbor.callsign}
|
||||
</.link>
|
||||
<%= if neighbor.packet do %>
|
||||
<% {symbol_table_id, symbol_code} =
|
||||
AprsWeb.MapLive.PacketUtils.get_symbol_info(neighbor.packet) %>
|
||||
<% symbol_table =
|
||||
if symbol_table_id in ["/", "\\", "]"], do: symbol_table_id, else: "/" %>
|
||||
<% symbol_code = symbol_code || ">" %>
|
||||
<% symbol_table_num =
|
||||
case symbol_table do
|
||||
"/" -> 0
|
||||
"\\" -> 1
|
||||
"]" -> 2
|
||||
_ -> 0
|
||||
end %>
|
||||
<% symbol_code_ord =
|
||||
symbol_code
|
||||
|> String.to_charlist()
|
||||
|> List.first()
|
||||
|> (fn c -> if is_integer(c), do: c, else: 63 end).() %>
|
||||
<% symbol_img = "/aprs-symbols/aprs-symbols-24-#{symbol_table_num}@2x.png" %>
|
||||
<% symbol_index = symbol_code_ord - 33 %>
|
||||
<img
|
||||
src={symbol_img}
|
||||
style={"object-fit: none; object-position: -#{symbol_index * 48}px 0; width: 48px; height: 48px; transform: scale(0.5); transform-origin: top left; display: inline-block; vertical-align: middle; margin-bottom: -6px; image-rendering: pixelated;"}
|
||||
alt="APRS symbol"
|
||||
/>
|
||||
<% end %>
|
||||
</td>
|
||||
<td class="px-4 py-2">{neighbor.distance}</td>
|
||||
<td class="px-4 py-2">{neighbor.last_heard}</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="text-gray-600">No recent packet data available for this callsign.</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
|
@ -38,6 +38,7 @@ defmodule AprsWeb.Router do
|
|||
live "/weather/:callsign", WeatherLive.CallsignView, :index
|
||||
live "/about", AboutLive, :index
|
||||
live "/api", ApiDocsLive, :index
|
||||
live "/info/:callsign", InfoLive.Show, :show
|
||||
end
|
||||
|
||||
live_session :map_pages, layout: {AprsWeb.Layouts, :map} do
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue