fix icons on pages
This commit is contained in:
parent
1b4ac972de
commit
bebead0fb0
3 changed files with 492 additions and 273 deletions
|
|
@ -16,133 +16,334 @@
|
||||||
<% _symbol_img = "/aprs-symbols/aprs-symbols-24-#{symbol_table_num}@2x.png" %>
|
<% _symbol_img = "/aprs-symbols/aprs-symbols-24-#{symbol_table_num}@2x.png" %>
|
||||||
<% _symbol_index = symbol_code_ord - 33 %>
|
<% _symbol_index = symbol_code_ord - 33 %>
|
||||||
|
|
||||||
<div class="max-w-3xl mx-auto mt-10 p-8 bg-white rounded-xl shadow-lg">
|
<div class="min-h-screen bg-gray-50">
|
||||||
<h1 class="text-3xl font-bold mb-2 flex items-center gap-2">
|
<!-- Page header -->
|
||||||
APRS station <span class="text-blue-700">{@callsign}</span>
|
<div class="bg-white shadow-sm border-b">
|
||||||
|
<div class="px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="py-4 md:flex md:items-center md:justify-between">
|
||||||
|
<div class="min-w-0 flex-1">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<h1 class="text-xl font-bold leading-7 text-gray-900 sm:truncate sm:text-2xl">
|
||||||
|
APRS Station
|
||||||
|
</h1>
|
||||||
|
<div class="ml-3 flex items-center space-x-3">
|
||||||
|
<span class="inline-flex items-center rounded-md bg-blue-50 px-2 py-1 text-sm font-medium text-blue-700 ring-1 ring-inset ring-blue-700/10">
|
||||||
|
{@callsign}
|
||||||
|
</span>
|
||||||
|
<%= if @packet do %>
|
||||||
|
<% {symbol_table_id, symbol_code} =
|
||||||
|
AprsmeWeb.MapLive.PacketUtils.get_symbol_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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 flex md:ml-4 md:mt-0">
|
||||||
|
<div class="flex space-x-2">
|
||||||
|
<.link
|
||||||
|
navigate={~p"/packets/#{@callsign}"}
|
||||||
|
class="inline-flex items-center rounded-md bg-white px-3 py-1.5 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
|
||||||
|
>
|
||||||
|
View packets
|
||||||
|
</.link>
|
||||||
|
<%= if AprsmeWeb.MapLive.PacketUtils.has_weather_packets?(@callsign) do %>
|
||||||
|
<.link
|
||||||
|
navigate={~p"/weather/#{@callsign}"}
|
||||||
|
class="inline-flex items-center rounded-md bg-blue-600 px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
|
||||||
|
>
|
||||||
|
Weather charts
|
||||||
|
</.link>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-4">
|
||||||
<%= if @packet do %>
|
<%= if @packet do %>
|
||||||
<% {symbol_table_id, symbol_code} = AprsmeWeb.MapLive.PacketUtils.get_symbol_info(@packet) %>
|
<!-- Comment section -->
|
||||||
<% symbol_table = if symbol_table_id in ["/", "\\", "]"], do: symbol_table_id, else: "/" %>
|
<%= if @packet.comment && @packet.comment != "" do %>
|
||||||
<% symbol_code = symbol_code || ">" %>
|
<div class="mb-4">
|
||||||
<% table_id =
|
<div class="rounded-lg bg-blue-50 p-3">
|
||||||
if symbol_table == "/", do: "0", else: if(symbol_table == "]", do: "2", else: "1") %>
|
<div class="flex">
|
||||||
<% symbol_code_ord =
|
<div class="flex-shrink-0">
|
||||||
symbol_code
|
<svg
|
||||||
|> String.to_charlist()
|
class="h-4 w-4 text-blue-400"
|
||||||
|> List.first()
|
viewBox="0 0 20 20"
|
||||||
|> (fn c -> if is_integer(c), do: c, else: 63 end).() %>
|
fill="currentColor"
|
||||||
<% index = symbol_code_ord - 33 %>
|
aria-hidden="true"
|
||||||
<% safe_index = max(0, min(index, 93)) %>
|
>
|
||||||
<% column = rem(safe_index, 16) %>
|
<path
|
||||||
<% row = div(safe_index, 16) %>
|
fill-rule="evenodd"
|
||||||
<% x = -column * 128 %>
|
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.75.75 0 00.736-.686L11.477 4.5a.75.75 0 00-1.491-.154L9.477 9.5H9z"
|
||||||
<% y = -row * 128 %>
|
clip-rule="evenodd"
|
||||||
<% symbol_img = "/aprs-symbols/aprs-symbols-128-#{table_id}@2x.png" %>
|
/>
|
||||||
<div
|
</svg>
|
||||||
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; overflow: hidden; display: inline-block; vertical-align: middle; margin-bottom: -6px;"}
|
</div>
|
||||||
title={"Symbol: #{symbol_code} (#{symbol_code_ord}) at row #{row}, col #{column}"}
|
<div class="ml-2">
|
||||||
>
|
<p class="text-sm text-blue-700">
|
||||||
</div>
|
{@packet.comment}
|
||||||
<% end %>
|
</p>
|
||||||
<.link navigate={~p"/packets/#{@callsign}"} class="ml-2 text-sm text-blue-600 hover:underline">
|
</div>
|
||||||
View packets
|
</div>
|
||||||
</.link>
|
</div>
|
||||||
<%= if AprsmeWeb.MapLive.PacketUtils.has_weather_packets?(@callsign) do %>
|
|
||||||
<.link
|
|
||||||
navigate={~p"/weather/#{@callsign}"}
|
|
||||||
class="ml-2 text-sm text-blue-600 hover:underline"
|
|
||||||
>
|
|
||||||
Weather charts
|
|
||||||
</.link>
|
|
||||||
<% 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>
|
||||||
<div class="mb-2">
|
<% end %>
|
||||||
<span class="font-semibold">Last position:</span> {AprsmeWeb.MapLive.PacketUtils.get_timestamp(
|
|
||||||
@packet
|
<!-- Station details -->
|
||||||
)}
|
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2 mb-6">
|
||||||
|
<!-- Position information -->
|
||||||
|
<div class="bg-white overflow-hidden shadow-sm rounded-lg border">
|
||||||
|
<div class="px-4 py-3">
|
||||||
|
<div class="flex items-center mb-3">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<svg
|
||||||
|
class="h-4 w-4 text-gray-400"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M15 10.5a3 3 0 11-6 0 3 3 0 016 0z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1115 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="ml-2">
|
||||||
|
<h3 class="text-sm font-medium text-gray-900">Position Information</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<dl class="grid grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<dt class="text-xs font-medium text-gray-500">Location</dt>
|
||||||
|
<dd class="mt-1 text-sm font-semibold text-gray-900">
|
||||||
|
{@packet.lat || ""}, {@packet.lon || ""}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt class="text-xs font-medium text-gray-500">Last Position</dt>
|
||||||
|
<dd class="mt-1 text-sm font-semibold text-gray-900">
|
||||||
|
{AprsmeWeb.MapLive.PacketUtils.get_timestamp(@packet)}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt class="text-xs font-medium text-gray-500">Altitude</dt>
|
||||||
|
<dd class="mt-1 text-sm font-semibold text-gray-900">{@packet.altitude} ft</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt class="text-xs font-medium text-gray-500">Course</dt>
|
||||||
|
<dd class="mt-1 text-sm font-semibold text-gray-900">{@packet.course}°</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt class="text-xs font-medium text-gray-500">Speed</dt>
|
||||||
|
<dd class="mt-1 text-sm font-semibold text-gray-900">{@packet.speed} MPH</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
</div>
|
</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>
|
<!-- Device information -->
|
||||||
<div class="mb-2"><span class="font-semibold">Speed:</span> {@packet.speed} MPH</div>
|
<div class="bg-white overflow-hidden shadow-sm rounded-lg border">
|
||||||
</div>
|
<div class="px-4 py-3">
|
||||||
<div>
|
<div class="flex items-center mb-3">
|
||||||
<div class="mb-2">
|
<div class="flex-shrink-0">
|
||||||
<span class="font-semibold">Device:</span> {@packet.manufacturer || ""} {@packet.equipment_type ||
|
<svg
|
||||||
""}
|
class="h-4 w-4 text-gray-400"
|
||||||
</div>
|
fill="none"
|
||||||
<div class="mb-2"><span class="font-semibold">Path:</span> {@packet.path || ""}</div>
|
viewBox="0 0 24 24"
|
||||||
<div class="mb-2">
|
stroke-width="1.5"
|
||||||
<span class="font-semibold">Raw:</span>
|
stroke="currentColor"
|
||||||
<%= if is_binary(@packet.raw_packet) do %>
|
>
|
||||||
{Aprsme.EncodingUtils.sanitize_string(@packet.raw_packet)}
|
<path
|
||||||
<% else %>
|
stroke-linecap="round"
|
||||||
{@packet.raw_packet || ""}
|
stroke-linejoin="round"
|
||||||
<% end %>
|
d="M9 17.25v1.007a3 3 0 01-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0115 18.257V17.25m6-12V15a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 15V5.25m18 0A2.25 2.25 0 0020.75 3H3.75A2.25 2.25 0 003 5.25m18 0V12a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 12V5.25"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="ml-2">
|
||||||
|
<h3 class="text-sm font-medium text-gray-900">Device Information</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<dl class="grid grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<dt class="text-xs font-medium text-gray-500">Device</dt>
|
||||||
|
<dd class="mt-1 text-sm font-semibold text-gray-900">
|
||||||
|
{@packet.manufacturer || ""} {@packet.equipment_type || ""}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt class="text-xs font-medium text-gray-500">Path</dt>
|
||||||
|
<dd class="mt-1 text-sm font-semibold text-gray-900">{@packet.path || ""}</dd>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-2">
|
||||||
|
<dt class="text-xs font-medium text-gray-500">Raw Packet</dt>
|
||||||
|
<dd class="mt-1 text-xs font-mono text-gray-900 break-all">
|
||||||
|
<%= if is_binary(@packet.raw_packet) do %>
|
||||||
|
{Aprsme.EncodingUtils.sanitize_string(@packet.raw_packet)}
|
||||||
|
<% else %>
|
||||||
|
{@packet.raw_packet || ""}
|
||||||
|
<% end %>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<h2 class="text-xl font-semibold mt-8 mb-2">Stations near current position</h2>
|
<!-- Neighboring stations -->
|
||||||
<div class="overflow-x-auto">
|
<div class="bg-white shadow-sm rounded-lg border">
|
||||||
<table class="min-w-full text-sm border rounded-lg">
|
<div class="px-4 py-3">
|
||||||
<thead>
|
<div class="flex items-center mb-3">
|
||||||
<tr class="bg-gray-100">
|
<div class="flex-shrink-0">
|
||||||
<th class="px-4 py-2 text-left">Callsign</th>
|
<svg
|
||||||
<th class="px-4 py-2 text-left">Distance</th>
|
class="h-4 w-4 text-gray-400"
|
||||||
<th class="px-4 py-2 text-left">Last heard</th>
|
fill="none"
|
||||||
</tr>
|
viewBox="0 0 24 24"
|
||||||
</thead>
|
stroke-width="1.5"
|
||||||
<tbody>
|
stroke="currentColor"
|
||||||
<%= for neighbor <- @neighbors do %>
|
>
|
||||||
<tr class="border-t">
|
<path
|
||||||
<td class="px-4 py-2 font-mono text-blue-700 flex items-center gap-2">
|
stroke-linecap="round"
|
||||||
<.link navigate={~p"/info/#{neighbor.callsign}"} class="hover:underline">
|
stroke-linejoin="round"
|
||||||
{neighbor.callsign}
|
d="M18 18.72a9.094 9.094 0 003.741-.479 3 3 0 00-4.682-2.72m.94 3.198l.001.031c0 .225-.012.447-.037.666A11.944 11.944 0 0112 21c-2.17 0-4.207-.576-5.963-1.584A6.062 6.062 0 016 18.719m12 0a5.971 5.971 0 00-.941-3.197m0 0A5.995 5.995 0 0012 12.75a5.995 5.995 0 00-5.058 2.772m0 0a3 3 0 00-4.681 2.72 8.986 8.986 0 003.74.477m.94-3.197a5.971 5.971 0 00-.94 3.197M15 6.75a3 3 0 11-6 0 3 3 0 016 0zm6 3a2.25 2.25 0 11-4.5 0 2.25 2.25 0 014.5 0zm-13.5 0a2.25 2.25 0 11-4.5 0 2.25 2.25 0 014.5 0z"
|
||||||
</.link>
|
/>
|
||||||
<%= if neighbor.packet do %>
|
</svg>
|
||||||
<% {symbol_table_id, symbol_code} =
|
</div>
|
||||||
AprsmeWeb.MapLive.PacketUtils.get_symbol_info(neighbor.packet) %>
|
<div class="ml-2">
|
||||||
<% symbol_table =
|
<h3 class="text-sm font-medium text-gray-900">Stations Near Current Position</h3>
|
||||||
if symbol_table_id in ["/", "\\", "]"], do: symbol_table_id, else: "/" %>
|
</div>
|
||||||
<% symbol_code = symbol_code || ">" %>
|
</div>
|
||||||
<% table_id =
|
<div class="overflow-x-auto">
|
||||||
if symbol_table == "/",
|
<table class="min-w-full divide-y divide-gray-200">
|
||||||
do: "0",
|
<thead class="bg-gray-50">
|
||||||
else: if(symbol_table == "]", do: "2", else: "1") %>
|
<tr>
|
||||||
<% symbol_code_ord =
|
<th
|
||||||
symbol_code
|
scope="col"
|
||||||
|> String.to_charlist()
|
class="py-2 pl-3 pr-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||||
|> 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; overflow: hidden; display: inline-block; vertical-align: middle; margin-bottom: -6px;"}
|
|
||||||
title={"Symbol: #{symbol_code} (#{symbol_code_ord}) at row #{row}, col #{column}"}
|
|
||||||
>
|
>
|
||||||
</div>
|
Callsign
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
class="px-2 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||||
|
>
|
||||||
|
Distance
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
class="px-2 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||||
|
>
|
||||||
|
Last Heard
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="bg-white divide-y divide-gray-200">
|
||||||
|
<%= for neighbor <- @neighbors do %>
|
||||||
|
<tr>
|
||||||
|
<td class="whitespace-nowrap py-2 pl-3 pr-2 text-sm font-medium text-gray-900">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<.link
|
||||||
|
navigate={~p"/info/#{neighbor.callsign}"}
|
||||||
|
class="text-blue-600 hover:text-blue-900"
|
||||||
|
>
|
||||||
|
{neighbor.callsign}
|
||||||
|
</.link>
|
||||||
|
<%= if neighbor.packet do %>
|
||||||
|
<% {symbol_table_id, symbol_code} =
|
||||||
|
AprsmeWeb.MapLive.PacketUtils.get_symbol_info(neighbor.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="whitespace-nowrap px-2 py-2 text-sm text-gray-500">
|
||||||
|
{neighbor.distance || ""}
|
||||||
|
</td>
|
||||||
|
<td class="whitespace-nowrap px-2 py-2 text-sm text-gray-500">
|
||||||
|
{neighbor.last_heard || ""}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<% end %>
|
<% end %>
|
||||||
</td>
|
</tbody>
|
||||||
<td class="px-4 py-2">{neighbor.distance || ""}</td>
|
</table>
|
||||||
<td class="px-4 py-2">{neighbor.last_heard || ""}</td>
|
</div>
|
||||||
</tr>
|
</div>
|
||||||
<% end %>
|
</div>
|
||||||
</tbody>
|
<% else %>
|
||||||
</table>
|
<div class="text-center py-8">
|
||||||
</div>
|
<svg
|
||||||
<% else %>
|
class="mx-auto h-8 w-8 text-gray-400"
|
||||||
<div class="text-gray-600">No recent packet data available for this callsign.</div>
|
fill="none"
|
||||||
<% end %>
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09zM18.259 8.715L18 9.75l-.259-1.035a3.375 3.375 0 00-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 002.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 002.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 00-2.456 2.456zM16.894 20.567L16.5 21.75l-.394-1.183a2.25 2.25 0 00-1.423-1.423L13.5 18.75l1.183-.394a2.25 2.25 0 001.423-1.423L16.5 15.75l.394 1.183a2.25 2.25 0 001.423 1.423L19.5 18.75l-1.183.394a2.25 2.25 0 00-1.423 1.423z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<h3 class="mt-2 text-sm font-semibold text-gray-900">No data available</h3>
|
||||||
|
<p class="mt-1 text-sm text-gray-500">
|
||||||
|
No recent packet data available for this callsign.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -68,4 +68,18 @@ defmodule AprsmeWeb.WeatherLive.CallsignView do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp calc_dew_point(_, _), do: nil
|
defp calc_dew_point(_, _), do: nil
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Gets weather field value, returning "0" instead of "N/A" for missing numeric data.
|
||||||
|
"""
|
||||||
|
def get_weather_field_zero(packet, key) do
|
||||||
|
value = PacketUtils.get_weather_field(packet, key)
|
||||||
|
|
||||||
|
# Return "0" for missing numeric fields, keep other values as-is
|
||||||
|
case value do
|
||||||
|
"N/A" -> "0"
|
||||||
|
nil -> "0"
|
||||||
|
_ -> value
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,160 +1,164 @@
|
||||||
<%= if @weather_packet do %>
|
<%= if @weather_packet do %>
|
||||||
<div class="max-w-lg mx-auto mt-8 p-6 bg-white rounded shadow">
|
<div class="w-full min-h-screen bg-slate-50 py-8 px-2 md:px-8">
|
||||||
<h1 class="text-2xl font-bold mb-2 flex items-center gap-2">
|
<section class="w-full bg-white rounded-xl shadow-lg p-6 md:p-10 mb-8">
|
||||||
Weather for {@callsign}
|
<h1 class="text-3xl font-bold mb-6 flex flex-wrap items-center gap-4">
|
||||||
<%= if @weather_packet do %>
|
Weather for {@callsign}
|
||||||
<% {symbol_table_id, symbol_code} =
|
<%= if @weather_packet do %>
|
||||||
AprsmeWeb.MapLive.PacketUtils.get_symbol_info(@weather_packet) %>
|
<% {symbol_table_id, symbol_code} =
|
||||||
<% symbol_table = if symbol_table_id in ["/", "\\", "]"], do: symbol_table_id, else: "/" %>
|
AprsmeWeb.MapLive.PacketUtils.get_symbol_info(@weather_packet) %>
|
||||||
<% symbol_code = symbol_code || ">" %>
|
<% symbol_table = if symbol_table_id in ["/", "\\", "]"], do: symbol_table_id, else: "/" %>
|
||||||
<% table_id =
|
<% symbol_code = symbol_code || ">" %>
|
||||||
if symbol_table == "/", do: "0", else: if(symbol_table == "]", do: "2", else: "1") %>
|
<% table_id =
|
||||||
<% symbol_code_ord =
|
if symbol_table == "/", do: "0", else: if(symbol_table == "]", do: "2", else: "1") %>
|
||||||
symbol_code
|
<% symbol_code_ord =
|
||||||
|> String.to_charlist()
|
symbol_code
|
||||||
|> List.first()
|
|> String.to_charlist()
|
||||||
|> (fn c -> if is_integer(c), do: c, else: 63 end).() %>
|
|> List.first()
|
||||||
<% index = symbol_code_ord - 33 %>
|
|> (fn c -> if is_integer(c), do: c, else: 63 end).() %>
|
||||||
<% safe_index = max(0, min(index, 93)) %>
|
<% index = symbol_code_ord - 33 %>
|
||||||
<% column = rem(safe_index, 16) %>
|
<% safe_index = max(0, min(index, 93)) %>
|
||||||
<% row = div(safe_index, 16) %>
|
<% column = rem(safe_index, 16) %>
|
||||||
<% x = -column * 128 %>
|
<% row = div(safe_index, 16) %>
|
||||||
<% y = -row * 128 %>
|
<% x = -column * 128 %>
|
||||||
<% symbol_img = "/aprs-symbols/aprs-symbols-128-#{table_id}@2x.png" %>
|
<% 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 %>
|
||||||
|
<.link
|
||||||
|
navigate={~p"/info/#{@callsign}"}
|
||||||
|
class="ml-2 text-blue-600 hover:underline text-base font-normal"
|
||||||
|
>
|
||||||
|
info
|
||||||
|
</.link>
|
||||||
|
<.link
|
||||||
|
navigate={~p"/#{@callsign}"}
|
||||||
|
class="ml-2 text-blue-600 hover:underline text-base font-normal"
|
||||||
|
>
|
||||||
|
view on map
|
||||||
|
</.link>
|
||||||
|
</h1>
|
||||||
|
<div class="text-base text-gray-600 mb-8">
|
||||||
|
{PacketUtils.get_timestamp(@weather_packet)}
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 mb-10">
|
||||||
|
<div>
|
||||||
|
<strong>Temperature:</strong>
|
||||||
|
{PacketUtils.get_weather_field(@weather_packet, :temperature)}°F
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Humidity:</strong>
|
||||||
|
{PacketUtils.get_weather_field(@weather_packet, :humidity)}%
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Wind:</strong>
|
||||||
|
{PacketUtils.get_weather_field(@weather_packet, :wind_direction)}° @ {PacketUtils.get_weather_field(
|
||||||
|
@weather_packet,
|
||||||
|
:wind_speed
|
||||||
|
)} mph
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Gusts:</strong>
|
||||||
|
{PacketUtils.get_weather_field(@weather_packet, :wind_gust)} mph
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Pressure:</strong>
|
||||||
|
{PacketUtils.get_weather_field(@weather_packet, :pressure)} hPa
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Rain (1h):</strong>
|
||||||
|
{get_weather_field_zero(@weather_packet, :rain_1h)} in
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Rain (24h):</strong>
|
||||||
|
{get_weather_field_zero(@weather_packet, :rain_24h)} in
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Rain (since midnight):</strong>
|
||||||
|
{get_weather_field_zero(@weather_packet, :rain_since_midnight)} in
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 text-sm text-gray-500">
|
||||||
|
<strong>Raw comment:</strong> {@weather_packet.comment || @weather_packet["comment"]}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="w-full bg-white rounded-xl shadow p-6 md:p-8 mt-8">
|
||||||
|
<h2 class="text-xl font-semibold mb-4">Weather History Graphs</h2>
|
||||||
|
<%= if Enum.any?(@weather_history, &(not is_nil(Map.get(&1, :temperature, nil)))) do %>
|
||||||
<div
|
<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; overflow: hidden; display: inline-block; vertical-align: middle; margin-bottom: -6px;"}
|
id="temp-chart"
|
||||||
title={"Symbol: #{symbol_code} (#{symbol_code_ord}) at row #{row}, col #{column}"}
|
phx-hook="PlotlyTempChart"
|
||||||
|
data-weather-history={@weather_history_json}
|
||||||
|
style="height:350px;"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<.link
|
<%= if Enum.any?(@weather_history, &(not is_nil(Map.get(&1, :humidity, nil)))) do %>
|
||||||
navigate={~p"/info/#{@callsign}"}
|
<div
|
||||||
class="ml-2 text-blue-600 hover:underline text-base font-normal"
|
id="humidity-chart"
|
||||||
>
|
phx-hook="PlotlyHumidityChart"
|
||||||
info
|
data-weather-history={@weather_history_json}
|
||||||
</.link>
|
style="height:350px;"
|
||||||
<.link
|
>
|
||||||
navigate={~p"/#{@callsign}"}
|
</div>
|
||||||
class="ml-2 text-blue-600 hover:underline text-base font-normal"
|
<% end %>
|
||||||
>
|
<%= if Enum.any?(@weather_history, &(not is_nil(Map.get(&1, :pressure, nil)))) do %>
|
||||||
view on map
|
<div
|
||||||
</.link>
|
id="pressure-chart"
|
||||||
</h1>
|
phx-hook="PlotlyPressureChart"
|
||||||
<div class="text-gray-600 mb-4">
|
data-weather-history={@weather_history_json}
|
||||||
{PacketUtils.get_timestamp(@weather_packet)}
|
style="height:350px;"
|
||||||
</div>
|
>
|
||||||
<div class="grid grid-cols-2 gap-4">
|
</div>
|
||||||
<div>
|
<% end %>
|
||||||
<strong>Temperature:</strong>
|
<%= if Enum.any?(@weather_history, &(not is_nil(Map.get(&1, :wind_direction, nil)))) do %>
|
||||||
{PacketUtils.get_weather_field(@weather_packet, :temperature)}°F
|
<div
|
||||||
</div>
|
id="wind-direction-chart"
|
||||||
<div>
|
phx-hook="PlotlyWindDirectionChart"
|
||||||
<strong>Humidity:</strong>
|
data-weather-history={@weather_history_json}
|
||||||
{PacketUtils.get_weather_field(@weather_packet, :humidity)}%
|
style="height:350px;"
|
||||||
</div>
|
>
|
||||||
<div>
|
</div>
|
||||||
<strong>Wind:</strong>
|
<% end %>
|
||||||
{PacketUtils.get_weather_field(@weather_packet, :wind_direction)}° @ {PacketUtils.get_weather_field(
|
<%= if Enum.any?(@weather_history, &(not is_nil(Map.get(&1, :wind_speed, nil)))) do %>
|
||||||
@weather_packet,
|
<div
|
||||||
:wind_speed
|
id="wind-speed-chart"
|
||||||
)} mph
|
phx-hook="PlotlyWindSpeedChart"
|
||||||
</div>
|
data-weather-history={@weather_history_json}
|
||||||
<div>
|
style="height:350px;"
|
||||||
<strong>Gusts:</strong>
|
>
|
||||||
{PacketUtils.get_weather_field(@weather_packet, :wind_gust)} mph
|
</div>
|
||||||
</div>
|
<% end %>
|
||||||
<div>
|
<%= if Enum.any?(@weather_history, &(not is_nil(Map.get(&1, :rain_1h, nil)))) or Enum.any?(@weather_history, &(not is_nil(Map.get(&1, :rain_24h, nil)))) or Enum.any?(@weather_history, &(not is_nil(Map.get(&1, :rain_since_midnight, nil)))) do %>
|
||||||
<strong>Pressure:</strong>
|
<div
|
||||||
{PacketUtils.get_weather_field(@weather_packet, :pressure)} hPa
|
id="rain-chart"
|
||||||
</div>
|
phx-hook="PlotlyRainChart"
|
||||||
<div>
|
data-weather-history={@weather_history_json}
|
||||||
<strong>Rain (1h):</strong>
|
style="height:350px;"
|
||||||
{PacketUtils.get_weather_field(@weather_packet, :rain_1h)} in
|
>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<% end %>
|
||||||
<strong>Rain (24h):</strong>
|
<%= if Enum.any?(@weather_history, &(not is_nil(Map.get(&1, :luminosity, nil)))) do %>
|
||||||
{PacketUtils.get_weather_field(@weather_packet, :rain_24h)} in
|
<div
|
||||||
</div>
|
id="luminosity-chart"
|
||||||
<div>
|
phx-hook="PlotlyLuminosityChart"
|
||||||
<strong>Rain (since midnight):</strong>
|
data-weather-history={@weather_history_json}
|
||||||
{PacketUtils.get_weather_field(@weather_packet, :rain_since_midnight)} in
|
style="height:350px;"
|
||||||
</div>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-4 text-sm text-gray-500">
|
<% end %>
|
||||||
<strong>Raw comment:</strong> {@weather_packet.comment || @weather_packet["comment"]}
|
<script src="https://cdn.plot.ly/plotly-latest.min.js">
|
||||||
</div>
|
</script>
|
||||||
</div>
|
</section>
|
||||||
<div class="max-w-3xl mx-auto mt-8 p-6 bg-white rounded shadow">
|
|
||||||
<h2 class="text-xl font-bold mb-4">Weather History Graphs</h2>
|
|
||||||
<%= if Enum.any?(@weather_history, &(not is_nil(Map.get(&1, :temperature, nil)))) do %>
|
|
||||||
<div
|
|
||||||
id="temp-chart"
|
|
||||||
phx-hook="PlotlyTempChart"
|
|
||||||
data-weather-history={@weather_history_json}
|
|
||||||
style="height:350px;"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<%= if Enum.any?(@weather_history, &(not is_nil(Map.get(&1, :humidity, nil)))) do %>
|
|
||||||
<div
|
|
||||||
id="humidity-chart"
|
|
||||||
phx-hook="PlotlyHumidityChart"
|
|
||||||
data-weather-history={@weather_history_json}
|
|
||||||
style="height:350px;"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<%= if Enum.any?(@weather_history, &(not is_nil(Map.get(&1, :pressure, nil)))) do %>
|
|
||||||
<div
|
|
||||||
id="pressure-chart"
|
|
||||||
phx-hook="PlotlyPressureChart"
|
|
||||||
data-weather-history={@weather_history_json}
|
|
||||||
style="height:350px;"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<%= if Enum.any?(@weather_history, &(not is_nil(Map.get(&1, :wind_direction, nil)))) do %>
|
|
||||||
<div
|
|
||||||
id="wind-direction-chart"
|
|
||||||
phx-hook="PlotlyWindDirectionChart"
|
|
||||||
data-weather-history={@weather_history_json}
|
|
||||||
style="height:350px;"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<%= if Enum.any?(@weather_history, &(not is_nil(Map.get(&1, :wind_speed, nil)))) do %>
|
|
||||||
<div
|
|
||||||
id="wind-speed-chart"
|
|
||||||
phx-hook="PlotlyWindSpeedChart"
|
|
||||||
data-weather-history={@weather_history_json}
|
|
||||||
style="height:350px;"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<%= if Enum.any?(@weather_history, &(not is_nil(Map.get(&1, :rain_1h, nil)))) or Enum.any?(@weather_history, &(not is_nil(Map.get(&1, :rain_24h, nil)))) or Enum.any?(@weather_history, &(not is_nil(Map.get(&1, :rain_since_midnight, nil)))) do %>
|
|
||||||
<div
|
|
||||||
id="rain-chart"
|
|
||||||
phx-hook="PlotlyRainChart"
|
|
||||||
data-weather-history={@weather_history_json}
|
|
||||||
style="height:350px;"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<%= if Enum.any?(@weather_history, &(not is_nil(Map.get(&1, :luminosity, nil)))) do %>
|
|
||||||
<div
|
|
||||||
id="luminosity-chart"
|
|
||||||
phx-hook="PlotlyLuminosityChart"
|
|
||||||
data-weather-history={@weather_history_json}
|
|
||||||
style="height:350px;"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<script src="https://cdn.plot.ly/plotly-latest.min.js">
|
|
||||||
</script>
|
|
||||||
</div>
|
</div>
|
||||||
<% else %>
|
<% else %>
|
||||||
<div class="max-w-lg mx-auto mt-8 p-6 bg-white rounded shadow text-center">
|
<div class="w-full min-h-screen bg-slate-50 py-8 px-2 md:px-8 flex items-center justify-center">
|
||||||
<h1 class="text-2xl font-bold mb-2">Weather for {@callsign}</h1>
|
<div class="w-full max-w-lg bg-white rounded-xl shadow-lg p-8 text-center">
|
||||||
<div class="text-gray-600">No recent weather data available for this callsign.</div>
|
<h1 class="text-2xl font-bold mb-2">Weather for {@callsign}</h1>
|
||||||
|
<div class="text-gray-600">No recent weather data available for this callsign.</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue