This commit is contained in:
Graham McIntire 2025-07-09 16:22:58 -05:00
parent 514d1b66d5
commit ef95f597b0
No known key found for this signature in database
6 changed files with 180 additions and 142 deletions

View file

@ -1,13 +1,13 @@
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.
"""
@ -15,9 +15,9 @@ defmodule AprsmeWeb.AprsSymbol do
@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",
@ -34,21 +34,22 @@ defmodule AprsmeWeb.AprsSymbol do
# 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()
|> (fn c -> if is_integer(c), do: c, else: 63 end).()
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)
@ -72,16 +73,17 @@ defmodule AprsmeWeb.AprsSymbol do
# 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()
|> (fn c -> if is_integer(c), do: c, else: 63 end).()
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)
@ -104,15 +106,21 @@ defmodule AprsmeWeb.AprsSymbol do
# 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
"#" -> "1" # Digipeater - green star background
"a" -> "1" # Diamond shape - APRS overlay symbol (alternate table)
"A" -> "1" # Square shape - APRS overlay symbol (alternate table)
"&" -> "1" # Diamond shape - alternate table
">" -> "1" # Arrow symbols
# 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"
"i" -> "1" # Black square background - alternate table
# Black square background - alternate table
"i" -> "1"
# Most other symbols that can be overlaid are in the alternate table
_ -> "1"
end
@ -125,16 +133,17 @@ defmodule AprsmeWeb.AprsSymbol do
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()
|> (fn c -> if is_integer(c), do: c, else: 63 end).()
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)
@ -150,9 +159,9 @@ defmodule AprsmeWeb.AprsSymbol do
@doc """
Normalizes a symbol table identifier.
## Examples
iex> AprsmeWeb.AprsSymbol.normalize_symbol_table("/")
"/"
@ -172,9 +181,9 @@ defmodule AprsmeWeb.AprsSymbol do
@doc """
Normalizes a symbol code.
## Examples
iex> AprsmeWeb.AprsSymbol.normalize_symbol_code("_")
"_"
@ -190,9 +199,9 @@ defmodule AprsmeWeb.AprsSymbol do
@doc """
Maps a symbol table to its sprite file ID.
## Examples
iex> AprsmeWeb.AprsSymbol.get_table_id("/")
"0"
@ -204,59 +213,64 @@ defmodule AprsmeWeb.AprsSymbol do
"""
def get_table_id(symbol_table) do
case symbol_table do
"/" -> "0" # Primary table
"\\" -> "1" # Alternate table
"]" -> "2" # Overlay table (A-Z, 0-9)
_ -> "0" # Default to primary table
# 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
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;">
@ -289,23 +303,23 @@ defmodule AprsmeWeb.AprsSymbol do
@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: "_"})
{"/", "_"}
@ -315,7 +329,7 @@ defmodule AprsmeWeb.AprsSymbol do
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
@ -329,4 +343,4 @@ defmodule AprsmeWeb.AprsSymbol do
Map.get(data_extended, to_string(field)) ||
default
end
end
end

View file

@ -91,7 +91,6 @@ defmodule AprsmeWeb.SymbolRenderer do
AprsmeWeb.AprsSymbol.get_sprite_info(symbol_table, symbol_code)
end
@doc """
Renders an APRS symbol for use in Leaflet markers.
Returns HTML string that can be used as marker content.

View file

@ -4,8 +4,8 @@ defmodule AprsmeWeb.InfoLive.Show do
use Gettext, backend: AprsmeWeb.Gettext
alias Aprsme.Packets
alias AprsmeWeb.MapLive.PacketUtils
alias AprsmeWeb.AprsSymbol
alias AprsmeWeb.MapLive.PacketUtils
@neighbor_radius_km 10
@neighbor_limit 10
@ -326,14 +326,14 @@ defmodule AprsmeWeb.InfoLive.Show do
def render_symbol_html(packet, size \\ 32) do
if packet do
{symbol_table_id, symbol_code} = AprsSymbol.extract_from_packet(packet)
# Check if this is an overlay symbol
if symbol_table_id && String.match?(symbol_table_id, ~r/^[A-Z0-9]$/) do
# Use layered sprite backgrounds for overlay symbols
sprite_info = AprsSymbol.get_sprite_info(symbol_table_id, symbol_code)
overlay_sprite_info = AprsSymbol.get_overlay_character_sprite_info(symbol_table_id)
raw """
raw("""
<div style="
position: relative;
width: #{size}px;
@ -348,16 +348,16 @@ defmodule AprsmeWeb.InfoLive.Show do
margin-bottom: -6px;
">
</div>
"""
""")
else
# Use style rendering for non-overlay symbols
raw """
raw("""
<div style="#{AprsSymbol.render_style(symbol_table_id, symbol_code, size)}"></div>
"""
""")
end
else
# Return empty if no packet
raw ""
raw("")
end
end

