361 lines
11 KiB
Elixir
361 lines
11 KiB
Elixir
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"
|
|
"""
|
|
@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(<div class="#{css_class}" style="#{full_style}"></div>))
|
|
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
|
|
case {symbol_table_id, symbol_code} do
|
|
# Primary Table (/)
|
|
{"/", "!"} -> "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"
|
|
# Secondary Table (\)
|
|
{"\\", "!"} -> "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"
|
|
{"\\", "t"} -> "Reserved"
|
|
{"\\", "u"} -> "Reserved"
|
|
{"\\", "v"} -> "Reserved"
|
|
{"\\", "w"} -> "Reserved"
|
|
{"\\", "x"} -> "Reserved"
|
|
{"\\", "y"} -> "Reserved"
|
|
{"\\", "z"} -> "Reserved"
|
|
# Unknown symbol
|
|
_ -> "Unknown symbol"
|
|
end
|
|
end
|
|
end
|