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") "