View file

@ -1,4 +1,3 @@
<div class="min-h-screen bg-base-200">
<!-- Page header -->
<div class="bg-base-100 shadow-sm">
@ -15,9 +14,13 @@
</span>
<%= if @packet do %>
<% {symbol_table, symbol_code} = AprsmeWeb.AprsSymbol.extract_from_packet(@packet) %>
<% display_symbol = if symbol_table && String.match?(symbol_table, ~r/^[A-Z0-9]$/), do: symbol_table, else: "#{AprsmeWeb.AprsSymbol.normalize_symbol_table(symbol_table)}#{AprsmeWeb.AprsSymbol.normalize_symbol_code(symbol_code)}" %>
<% display_symbol =
if symbol_table && String.match?(symbol_table, ~r/^[A-Z0-9]$/),
do: symbol_table,
else:
"#{AprsmeWeb.AprsSymbol.normalize_symbol_table(symbol_table)}#{AprsmeWeb.AprsSymbol.normalize_symbol_code(symbol_code)}" %>
<div title={display_symbol}>
<%= render_symbol_html(@packet) %>
{render_symbol_html(@packet)}
</div>
<% end %>
</div>
@ -251,10 +254,15 @@
{ssid_info.callsign}
</.link>
<%= if ssid_info.packet do %>
<% {symbol_table, symbol_code} = AprsmeWeb.AprsSymbol.extract_from_packet(ssid_info.packet) %>
<% display_symbol = if symbol_table && String.match?(symbol_table, ~r/^[A-Z0-9]$/), do: symbol_table, else: "#{AprsmeWeb.AprsSymbol.normalize_symbol_table(symbol_table)}#{AprsmeWeb.AprsSymbol.normalize_symbol_code(symbol_code)}" %>
<% {symbol_table, symbol_code} =
AprsmeWeb.AprsSymbol.extract_from_packet(ssid_info.packet) %>
<% display_symbol =
if symbol_table && String.match?(symbol_table, ~r/^[A-Z0-9]$/),
do: symbol_table,
else:
"#{AprsmeWeb.AprsSymbol.normalize_symbol_table(symbol_table)}#{AprsmeWeb.AprsSymbol.normalize_symbol_code(symbol_code)}" %>
<div title={display_symbol}>
<%= render_symbol_html(ssid_info.packet) %>
{render_symbol_html(ssid_info.packet)}
</div>
<% end %>
</div>
@ -353,10 +361,15 @@
{neighbor.callsign}
</.link>
<%= if neighbor.packet do %>
<% {symbol_table, symbol_code} = AprsmeWeb.AprsSymbol.extract_from_packet(neighbor.packet) %>
<% display_symbol = if symbol_table && String.match?(symbol_table, ~r/^[A-Z0-9]$/), do: symbol_table, else: "#{AprsmeWeb.AprsSymbol.normalize_symbol_table(symbol_table)}#{AprsmeWeb.AprsSymbol.normalize_symbol_code(symbol_code)}" %>
<% {symbol_table, symbol_code} =
AprsmeWeb.AprsSymbol.extract_from_packet(neighbor.packet) %>
<% display_symbol =
if symbol_table && String.match?(symbol_table, ~r/^[A-Z0-9]$/),
do: symbol_table,
else:
"#{AprsmeWeb.AprsSymbol.normalize_symbol_table(symbol_table)}#{AprsmeWeb.AprsSymbol.normalize_symbol_code(symbol_code)}" %>
<div title={display_symbol}>
<%= render_symbol_html(neighbor.packet) %>
{render_symbol_html(neighbor.packet)}
</div>
<% end %>
</div>

View file

@ -1,15 +1,16 @@
defmodule AprsmeWeb.AprsSymbolTest do
use ExUnit.Case, async: true
alias AprsmeWeb.AprsSymbol
describe "overlay symbol rendering" do
test "D& should show black diamond background from table 1" do
# Test the get_sprite_info for overlay symbol D&
sprite_info = AprsSymbol.get_sprite_info("D", "&")
# Should use table 1 for the diamond background (alternate table)
assert sprite_info.sprite_file == "/aprs-symbols/aprs-symbols-128-1@2x.png"
# Calculate expected position for & (ASCII 38)
# index = 38 - 33 = 5
# column = 5 % 16 = 5
@ -23,10 +24,10 @@ defmodule AprsmeWeb.AprsSymbolTest do
test "N# should show green star background from table 1" do
# Test the get_sprite_info for overlay symbol N#
sprite_info = AprsSymbol.get_sprite_info("N", "#")
# Should use table 1 for the green star background
assert sprite_info.sprite_file == "/aprs-symbols/aprs-symbols-128-1@2x.png"
# Calculate expected position for # (ASCII 35)
# index = 35 - 33 = 2
# column = 2 % 16 = 2
@ -40,10 +41,10 @@ defmodule AprsmeWeb.AprsSymbolTest do
test "overlay character sprite info for D" do
# Test getting the overlay character sprite for letter D
overlay_info = AprsSymbol.get_overlay_character_sprite_info("D")
# Should use table 2 for overlay characters
assert overlay_info.sprite_file == "/aprs-symbols/aprs-symbols-128-2@2x.png"
# Calculate expected position for D (ASCII 68)
# index = 68 - 33 = 35
# column = 35 % 16 = 3
@ -57,10 +58,10 @@ defmodule AprsmeWeb.AprsSymbolTest do
test "overlay character sprite info for N" do
# Test getting the overlay character sprite for letter N
overlay_info = AprsSymbol.get_overlay_character_sprite_info("N")
# Should use table 2 for overlay characters
assert overlay_info.sprite_file == "/aprs-symbols/aprs-symbols-128-2@2x.png"
# Calculate expected position for N (ASCII 78)
# index = 78 - 33 = 45
# column = 45 % 16 = 13
@ -73,26 +74,28 @@ defmodule AprsmeWeb.AprsSymbolTest do
test "render_marker_html for overlay symbol D&" do
html = AprsSymbol.render_marker_html("D", "&", "W5MRC-15", 32)
# Should contain both background images (overlay first from table 2, base from table 1)
assert html =~ "background-image: url(/aprs-symbols/aprs-symbols-128-2@2x.png), url(/aprs-symbols/aprs-symbols-128-1@2x.png)"
assert html =~
"background-image: url(/aprs-symbols/aprs-symbols-128-2@2x.png), url(/aprs-symbols/aprs-symbols-128-1@2x.png)"
# Should have both background positions (overlay first, then base)
assert html =~ "background-position: -96.0px -64.0px, -160.0px 0.0px"
# Should include the callsign
assert html =~ "W5MRC-15"
end
test "render_marker_html for overlay symbol N#" do
html = AprsSymbol.render_marker_html("N", "#", "TEST-1", 32)
# Should contain both background images (overlay first from table 2, base from table 1)
assert html =~ "background-image: url(/aprs-symbols/aprs-symbols-128-2@2x.png), url(/aprs-symbols/aprs-symbols-128-1@2x.png)"
assert html =~
"background-image: url(/aprs-symbols/aprs-symbols-128-2@2x.png), url(/aprs-symbols/aprs-symbols-128-1@2x.png)"
# Should have both background positions (overlay first, then base)
assert html =~ "background-position: -416.0px -64.0px, -64.0px 0.0px"
# Should include the callsign
assert html =~ "TEST-1"
end
@ -100,9 +103,9 @@ defmodule AprsmeWeb.AprsSymbolTest do
test "normal symbol /_" do
# Test a normal symbol from primary table
sprite_info = AprsSymbol.get_sprite_info("/", "_")
assert sprite_info.sprite_file == "/aprs-symbols/aprs-symbols-128-0@2x.png"
# Calculate expected position for _ (ASCII 95)
# index = 95 - 33 = 62
# column = 62 % 16 = 14
@ -116,9 +119,9 @@ defmodule AprsmeWeb.AprsSymbolTest do
test "alternate table symbol \\#" do
# Test a symbol from alternate table
sprite_info = AprsSymbol.get_sprite_info("\\", "#")
assert sprite_info.sprite_file == "/aprs-symbols/aprs-symbols-128-1@2x.png"
# Calculate expected position for # (ASCII 35)
# index = 35 - 33 = 2
# column = 2 % 16 = 2
@ -131,12 +134,18 @@ defmodule AprsmeWeb.AprsSymbolTest do
test "get_overlay_base_table_id mapping" do
# Test the table mapping for overlay base symbols
assert AprsSymbol.get_overlay_base_table_id("#") == "1" # Green star in alternate table
assert AprsSymbol.get_overlay_base_table_id("&") == "1" # Diamond in alternate table
assert AprsSymbol.get_overlay_base_table_id("i") == "1" # Black square in alternate table
assert AprsSymbol.get_overlay_base_table_id(">") == "1" # Arrow in alternate table
assert AprsSymbol.get_overlay_base_table_id("^") == "1" # Arrow in alternate table
assert AprsSymbol.get_overlay_base_table_id("?") == "1" # Default to alternate table
# Green star in alternate table
assert AprsSymbol.get_overlay_base_table_id("#") == "1"
# Diamond in alternate table
assert AprsSymbol.get_overlay_base_table_id("&") == "1"
# Black square in alternate table
assert AprsSymbol.get_overlay_base_table_id("i") == "1"
# Arrow in alternate table
assert AprsSymbol.get_overlay_base_table_id(">") == "1"
# Arrow in alternate table
assert AprsSymbol.get_overlay_base_table_id("^") == "1"
# Default to alternate table
assert AprsSymbol.get_overlay_base_table_id("?") == "1"
end
end
end
end

