aprs.me/lib/aprsme_web/aprs_symbol.ex
2025-07-09 16:22:58 -05:00

346 lines
11 KiB
Elixir

defmodule AprsmeWeb.AprsSymbol do
@moduledoc """
Shared library for APRS symbol handling and rendering.
This module provides centralized functions for:
- Symbol table and code normalization
- Sprite file mapping
- Symbol positioning calculations
- HTML generation for symbols
All APRS symbol logic should use this module to ensure consistency
across the application.
"""
@doc """
Gets sprite information for a given symbol table and code.
Returns a map with sprite_file, background_position, and background_size.
## Examples
iex> AprsmeWeb.AprsSymbol.get_sprite_info("/", "_")
%{
sprite_file: "/aprs-symbols/aprs-symbols-128-0@2x.png",
background_position: "-352px -32px",
background_size: "512px 192px"
}
"""
def get_sprite_info(symbol_table, symbol_code) do
# For overlay symbols (A-Z, 0-9), display the base symbol with overlay
if symbol_table && String.match?(symbol_table, ~r/^[A-Z0-9]$/) do
# Use the base symbol from the overlay table, not the overlay character itself
get_overlay_base_symbol_info(symbol_code)
else
# Normal symbol table processing
symbol_table = normalize_symbol_table(symbol_table)
symbol_code = normalize_symbol_code(symbol_code)
# Map symbol table to sprite file ID
table_id = get_table_id(symbol_table)
sprite_file = "/aprs-symbols/aprs-symbols-128-#{table_id}@2x.png"
# Get symbol position using ASCII-based calculation
symbol_code_ord =
symbol_code
|> String.to_charlist()
|> List.first()
|> then(fn c -> if is_integer(c), do: c, else: 63 end)
index = symbol_code_ord - 33
safe_index = max(0, min(index, 93))
# Calculate positioning for 16-column grid
column = rem(safe_index, 16)
row = div(safe_index, 16)
x = -column * 128
y = -row * 128
%{
sprite_file: sprite_file,
background_position: "#{x / 4}px #{y / 4}px",
background_size: "512px 192px"
}
end
end
@doc """
Gets sprite information for overlay symbols (A-Z, 0-9).
These symbols display the base symbol from the overlay table.
"""
def get_overlay_base_symbol_info(base_symbol_code) do
# Some overlay base symbols are in the alternate table (1), others in overlay table (2)
# Check which table to use based on the symbol code
table_id = get_overlay_base_table_id(base_symbol_code)
sprite_file = "/aprs-symbols/aprs-symbols-128-#{table_id}@2x.png"
# Get position of the base symbol in the appropriate table
base_symbol_ord =
base_symbol_code
|> String.to_charlist()
|> List.first()
|> then(fn c -> if is_integer(c), do: c, else: 63 end)
index = base_symbol_ord - 33
safe_index = max(0, min(index, 93))
# Calculate positioning for 16-column grid
column = rem(safe_index, 16)
row = div(safe_index, 16)
x = -column * 128
y = -row * 128
%{
sprite_file: sprite_file,
background_position: "#{x / 4}px #{y / 4}px",
background_size: "512px 192px"
}
end
@doc """
Determines which sprite table to use for overlay base symbols.
Some symbols are in the alternate table (1), others in overlay table (2).
"""
def get_overlay_base_table_id(base_symbol_code) do
# Map symbols to the correct sprite table based on APRS specification
# Most overlay symbols are in the alternate table (1)
case base_symbol_code do
# Digipeater symbols are often in the alternate table (1) and have colored backgrounds
# Digipeater - green star background
"#" -> "1"
# Diamond shape - APRS overlay symbol (alternate table)
"a" -> "1"
# Square shape - APRS overlay symbol (alternate table)
"A" -> "1"
# Diamond shape - alternate table
"&" -> "1"
# Arrow symbols
">" -> "1"
"<" -> "1"
"^" -> "1"
"v" -> "1"
# Black square background - alternate table
"i" -> "1"
# Most other symbols that can be overlaid are in the alternate table
_ -> "1"
end
end
@doc """
Gets sprite information for overlay characters (A-Z, 0-9).
These are rendered from the overlay table.
"""
def get_overlay_character_sprite_info(overlay_char) do
# Use overlay table (table 2) for the overlay character
sprite_file = "/aprs-symbols/aprs-symbols-128-2@2x.png"
# Get position of the overlay character in the overlay table
overlay_char_ord =
overlay_char
|> String.to_charlist()
|> List.first()
|> then(fn c -> if is_integer(c), do: c, else: 63 end)
index = overlay_char_ord - 33
safe_index = max(0, min(index, 93))
# Calculate positioning for 16-column grid
column = rem(safe_index, 16)
row = div(safe_index, 16)
x = -column * 128
y = -row * 128
%{
sprite_file: sprite_file,
background_position: "#{x / 4}px #{y / 4}px",
background_size: "512px 192px"
}
end
@doc """
Normalizes a symbol table identifier.
## Examples
iex> AprsmeWeb.AprsSymbol.normalize_symbol_table("/")
"/"
iex> AprsmeWeb.AprsSymbol.normalize_symbol_table("A")
"]"
iex> AprsmeWeb.AprsSymbol.normalize_symbol_table("invalid")
"/"
"""
def normalize_symbol_table(symbol_table) do
cond do
symbol_table in ["/", "\\", "]"] -> symbol_table
symbol_table && String.match?(symbol_table, ~r/^[A-Z0-9]$/) -> "]"
true -> "/"
end
end
@doc """
Normalizes a symbol code.
## Examples
iex> AprsmeWeb.AprsSymbol.normalize_symbol_code("_")
"_"
iex> AprsmeWeb.AprsSymbol.normalize_symbol_code(nil)
">"
iex> AprsmeWeb.AprsSymbol.normalize_symbol_code("")
">"
"""
def normalize_symbol_code(symbol_code) do
if symbol_code && symbol_code != "", do: symbol_code, else: ">"
end
@doc """
Maps a symbol table to its sprite file ID.
## Examples
iex> AprsmeWeb.AprsSymbol.get_table_id("/")
"0"
iex> AprsmeWeb.AprsSymbol.get_table_id("\\")
"1"
iex> AprsmeWeb.AprsSymbol.get_table_id("]")
"2"
"""
def get_table_id(symbol_table) do
case symbol_table do
# Primary table
"/" -> "0"
# Alternate table
"\\" -> "1"
# Overlay table (A-Z, 0-9)
"]" -> "2"
# Default to primary table
_ -> "0"
end
end
@doc """
Renders an APRS symbol as HTML for use in Leaflet markers.
Returns HTML string that can be used as marker content.
## Examples
iex> AprsmeWeb.AprsSymbol.render_marker_html("/", "_", "W1AW")
"<div style=\"position: relative; width: 32px; height: 32px; display: flex; align-items: center;\">..."
"""
def render_marker_html(symbol_table, symbol_code, callsign \\ nil, size \\ 32) do
sprite_info = get_sprite_info(symbol_table, symbol_code)
# Check if this is an overlay symbol
is_overlay = symbol_table && String.match?(symbol_table, ~r/^[A-Z0-9]$/)
symbol_html =
if is_overlay do
# For overlay symbols, we need both the base symbol background and the overlay character
overlay_sprite_info = get_overlay_character_sprite_info(symbol_table)
"""
<div style="
position: relative;
width: #{size}px;
height: #{size}px;
background-image: url(#{overlay_sprite_info.sprite_file}), url(#{sprite_info.sprite_file});
background-position: #{overlay_sprite_info.background_position}, #{sprite_info.background_position};
background-size: #{overlay_sprite_info.background_size}, #{sprite_info.background_size};
background-repeat: no-repeat, no-repeat;
image-rendering: pixelated;
" title="#{symbol_table}#{symbol_code}">
</div>
"""
else
"""
<div style="
width: #{size}px;
height: #{size}px;
background-image: url(#{sprite_info.sprite_file});
background-position: #{sprite_info.background_position};
background-size: #{sprite_info.background_size};
background-repeat: no-repeat;
image-rendering: pixelated;
" title="#{symbol_table}#{symbol_code}"></div>
"""
end
if callsign do
"""
<div style="position: relative; width: #{size}px; height: #{size}px; display: flex; align-items: center;">
#{symbol_html}
<div style="
position: absolute;
left: #{size + 4}px;
top: 50%;
transform: translateY(-50%);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 11px;
font-weight: 600;
color: #1e40af;
background-color: rgba(255, 255, 255, 0.9);
padding: 1px 4px;
border-radius: 3px;
white-space: nowrap;
border: 1px solid rgba(30, 64, 175, 0.3);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
pointer-events: none;
z-index: 1000;
">#{callsign}</div>
</div>
"""
else
symbol_html
end
end
@doc """
Renders an APRS symbol as a style string for use in templates.
Returns a CSS style string that can be used directly in HTML.
## Examples
iex> AprsmeWeb.AprsSymbol.render_style("/", "_", 32)
"width: 32px; height: 32px; background-image: url(/aprs-symbols/aprs-symbols-128-0@2x.png); ..."
"""
def render_style(symbol_table, symbol_code, size \\ 32) do
sprite_info = get_sprite_info(symbol_table, symbol_code)
"width: #{size}px; height: #{size}px; background-image: url(#{sprite_info.sprite_file}); background-position: #{sprite_info.background_position}; background-size: #{sprite_info.background_size}; background-repeat: no-repeat; image-rendering: pixelated; opacity: 1.0; display: inline-block; vertical-align: middle; margin-bottom: -6px;"
end
@doc """
Extracts symbol information from a packet with fallbacks.
## Examples
iex> AprsmeWeb.AprsSymbol.extract_from_packet(%{symbol_table_id: "/", symbol_code: "_"})
{"/", "_"}
iex> AprsmeWeb.AprsSymbol.extract_from_packet(%{})
{"/", ">"}
"""
def extract_from_packet(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
# Helper function to safely extract a value from a packet or data_extended map
defp get_packet_field(packet, field, default) 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
end