defmodule AprsWeb.Helpers.AprsSymbols do @moduledoc """ Helper functions for working with APRS symbols. APRS symbols are organized in sprite sheets: - Primary table (symbol_table_id = "/"): aprs-symbols-24-0.png - Secondary table (symbol_table_id = "\\"): aprs-symbols-24-1.png - Overlay characters: aprs-symbols-24-2.png Each sprite sheet is a 16x6 grid (96 symbols total). Symbol positioning is based on ASCII code of the symbol_code. """ @symbol_descriptions %{ {"/", "!"} => "Police/Sheriff", {"/", "\""} => "Reserved", {"/", "#"} => "Digipeater", {"/", "$"} => "Phone", {"/", "%"} => "DX Cluster", {"/", "&"} => "HF Gateway", {"/", "'"} => "Small Aircraft", {"/", "("} => "Mobile Satellite Station", {"/", ")"} => "Wheelchair", {"/", "*"} => "Snowmobile", {"/", "+"} => "Red Cross", {"/", ","} => "Boy Scout", {"/", "-"} => "House", {"/", "."} => "X", {"/", "/"} => "Position", {"/", "0"} => "Circle", {"/", "1"} => "Circle", {"/", "2"} => "Circle", {"/", "3"} => "Circle", {"/", "4"} => "Circle", {"/", "5"} => "Circle", {"/", "6"} => "Circle", {"/", "7"} => "Circle", {"/", "8"} => "Circle", {"/", "9"} => "Circle", {"/", ":"} => "Fire Department", {"/", ";"} => "Campground", {"/", "<"} => "Motorcycle", {"/", "="} => "Rail Engine", {"/", ">"} => "Car", {"/", "?"} => "File Server", {"/", "@"} => "HC Future", {"/", "A"} => "Aid Station", {"/", "B"} => "BBS", {"/", "C"} => "Canoe", {"/", "D"} => "Reserved", {"/", "E"} => "Eyeball", {"/", "F"} => "Tractor", {"/", "G"} => "Grid Square", {"/", "H"} => "Hotel", {"/", "I"} => "TCP/IP", {"/", "J"} => "Phone", {"/", "K"} => "School", {"/", "L"} => "PC User", {"/", "M"} => "MacAPRS", {"/", "N"} => "NTS Station", {"/", "O"} => "Balloon", {"/", "P"} => "Police", {"/", "Q"} => "TBD", {"/", "R"} => "Recreational Vehicle", {"/", "S"} => "Shuttle", {"/", "T"} => "SSTV", {"/", "U"} => "Bus", {"/", "V"} => "ATV", {"/", "W"} => "National Weather Service", {"/", "X"} => "Helo", {"/", "Y"} => "Yacht", {"/", "Z"} => "WinAPRS", {"/", "["} => "Jogger", {"/", "\\"} => "Triangle", {"/", "]"} => "PBBS", {"/", "^"} => "Aircraft", {"/", "_"} => "Weather Station", {"/", "`"} => "Dish Antenna", {"/", "a"} => "Ambulance", {"/", "b"} => "Bike", {"/", "c"} => "Incident Command Post", {"/", "d"} => "Fire Depts", {"/", "e"} => "Horse", {"/", "f"} => "Fire Truck", {"/", "g"} => "Glider", {"/", "h"} => "Hospital", {"/", "i"} => "IOTA", {"/", "j"} => "Jeep", {"/", "k"} => "Truck", {"/", "l"} => "Laptop", {"/", "m"} => "Mic-E", {"/", "n"} => "Node", {"/", "o"} => "EOC", {"/", "p"} => "Dog", {"/", "q"} => "Grid", {"/", "r"} => "Repeater", {"/", "s"} => "Ship", {"/", "t"} => "Truck Stop", {"/", "u"} => "Truck", {"/", "v"} => "Van", {"/", "w"} => "Water Station", {"/", "x"} => "X-APRS", {"/", "y"} => "Yagi", {"/", "z"} => "Shelter", {"\\", "!"} => "Emergency", {"\\", "\""} => "Reserved", {"\\", "#"} => "Digipeater", {"\\", "$"} => "Bank", {"\\", "%"} => "Reserved", {"\\", "&"} => "Reserved", {"\\", "'"} => "Reserved", {"\\", "("} => "Reserved", {"\\", ")"} => "Reserved", {"\\", "*"} => "Reserved", {"\\", "+"} => "Reserved", {"\\", ","} => "Reserved", {"\\", "-"} => "Reserved", {"\\", "."} => "Reserved", {"\\", "/"} => "Triangle", {"\\", "0"} => "Reserved", {"\\", "1"} => "Reserved", {"\\", "2"} => "Reserved", {"\\", "3"} => "Reserved", {"\\", "4"} => "Reserved", {"\\", "5"} => "Reserved", {"\\", "6"} => "Reserved", {"\\", "7"} => "Reserved", {"\\", "8"} => "Reserved", {"\\", "9"} => "Reserved", {"\\", ":"} => "Reserved", {"\\", ";"} => "Reserved", {"\\", "<"} => "Reserved", {"\\", "="} => "Reserved", {"\\", ">"} => "Car (alternate)", {"\\", "?"} => "Reserved", {"\\", "@"} => "Reserved", {"\\", "A"} => "Reserved", {"\\", "B"} => "Reserved", {"\\", "C"} => "Reserved", {"\\", "D"} => "Reserved", {"\\", "E"} => "Reserved", {"\\", "F"} => "Reserved", {"\\", "G"} => "Reserved", {"\\", "H"} => "Reserved", {"\\", "I"} => "Reserved", {"\\", "J"} => "Reserved", {"\\", "K"} => "Reserved", {"\\", "L"} => "Reserved", {"\\", "M"} => "Reserved", {"\\", "N"} => "Reserved", {"\\", "O"} => "Reserved", {"\\", "P"} => "Reserved", {"\\", "Q"} => "Reserved", {"\\", "R"} => "Reserved", {"\\", "S"} => "Reserved", {"\\", "T"} => "Reserved", {"\\", "U"} => "Reserved", {"\\", "V"} => "Reserved", {"\\", "W"} => "Reserved", {"\\", "X"} => "Reserved", {"\\", "Y"} => "Reserved", {"\\", "Z"} => "Reserved", {"\\", "["} => "Reserved", {"\\", "\\"} => "Reserved", {"\\", "]"} => "Reserved", {"\\", "^"} => "Reserved", {"\\", "_"} => "Reserved", {"\\", "`"} => "Reserved", {"\\", "a"} => "Reserved", {"\\", "b"} => "Reserved", {"\\", "c"} => "Reserved", {"\\", "d"} => "Reserved", {"\\", "e"} => "Reserved", {"\\", "f"} => "Reserved", {"\\", "g"} => "Reserved", {"\\", "h"} => "Reserved", {"\\", "i"} => "Reserved", {"\\", "j"} => "Reserved", {"\\", "k"} => "Reserved", {"\\", "l"} => "Reserved", {"\\", "m"} => "Reserved", {"\\", "n"} => "Reserved", {"\\", "o"} => "Reserved", {"\\", "p"} => "Reserved", {"\\", "q"} => "Reserved", {"\\", "r"} => "Reserved", {"\\", "s"} => "Reserved" } @doc """ Get the sprite sheet filename for a given symbol table ID. ## Examples iex> AprsWeb.Helpers.AprsSymbols.get_sprite_filename("/") "aprs-symbols-24-0.png" iex> AprsWeb.Helpers.AprsSymbols.get_sprite_filename("\\") "aprs-symbols-24-1.png" """ @spec get_sprite_filename(String.t()) :: String.t() def get_sprite_filename(symbol_table_id) do case symbol_table_id do "/" -> "aprs-symbols-24-0.png" "\\" -> "aprs-symbols-24-1.png" # Default to primary table _ -> "aprs-symbols-24-0.png" end end @doc """ Get the high-resolution sprite sheet filename for retina displays. """ @spec get_sprite_filename_2x(String.t()) :: String.t() def get_sprite_filename_2x(symbol_table_id) do case symbol_table_id do "/" -> "aprs-symbols-24-0@2x.png" "\\" -> "aprs-symbols-24-1@2x.png" _ -> "aprs-symbols-24-0@2x.png" end end @doc """ Calculate the CSS background position for a symbol in the sprite sheet. The sprite sheet is organized as a 16x6 grid. ASCII codes 32-127 map to positions 0-95. ## Examples iex> AprsWeb.Helpers.AprsSymbols.get_symbol_position(">") {-1440, 0} # ASCII 62, position 30 -> column 14, row 1 iex> AprsWeb.Helpers.AprsSymbols.get_symbol_position("!") {-216, 0} # ASCII 33, position 1 -> column 1, row 0 """ @spec get_symbol_position(String.t() | integer()) :: {integer(), integer()} def get_symbol_position(symbol_code) when is_binary(symbol_code) do # Get first character if string char_code = symbol_code |> String.first() |> String.to_charlist() |> List.first() get_symbol_position_by_ascii(char_code) end def get_symbol_position(symbol_code) when is_integer(symbol_code) do get_symbol_position_by_ascii(symbol_code) end defp get_symbol_position_by_ascii(ascii_code) do # ASCII 32 (space) is position 0, ASCII 127 (DEL) is position 95 position = ascii_code - 32 # Clamp position to valid range 0-95 position = max(0, min(95, position)) # Calculate row and column (16 symbols per row) col = rem(position, 16) row = div(position, 16) # Each symbol is 24x24 pixels x = -col * 24 y = -row * 24 {x, y} end @doc """ Generate CSS style for displaying an APRS symbol using sprite positioning. ## Examples iex> AprsWeb.Helpers.AprsSymbols.symbol_css_style("/", ">") "background-image: url('/aprs-symbols/aprs-symbols-24-0.png'); background-position: -1440px 0px; width: 24px; height: 24px;" """ @spec symbol_css_style(String.t(), String.t() | integer()) :: String.t() def symbol_css_style(symbol_table_id, symbol_code) do sprite_file = get_sprite_filename(symbol_table_id) {x, y} = get_symbol_position(symbol_code) "background-image: url('/aprs-symbols/#{sprite_file}'); " <> "background-position: #{x}px #{y}px; " <> "width: 24px; height: 24px; " <> "background-repeat: no-repeat;" end @doc """ Generate HTML for an APRS symbol with proper sprite positioning. """ @spec symbol_html(String.t(), String.t() | integer(), keyword()) :: {:safe, String.t()} def symbol_html(symbol_table_id, symbol_code, opts \\ []) do css_class = Keyword.get(opts, :class, "aprs-symbol") extra_style = Keyword.get(opts, :style, "") base_style = symbol_css_style(symbol_table_id, symbol_code) full_style = if extra_style == "", do: base_style, else: base_style <> " " <> extra_style Phoenix.HTML.raw(~s(
)) end @doc """ Get a data URL for the symbol that can be used in JavaScript/Leaflet. This creates a small canvas with just the symbol extracted from the sprite. """ @spec get_symbol_data_attributes(String.t(), String.t() | integer()) :: map() def get_symbol_data_attributes(symbol_table_id, symbol_code) do sprite_file = get_sprite_filename(symbol_table_id) sprite_file_2x = get_sprite_filename_2x(symbol_table_id) {x, y} = get_symbol_position(symbol_code) %{ "data-sprite" => sprite_file, "data-sprite-2x" => sprite_file_2x, "data-pos-x" => x, "data-pos-y" => y, "data-symbol-table" => symbol_table_id, "data-symbol-code" => symbol_code } end @doc """ Get the default symbol for unknown or invalid symbols. """ @spec default_symbol() :: {String.t(), String.t()} def default_symbol do # Car icon as default {"/", ">"} end @doc """ Validate if a symbol table ID and code combination is valid. """ @spec valid_symbol?(any(), any()) :: boolean() def valid_symbol?(symbol_table_id, symbol_code) do case {symbol_table_id, symbol_code} do {table, code} when table in ["/", "\\"] and is_binary(code) and byte_size(code) > 0 -> char_code = code |> String.first() |> String.to_charlist() |> List.first() char_code >= 32 and char_code <= 127 _ -> false end end @doc """ Get a human-readable description for common APRS symbols. This is a subset of common symbols - for a complete list, you'd want to reference the official APRS symbol specification. """ @spec symbol_description(String.t(), String.t()) :: String.t() def symbol_description(symbol_table_id, symbol_code) do Map.get(@symbol_descriptions, {symbol_table_id, symbol_code}, "Unknown symbol") end end