View file

@ -1,5 +1,6 @@
defmodule AprsmeWeb.MapLive.OverlayRenderingTest do
use ExUnit.Case, async: true
alias AprsmeWeb.MapLive.PacketUtils
describe "overlay symbol rendering in map" do
@ -21,18 +22,19 @@ defmodule AprsmeWeb.MapLive.OverlayRenderingTest do
# Process the packet through PacketUtils
result = PacketUtils.build_packet_data(packet, true, "en-US")
# Check that symbol_html was generated
assert Map.has_key?(result, "symbol_html")
symbol_html = result["symbol_html"]
# Verify the overlay symbol is rendered correctly with overlay character on top
# The overlay character (D) should be first in the background-image list
assert symbol_html =~ "background-image: url(/aprs-symbols/aprs-symbols-128-2@2x.png), url(/aprs-symbols/aprs-symbols-128-2@2x.png)"
assert symbol_html =~
"background-image: url(/aprs-symbols/aprs-symbols-128-2@2x.png), url(/aprs-symbols/aprs-symbols-128-2@2x.png)"
# Verify the positions (overlay character D first at -96.0px -64.0px, then base & at -160.0px 0.0px)
assert symbol_html =~ "background-position: -96.0px -64.0px, -160.0px 0.0px"
# Verify callsign is included
assert symbol_html =~ "W5MRC-15"
end
@ -56,14 +58,15 @@ defmodule AprsmeWeb.MapLive.OverlayRenderingTest do
# Process the packet
result = PacketUtils.build_packet_data(packet, true, "en-US")
symbol_html = result["symbol_html"]
# Verify the overlay uses different sprite tables
# Overlay character N from table 2, base # from table 1
assert symbol_html =~ "background-image: url(/aprs-symbols/aprs-symbols-128-2@2x.png), url(/aprs-symbols/aprs-symbols-128-1@2x.png)"
assert symbol_html =~
"background-image: url(/aprs-symbols/aprs-symbols-128-2@2x.png), url(/aprs-symbols/aprs-symbols-128-1@2x.png)"
# Verify the positions (overlay N first, then base #)
assert symbol_html =~ "background-position: -416.0px -64.0px, -64.0px 0.0px"
# Verify callsign
assert symbol_html =~ "TEST-1"
end
@ -87,16 +90,16 @@ defmodule AprsmeWeb.MapLive.OverlayRenderingTest do
# Process the packet
result = PacketUtils.build_packet_data(packet, true, "en-US")
symbol_html = result["symbol_html"]
# Verify it's a single background image (no overlay)
assert symbol_html =~ "background-image: url(/aprs-symbols/aprs-symbols-128-0@2x.png)"
assert not (symbol_html =~ "background-image: url(/aprs-symbols/aprs-symbols-128-0@2x.png), url")
# Verify the position for _ symbol
assert symbol_html =~ "background-position: -448.0px -96.0px"
# Verify callsign
assert symbol_html =~ "WEATHER-1"
end
end
end
end