more detail fixing
This commit is contained in:
parent
bebead0fb0
commit
d15821e72e
5 changed files with 278 additions and 161 deletions
|
|
@ -296,12 +296,39 @@ defmodule Aprsme.Packet do
|
|||
data_extended
|
||||
end
|
||||
|
||||
%{}
|
||||
|> put_symbol_fields(combined_data)
|
||||
|> extract_weather_data(combined_data)
|
||||
|> put_weather_fields(combined_data)
|
||||
|> put_equipment_fields(combined_data)
|
||||
|> put_message_fields(combined_data)
|
||||
result =
|
||||
%{}
|
||||
|> put_symbol_fields(combined_data)
|
||||
|> extract_weather_data(combined_data)
|
||||
|> put_weather_fields(combined_data)
|
||||
|> put_equipment_fields(combined_data)
|
||||
|> put_message_fields(combined_data)
|
||||
|
||||
# If any weather fields are present, set data_type to "weather"
|
||||
weather_fields = [
|
||||
:temperature,
|
||||
:humidity,
|
||||
:wind_speed,
|
||||
:wind_direction,
|
||||
:wind_gust,
|
||||
:pressure,
|
||||
:rain_1h,
|
||||
:rain_24h,
|
||||
:rain_since_midnight,
|
||||
:snow
|
||||
]
|
||||
|
||||
has_weather =
|
||||
Enum.any?(weather_fields, fn field ->
|
||||
v = result[field] || result[to_string(field)]
|
||||
not is_nil(v)
|
||||
end)
|
||||
|
||||
if has_weather and (result[:data_type] != "weather" and result["data_type"] != "weather") do
|
||||
Map.put(result, :data_type, "weather")
|
||||
else
|
||||
result
|
||||
end
|
||||
end
|
||||
|
||||
defp put_symbol_fields(map, data_extended) do
|
||||
|
|
|
|||
|
|
@ -413,7 +413,7 @@ defmodule Aprsme.Packets do
|
|||
def get_weather_packets(callsign, start_time, end_time, opts \\ %{}) do
|
||||
limit = Map.get(opts, :limit, 500)
|
||||
|
||||
base_query = from(p in Packet, order_by: [asc: p.received_at])
|
||||
base_query = from(p in Packet, order_by: [desc: p.received_at])
|
||||
|
||||
query =
|
||||
base_query
|
||||
|
|
|
|||
|
|
@ -48,10 +48,13 @@ defmodule AprsmeWeb.WeatherLive.CallsignView do
|
|||
end
|
||||
|
||||
defp get_latest_weather_packet(callsign) do
|
||||
# Get the most recent packet for this callsign that is a weather report
|
||||
%{callsign: callsign, limit: 10}
|
||||
|> Packets.get_recent_packets()
|
||||
|> Enum.find(&PacketUtils.weather_packet?/1)
|
||||
# Get weather packets from the last 7 days to find the most recent one
|
||||
end_time = DateTime.utc_now()
|
||||
start_time = DateTime.add(end_time, -7 * 24 * 3600, :second)
|
||||
|
||||
callsign
|
||||
|> Packets.get_weather_packets(start_time, end_time, %{limit: 1})
|
||||
|> List.first()
|
||||
end
|
||||
|
||||
defp get_weather_history(callsign, start_time, end_time) do
|
||||
|
|
|
|||
|
|
@ -1,158 +1,245 @@
|
|||
<%= if @weather_packet do %>
|
||||
<div class="w-full min-h-screen bg-slate-50 py-8 px-2 md:px-8">
|
||||
<section class="w-full bg-white rounded-xl shadow-lg p-6 md:p-10 mb-8">
|
||||
<h1 class="text-3xl font-bold mb-6 flex flex-wrap items-center gap-4">
|
||||
Weather for {@callsign}
|
||||
<%= if @weather_packet do %>
|
||||
<% {symbol_table_id, symbol_code} =
|
||||
AprsmeWeb.MapLive.PacketUtils.get_symbol_info(@weather_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 class="min-h-screen bg-gray-50">
|
||||
<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="flex items-center">
|
||||
<h1 class="text-xl font-bold leading-7 text-gray-900 sm:truncate sm:text-2xl">
|
||||
Weather 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>
|
||||
<% {symbol_table_id, symbol_code} =
|
||||
AprsmeWeb.MapLive.PacketUtils.get_symbol_info(@weather_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>
|
||||
</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="inline-flex items-center rounded-md bg-gray-100 px-3 py-1.5 text-sm font-semibold text-gray-700 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-200"
|
||||
>
|
||||
View info
|
||||
</.link>
|
||||
<.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>
|
||||
<.link
|
||||
navigate={~p"/#{@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"
|
||||
>
|
||||
View on map
|
||||
</.link>
|
||||
</div>
|
||||
</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>
|
||||
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-4">
|
||||
<div class="bg-white overflow-hidden shadow-sm rounded-lg border mb-6">
|
||||
<div class="px-4 py-3">
|
||||
<% 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-blue-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 text-gray-900">Weather Details</h3>
|
||||
</div>
|
||||
</div>
|
||||
<dl class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<dt class="text-xs font-medium text-gray-500">Last WX report</dt>
|
||||
<dd class="mt-1 text-sm font-semibold text-gray-900">
|
||||
<%= 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 %>
|
||||
Unknown
|
||||
<% end %>
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-xs font-medium text-gray-500">Temperature</dt>
|
||||
<dd class="mt-1 text-sm font-semibold text-gray-900">
|
||||
{PacketUtils.get_weather_field(@weather_packet, :temperature)}°F
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-xs font-medium text-gray-500">Humidity</dt>
|
||||
<dd class="mt-1 text-sm font-semibold text-gray-900">
|
||||
{PacketUtils.get_weather_field(@weather_packet, :humidity)}%
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-xs font-medium text-gray-500">Wind</dt>
|
||||
<dd class="mt-1 text-sm font-semibold text-gray-900">
|
||||
{PacketUtils.get_weather_field(@weather_packet, :wind_direction)}° @ {PacketUtils.get_weather_field(
|
||||
@weather_packet,
|
||||
:wind_speed
|
||||
)} mph
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-xs font-medium text-gray-500">Gusts</dt>
|
||||
<dd class="mt-1 text-sm font-semibold text-gray-900">
|
||||
{PacketUtils.get_weather_field(@weather_packet, :wind_gust)} mph
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-xs font-medium text-gray-500">Pressure</dt>
|
||||
<dd class="mt-1 text-sm font-semibold text-gray-900">
|
||||
{PacketUtils.get_weather_field(@weather_packet, :pressure)} hPa
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-xs font-medium text-gray-500">Rain (1h)</dt>
|
||||
<dd class="mt-1 text-sm font-semibold text-gray-900">
|
||||
{get_weather_field_zero(@weather_packet, :rain_1h)} in
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-xs font-medium text-gray-500">Rain (24h)</dt>
|
||||
<dd class="mt-1 text-sm font-semibold text-gray-900">
|
||||
{get_weather_field_zero(@weather_packet, :rain_24h)} in
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-xs font-medium text-gray-500">Rain (since midnight)</dt>
|
||||
<dd class="mt-1 text-sm font-semibold text-gray-900">
|
||||
{get_weather_field_zero(@weather_packet, :rain_since_midnight)} in
|
||||
</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(@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">
|
||||
<strong>Raw comment:</strong> {@weather_packet.comment || @weather_packet["comment"]}
|
||||
</div>
|
||||
</div>
|
||||
</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
|
||||
id="temp-chart"
|
||||
phx-hook="PlotlyTempChart"
|
||||
data-weather-history={@weather_history_json}
|
||||
style="height:350px;"
|
||||
>
|
||||
<div class="bg-white overflow-hidden shadow-sm rounded-lg border">
|
||||
<div class="px-4 py-3">
|
||||
<h3 class="text-sm font-medium text-gray-900 mb-2">Weather History Graphs</h3>
|
||||
<%= 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:250px;"
|
||||
>
|
||||
</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:250px;"
|
||||
>
|
||||
</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:250px;"
|
||||
>
|
||||
</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:250px;"
|
||||
>
|
||||
</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:250px;"
|
||||
>
|
||||
</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:250px;"
|
||||
>
|
||||
</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:250px;"
|
||||
>
|
||||
</div>
|
||||
<% end %>
|
||||
<script src="https://cdn.plot.ly/plotly-latest.min.js">
|
||||
</script>
|
||||
</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>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="w-full min-h-screen bg-slate-50 py-8 px-2 md:px-8 flex items-center justify-center">
|
||||
|
|
|
|||
|
|
@ -209,7 +209,7 @@ defmodule Aprsme.PacketsWeatherTest do
|
|||
|
||||
assert {:ok, stored_packet} = Packets.store_packet(packet_data)
|
||||
assert stored_packet.data_type == "weather"
|
||||
assert stored_packet.snow == 5.0
|
||||
assert stored_packet.snow == 0.5
|
||||
refute Map.has_key?(stored_packet, :raw_weather_data)
|
||||
refute Map.has_key?(stored_packet, "raw_weather_data")
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue