add decoded path and other ssids
This commit is contained in:
parent
abdbe399f0
commit
f47b3d9f09
5 changed files with 459 additions and 27 deletions
|
|
@ -21,6 +21,7 @@ defmodule AprsmeWeb.InfoLive.Show do
|
||||||
packet = enrich_packet_with_device_info(packet)
|
packet = enrich_packet_with_device_info(packet)
|
||||||
neighbors = get_neighbors(packet, normalized_callsign)
|
neighbors = get_neighbors(packet, normalized_callsign)
|
||||||
has_weather_packets = PacketUtils.has_weather_packets?(normalized_callsign)
|
has_weather_packets = PacketUtils.has_weather_packets?(normalized_callsign)
|
||||||
|
other_ssids = get_other_ssids(normalized_callsign)
|
||||||
|
|
||||||
socket =
|
socket =
|
||||||
socket
|
socket
|
||||||
|
|
@ -29,6 +30,7 @@ defmodule AprsmeWeb.InfoLive.Show do
|
||||||
|> assign(:neighbors, neighbors)
|
|> assign(:neighbors, neighbors)
|
||||||
|> assign(:page_title, "APRS station #{normalized_callsign}")
|
|> assign(:page_title, "APRS station #{normalized_callsign}")
|
||||||
|> assign(:has_weather_packets, has_weather_packets)
|
|> assign(:has_weather_packets, has_weather_packets)
|
||||||
|
|> assign(:other_ssids, other_ssids)
|
||||||
|
|
||||||
{:ok, socket}
|
{:ok, socket}
|
||||||
end
|
end
|
||||||
|
|
@ -42,12 +44,14 @@ defmodule AprsmeWeb.InfoLive.Show do
|
||||||
packet = enrich_packet_with_device_info(packet)
|
packet = enrich_packet_with_device_info(packet)
|
||||||
neighbors = get_neighbors(packet, socket.assigns.callsign)
|
neighbors = get_neighbors(packet, socket.assigns.callsign)
|
||||||
has_weather_packets = PacketUtils.has_weather_packets?(socket.assigns.callsign)
|
has_weather_packets = PacketUtils.has_weather_packets?(socket.assigns.callsign)
|
||||||
|
other_ssids = get_other_ssids(socket.assigns.callsign)
|
||||||
|
|
||||||
socket =
|
socket =
|
||||||
socket
|
socket
|
||||||
|> assign(:packet, packet)
|
|> assign(:packet, packet)
|
||||||
|> assign(:neighbors, neighbors)
|
|> assign(:neighbors, neighbors)
|
||||||
|> assign(:has_weather_packets, has_weather_packets)
|
|> assign(:has_weather_packets, has_weather_packets)
|
||||||
|
|> assign(:other_ssids, other_ssids)
|
||||||
|
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
else
|
else
|
||||||
|
|
@ -63,9 +67,9 @@ defmodule AprsmeWeb.InfoLive.Show do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp get_latest_packet(callsign) do
|
defp get_latest_packet(callsign) do
|
||||||
%{callsign: callsign, limit: 1}
|
# Get the most recent packet for this callsign, regardless of type
|
||||||
|> Packets.get_recent_packets()
|
# This ensures we show the most recent activity, not just position packets
|
||||||
|> List.first()
|
Packets.get_latest_packet_for_callsign(callsign)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp enrich_packet_with_device_info(nil), do: nil
|
defp enrich_packet_with_device_info(nil), do: nil
|
||||||
|
|
@ -145,6 +149,12 @@ defmodule AprsmeWeb.InfoLive.Show do
|
||||||
|
|
||||||
defp haversine(lat1, lon1, lat2, lon2) do
|
defp haversine(lat1, lon1, lat2, lon2) do
|
||||||
# Returns distance in km
|
# Returns distance in km
|
||||||
|
# Convert Decimal to float if needed
|
||||||
|
lat1 = to_float(lat1)
|
||||||
|
lon1 = to_float(lon1)
|
||||||
|
lat2 = to_float(lat2)
|
||||||
|
lon2 = to_float(lon2)
|
||||||
|
|
||||||
r = 6371
|
r = 6371
|
||||||
dlat = :math.pi() / 180 * (lat2 - lat1)
|
dlat = :math.pi() / 180 * (lat2 - lat1)
|
||||||
dlon = :math.pi() / 180 * (lon2 - lon1)
|
dlon = :math.pi() / 180 * (lon2 - lon1)
|
||||||
|
|
@ -158,6 +168,19 @@ defmodule AprsmeWeb.InfoLive.Show do
|
||||||
r * c
|
r * c
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp to_float(%Decimal{} = decimal), do: Decimal.to_float(decimal)
|
||||||
|
defp to_float(value) when is_float(value), do: value
|
||||||
|
defp to_float(value) when is_integer(value), do: value * 1.0
|
||||||
|
|
||||||
|
defp to_float(value) when is_binary(value) do
|
||||||
|
case Float.parse(value) do
|
||||||
|
{f, _} -> f
|
||||||
|
:error -> 0.0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp to_float(_), do: 0.0
|
||||||
|
|
||||||
defp format_distance(km) when km < 1.0 do
|
defp format_distance(km) when km < 1.0 do
|
||||||
"#{Float.round(km * 1000, 0)} m"
|
"#{Float.round(km * 1000, 0)} m"
|
||||||
end
|
end
|
||||||
|
|
@ -168,6 +191,12 @@ defmodule AprsmeWeb.InfoLive.Show do
|
||||||
|
|
||||||
defp calculate_course(lat1, lon1, lat2, lon2) do
|
defp calculate_course(lat1, lon1, lat2, lon2) do
|
||||||
# Calculate bearing from point 1 to point 2
|
# Calculate bearing from point 1 to point 2
|
||||||
|
# Convert Decimal to float if needed
|
||||||
|
lat1 = to_float(lat1)
|
||||||
|
lon1 = to_float(lon1)
|
||||||
|
lat2 = to_float(lat2)
|
||||||
|
lon2 = to_float(lon2)
|
||||||
|
|
||||||
dlon = :math.pi() / 180 * (lon2 - lon1)
|
dlon = :math.pi() / 180 * (lon2 - lon1)
|
||||||
|
|
||||||
lat1_rad = :math.pi() / 180 * lat1
|
lat1_rad = :math.pi() / 180 * lat1
|
||||||
|
|
@ -180,4 +209,278 @@ defmodule AprsmeWeb.InfoLive.Show do
|
||||||
# Convert to 0-360 range
|
# Convert to 0-360 range
|
||||||
if bearing < 0, do: bearing + 360, else: bearing
|
if bearing < 0, do: bearing + 360, else: bearing
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp get_other_ssids(callsign) do
|
||||||
|
import Ecto.Query
|
||||||
|
alias Aprsme.Packet
|
||||||
|
alias Aprsme.Repo
|
||||||
|
# Extract base callsign from the full callsign (remove SSID if present)
|
||||||
|
base_callsign = extract_base_callsign(callsign)
|
||||||
|
|
||||||
|
# Query directly for packets with the same base_callsign
|
||||||
|
|
||||||
|
# Get recent packets for the base callsign to find other SSIDs
|
||||||
|
one_hour_ago = DateTime.add(DateTime.utc_now(), -3600, :second)
|
||||||
|
|
||||||
|
query =
|
||||||
|
from p in Packet,
|
||||||
|
where: p.base_callsign == ^base_callsign,
|
||||||
|
where: p.received_at >= ^one_hour_ago,
|
||||||
|
order_by: [desc: p.received_at],
|
||||||
|
limit: 100
|
||||||
|
|
||||||
|
query
|
||||||
|
|> Repo.all()
|
||||||
|
|> Enum.map(fn p ->
|
||||||
|
%{
|
||||||
|
callsign: p.sender,
|
||||||
|
ssid: p.ssid,
|
||||||
|
last_heard: PacketUtils.get_timestamp(p),
|
||||||
|
packet: p
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
|> uniq_by(& &1.callsign)
|
||||||
|
|> Enum.filter(fn ssid_info -> ssid_info.callsign != callsign end)
|
||||||
|
|> Enum.sort_by(& &1.last_heard, :desc)
|
||||||
|
|> Enum.take(10)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp extract_base_callsign(callsign) do
|
||||||
|
case String.split(callsign, "-") do
|
||||||
|
[base, _ssid] -> base
|
||||||
|
[base] -> base
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp decode_aprs_path(path) when is_binary(path) and path != "" do
|
||||||
|
path_elements = String.split(path, ",")
|
||||||
|
|
||||||
|
decoded_elements = Enum.map(path_elements, &decode_path_element/1)
|
||||||
|
|
||||||
|
# Filter out nil results and join with explanations
|
||||||
|
decoded_elements
|
||||||
|
|> Enum.reject(&is_nil/1)
|
||||||
|
|> Enum.join(" → ")
|
||||||
|
end
|
||||||
|
|
||||||
|
defp decode_aprs_path(_), do: nil
|
||||||
|
|
||||||
|
defp decode_path_element(element) do
|
||||||
|
element = String.trim(element)
|
||||||
|
|
||||||
|
cond do
|
||||||
|
# WIDE digipeaters
|
||||||
|
String.starts_with?(element, "WIDE") ->
|
||||||
|
case element do
|
||||||
|
"WIDE1-1" -> "WIDE1-1 (Wide area digipeater, 1 hop)"
|
||||||
|
"WIDE2-1" -> "WIDE2-1 (Wide area digipeater, 2 hops)"
|
||||||
|
"WIDE3-1" -> "WIDE3-1 (Wide area digipeater, 3 hops)"
|
||||||
|
"WIDE4-1" -> "WIDE4-1 (Wide area digipeater, 4 hops)"
|
||||||
|
"WIDE5-1" -> "WIDE5-1 (Wide area digipeater, 5 hops)"
|
||||||
|
"WIDE6-1" -> "WIDE6-1 (Wide area digipeater, 6 hops)"
|
||||||
|
"WIDE7-1" -> "WIDE7-1 (WIDE area digipeater, 7 hops)"
|
||||||
|
"WIDE1-2" -> "WIDE1-2 (Wide area digipeater, 1 hop, 2nd attempt)"
|
||||||
|
"WIDE2-2" -> "WIDE2-2 (Wide area digipeater, 2 hops, 2nd attempt)"
|
||||||
|
_ -> "WIDE digipeater (#{element})"
|
||||||
|
end
|
||||||
|
|
||||||
|
# TRACE digipeaters
|
||||||
|
String.starts_with?(element, "TRACE") ->
|
||||||
|
case element do
|
||||||
|
"TRACE1-1" -> "TRACE1-1 (Trace digipeater, 1 hop)"
|
||||||
|
"TRACE2-1" -> "TRACE2-1 (Trace digipeater, 2 hops)"
|
||||||
|
"TRACE3-1" -> "TRACE3-1 (Trace digipeater, 3 hops)"
|
||||||
|
"TRACE4-1" -> "TRACE4-1 (Trace digipeater, 4 hops)"
|
||||||
|
"TRACE5-1" -> "TRACE5-1 (Trace digipeater, 5 hops)"
|
||||||
|
"TRACE6-1" -> "TRACE6-1 (Trace digipeater, 6 hops)"
|
||||||
|
"TRACE7-1" -> "TRACE7-1 (Trace digipeater, 7 hops)"
|
||||||
|
_ -> "TRACE digipeater (#{element})"
|
||||||
|
end
|
||||||
|
|
||||||
|
# RELAY digipeaters
|
||||||
|
String.starts_with?(element, "RELAY") ->
|
||||||
|
case element do
|
||||||
|
"RELAY" -> "RELAY (Relay digipeater)"
|
||||||
|
"RELAY-1" -> "RELAY-1 (Relay digipeater, 1 hop)"
|
||||||
|
"RELAY-2" -> "RELAY-2 (Relay digipeater, 2 hops)"
|
||||||
|
_ -> "RELAY digipeater (#{element})"
|
||||||
|
end
|
||||||
|
|
||||||
|
# qAC (APRS-IS connection)
|
||||||
|
element == "qAC" ->
|
||||||
|
"qAC (APRS-IS connection)"
|
||||||
|
|
||||||
|
# qAO (APRS-IS origin)
|
||||||
|
element == "qAO" ->
|
||||||
|
"qAO (APRS-IS origin)"
|
||||||
|
|
||||||
|
# qAR (APRS-IS relay)
|
||||||
|
element == "qAR" ->
|
||||||
|
"qAR (APRS-IS relay)"
|
||||||
|
|
||||||
|
# qAS (APRS-IS server)
|
||||||
|
element == "qAS" ->
|
||||||
|
"qAS (APRS-IS server)"
|
||||||
|
|
||||||
|
# qAX (APRS-IS client)
|
||||||
|
element == "qAX" ->
|
||||||
|
"qAX (APRS-IS client)"
|
||||||
|
|
||||||
|
# qAY (APRS-IS gateway)
|
||||||
|
element == "qAY" ->
|
||||||
|
"qAY (APRS-IS gateway)"
|
||||||
|
|
||||||
|
# qAZ (APRS-IS zone)
|
||||||
|
element == "qAZ" ->
|
||||||
|
"qAZ (APRS-IS zone)"
|
||||||
|
|
||||||
|
# qBU (APRS-IS user)
|
||||||
|
element == "qBU" ->
|
||||||
|
"qBU (APRS-IS user)"
|
||||||
|
|
||||||
|
# qBV (APRS-IS vendor)
|
||||||
|
element == "qBV" ->
|
||||||
|
"qBV (APRS-IS vendor)"
|
||||||
|
|
||||||
|
# qBW (APRS-IS web)
|
||||||
|
element == "qBW" ->
|
||||||
|
"qBW (APRS-IS web)"
|
||||||
|
|
||||||
|
# qBX (APRS-IS experimental)
|
||||||
|
element == "qBX" ->
|
||||||
|
"qBX (APRS-IS experimental)"
|
||||||
|
|
||||||
|
# qBY (APRS-IS Y2K)
|
||||||
|
element == "qBY" ->
|
||||||
|
"qBY (APRS-IS Y2K)"
|
||||||
|
|
||||||
|
# qBZ (APRS-IS Zulu)
|
||||||
|
element == "qBZ" ->
|
||||||
|
"qBZ (APRS-IS Zulu)"
|
||||||
|
|
||||||
|
# qCA (APRS-IS client application)
|
||||||
|
element == "qCA" ->
|
||||||
|
"qCA (APRS-IS client application)"
|
||||||
|
|
||||||
|
# qCB (APRS-IS client browser)
|
||||||
|
element == "qCB" ->
|
||||||
|
"qCB (APRS-IS client browser)"
|
||||||
|
|
||||||
|
# qCC (APRS-IS client console)
|
||||||
|
element == "qCC" ->
|
||||||
|
"qCC (APRS-IS client console)"
|
||||||
|
|
||||||
|
# qCD (APRS-IS client daemon)
|
||||||
|
element == "qCD" ->
|
||||||
|
"qCD (APRS-IS client daemon)"
|
||||||
|
|
||||||
|
# qCE (APRS-IS client editor)
|
||||||
|
element == "qCE" ->
|
||||||
|
"qCE (APRS-IS client editor)"
|
||||||
|
|
||||||
|
# qCF (APRS-IS client filter)
|
||||||
|
element == "qCF" ->
|
||||||
|
"qCF (APRS-IS client filter)"
|
||||||
|
|
||||||
|
# qCG (APRS-IS client gateway)
|
||||||
|
element == "qCG" ->
|
||||||
|
"qCG (APRS-IS client gateway)"
|
||||||
|
|
||||||
|
# qCH (APRS-IS client host)
|
||||||
|
element == "qCH" ->
|
||||||
|
"qCH (APRS-IS client host)"
|
||||||
|
|
||||||
|
# qCI (APRS-IS client interface)
|
||||||
|
element == "qCI" ->
|
||||||
|
"qCI (APRS-IS client interface)"
|
||||||
|
|
||||||
|
# qCJ (APRS-IS client java)
|
||||||
|
element == "qCJ" ->
|
||||||
|
"qCJ (APRS-IS client java)"
|
||||||
|
|
||||||
|
# qCK (APRS-IS client kernel)
|
||||||
|
element == "qCK" ->
|
||||||
|
"qCK (APRS-IS client kernel)"
|
||||||
|
|
||||||
|
# qCL (APRS-IS client library)
|
||||||
|
element == "qCL" ->
|
||||||
|
"qCL (APRS-IS client library)"
|
||||||
|
|
||||||
|
# qCM (APRS-IS client module)
|
||||||
|
element == "qCM" ->
|
||||||
|
"qCM (APRS-IS client module)"
|
||||||
|
|
||||||
|
# qCN (APRS-IS client network)
|
||||||
|
element == "qCN" ->
|
||||||
|
"qCN (APRS-IS client network)"
|
||||||
|
|
||||||
|
# qCO (APRS-IS client object)
|
||||||
|
element == "qCO" ->
|
||||||
|
"qCO (APRS-IS client object)"
|
||||||
|
|
||||||
|
# qCP (APRS-IS client protocol)
|
||||||
|
element == "qCP" ->
|
||||||
|
"qCP (APRS-IS client protocol)"
|
||||||
|
|
||||||
|
# qCQ (APRS-IS client query)
|
||||||
|
element == "qCQ" ->
|
||||||
|
"qCQ (APRS-IS client query)"
|
||||||
|
|
||||||
|
# qCR (APRS-IS client router)
|
||||||
|
element == "qCR" ->
|
||||||
|
"qCR (APRS-IS client router)"
|
||||||
|
|
||||||
|
# qCS (APRS-IS client server)
|
||||||
|
element == "qCS" ->
|
||||||
|
"qCS (APRS-IS client server)"
|
||||||
|
|
||||||
|
# qCT (APRS-IS client terminal)
|
||||||
|
element == "qCT" ->
|
||||||
|
"qCT (APRS-IS client terminal)"
|
||||||
|
|
||||||
|
# qCU (APRS-IS client user)
|
||||||
|
element == "qCU" ->
|
||||||
|
"qCU (APRS-IS client user)"
|
||||||
|
|
||||||
|
# qCV (APRS-IS client vendor)
|
||||||
|
element == "qCV" ->
|
||||||
|
"qCV (APRS-IS client vendor)"
|
||||||
|
|
||||||
|
# qCW (APRS-IS client web)
|
||||||
|
element == "qCW" ->
|
||||||
|
"qCW (APRS-IS client web)"
|
||||||
|
|
||||||
|
# qCX (APRS-IS client experimental)
|
||||||
|
element == "qCX" ->
|
||||||
|
"qCX (APRS-IS client experimental)"
|
||||||
|
|
||||||
|
# qCY (APRS-IS client Y2K)
|
||||||
|
element == "qCY" ->
|
||||||
|
"qCY (APRS-IS client Y2K)"
|
||||||
|
|
||||||
|
# qCZ (APRS-IS client Zulu)
|
||||||
|
element == "qCZ" ->
|
||||||
|
"qCZ (APRS-IS client Zulu)"
|
||||||
|
|
||||||
|
# TCPIP digipeaters
|
||||||
|
String.starts_with?(element, "TCPIP") ->
|
||||||
|
case element do
|
||||||
|
"TCPIP" -> "TCPIP (Internet gateway)"
|
||||||
|
"TCPIP*" -> "TCPIP* (Internet gateway, no forward)"
|
||||||
|
_ -> "TCPIP gateway (#{element})"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Generic callsign with SSID (likely a digipeater)
|
||||||
|
Regex.match?(~r/^[A-Z0-9]+-\d+$/, element) ->
|
||||||
|
"#{element} (Digipeater)"
|
||||||
|
|
||||||
|
# Generic callsign without SSID
|
||||||
|
Regex.match?(~r/^[A-Z0-9]+$/, element) ->
|
||||||
|
"#{element} (Station)"
|
||||||
|
|
||||||
|
# Default case
|
||||||
|
true ->
|
||||||
|
"#{element} (Unknown)"
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -116,10 +116,32 @@
|
||||||
{@packet.lat || ""}, {@packet.lon || ""}
|
{@packet.lat || ""}, {@packet.lon || ""}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
|
<%= if @packet.lat && @packet.lon do %>
|
||||||
|
<div>
|
||||||
|
<dt class="text-xs font-medium opacity-70">Grid Square</dt>
|
||||||
|
<dd class="mt-1 text-sm font-semibold">
|
||||||
|
<%= case Gridsquare.encode(@packet.lon, @packet.lat) do %>
|
||||||
|
<% %Gridsquare.EncodeResult{grid_reference: grid_ref} -> %>
|
||||||
|
{grid_ref}
|
||||||
|
<% _ -> %>
|
||||||
|
-
|
||||||
|
<% end %>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
<div>
|
<div>
|
||||||
<dt class="text-xs font-medium opacity-70">Last Position</dt>
|
<dt class="text-xs font-medium opacity-70">Last Position</dt>
|
||||||
<dd class="mt-1 text-sm font-semibold">
|
<dd class="mt-1 text-sm">
|
||||||
{AprsmeWeb.MapLive.PacketUtils.get_timestamp(@packet)}
|
<%= if @packet.received_at do %>
|
||||||
|
<div class="font-semibold">
|
||||||
|
{AprsmeWeb.TimeHelpers.time_ago_in_words(@packet.received_at)}
|
||||||
|
</div>
|
||||||
|
<div class="text-xs opacity-70 font-mono">
|
||||||
|
{Calendar.strftime(@packet.received_at, "%Y-%m-%d %H:%M:%S UTC")}
|
||||||
|
</div>
|
||||||
|
<% else %>
|
||||||
|
<span class="opacity-70">Unknown</span>
|
||||||
|
<% end %>
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
<%= if @packet.altitude do %>
|
<%= if @packet.altitude do %>
|
||||||
|
|
@ -193,6 +215,14 @@
|
||||||
<dt class="text-xs font-medium opacity-70">Path</dt>
|
<dt class="text-xs font-medium opacity-70">Path</dt>
|
||||||
<dd class="mt-1 text-sm font-semibold">{@packet.path || ""}</dd>
|
<dd class="mt-1 text-sm font-semibold">{@packet.path || ""}</dd>
|
||||||
</div>
|
</div>
|
||||||
|
<%= if @packet.path && @packet.path != "" do %>
|
||||||
|
<div class="col-span-2">
|
||||||
|
<dt class="text-xs font-medium opacity-70">Decoded Path</dt>
|
||||||
|
<dd class="mt-1 text-sm">
|
||||||
|
{decode_aprs_path(@packet.path)}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
<div class="col-span-2">
|
<div class="col-span-2">
|
||||||
<dt class="text-xs font-medium opacity-70">Raw Packet</dt>
|
<dt class="text-xs font-medium opacity-70">Raw Packet</dt>
|
||||||
<dd class="mt-1 text-xs font-mono break-all">
|
<dd class="mt-1 text-xs font-mono break-all">
|
||||||
|
|
@ -208,6 +238,125 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Other SSIDs -->
|
||||||
|
<%= if length(@other_ssids) > 0 do %>
|
||||||
|
<div class="card bg-base-100 shadow-xl mb-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="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="ml-2">
|
||||||
|
<h3 class="text-sm font-medium">Other SSIDs</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="table table-zebra">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="text-xs font-medium uppercase tracking-wider">
|
||||||
|
Callsign
|
||||||
|
</th>
|
||||||
|
<th class="text-xs font-medium uppercase tracking-wider">
|
||||||
|
Distance & Direction
|
||||||
|
</th>
|
||||||
|
<th class="text-xs font-medium uppercase tracking-wider">
|
||||||
|
Last Heard
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<%= for ssid_info <- @other_ssids do %>
|
||||||
|
<tr>
|
||||||
|
<td class="font-medium">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<.link
|
||||||
|
navigate={~p"/info/#{ssid_info.callsign}"}
|
||||||
|
class="link link-primary"
|
||||||
|
>
|
||||||
|
{ssid_info.callsign}
|
||||||
|
</.link>
|
||||||
|
<%= if ssid_info.packet do %>
|
||||||
|
<% {symbol_table_id, symbol_code} =
|
||||||
|
AprsmeWeb.MapLive.PacketUtils.get_symbol_info(ssid_info.packet) %>
|
||||||
|
<% symbol_table =
|
||||||
|
if symbol_table_id in ["/", "\\", "]"],
|
||||||
|
do: symbol_table_id,
|
||||||
|
else: "/" %>
|
||||||
|
<% symbol_code = symbol_code || ">" %>
|
||||||
|
<% table_id =
|
||||||
|
if symbol_table == "/",
|
||||||
|
do: "0",
|
||||||
|
else: if(symbol_table == "]", do: "2", else: "1") %>
|
||||||
|
<% symbol_code_ord =
|
||||||
|
symbol_code
|
||||||
|
|> String.to_charlist()
|
||||||
|
|> List.first()
|
||||||
|
|> (fn c -> if is_integer(c), do: c, else: 63 end).() %>
|
||||||
|
<% index = symbol_code_ord - 33 %>
|
||||||
|
<% safe_index = max(0, min(index, 93)) %>
|
||||||
|
<% column = rem(safe_index, 16) %>
|
||||||
|
<% row = div(safe_index, 16) %>
|
||||||
|
<% x = -column * 128 %>
|
||||||
|
<% y = -row * 128 %>
|
||||||
|
<% symbol_img = "/aprs-symbols/aprs-symbols-128-#{table_id}@2x.png" %>
|
||||||
|
<div
|
||||||
|
style={"width: 32px; height: 32px; background-image: url(#{symbol_img}); background-position: #{x / 4}px #{y / 4}px; background-size: 512px 192px; background-repeat: no-repeat; image-rendering: pixelated; opacity: 1.0; display: inline-block; vertical-align: middle; margin-bottom: -6px;"}
|
||||||
|
title={"Symbol: #{symbol_code} (#{symbol_code_ord}) at row #{row}, col #{column}"}
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="opacity-70">
|
||||||
|
<%= if ssid_info.packet && ssid_info.packet.lat && ssid_info.packet.lon && @packet && @packet.lat && @packet.lon do %>
|
||||||
|
<% dist =
|
||||||
|
haversine(
|
||||||
|
@packet.lat,
|
||||||
|
@packet.lon,
|
||||||
|
ssid_info.packet.lat,
|
||||||
|
ssid_info.packet.lon
|
||||||
|
) %>
|
||||||
|
<% course =
|
||||||
|
calculate_course(
|
||||||
|
@packet.lat,
|
||||||
|
@packet.lon,
|
||||||
|
ssid_info.packet.lat,
|
||||||
|
ssid_info.packet.lon
|
||||||
|
) %>
|
||||||
|
<%= if course do %>
|
||||||
|
{format_distance(dist)} @ {Float.round(course, 0)}°
|
||||||
|
<% else %>
|
||||||
|
{format_distance(dist)}
|
||||||
|
<% end %>
|
||||||
|
<% else %>
|
||||||
|
-
|
||||||
|
<% end %>
|
||||||
|
</td>
|
||||||
|
<td class="opacity-70">
|
||||||
|
{ssid_info.last_heard || ""}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<% end %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<!-- Neighboring stations -->
|
<!-- Neighboring stations -->
|
||||||
<div class="card bg-base-100 shadow-xl">
|
<div class="card bg-base-100 shadow-xl">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|
|
||||||
|
|
@ -208,28 +208,6 @@ defmodule AprsmeWeb.StatusLive.Index do
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Application Information -->
|
|
||||||
<div class="mb-8">
|
|
||||||
<h2 class="text-xl font-semibold mb-4">Application Information</h2>
|
|
||||||
<div class="card bg-base-200">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<span class="text-sm font-medium opacity-70 mr-2">Version:</span>
|
|
||||||
<span class="text-sm font-mono">{@version}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center">
|
|
||||||
<span class="text-sm font-medium opacity-70 mr-2">Current Time:</span>
|
|
||||||
<span class="text-sm font-mono">
|
|
||||||
{Calendar.strftime(@current_time, "%Y-%m-%d %H:%M:%S UTC")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
1
mix.exs
1
mix.exs
|
|
@ -82,6 +82,7 @@ defmodule Aprsme.MixProject do
|
||||||
github: "tailwindlabs/heroicons", tag: "v2.1.1", sparse: "optimized", app: false, compile: false, depth: 1},
|
github: "tailwindlabs/heroicons", tag: "v2.1.1", sparse: "optimized", app: false, compile: false, depth: 1},
|
||||||
{:bandit, "~> 1.5"},
|
{:bandit, "~> 1.5"},
|
||||||
{:req, "~> 0.5"},
|
{:req, "~> 0.5"},
|
||||||
|
{:gridsquare, "~> 0.2.0"},
|
||||||
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
|
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
|
||||||
{:dialyxir, "~> 1.0", only: :dev, runtime: false},
|
{:dialyxir, "~> 1.0", only: :dev, runtime: false},
|
||||||
{:exvcr, "~> 0.15", only: :test},
|
{:exvcr, "~> 0.15", only: :test},
|
||||||
|
|
|
||||||
1
mix.lock
1
mix.lock
|
|
@ -26,6 +26,7 @@
|
||||||
"geo_postgis": {:hex, :geo_postgis, "3.7.1", "614f25b42334a615bd54bb09c22030b1aac7bac8f829bd823ab1faccf093a324", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:geo, "~> 3.6 or ~> 4.0", [hex: :geo, repo: "hexpm", optional: false]}, {:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0 or ~> 4.0 or ~> 5.0 or ~> 6.0", [hex: :poison, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: false]}], "hexpm", "c20d823c600d35b7fe9ddd5be03052bb7136c57d6f1775dbd46871545e405280"},
|
"geo_postgis": {:hex, :geo_postgis, "3.7.1", "614f25b42334a615bd54bb09c22030b1aac7bac8f829bd823ab1faccf093a324", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:geo, "~> 3.6 or ~> 4.0", [hex: :geo, repo: "hexpm", optional: false]}, {:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0 or ~> 4.0 or ~> 5.0 or ~> 6.0", [hex: :poison, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: false]}], "hexpm", "c20d823c600d35b7fe9ddd5be03052bb7136c57d6f1775dbd46871545e405280"},
|
||||||
"geocalc": {:hex, :geocalc, "0.8.5", "b9886679e44c323e5b72dcd90a64f834d775d2600af0b656ea9f07ccdacaa5a6", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "3870c25c78513ec0456b69324c2be1af2202961002e81fb659559e3db162c802"},
|
"geocalc": {:hex, :geocalc, "0.8.5", "b9886679e44c323e5b72dcd90a64f834d775d2600af0b656ea9f07ccdacaa5a6", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "3870c25c78513ec0456b69324c2be1af2202961002e81fb659559e3db162c802"},
|
||||||
"gettext": {:hex, :gettext, "0.26.2", "5978aa7b21fada6deabf1f6341ddba50bc69c999e812211903b169799208f2a8", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "aa978504bcf76511efdc22d580ba08e2279caab1066b76bb9aa81c4a1e0a32a5"},
|
"gettext": {:hex, :gettext, "0.26.2", "5978aa7b21fada6deabf1f6341ddba50bc69c999e812211903b169799208f2a8", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "aa978504bcf76511efdc22d580ba08e2279caab1066b76bb9aa81c4a1e0a32a5"},
|
||||||
|
"gridsquare": {:hex, :gridsquare, "0.2.0", "c556a5b5101db89743264b0d728601023035863487bbe36e9e50e93839adb4d7", [:mix], [], "hexpm", "dbf0dbb484681a1e97819b0667bfeaa8c8a48cf06a54bf96a8f26ee4158ad31a"},
|
||||||
"hackney": {:hex, :hackney, "1.24.1", "f5205a125bba6ed4587f9db3cc7c729d11316fa8f215d3e57ed1c067a9703fa9", [:rebar3], [{:certifi, "~> 2.15.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.4", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "f4a7392a0b53d8bbc3eb855bdcc919cd677358e65b2afd3840b5b3690c4c8a39"},
|
"hackney": {:hex, :hackney, "1.24.1", "f5205a125bba6ed4587f9db3cc7c729d11316fa8f215d3e57ed1c067a9703fa9", [:rebar3], [{:certifi, "~> 2.15.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.4", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "f4a7392a0b53d8bbc3eb855bdcc919cd677358e65b2afd3840b5b3690c4c8a39"},
|
||||||
"heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "88ab3a0d790e6a47404cba02800a6b25d2afae50", [tag: "v2.1.1", sparse: "optimized", depth: 1]},
|
"heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "88ab3a0d790e6a47404cba02800a6b25d2afae50", [tag: "v2.1.1", sparse: "optimized", depth: 1]},
|
||||||
"hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"},
|
"hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"},
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue