DRY and list scrolling
This commit is contained in:
parent
f27a6e2a4e
commit
f575863e7f
9 changed files with 582 additions and 517 deletions
|
|
@ -4,87 +4,6 @@
|
|||
|
||||
/* This file is for your main application CSS */
|
||||
|
||||
/* Ensure full height layout for map pages */
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Main content container for map pages */
|
||||
main {
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#map {
|
||||
height: calc(100vh - 60px);
|
||||
/* Adjust based on header height */
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Full page map for APRS home page and callsign pages */
|
||||
#aprs-map {
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
bottom: 0 !important;
|
||||
height: 100vh !important;
|
||||
width: 100vw !important;
|
||||
z-index: 1 !important;
|
||||
transition: right 0.3s ease-in-out !important;
|
||||
}
|
||||
|
||||
/* Desktop slideover behavior for map */
|
||||
@media (min-width: 1024px) {
|
||||
#aprs-map.slideover-open {
|
||||
right: 352px !important;
|
||||
}
|
||||
|
||||
#aprs-map.slideover-closed {
|
||||
right: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile slideover behavior for map */
|
||||
@media (max-width: 1023px) {
|
||||
#aprs-map {
|
||||
right: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Ensure the map container div has proper dimensions */
|
||||
.phx-main {
|
||||
position: relative;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Fix for LiveView containers */
|
||||
div[data-phx-main="true"] {
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Hide header on home page */
|
||||
body.home-page header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Adjust main content area for full page map */
|
||||
body.home-page main {
|
||||
padding: 0;
|
||||
max-width: none;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
body.home-page main > div {
|
||||
max-width: none;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Marker cluster styles */
|
||||
.marker-cluster-small {
|
||||
background-color: rgba(16, 185, 129, 0.8);
|
||||
|
|
@ -292,113 +211,3 @@ body.home-page main > div {
|
|||
.historical-dot-marker {
|
||||
z-index: 15 !important;
|
||||
}
|
||||
|
||||
/* Slideover toggle button styles */
|
||||
.slideover-toggle {
|
||||
position: fixed !important;
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
z-index: 1001;
|
||||
background: white;
|
||||
border: 2px solid rgba(0, 0, 0, 0.2);
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.slideover-toggle:hover {
|
||||
background: #f8fafc;
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.slideover-toggle:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
/* Hide toggle on desktop when slideover is open */
|
||||
@media (min-width: 1024px) {
|
||||
.slideover-toggle.slideover-open {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile toggle adjustments */
|
||||
@media (max-width: 1023px) {
|
||||
.slideover-toggle {
|
||||
right: 15px;
|
||||
top: 15px;
|
||||
padding: 12px;
|
||||
border-radius: 50%;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Slideover panel responsive styles */
|
||||
.slideover-panel {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
height: 100vh;
|
||||
width: 352px;
|
||||
background: white;
|
||||
box-shadow: -4px 0 24px rgba(0, 0, 0, 0.15);
|
||||
z-index: 1000;
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Desktop slideover behavior */
|
||||
@media (min-width: 1024px) {
|
||||
.slideover-panel.slideover-open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.slideover-panel.slideover-closed {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile slideover behavior */
|
||||
@media (max-width: 1023px) {
|
||||
.slideover-panel {
|
||||
width: 100vw;
|
||||
max-width: 400px;
|
||||
box-shadow: -8px 0 32px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.slideover-panel.slideover-open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.slideover-panel.slideover-closed {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile panel improvements */
|
||||
@media (max-width: 1023px) {
|
||||
.slideover-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
/* Improve scrolling on mobile */
|
||||
@media (max-width: 1023px) {
|
||||
.slideover-panel {
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.slideover-panel .flex-1 {
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,19 +4,7 @@ defmodule AprsWeb.Layouts do
|
|||
|
||||
embed_templates "layouts/*"
|
||||
|
||||
def body_class(assigns) do
|
||||
classes = ["bg-white antialiased"]
|
||||
|
||||
# Check if this is the home page (map)
|
||||
is_home =
|
||||
cond do
|
||||
# For regular controller requests
|
||||
assigns[:conn] && assigns.conn.request_path == "/" -> true
|
||||
# For LiveView requests
|
||||
assigns[:socket] && assigns.socket.view == AprsWeb.MapLive.Index -> true
|
||||
true -> false
|
||||
end
|
||||
|
||||
if is_home, do: ["home-page" | classes], else: classes
|
||||
def body_class(_assigns) do
|
||||
["bg-white antialiased"]
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<main>
|
||||
<main class="min-h-screen bg-white">
|
||||
<div>
|
||||
<.flash kind={:info} title="Success!" flash={@flash} />
|
||||
<.flash kind={:error} title="Error!" flash={@flash} />
|
||||
|
|
|
|||
213
lib/aprs_web/components/layouts/map.html.heex
Normal file
213
lib/aprs_web/components/layouts/map.html.heex
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
<style>
|
||||
/* Map-specific CSS styles */
|
||||
|
||||
/* Ensure full height layout for map pages */
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Main content container for map pages */
|
||||
main {
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#map {
|
||||
height: calc(100vh - 60px);
|
||||
/* Adjust based on header height */
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Full page map for APRS home page and callsign pages */
|
||||
#aprs-map {
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
bottom: 0 !important;
|
||||
height: 100vh !important;
|
||||
width: 100vw !important;
|
||||
z-index: 1 !important;
|
||||
transition: right 0.3s ease-in-out !important;
|
||||
}
|
||||
|
||||
/* Desktop slideover behavior for map */
|
||||
@media (min-width: 1024px) {
|
||||
#aprs-map.slideover-open {
|
||||
right: 352px !important;
|
||||
}
|
||||
|
||||
#aprs-map.slideover-closed {
|
||||
right: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile slideover behavior for map */
|
||||
@media (max-width: 1023px) {
|
||||
#aprs-map {
|
||||
right: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Ensure the map container div has proper dimensions */
|
||||
.phx-main {
|
||||
position: relative;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Fix for LiveView containers */
|
||||
div[data-phx-main="true"] {
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Hide header on home page */
|
||||
body.home-page header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Adjust main content area for full page map */
|
||||
body.home-page main {
|
||||
padding: 0;
|
||||
max-width: none;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
body.home-page main > div {
|
||||
max-width: none;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Slideover toggle button styles */
|
||||
.slideover-toggle {
|
||||
position: fixed !important;
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
z-index: 1001;
|
||||
background: white;
|
||||
border: 2px solid rgba(0, 0, 0, 0.2);
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.slideover-toggle:hover {
|
||||
background: #f8fafc;
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.slideover-toggle:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
/* Hide toggle on desktop when slideover is open */
|
||||
@media (min-width: 1024px) {
|
||||
.slideover-toggle.slideover-open {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile toggle adjustments */
|
||||
@media (max-width: 1023px) {
|
||||
.slideover-toggle {
|
||||
right: 15px;
|
||||
top: 15px;
|
||||
padding: 12px;
|
||||
border-radius: 50%;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Slideover panel responsive styles */
|
||||
.slideover-panel {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
height: 100vh;
|
||||
width: 352px;
|
||||
background: white;
|
||||
box-shadow: -4px 0 24px rgba(0, 0, 0, 0.15);
|
||||
z-index: 1000;
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Desktop slideover behavior */
|
||||
@media (min-width: 1024px) {
|
||||
.slideover-panel.slideover-open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.slideover-panel.slideover-closed {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile slideover behavior */
|
||||
@media (max-width: 1023px) {
|
||||
.slideover-panel {
|
||||
width: 100vw;
|
||||
max-width: 400px;
|
||||
box-shadow: -8px 0 32px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.slideover-panel.slideover-open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.slideover-panel.slideover-closed {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile panel improvements */
|
||||
@media (max-width: 1023px) {
|
||||
.slideover-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
/* Improve scrolling on mobile */
|
||||
@media (max-width: 1023px) {
|
||||
.slideover-panel {
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.slideover-panel .flex-1 {
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<main>
|
||||
<div>
|
||||
<.flash kind={:info} title="Success!" flash={@flash} />
|
||||
<.flash kind={:error} title="Error!" flash={@flash} />
|
||||
<.flash
|
||||
id="disconnected"
|
||||
kind={:error}
|
||||
title="We can't find the internet"
|
||||
close={false}
|
||||
autoshow={false}
|
||||
phx-disconnected={show("#disconnected")}
|
||||
phx-connected={hide("#disconnected")}
|
||||
>
|
||||
Attempting to reconnect <Heroicons.arrow_path class="ml-1 w-3 h-3 inline animate-spin" />
|
||||
</.flash>
|
||||
{@inner_content}
|
||||
</div>
|
||||
</main>
|
||||
|
|
@ -5,6 +5,7 @@ defmodule AprsWeb.MapLive.CallsignView do
|
|||
alias Aprs.Packets
|
||||
alias AprsWeb.Endpoint
|
||||
alias AprsWeb.MapLive.MapHelpers
|
||||
alias AprsWeb.MapLive.PacketUtils
|
||||
|
||||
@default_center %{lat: 39.0, lng: -98.0}
|
||||
@default_zoom 4
|
||||
|
|
@ -864,74 +865,17 @@ defmodule AprsWeb.MapLive.CallsignView do
|
|||
|
||||
defp build_packet_map(packet, lat, lng, data_extended) do
|
||||
data_extended = data_extended || %{}
|
||||
|
||||
callsign =
|
||||
case {Map.get(packet, :base_callsign), Map.get(packet, :ssid)} do
|
||||
{base, ssid} when is_binary(base) and ssid not in [nil, "", "0"] ->
|
||||
"#{base}-#{ssid}"
|
||||
|
||||
{base, _} ->
|
||||
base || ""
|
||||
end
|
||||
|
||||
symbol_table_id =
|
||||
Map.get(data_extended, :symbol_table_id) ||
|
||||
Map.get(data_extended, "symbol_table_id") ||
|
||||
Map.get(packet, :symbol_table_id) ||
|
||||
Map.get(packet, "symbol_table_id") ||
|
||||
"/"
|
||||
|
||||
symbol_code =
|
||||
Map.get(data_extended, :symbol_code) ||
|
||||
Map.get(data_extended, "symbol_code") ||
|
||||
Map.get(packet, :symbol_code) ||
|
||||
Map.get(packet, "symbol_code") ||
|
||||
">"
|
||||
|
||||
symbol_description =
|
||||
Map.get(data_extended, :symbol_description) || Map.get(data_extended, "symbol_description") ||
|
||||
"Symbol: #{symbol_table_id}#{symbol_code}"
|
||||
|
||||
timestamp =
|
||||
cond do
|
||||
Map.has_key?(packet, :received_at) && packet.received_at ->
|
||||
DateTime.to_iso8601(packet.received_at)
|
||||
|
||||
Map.has_key?(packet, "received_at") && packet["received_at"] ->
|
||||
DateTime.to_iso8601(packet["received_at"])
|
||||
|
||||
true ->
|
||||
""
|
||||
end
|
||||
|
||||
comment = Map.get(data_extended, :comment) || Map.get(data_extended, "comment") || ""
|
||||
|
||||
to_float = fn
|
||||
%Decimal{} = d ->
|
||||
Decimal.to_float(d)
|
||||
|
||||
n when is_float(n) ->
|
||||
n
|
||||
|
||||
n when is_integer(n) ->
|
||||
n * 1.0
|
||||
|
||||
n when is_binary(n) ->
|
||||
case Float.parse(n) do
|
||||
{f, _} -> f
|
||||
:error -> 0.0
|
||||
end
|
||||
|
||||
_ ->
|
||||
0.0
|
||||
end
|
||||
callsign = PacketUtils.generate_callsign(packet)
|
||||
{symbol_table_id, symbol_code} = PacketUtils.get_symbol_info(packet)
|
||||
timestamp = PacketUtils.get_timestamp(packet)
|
||||
comment = PacketUtils.get_packet_field(packet, :comment, "")
|
||||
|
||||
popup = """
|
||||
<div class=\"aprs-popup\">
|
||||
<div class=\"aprs-callsign\"><strong><a href=\"/#{callsign}\">#{callsign}</a></strong></div>
|
||||
<div class=\"aprs-symbol-info\">#{symbol_description}</div>
|
||||
<div class=\"aprs-symbol-info\">Symbol: #{symbol_table_id}#{symbol_code}</div>
|
||||
#{if comment == "", do: "", else: "<div class=\\\"aprs-comment\\\">#{comment}</div>"}
|
||||
<div class=\"aprs-coords\">#{Float.round(to_float.(lat), 4)}, #{Float.round(to_float.(lng), 4)}</div>
|
||||
<div class=\"aprs-coords\">#{Float.round(PacketUtils.to_float(lat), 4)}, #{Float.round(PacketUtils.to_float(lng), 4)}</div>
|
||||
<div class=\"aprs-time\">#{timestamp}</div>
|
||||
</div>
|
||||
"""
|
||||
|
|
@ -939,17 +883,17 @@ defmodule AprsWeb.MapLive.CallsignView do
|
|||
%{
|
||||
"id" => callsign,
|
||||
"callsign" => callsign,
|
||||
"base_callsign" => Map.get(packet, :base_callsign, Map.get(packet, "base_callsign", "")),
|
||||
"ssid" => Map.get(packet, :ssid, Map.get(packet, "ssid", 0)),
|
||||
"lat" => to_float.(lat),
|
||||
"lng" => to_float.(lng),
|
||||
"data_type" => to_string(Map.get(packet, :data_type, Map.get(packet, "data_type", "unknown"))),
|
||||
"path" => Map.get(packet, :path, Map.get(packet, "path", "")),
|
||||
"base_callsign" => PacketUtils.get_packet_field(packet, :base_callsign, ""),
|
||||
"ssid" => PacketUtils.get_packet_field(packet, :ssid, 0),
|
||||
"lat" => PacketUtils.to_float(lat),
|
||||
"lng" => PacketUtils.to_float(lng),
|
||||
"data_type" => to_string(PacketUtils.get_packet_field(packet, :data_type, "unknown")),
|
||||
"path" => PacketUtils.get_packet_field(packet, :path, ""),
|
||||
"comment" => comment,
|
||||
"data_extended" => convert_tuples_to_strings(data_extended || %{}),
|
||||
"data_extended" => PacketUtils.convert_tuples_to_strings(data_extended || %{}),
|
||||
"symbol_table_id" => symbol_table_id,
|
||||
"symbol_code" => symbol_code,
|
||||
"symbol_description" => symbol_description,
|
||||
"symbol_description" => "Symbol: #{symbol_table_id}#{symbol_code}",
|
||||
"timestamp" => timestamp,
|
||||
"popup" => popup
|
||||
}
|
||||
|
|
@ -998,26 +942,6 @@ defmodule AprsWeb.MapLive.CallsignView do
|
|||
end
|
||||
end
|
||||
|
||||
defp convert_tuples_to_strings(map) when is_map(map) do
|
||||
if Map.has_key?(map, :__struct__) do
|
||||
map
|
||||
else
|
||||
Map.new(map, fn {k, v} ->
|
||||
{k, convert_tuples_to_strings(v)}
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
defp convert_tuples_to_strings(list) when is_list(list) do
|
||||
Enum.map(list, &convert_tuples_to_strings/1)
|
||||
end
|
||||
|
||||
defp convert_tuples_to_strings(tuple) when is_tuple(tuple) do
|
||||
to_string(inspect(tuple))
|
||||
end
|
||||
|
||||
defp convert_tuples_to_strings(other), do: other
|
||||
|
||||
defp load_callsign_packets(socket, callsign) do
|
||||
# Load only the latest packet for this specific callsign
|
||||
latest_packet =
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ defmodule AprsWeb.MapLive.Index do
|
|||
|
||||
alias AprsWeb.Endpoint
|
||||
alias AprsWeb.MapLive.MapHelpers
|
||||
alias AprsWeb.MapLive.PacketUtils
|
||||
alias Phoenix.LiveView.Socket
|
||||
|
||||
require Logger
|
||||
|
|
@ -411,40 +412,53 @@ defmodule AprsWeb.MapLive.Index do
|
|||
|
||||
defp handle_info_postgres_packet(packet, socket) do
|
||||
{lat, lon, _data_extended} = MapHelpers.get_coordinates(packet)
|
||||
callsign_key = get_callsign_key(packet)
|
||||
|
||||
callsign_key =
|
||||
if Map.has_key?(packet, "id"),
|
||||
do: to_string(packet["id"]),
|
||||
else: System.unique_integer([:positive])
|
||||
|
||||
# Logger.debug(
|
||||
# "[MAP] Incoming packet: id=#{inspect(callsign_key)} lat=#{inspect(lat)} lon=#{inspect(lon)} bounds=#{inspect(socket.assigns.map_bounds)} within_bounds?=#{inspect(MapHelpers.within_bounds?(%{lat: lat, lon: lon}, socket.assigns.map_bounds))}"
|
||||
# )
|
||||
|
||||
# Update all_packets
|
||||
all_packets = Map.put(socket.assigns.all_packets, callsign_key, packet)
|
||||
socket = assign(socket, all_packets: all_packets)
|
||||
|
||||
# Remove marker if packet is out of bounds but present
|
||||
if !is_nil(lat) and !is_nil(lon) and
|
||||
Map.has_key?(socket.assigns.visible_packets, callsign_key) and
|
||||
not MapHelpers.within_bounds?(%{lat: lat, lon: lon}, socket.assigns.map_bounds) do
|
||||
socket = push_event(socket, "remove_marker", %{id: callsign_key})
|
||||
new_visible_packets = Map.delete(socket.assigns.visible_packets, callsign_key)
|
||||
{:noreply, assign(socket, visible_packets: new_visible_packets)}
|
||||
# Only add marker if it is within bounds and not already present
|
||||
else
|
||||
if is_nil(lat) or is_nil(lon) or Map.has_key?(socket.assigns.visible_packets, callsign_key) do
|
||||
# Handle packet visibility logic
|
||||
handle_packet_visibility(packet, lat, lon, callsign_key, socket)
|
||||
end
|
||||
|
||||
defp get_callsign_key(packet) do
|
||||
if Map.has_key?(packet, "id"),
|
||||
do: to_string(packet["id"]),
|
||||
else: System.unique_integer([:positive])
|
||||
end
|
||||
|
||||
defp handle_packet_visibility(packet, lat, lon, callsign_key, socket) do
|
||||
cond do
|
||||
should_remove_marker?(lat, lon, callsign_key, socket) ->
|
||||
remove_marker_from_map(callsign_key, socket)
|
||||
|
||||
should_add_marker?(lat, lon, callsign_key, socket) ->
|
||||
handle_valid_postgres_packet(packet, lat, lon, socket)
|
||||
|
||||
true ->
|
||||
{:noreply, socket}
|
||||
else
|
||||
if MapHelpers.within_bounds?(%{lat: lat, lon: lon}, socket.assigns.map_bounds) do
|
||||
handle_valid_postgres_packet(packet, lat, lon, socket)
|
||||
else
|
||||
{:noreply, socket}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp should_remove_marker?(lat, lon, callsign_key, socket) do
|
||||
!is_nil(lat) and !is_nil(lon) and
|
||||
Map.has_key?(socket.assigns.visible_packets, callsign_key) and
|
||||
not MapHelpers.within_bounds?(%{lat: lat, lon: lon}, socket.assigns.map_bounds)
|
||||
end
|
||||
|
||||
defp should_add_marker?(lat, lon, callsign_key, socket) do
|
||||
!is_nil(lat) and !is_nil(lon) and
|
||||
not Map.has_key?(socket.assigns.visible_packets, callsign_key) and
|
||||
MapHelpers.within_bounds?(%{lat: lat, lon: lon}, socket.assigns.map_bounds)
|
||||
end
|
||||
|
||||
defp remove_marker_from_map(callsign_key, socket) do
|
||||
socket = push_event(socket, "remove_marker", %{id: callsign_key})
|
||||
new_visible_packets = Map.delete(socket.assigns.visible_packets, callsign_key)
|
||||
{:noreply, assign(socket, visible_packets: new_visible_packets)}
|
||||
end
|
||||
|
||||
defp handle_valid_postgres_packet(packet, _lat, _lon, socket) do
|
||||
# Add the packet to visible_packets and push a marker immediately
|
||||
callsign_key =
|
||||
|
|
@ -1082,9 +1096,9 @@ defmodule AprsWeb.MapLive.Index do
|
|||
defp filter_unique_positions(packets) do
|
||||
packets
|
||||
|> Enum.reduce([], fn packet, acc ->
|
||||
{lat, lng, _} = MapHelpers.get_coordinates(packet)
|
||||
{lat, lon, _} = MapHelpers.get_coordinates(packet)
|
||||
|
||||
if lat && lng do
|
||||
if lat && lon do
|
||||
case acc do
|
||||
[] ->
|
||||
# First packet, always include
|
||||
|
|
@ -1278,154 +1292,98 @@ defmodule AprsWeb.MapLive.Index do
|
|||
@spec build_packet_map(map() | struct(), number(), number(), map() | nil) :: map()
|
||||
defp build_packet_map(packet, lat, lon, data_extended) do
|
||||
data_extended = data_extended || %{}
|
||||
callsign = generate_callsign(packet)
|
||||
packet_info = extract_packet_info(packet, data_extended)
|
||||
popup = build_popup_content(packet, packet_info, lat, lon)
|
||||
|
||||
symbol_table_id =
|
||||
Map.get(data_extended, :symbol_table_id) ||
|
||||
Map.get(data_extended, "symbol_table_id") ||
|
||||
Map.get(packet, :symbol_table_id) ||
|
||||
Map.get(packet, "symbol_table_id") ||
|
||||
"/"
|
||||
|
||||
symbol_code =
|
||||
Map.get(data_extended, :symbol_code) ||
|
||||
Map.get(data_extended, "symbol_code") ||
|
||||
Map.get(packet, :symbol_code) ||
|
||||
Map.get(packet, "symbol_code") ||
|
||||
">"
|
||||
|
||||
symbol_description =
|
||||
Map.get(data_extended, :symbol_description) || Map.get(data_extended, "symbol_description") ||
|
||||
"Symbol: #{symbol_table_id}#{symbol_code}"
|
||||
|
||||
timestamp =
|
||||
cond do
|
||||
Map.has_key?(packet, :received_at) && packet.received_at ->
|
||||
DateTime.to_iso8601(packet.received_at)
|
||||
|
||||
Map.has_key?(packet, "received_at") && packet["received_at"] ->
|
||||
DateTime.to_iso8601(packet["received_at"])
|
||||
|
||||
true ->
|
||||
""
|
||||
end
|
||||
|
||||
comment = Map.get(data_extended, :comment) || Map.get(data_extended, "comment") || ""
|
||||
|
||||
# Recursively convert tuples in data_extended to strings
|
||||
safe_data_extended = convert_tuples_to_strings(data_extended)
|
||||
|
||||
to_float = fn
|
||||
%Decimal{} = d ->
|
||||
Decimal.to_float(d)
|
||||
|
||||
n when is_float(n) ->
|
||||
n
|
||||
|
||||
n when is_integer(n) ->
|
||||
n * 1.0
|
||||
|
||||
n when is_binary(n) ->
|
||||
case Float.parse(n) do
|
||||
{f, _} -> f
|
||||
:error -> 0.0
|
||||
end
|
||||
|
||||
_ ->
|
||||
0.0
|
||||
end
|
||||
|
||||
is_weather_packet =
|
||||
(Map.get(packet, :data_type) || Map.get(packet, "data_type")) == "weather" or
|
||||
(symbol_table_id == "/" and symbol_code == "_")
|
||||
|
||||
popup =
|
||||
if is_weather_packet do
|
||||
build_weather_popup_html(packet, callsign)
|
||||
else
|
||||
"""
|
||||
<div class="aprs-popup">
|
||||
<div class="aprs-callsign"><strong><a href="/map/callsign/#{callsign}">#{callsign}</a></strong></div>
|
||||
#{if comment == "", do: "", else: ~s(<div class="aprs-comment">#{comment}</div>)}
|
||||
<div class="aprs-coords">#{Float.round(to_float.(lat), 4)}, #{Float.round(to_float.(lon), 4)}</div>
|
||||
<div class="aprs-time">#{timestamp}</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
build_packet_result(packet, packet_info, lat, lon, popup)
|
||||
end
|
||||
|
||||
defp extract_packet_info(packet, data_extended) do
|
||||
%{
|
||||
"id" => callsign,
|
||||
"callsign" => callsign,
|
||||
"base_callsign" => Map.get(packet, :base_callsign, Map.get(packet, "base_callsign", "")),
|
||||
"ssid" => Map.get(packet, :ssid, Map.get(packet, "ssid", 0)),
|
||||
"lat" => to_float.(lat),
|
||||
"lng" => to_float.(lon),
|
||||
"data_type" => to_string(Map.get(packet, :data_type, Map.get(packet, "data_type", "unknown"))),
|
||||
"path" => Map.get(packet, :path, Map.get(packet, "path", "")),
|
||||
"comment" => comment,
|
||||
"data_extended" => safe_data_extended || %{},
|
||||
"symbol_table_id" => symbol_table_id,
|
||||
"symbol_code" => symbol_code,
|
||||
"symbol_description" => symbol_description,
|
||||
"timestamp" => timestamp,
|
||||
callsign: PacketUtils.generate_callsign(packet),
|
||||
symbol_table_id: PacketUtils.get_packet_field(packet, :symbol_table_id, "/"),
|
||||
symbol_code: PacketUtils.get_packet_field(packet, :symbol_code, ">"),
|
||||
timestamp: PacketUtils.get_timestamp(packet),
|
||||
comment: PacketUtils.get_packet_field(packet, :comment, ""),
|
||||
safe_data_extended: PacketUtils.convert_tuples_to_strings(data_extended),
|
||||
is_weather_packet: PacketUtils.is_weather_packet?(packet)
|
||||
}
|
||||
end
|
||||
|
||||
defp build_popup_content(packet, packet_info, lat, lon) do
|
||||
if packet_info.is_weather_packet do
|
||||
build_weather_popup_html(packet, packet_info.callsign)
|
||||
else
|
||||
build_standard_popup_html(packet_info, lat, lon)
|
||||
end
|
||||
end
|
||||
|
||||
defp build_standard_popup_html(packet_info, lat, lon) do
|
||||
comment_html =
|
||||
if packet_info.comment == "",
|
||||
do: "",
|
||||
else: ~s(<div class="aprs-comment">#{packet_info.comment}</div>)
|
||||
|
||||
"""
|
||||
<div class="aprs-popup">
|
||||
<div class="aprs-callsign"><strong><a href="/map/callsign/#{packet_info.callsign}">#{packet_info.callsign}</a></strong></div>
|
||||
#{comment_html}
|
||||
<div class="aprs-coords">#{Float.round(PacketUtils.to_float(lat), 4)}, #{Float.round(PacketUtils.to_float(lon), 4)}</div>
|
||||
<div class="aprs-time">#{packet_info.timestamp}</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp build_packet_result(packet, packet_info, lat, lon, popup) do
|
||||
%{
|
||||
"id" => packet_info.callsign,
|
||||
"callsign" => packet_info.callsign,
|
||||
"base_callsign" => PacketUtils.get_packet_field(packet, :base_callsign, ""),
|
||||
"ssid" => PacketUtils.get_packet_field(packet, :ssid, 0),
|
||||
"lat" => PacketUtils.to_float(lat),
|
||||
"lng" => PacketUtils.to_float(lon),
|
||||
"data_type" => to_string(PacketUtils.get_packet_field(packet, :data_type, "unknown")),
|
||||
"path" => PacketUtils.get_packet_field(packet, :path, ""),
|
||||
"comment" => packet_info.comment,
|
||||
"data_extended" => packet_info.safe_data_extended || %{},
|
||||
"symbol_table_id" => packet_info.symbol_table_id,
|
||||
"symbol_code" => packet_info.symbol_code,
|
||||
"symbol_description" => "Symbol: #{packet_info.symbol_table_id}#{packet_info.symbol_code}",
|
||||
"timestamp" => packet_info.timestamp,
|
||||
"popup" => popup
|
||||
}
|
||||
end
|
||||
|
||||
defp convert_tuples_to_strings(map) when is_map(map) do
|
||||
if Map.has_key?(map, :__struct__) do
|
||||
map
|
||||
else
|
||||
Map.new(map, fn {k, v} ->
|
||||
{k, convert_tuples_to_strings(v)}
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
defp convert_tuples_to_strings(list) when is_list(list) do
|
||||
Enum.map(list, &convert_tuples_to_strings/1)
|
||||
end
|
||||
|
||||
defp convert_tuples_to_strings(tuple) when is_tuple(tuple) do
|
||||
to_string(inspect(tuple))
|
||||
end
|
||||
|
||||
defp convert_tuples_to_strings(other), do: other
|
||||
|
||||
defp build_weather_popup_html(packet, callsign) do
|
||||
received_at =
|
||||
cond do
|
||||
Map.has_key?(packet, :received_at) -> packet.received_at
|
||||
Map.has_key?(packet, "received_at") -> packet["received_at"]
|
||||
true -> nil
|
||||
end
|
||||
|
||||
timestamp_str =
|
||||
if received_at,
|
||||
do: Calendar.strftime(received_at, "%Y-%m-%d %H:%M:%S"),
|
||||
else: "N/A"
|
||||
received_at = get_received_at(packet)
|
||||
timestamp_str = format_timestamp(received_at)
|
||||
|
||||
"""
|
||||
<strong>#{callsign} - Weather Report</strong><br>
|
||||
<small>#{timestamp_str} UTC</small>
|
||||
<hr>
|
||||
Temperature: #{get_weather_field(packet, :temperature)}°F<br>
|
||||
Humidity: #{get_weather_field(packet, :humidity)}%<br>
|
||||
Wind: #{get_weather_field(packet, :wind_direction)}° at #{get_weather_field(packet, :wind_speed)} mph, gusts to #{get_weather_field(packet, :wind_gust)} mph<br>
|
||||
Pressure: #{get_weather_field(packet, :pressure)} hPa<br>
|
||||
Rain (1h): #{get_weather_field(packet, :rain_1h)} in.<br>
|
||||
Rain (24h): #{get_weather_field(packet, :rain_24h)} in.<br>
|
||||
Rain (since midnight): #{get_weather_field(packet, :rain_since_midnight)} in.<br>
|
||||
Temperature: #{PacketUtils.get_weather_field(packet, :temperature)}°F<br>
|
||||
Humidity: #{PacketUtils.get_weather_field(packet, :humidity)}%<br>
|
||||
Wind: #{PacketUtils.get_weather_field(packet, :wind_direction)}° at #{PacketUtils.get_weather_field(packet, :wind_speed)} mph, gusts to #{PacketUtils.get_weather_field(packet, :wind_gust)} mph<br>
|
||||
Pressure: #{PacketUtils.get_weather_field(packet, :pressure)} hPa<br>
|
||||
Rain (1h): #{PacketUtils.get_weather_field(packet, :rain_1h)} in.<br>
|
||||
Rain (24h): #{PacketUtils.get_weather_field(packet, :rain_24h)} in.<br>
|
||||
Rain (since midnight): #{PacketUtils.get_weather_field(packet, :rain_since_midnight)} in.<br>
|
||||
"""
|
||||
end
|
||||
|
||||
defp get_weather_field(packet, key) do
|
||||
data_extended = Map.get(packet, "data_extended", %{})
|
||||
defp get_received_at(packet) do
|
||||
cond do
|
||||
Map.has_key?(packet, :received_at) -> packet.received_at
|
||||
Map.has_key?(packet, "received_at") -> packet["received_at"]
|
||||
true -> nil
|
||||
end
|
||||
end
|
||||
|
||||
Map.get(packet, key) ||
|
||||
Map.get(packet, to_string(key)) ||
|
||||
Map.get(data_extended, key) ||
|
||||
Map.get(data_extended, to_string(key)) || "N/A"
|
||||
defp format_timestamp(received_at) do
|
||||
if received_at,
|
||||
do: Calendar.strftime(received_at, "%Y-%m-%d %H:%M:%S"),
|
||||
else: "N/A"
|
||||
end
|
||||
|
||||
@spec generate_callsign(map() | struct()) :: String.t()
|
||||
|
|
|
|||
137
lib/aprs_web/live/map_live/packet_utils.ex
Normal file
137
lib/aprs_web/live/map_live/packet_utils.ex
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
defmodule AprsWeb.MapLive.PacketUtils do
|
||||
@moduledoc """
|
||||
Shared utilities for extracting and processing packet data in map views.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Safely extracts a value from a packet or data_extended map with fallback support.
|
||||
Checks both atom and string keys, and provides a default value.
|
||||
"""
|
||||
@spec get_packet_field(map(), atom() | String.t(), any()) :: any()
|
||||
def get_packet_field(packet, field, default \\ nil) do
|
||||
data_extended = Map.get(packet, :data_extended, Map.get(packet, "data_extended", %{})) || %{}
|
||||
|
||||
Map.get(packet, field) ||
|
||||
Map.get(packet, to_string(field)) ||
|
||||
Map.get(data_extended, field) ||
|
||||
Map.get(data_extended, to_string(field)) ||
|
||||
default
|
||||
end
|
||||
|
||||
@doc """
|
||||
Extracts symbol information from a packet with fallbacks.
|
||||
"""
|
||||
@spec get_symbol_info(map()) :: {String.t(), String.t()}
|
||||
def get_symbol_info(packet) do
|
||||
symbol_table_id = get_packet_field(packet, :symbol_table_id, "/")
|
||||
symbol_code = get_packet_field(packet, :symbol_code, ">")
|
||||
|
||||
{symbol_table_id, symbol_code}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Extracts timestamp from a packet in ISO8601 format.
|
||||
"""
|
||||
@spec get_timestamp(map()) :: String.t()
|
||||
def get_timestamp(packet) do
|
||||
cond do
|
||||
Map.has_key?(packet, :received_at) && packet.received_at ->
|
||||
DateTime.to_iso8601(packet.received_at)
|
||||
|
||||
Map.has_key?(packet, "received_at") && packet["received_at"] ->
|
||||
DateTime.to_iso8601(packet["received_at"])
|
||||
|
||||
true ->
|
||||
""
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts various numeric types to float for consistent display.
|
||||
"""
|
||||
@spec to_float(any()) :: float()
|
||||
def to_float(value) do
|
||||
case value do
|
||||
%Decimal{} = d ->
|
||||
Decimal.to_float(d)
|
||||
|
||||
n when is_float(n) ->
|
||||
n
|
||||
|
||||
n when is_integer(n) ->
|
||||
n * 1.0
|
||||
|
||||
n when is_binary(n) ->
|
||||
case Float.parse(n) do
|
||||
{f, _} -> f
|
||||
:error -> 0.0
|
||||
end
|
||||
|
||||
_ ->
|
||||
0.0
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Generates a callsign string with SSID if present.
|
||||
"""
|
||||
@spec generate_callsign(map()) :: String.t()
|
||||
def generate_callsign(packet) do
|
||||
base_callsign = get_packet_field(packet, :base_callsign, "")
|
||||
ssid = get_packet_field(packet, :ssid, 0)
|
||||
|
||||
if ssid != 0 and ssid != "" and ssid != nil do
|
||||
"#{base_callsign}-#{ssid}"
|
||||
else
|
||||
base_callsign
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Determines if a packet is a weather packet.
|
||||
"""
|
||||
@spec is_weather_packet?(map()) :: boolean()
|
||||
def is_weather_packet?(packet) do
|
||||
data_type = get_packet_field(packet, :data_type, "")
|
||||
{symbol_table_id, symbol_code} = get_symbol_info(packet)
|
||||
|
||||
data_type == "weather" or (symbol_table_id == "/" and symbol_code == "_")
|
||||
end
|
||||
|
||||
@doc """
|
||||
Recursively converts tuples in data structures to strings for JSON serialization.
|
||||
"""
|
||||
@spec convert_tuples_to_strings(any()) :: any()
|
||||
def convert_tuples_to_strings(map) when is_map(map) do
|
||||
if Map.has_key?(map, :__struct__) do
|
||||
map
|
||||
else
|
||||
Map.new(map, fn {k, v} ->
|
||||
{k, convert_tuples_to_strings(v)}
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
def convert_tuples_to_strings(list) when is_list(list) do
|
||||
Enum.map(list, &convert_tuples_to_strings/1)
|
||||
end
|
||||
|
||||
def convert_tuples_to_strings(tuple) when is_tuple(tuple) do
|
||||
to_string(inspect(tuple))
|
||||
end
|
||||
|
||||
def convert_tuples_to_strings(other), do: other
|
||||
|
||||
@doc """
|
||||
Extracts weather field data with fallback support.
|
||||
"""
|
||||
@spec get_weather_field(map(), atom()) :: String.t()
|
||||
def get_weather_field(packet, key) do
|
||||
data_extended = Map.get(packet, "data_extended", %{}) || %{}
|
||||
|
||||
Map.get(packet, key) ||
|
||||
Map.get(packet, to_string(key)) ||
|
||||
Map.get(data_extended, key) ||
|
||||
Map.get(data_extended, to_string(key)) || "N/A"
|
||||
end
|
||||
end
|
||||
|
|
@ -29,13 +29,18 @@ defmodule AprsWeb.Router do
|
|||
pipe_through :browser
|
||||
|
||||
live_dashboard "/dashboard", metrics: AprsWeb.Telemetry
|
||||
live "/", MapLive.Index, :index
|
||||
live "/status", StatusLive.Index, :index
|
||||
|
||||
live "/packets", PacketsLive.Index, :index
|
||||
live "/packets/:callsign", PacketsLive.CallsignView, :index
|
||||
live "/badpackets", BadPacketsLive.Index, :index
|
||||
live "/:callsign", MapLive.CallsignView, :index
|
||||
live_session :map_pages, layout: {AprsWeb.Layouts, :map} do
|
||||
live "/", MapLive.Index, :index
|
||||
live "/:callsign", MapLive.CallsignView, :index
|
||||
end
|
||||
|
||||
live_session :regular_pages do
|
||||
live "/status", StatusLive.Index, :index
|
||||
live "/packets", PacketsLive.Index, :index
|
||||
live "/packets/:callsign", PacketsLive.CallsignView, :index
|
||||
live "/badpackets", BadPacketsLive.Index, :index
|
||||
end
|
||||
end
|
||||
|
||||
# Other scopes may use custom stacks.
|
||||
|
|
|
|||
|
|
@ -63,62 +63,12 @@ defmodule Parser.MicE do
|
|||
try do
|
||||
<<c1, c2, c3, c4, c5, c6>> = destination
|
||||
|
||||
d1 = decode_digit(c1)
|
||||
d2 = decode_digit(c2)
|
||||
d3 = decode_digit(c3)
|
||||
d4 = decode_digit(c4)
|
||||
d5 = decode_digit(c5)
|
||||
d6 = decode_digit(c6)
|
||||
digits = decode_destination_digits([c1, c2, c3, c4, c5, c6])
|
||||
lat_info = calculate_latitude_info(digits, c4)
|
||||
lon_info = calculate_longitude_info(c5, c6)
|
||||
message_info = extract_message_info(digits)
|
||||
|
||||
lat_degrees = d1.digit * 10 + d2.digit
|
||||
lat_minutes = d3.digit * 10 + d4.digit
|
||||
lat_hundredths = d5.digit * 10 + d6.digit
|
||||
|
||||
lat_direction =
|
||||
case c4 do
|
||||
c when c in ?0..?9 -> :south
|
||||
?L -> :south
|
||||
c when c in ?P..?Z -> :north
|
||||
_ -> :unknown
|
||||
end
|
||||
|
||||
longitude_offset =
|
||||
case c5 do
|
||||
c when c in ?0..?9 -> 0
|
||||
?L -> 0
|
||||
c when c in ?P..?Z -> 100
|
||||
_ -> 0
|
||||
end
|
||||
|
||||
lon_direction =
|
||||
case c6 do
|
||||
c when c in ?0..?9 -> :east
|
||||
?L -> :east
|
||||
c when c in ?P..?Z -> :west
|
||||
_ -> :unknown
|
||||
end
|
||||
|
||||
message_bits = {d1.msg_bit, d2.msg_bit, d3.msg_bit}
|
||||
|
||||
message_type =
|
||||
cond do
|
||||
d1.msg_type != nil -> d1.msg_type
|
||||
d2.msg_type != nil -> d2.msg_type
|
||||
d3.msg_type != nil -> d3.msg_type
|
||||
true -> nil
|
||||
end
|
||||
|
||||
{:ok,
|
||||
%{
|
||||
lat_degrees: lat_degrees,
|
||||
lat_minutes: lat_minutes,
|
||||
lat_hundredths: lat_hundredths,
|
||||
lat_direction: lat_direction,
|
||||
lon_direction: lon_direction,
|
||||
longitude_offset: longitude_offset,
|
||||
message_bits: message_bits,
|
||||
message_type: message_type
|
||||
}}
|
||||
{:ok, Map.merge(lat_info, Map.merge(lon_info, message_info))}
|
||||
rescue
|
||||
_ -> {:error, :invalid_character_in_destination}
|
||||
end
|
||||
|
|
@ -127,6 +77,87 @@ defmodule Parser.MicE do
|
|||
end
|
||||
end
|
||||
|
||||
defp decode_destination_digits([c1, c2, c3, c4, c5, c6]) do
|
||||
[
|
||||
decode_digit(c1),
|
||||
decode_digit(c2),
|
||||
decode_digit(c3),
|
||||
decode_digit(c4),
|
||||
decode_digit(c5),
|
||||
decode_digit(c6)
|
||||
]
|
||||
end
|
||||
|
||||
defp calculate_latitude_info([d1, d2, d3, d4, d5, d6], c4) do
|
||||
lat_degrees = d1.digit * 10 + d2.digit
|
||||
lat_minutes = d3.digit * 10 + d4.digit
|
||||
lat_hundredths = d5.digit * 10 + d6.digit
|
||||
lat_direction = determine_lat_direction(c4)
|
||||
|
||||
%{
|
||||
lat_degrees: lat_degrees,
|
||||
lat_minutes: lat_minutes,
|
||||
lat_hundredths: lat_hundredths,
|
||||
lat_direction: lat_direction
|
||||
}
|
||||
end
|
||||
|
||||
defp determine_lat_direction(c4) do
|
||||
case c4 do
|
||||
c when c in ?0..?9 -> :south
|
||||
?L -> :south
|
||||
c when c in ?P..?Z -> :north
|
||||
_ -> :unknown
|
||||
end
|
||||
end
|
||||
|
||||
defp calculate_longitude_info(c5, c6) do
|
||||
longitude_offset = determine_longitude_offset(c5)
|
||||
lon_direction = determine_lon_direction(c6)
|
||||
|
||||
%{
|
||||
lon_direction: lon_direction,
|
||||
longitude_offset: longitude_offset
|
||||
}
|
||||
end
|
||||
|
||||
defp determine_longitude_offset(c5) do
|
||||
case c5 do
|
||||
c when c in ?0..?9 -> 0
|
||||
?L -> 0
|
||||
c when c in ?P..?Z -> 100
|
||||
_ -> 0
|
||||
end
|
||||
end
|
||||
|
||||
defp determine_lon_direction(c6) do
|
||||
case c6 do
|
||||
c when c in ?0..?9 -> :east
|
||||
?L -> :east
|
||||
c when c in ?P..?Z -> :west
|
||||
_ -> :unknown
|
||||
end
|
||||
end
|
||||
|
||||
defp extract_message_info([d1, d2, d3, _d4, _d5, _d6]) do
|
||||
message_bits = {d1.msg_bit, d2.msg_bit, d3.msg_bit}
|
||||
message_type = determine_message_type([d1, d2, d3])
|
||||
|
||||
%{
|
||||
message_bits: message_bits,
|
||||
message_type: message_type
|
||||
}
|
||||
end
|
||||
|
||||
defp determine_message_type([d1, d2, d3]) do
|
||||
cond do
|
||||
d1.msg_type != nil -> d1.msg_type
|
||||
d2.msg_type != nil -> d2.msg_type
|
||||
d3.msg_type != nil -> d3.msg_type
|
||||
true -> nil
|
||||
end
|
||||
end
|
||||
|
||||
defp decode_digit(char) do
|
||||
case char do
|
||||
c when c in ?0..?9 -> %{digit: c - ?0, msg_bit: 0, msg_type: nil}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue