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. """ @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" """ 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. """ 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 """ 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;" """ 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. """ 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. """ 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. """ def default_symbol do # Car icon as default {"/", ">"} end @doc """ Validate if a symbol table ID and code combination is valid. """ 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. """ def symbol_description(symbol_table_id, symbol_code) do case {symbol_table_id, symbol_code} do {"/", ">"} -> "Car" {"/", "k"} -> "Truck" {"/", "j"} -> "Jeep" {"/", "f"} -> "Fire truck" {"/", "a"} -> "Ambulance" {"/", "b"} -> "Bike" {"/", "g"} -> "Glider" {"/", "^"} -> "Aircraft" {"/", "s"} -> "Ship" {"/", "Y"} -> "Yacht" {"/", "-"} -> "House" {"/", "r"} -> "Repeater" {"/", "/"} -> "Position" {"\\", "/"} -> "Triangle" {"\\", ">"} -> "Car (alternate)" _ -> "Unknown symbol" end end end