352 lines
11 KiB
Elixir
352 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.
|
|
"""
|
|
|
|
@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(<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
|
|
Map.get(@symbol_descriptions, {symbol_table_id, symbol_code}, "Unknown symbol")
|
|
end
|
|
end
|