- Delete ~170 lines of commented-out PostGIS spatial query functions - Consolidate coordinate validation into CoordinateUtils - Replace private normalize_data_type with EncodingUtils delegation - Inline to_float/to_decimal wrappers at call sites - Fix has_weather_data? missing wind_gust field - Remove BoundsManager passthrough delegation module - Simplify MapLive.PacketUtils by removing pure-passthrough delegations
278 lines
14 KiB
Text
278 lines
14 KiB
Text
<%= if @weather_packet do %>
|
|
<div class="bg-gray-50 dark:bg-gray-900">
|
|
<div class="bg-white shadow-sm dark:bg-gray-800/50 dark:shadow-none dark:outline dark:-outline-offset-1 dark:outline-white/10">
|
|
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
|
|
<div class="py-4 md:flex md:items-center md:justify-between">
|
|
<div class="flex items-center">
|
|
<h1 class="text-xl font-bold leading-7 sm:truncate sm:text-2xl">
|
|
{gettext("Weather Station")}
|
|
</h1>
|
|
<div class="ml-3 flex items-center space-x-3">
|
|
<span class="inline-flex items-center rounded-md bg-indigo-100 px-2 py-1 text-xs font-medium text-indigo-700 dark:bg-indigo-400/10 dark:text-indigo-400">
|
|
{@callsign}
|
|
</span>
|
|
<% {symbol_table_id, symbol_code} =
|
|
AprsmeWeb.Live.Shared.PacketUtils.get_symbol_info(@weather_packet) %>
|
|
<% symbol_table =
|
|
if symbol_table_id in ["/", "\\", "]"], do: symbol_table_id, else: "/" %>
|
|
<% symbol_code = (symbol_code || ">") |> to_string() %>
|
|
<% 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>
|
|
</div>
|
|
</div>
|
|
<div class="mt-3 flex md:ml-4 md:mt-0">
|
|
<div class="flex space-x-2">
|
|
<.link
|
|
navigate={~p"/info/#{@callsign}"}
|
|
class="rounded-md bg-white px-2.5 py-1.5 text-sm font-semibold text-gray-900 shadow-xs ring-1 ring-gray-300 ring-inset hover:bg-gray-50 dark:bg-white/10 dark:text-white dark:ring-white/10 dark:hover:bg-white/20"
|
|
>
|
|
{gettext("View info")}
|
|
</.link>
|
|
<.link
|
|
navigate={~p"/packets/#{@callsign}"}
|
|
class="rounded-md bg-white px-2.5 py-1.5 text-sm font-semibold text-gray-900 shadow-xs ring-1 ring-gray-300 ring-inset hover:bg-gray-50 dark:bg-white/10 dark:text-white dark:ring-white/10 dark:hover:bg-white/20"
|
|
>
|
|
{gettext("View packets")}
|
|
</.link>
|
|
<.link
|
|
navigate={"/?call=#{@callsign}"}
|
|
class="rounded-md bg-indigo-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-xs hover:bg-indigo-500 dark:bg-indigo-500 dark:hover:bg-indigo-400"
|
|
>
|
|
{gettext("View on map")}
|
|
</.link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="container mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-4">
|
|
<div class="bg-white shadow-sm sm:rounded-lg dark:bg-gray-800/50 dark:shadow-none dark:outline dark:-outline-offset-1 dark:outline-white/10 mb-6">
|
|
<div class="px-6 py-6">
|
|
<% dt =
|
|
AprsmeWeb.TimeHelpers.to_datetime(
|
|
Map.get(@weather_packet, :inserted_at) || Map.get(@weather_packet, "inserted_at")
|
|
) %>
|
|
<div class="flex items-center mb-3">
|
|
<div class="flex-shrink-0">
|
|
<svg
|
|
class="h-4 w-4 text-indigo-600 dark:text-indigo-400"
|
|
viewBox="0 0 20 20"
|
|
fill="currentColor"
|
|
aria-hidden="true"
|
|
>
|
|
<path
|
|
fill-rule="evenodd"
|
|
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"
|
|
clip-rule="evenodd"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
<div class="ml-2">
|
|
<h3 class="text-sm font-medium">{gettext("Weather Details")}</h3>
|
|
</div>
|
|
</div>
|
|
<dl class="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">{gettext("Last WX report")}</dt>
|
|
<dd class="mt-1 text-sm font-semibold">
|
|
<%= if dt do %>
|
|
{Calendar.strftime(dt, "%Y-%m-%d %H:%M:%S UTC")} ({AprsmeWeb.TimeHelpers.time_ago_in_words(dt)})<br />
|
|
<%!-- <span class="text-xs text-gray-500">{Calendar.strftime(dt, "%Y-%m-%d %H:%M:%S %Z")} local time at {Map.get(@weather_packet, :region) || Map.get(@weather_packet, "region") || "Unknown location"} [?]</span> --%>
|
|
<% else %>
|
|
{gettext("Unknown")}
|
|
<% end %>
|
|
</dd>
|
|
</div>
|
|
<%= if has_weather_field?(@weather_packet, :temperature) do %>
|
|
<div>
|
|
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">{gettext("Temperature")}</dt>
|
|
<dd class="mt-1 text-sm font-semibold">
|
|
{format_weather_value(@weather_packet, :temperature, @locale)}
|
|
</dd>
|
|
</div>
|
|
<% end %>
|
|
<%= if has_weather_field?(@weather_packet, :humidity) do %>
|
|
<div>
|
|
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">{gettext("Humidity")}</dt>
|
|
<dd class="mt-1 text-sm font-semibold">
|
|
{PacketUtils.get_weather_field(@weather_packet, :humidity)}%
|
|
</dd>
|
|
</div>
|
|
<% end %>
|
|
<%= if has_weather_field?(@weather_packet, :wind_direction) || has_weather_field?(@weather_packet, :wind_speed) do %>
|
|
<div>
|
|
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">{gettext("Wind")}</dt>
|
|
<dd class="mt-1 text-sm font-semibold">
|
|
<%= if has_weather_field?(@weather_packet, :wind_direction) do %>
|
|
{PacketUtils.get_weather_field(@weather_packet, :wind_direction)}°
|
|
<% end %>
|
|
<%= if has_weather_field?(@weather_packet, :wind_speed) do %>
|
|
@ {format_weather_value(@weather_packet, :wind_speed, @locale)}
|
|
<% end %>
|
|
</dd>
|
|
</div>
|
|
<% end %>
|
|
<%= if has_weather_field?(@weather_packet, :wind_gust) do %>
|
|
<div>
|
|
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">{gettext("Gusts")}</dt>
|
|
<dd class="mt-1 text-sm font-semibold">
|
|
{format_weather_value(@weather_packet, :wind_gust, @locale)}
|
|
</dd>
|
|
</div>
|
|
<% end %>
|
|
<%= if has_weather_field?(@weather_packet, :pressure) do %>
|
|
<div>
|
|
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">{gettext("Pressure")}</dt>
|
|
<dd class="mt-1 text-sm font-semibold">
|
|
{PacketUtils.get_weather_field(@weather_packet, :pressure)} hPa
|
|
</dd>
|
|
</div>
|
|
<% end %>
|
|
<%= if has_weather_field?(@weather_packet, :rain_1h) do %>
|
|
<div>
|
|
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">{gettext("Rain (1h)")}</dt>
|
|
<dd class="mt-1 text-sm font-semibold">
|
|
{format_weather_value(@weather_packet, :rain_1h, @locale)}
|
|
</dd>
|
|
</div>
|
|
<% end %>
|
|
<%= if has_weather_field?(@weather_packet, :rain_24h) do %>
|
|
<div>
|
|
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">{gettext("Rain (24h)")}</dt>
|
|
<dd class="mt-1 text-sm font-semibold">
|
|
{format_weather_value(@weather_packet, :rain_24h, @locale)}
|
|
</dd>
|
|
</div>
|
|
<% end %>
|
|
<%= if has_weather_field?(@weather_packet, :rain_since_midnight) do %>
|
|
<div>
|
|
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">{gettext("Rain (since midnight)")}</dt>
|
|
<dd class="mt-1 text-sm font-semibold">
|
|
{format_weather_value(@weather_packet, :rain_since_midnight, @locale)}
|
|
</dd>
|
|
</div>
|
|
<% end %>
|
|
<div class="col-span-2">
|
|
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">{gettext("Raw Packet")}</dt>
|
|
<dd class="mt-1 text-xs font-mono break-all">
|
|
<%= if is_binary(@weather_packet.raw_packet) do %>
|
|
{Aprsme.EncodingUtils.sanitize_string(@weather_packet.raw_packet)}
|
|
<% else %>
|
|
{@weather_packet.raw_packet || ""}
|
|
<% end %>
|
|
</dd>
|
|
</div>
|
|
</dl>
|
|
<div class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
|
<strong>{gettext("Raw comment")}:</strong> {@weather_packet.comment ||
|
|
@weather_packet["comment"]}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="bg-white shadow-sm sm:rounded-lg dark:bg-gray-800/50 dark:shadow-none dark:outline dark:-outline-offset-1 dark:outline-white/10">
|
|
<div class="px-6 py-6">
|
|
<h3 class="text-sm font-medium mb-2">{gettext("Weather History Graphs")}</h3>
|
|
<%= if Enum.any?(@weather_history, &(not is_nil(Map.get(&1, :temperature, nil)))) do %>
|
|
<div
|
|
id="temp-chart"
|
|
phx-hook="ChartJSTempChart"
|
|
data-weather-history={@weather_history_json}
|
|
data-chart-labels={Jason.encode!(@chart_labels)}
|
|
class="bg-gray-100 dark:bg-gray-800 rounded-lg p-4 mb-4"
|
|
style="height:250px;"
|
|
>
|
|
<canvas></canvas>
|
|
</div>
|
|
<% end %>
|
|
<%= if Enum.any?(@weather_history, &(not is_nil(Map.get(&1, :humidity, nil)))) do %>
|
|
<div
|
|
id="humidity-chart"
|
|
phx-hook="ChartJSHumidityChart"
|
|
data-weather-history={@weather_history_json}
|
|
data-chart-labels={Jason.encode!(@chart_labels)}
|
|
class="bg-gray-100 dark:bg-gray-800 rounded-lg p-4 mb-4"
|
|
style="height:250px;"
|
|
>
|
|
<canvas></canvas>
|
|
</div>
|
|
<% end %>
|
|
<%= if Enum.any?(@weather_history, &(not is_nil(Map.get(&1, :pressure, nil)))) do %>
|
|
<div
|
|
id="pressure-chart"
|
|
phx-hook="ChartJSPressureChart"
|
|
data-weather-history={@weather_history_json}
|
|
data-chart-labels={Jason.encode!(@chart_labels)}
|
|
class="bg-gray-100 dark:bg-gray-800 rounded-lg p-4 mb-4"
|
|
style="height:250px;"
|
|
>
|
|
<canvas></canvas>
|
|
</div>
|
|
<% end %>
|
|
<%= if Enum.any?(@weather_history, &(not is_nil(Map.get(&1, :wind_speed, nil)))) do %>
|
|
<div
|
|
id="wind-chart"
|
|
phx-hook="ChartJSWindChart"
|
|
data-weather-history={@weather_history_json}
|
|
data-chart-labels={Jason.encode!(@chart_labels)}
|
|
class="bg-gray-100 dark:bg-gray-800 rounded-lg p-4 mb-4"
|
|
style="height:250px;"
|
|
>
|
|
<canvas></canvas>
|
|
</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="ChartJSRainChart"
|
|
data-weather-history={@weather_history_json}
|
|
data-chart-labels={Jason.encode!(@chart_labels)}
|
|
class="bg-gray-100 dark:bg-gray-800 rounded-lg p-4 mb-4"
|
|
style="height:250px;"
|
|
>
|
|
<canvas></canvas>
|
|
</div>
|
|
<% end %>
|
|
<%= if Enum.any?(@weather_history, &(not is_nil(Map.get(&1, :luminosity, nil)))) do %>
|
|
<div
|
|
id="luminosity-chart"
|
|
phx-hook="ChartJSLuminosityChart"
|
|
data-weather-history={@weather_history_json}
|
|
data-chart-labels={Jason.encode!(@chart_labels)}
|
|
class="bg-gray-100 dark:bg-gray-800 rounded-lg p-4 mb-4"
|
|
style="height:250px;"
|
|
>
|
|
<canvas></canvas>
|
|
</div>
|
|
<% end %>
|
|
<%!-- Chart.js and adapter are now loaded from vendor.js bundle --%>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<% else %>
|
|
<div class="bg-gray-50 dark:bg-gray-900 flex items-center justify-center min-h-screen">
|
|
<div class="bg-white shadow-sm sm:rounded-lg dark:bg-gray-800/50 dark:shadow-none dark:outline dark:-outline-offset-1 dark:outline-white/10 max-w-lg">
|
|
<div class="px-6 py-8 text-center">
|
|
<h1 class="text-2xl font-bold mb-2">Weather for {@callsign}</h1>
|
|
<div class="text-gray-500 dark:text-gray-400">No recent weather data available for this callsign.</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<% end %>
|