diff --git a/assets/js/minimal_map.js b/assets/js/minimal_map.js
index 60b52b8..f7d858b 100644
--- a/assets/js/minimal_map.js
+++ b/assets/js/minimal_map.js
@@ -20,7 +20,8 @@ let MinimalAPRSMap = {
// Add OpenStreetMap tile layer
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
- attribution: '© OpenStreetMap contributors | APRS.me',
+ attribution:
+ '© OpenStreetMap contributors | APRS.me',
maxZoom: 19,
}).addTo(this.map);
@@ -39,7 +40,7 @@ let MinimalAPRSMap = {
});
// Send bounds to LiveView when map moves
- this.map.on('moveend', () => {
+ this.map.on("moveend", () => {
if (this.boundsTimer) clearTimeout(this.boundsTimer);
this.boundsTimer = setTimeout(() => {
this.sendBoundsToServer();
@@ -64,7 +65,7 @@ let MinimalAPRSMap = {
// Add multiple markers at once
this.handleEvent("add_markers", (data) => {
if (data.markers && Array.isArray(data.markers)) {
- data.markers.forEach(marker => this.addMarker(marker));
+ data.markers.forEach((marker) => this.addMarker(marker));
}
});
@@ -92,7 +93,7 @@ let MinimalAPRSMap = {
this.map.setView([lat, lng], zoom, {
animate: true,
- duration: 1
+ duration: 1,
});
}
});
@@ -108,13 +109,50 @@ let MinimalAPRSMap = {
(error) => {
console.warn("Geolocation error:", error.message);
this.pushEvent("geolocation_error", { error: error.message });
- }
+ },
);
} else {
console.warn("Geolocation not available");
this.pushEvent("geolocation_error", { error: "Geolocation not supported" });
}
});
+
+ // Handle new packets from LiveView
+ this.handleEvent("new_packet", (data) => {
+ this.addMarker({
+ ...data,
+ historical: false,
+ popup: this.buildPopupContent(data),
+ });
+ });
+
+ // Handle historical packets during replay
+ this.handleEvent("historical_packet", (data) => {
+ this.addMarker({
+ ...data,
+ historical: true,
+ popup: this.buildPopupContent(data),
+ });
+ });
+
+ // Handle refresh markers event
+ this.handleEvent("refresh_markers", () => {
+ // Remove markers for packets that are no longer visible
+ // This could be implemented to clean up old markers based on timestamp
+ console.log("Refresh markers event received");
+ });
+
+ // Handle clearing historical packets
+ this.handleEvent("clear_historical_packets", () => {
+ // Remove all historical markers
+ const markersToRemove = [];
+ this.markers.forEach((marker, id) => {
+ if (marker._isHistorical) {
+ markersToRemove.push(id);
+ }
+ });
+ markersToRemove.forEach((id) => this.removeMarker(id));
+ });
},
sendBoundsToServer() {
@@ -133,9 +171,9 @@ let MinimalAPRSMap = {
},
center: {
lat: center.lat,
- lng: center.lng
+ lng: center.lng,
},
- zoom: zoom
+ zoom: zoom,
});
},
@@ -169,15 +207,20 @@ let MinimalAPRSMap = {
}
// Handle marker click
- marker.on('click', () => {
+ marker.on("click", () => {
this.pushEvent("marker_clicked", {
id: data.id,
callsign: data.callsign,
lat: lat,
- lng: lng
+ lng: lng,
});
});
+ // Mark historical markers for identification
+ if (data.historical) {
+ marker._isHistorical = true;
+ }
+
// Add to map and store reference
marker.addTo(this.markerLayer);
this.markers.set(data.id, marker);
@@ -227,29 +270,74 @@ let MinimalAPRSMap = {
},
createMarkerIcon(data) {
- // Determine color based on symbol table or provided color
- let color = data.color || (data.symbol_table === "/" ? "#2563eb" : "#dc2626");
+ // Get symbol information
+ const symbolTableId = data.symbol_table_id || "/";
+ const symbolCode = data.symbol_code || ">";
+
+ // Determine sprite file based on symbol table
+ let spriteFile;
+ switch (symbolTableId) {
+ case "/":
+ spriteFile = "/aprs-symbols/aprs-symbols-24-0.png";
+ break;
+ case "\\":
+ spriteFile = "/aprs-symbols/aprs-symbols-24-1.png";
+ break;
+ default:
+ spriteFile = "/aprs-symbols/aprs-symbols-24-0.png";
+ }
+
+ // Calculate sprite position
+ const charCode = symbolCode.charCodeAt(0);
+ const position = charCode - 32;
+ const col = position % 16;
+ const row = Math.floor(position / 16);
+ const x = -col * 24;
+ const y = -row * 24;
// Use different opacity for historical markers
- const opacity = data.historical ? 0.6 : 1.0;
+ const opacity = data.historical ? 0.7 : 1.0;
return L.divIcon({
html: `
`,
className: data.historical ? "aprs-marker historical-marker" : "aprs-marker",
- iconSize: [16, 16],
- iconAnchor: [8, 8],
- popupAnchor: [0, -8],
+ iconSize: [24, 24],
+ iconAnchor: [12, 12],
+ popupAnchor: [0, -12],
});
},
+ buildPopupContent(data) {
+ const callsign = data.callsign || data.id || "Unknown";
+ const comment = data.comment || "";
+ const symbolDesc = data.symbol_description || "Unknown symbol";
+
+ let content = ``;
+ return content;
+ },
+
destroyed() {
console.log("MinimalAPRSMap hook destroyed");
@@ -272,7 +360,7 @@ let MinimalAPRSMap = {
this.map.remove();
this.map = null;
}
- }
+ },
};
export default MinimalAPRSMap;
diff --git a/docs/APRS_SYMBOLS.md b/docs/APRS_SYMBOLS.md
new file mode 100644
index 0000000..34ca7a3
--- /dev/null
+++ b/docs/APRS_SYMBOLS.md
@@ -0,0 +1,173 @@
+# APRS Symbol Implementation
+
+This document describes the implementation of APRS icons/symbols in the LiveView-based map system, replacing the generic colored dots with proper APRS symbols.
+
+## Overview
+
+The APRS.me application now displays proper APRS symbols instead of generic dots for packet markers on the map. This implementation uses the high-resolution APRS symbol set from [hessu/aprs-symbols](https://github.com/hessu/aprs-symbols).
+
+## Map Versions
+
+The application now has three map implementations:
+
+- `/` - **Default LiveView Map** - Minimal LiveView-based map with APRS symbols (recommended)
+- `/enhanced` - **Enhanced LiveView Map** - Feature-rich LiveView map with additional controls
+- `/old` - **Legacy Map** - Original JavaScript-heavy implementation (deprecated)
+
+## APRS Symbol System
+
+### Symbol Tables
+
+APRS uses two main symbol tables:
+
+- **Primary Table** (`/`): `aprs-symbols-24-0.png` - Standard symbols
+- **Secondary Table** (`\`): `aprs-symbols-24-1.png` - Alternate symbols
+- **Overlay Characters**: `aprs-symbols-24-2.png` - Overlay digits and letters
+
+### Symbol Structure
+
+Each symbol is identified by:
+- **Symbol Table ID**: `/` (primary) or `\` (secondary)
+- **Symbol Code**: ASCII character (33-126) representing the symbol position
+
+### Symbol Positioning
+
+Symbols are arranged in a 16×6 grid (96 symbols total) in each sprite sheet:
+- Position = ASCII code - 32
+- Column = Position % 16
+- Row = Position ÷ 16
+- Each symbol is 24×24 pixels
+
+## Implementation Details
+
+### Backend Components
+
+#### AprsWeb.Helpers.AprsSymbols
+
+Helper module for APRS symbol operations:
+
+```elixir
+# Get sprite sheet filename
+AprsSymbols.get_sprite_filename("/") # → "aprs-symbols-24-0.png"
+
+# Calculate symbol position
+AprsSymbols.get_symbol_position(">") # → {-336, -24}
+
+# Generate CSS for symbol display
+AprsSymbols.symbol_css_style("/", ">")
+
+# Get human-readable description
+AprsSymbols.symbol_description("/", ">") # → "Car"
+```
+
+#### Packet Processing
+
+Modified `build_packet_data/1` in `MapLive.Index`:
+- Extracts symbol information from packet data
+- Validates symbols and provides defaults
+- Includes coordinates and symbol metadata for frontend
+
+#### Struct Conversion
+
+Enhanced `struct_to_map/1` in `Aprs.Is`:
+- Recursively converts nested structs to maps
+- Preserves type information for proper data extraction
+- Handles MicE packets specially
+
+### Frontend Components
+
+#### JavaScript Updates
+
+Updated `minimal_map.js`:
+- Added handlers for `new_packet` and `historical_packet` events
+- Modified `createMarkerIcon()` to use sprite-based symbols
+- Added popup content generation with symbol descriptions
+
+#### Symbol Rendering
+
+Symbols are displayed using CSS sprites:
+- Background image points to appropriate sprite sheet
+- Background position calculated from symbol code
+- High-DPI support with @2x variants
+- Opacity adjustment for historical markers
+
+### Asset Files
+
+Downloaded APRS symbol files in `priv/static/aprs-symbols/`:
+- `aprs-symbols-24-0.png` - Primary table (24×24)
+- `aprs-symbols-24-1.png` - Secondary table (24×24)
+- `aprs-symbols-24-0@2x.png` - Primary table (48×48, retina)
+- `aprs-symbols-24-1@2x.png` - Secondary table (48×48, retina)
+- `aprs-symbols-24-2.png` - Overlay characters (24×24)
+- `aprs-symbols-24-2@2x.png` - Overlay characters (48×48, retina)
+
+## Common APRS Symbols
+
+| Symbol Code | Table | Description |
+|-------------|-------|-------------|
+| `>` | `/` | Car |
+| `k` | `/` | Truck |
+| `j` | `/` | Jeep |
+| `f` | `/` | Fire truck |
+| `a` | `/` | Ambulance |
+| `b` | `/` | Bike |
+| `^` | `/` | Aircraft |
+| `s` | `/` | Ship |
+| `-` | `/` | House |
+| `r` | `/` | Repeater |
+| `_` | `/` | Weather |
+
+## Configuration
+
+### Default Symbols
+
+- **Unknown packets**: Car symbol (`/`, `>`)
+- **MicE packets**: Car symbol (`/`, `>`)
+- **Invalid symbols**: Fall back to car symbol
+
+### Styling
+
+CSS classes for customization:
+- `.aprs-marker` - Base marker styling
+- `.historical-marker` - Historical packet opacity
+- `.aprs-popup` - Popup content styling
+- `.aprs-callsign` - Callsign display
+- `.aprs-symbol-info` - Symbol description
+- `.aprs-comment` - Packet comment
+- `.aprs-coords` - Coordinate display
+
+## High-DPI Support
+
+Automatic retina display support using CSS media queries:
+```css
+@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
+ .aprs-marker div[style*="aprs-symbols-24-0.png"] {
+ background-image: url('/aprs-symbols/aprs-symbols-24-0@2x.png') !important;
+ background-size: 384px 144px !important;
+ }
+}
+```
+
+## Navigation
+
+Added floating navigation headers to all map versions:
+- Links between different map implementations
+- Access to status and packet pages
+- Consistent UI across all versions
+
+## Symbol Credits
+
+APRS symbols from [hessu/aprs-symbols](https://github.com/hessu/aprs-symbols):
+- Created by Heikki Hannikainen OH7LZB
+- High-resolution vector-based symbol set
+- Released to APRS community for free use
+- Compatible with Updated APRS Symbol Set (Rev H)
+
+## Future Enhancements
+
+Potential improvements:
+- Overlay character support for numbered/lettered symbols
+- Symbol rotation based on course/heading
+- Custom symbol upload functionality
+- Symbol animation for moving objects
+- Symbol filtering and categorization
\ No newline at end of file
diff --git a/lib/aprs/is/is.ex b/lib/aprs/is/is.ex
index 103b38d..72f5671 100644
--- a/lib/aprs/is/is.ex
+++ b/lib/aprs/is/is.ex
@@ -275,7 +275,7 @@ defmodule Aprs.Is do
packet_data = Map.put(parsed_message, :received_at, current_time)
# Convert to map before storing to avoid struct conversion issues
- attrs = Map.from_struct(packet_data)
+ attrs = struct_to_map(packet_data)
# Extract additional data from the parsed packet including raw packet
attrs = Aprs.Packet.extract_additional_data(attrs, message)
@@ -462,4 +462,23 @@ defmodule Aprs.Is do
last_second_timestamp: System.system_time(:second)
}
end
+
+ # Helper function to recursively convert structs to maps
+ # This handles nested structs that Map.from_struct/1 cannot handle
+ defp struct_to_map(%{__struct__: struct_type} = struct) do
+ converted_map =
+ struct
+ |> Map.from_struct()
+ |> Enum.map(fn {k, v} -> {k, struct_to_map(v)} end)
+ |> Enum.into(%{})
+
+ # Add type information to help with later processing
+ Map.put(converted_map, :__original_struct__, struct_type)
+ end
+
+ defp struct_to_map(value) when is_list(value) do
+ Enum.map(value, &struct_to_map/1)
+ end
+
+ defp struct_to_map(value), do: value
end
diff --git a/lib/aprs/packet.ex b/lib/aprs/packet.ex
index 9b3a8e5..505abf8 100644
--- a/lib/aprs/packet.ex
+++ b/lib/aprs/packet.ex
@@ -158,6 +158,9 @@ defmodule Aprs.Packet do
# Extract data based on the type of data_extended
additional_data =
case data_extended do
+ %{__original_struct__: Parser.Types.MicE} = mic_e_map ->
+ extract_from_mic_e_map(mic_e_map)
+
%{} when is_map(data_extended) ->
extract_from_map(data_extended)
@@ -207,8 +210,20 @@ defmodule Aprs.Packet do
|> maybe_put(:equipment_type, mic_e.equipment_type)
|> maybe_put(:course, mic_e.course)
|> maybe_put(:speed, mic_e.speed)
- |> maybe_put(:symbol_code, mic_e.symbol_code)
- |> maybe_put(:symbol_table_id, mic_e.symbol_table_id)
+ |> maybe_put(:symbol_code, ">") # Default car symbol for MicE
+ |> maybe_put(:symbol_table_id, "/") # Primary table
+ end
+
+ # Extract data from converted MicE map (from struct_to_map conversion)
+ defp extract_from_mic_e_map(mic_e_map) do
+ %{}
+ |> maybe_put(:comment, mic_e_map[:message])
+ |> maybe_put(:manufacturer, mic_e_map[:manufacturer])
+ |> maybe_put(:equipment_type, mic_e_map[:equipment_type])
+ |> maybe_put(:course, mic_e_map[:heading])
+ |> maybe_put(:speed, mic_e_map[:speed])
+ |> maybe_put(:symbol_code, ">") # Default car symbol for MicE
+ |> maybe_put(:symbol_table_id, "/") # Primary table
end
# Extract weather data from various formats
diff --git a/lib/aprs_web/controllers/page_controller.ex b/lib/aprs_web/controllers/page_controller.ex
index 4c3d779..15f76ca 100644
--- a/lib/aprs_web/controllers/page_controller.ex
+++ b/lib/aprs_web/controllers/page_controller.ex
@@ -6,6 +6,7 @@ defmodule AprsWeb.PageController do
end
def map(conn, _params) do
+ # This serves the legacy JavaScript-based map at /old
render(conn, :map)
end
diff --git a/lib/aprs_web/controllers/page_html/map.html.heex b/lib/aprs_web/controllers/page_html/map.html.heex
index f193bb1..c12b3a8 100644
--- a/lib/aprs_web/controllers/page_html/map.html.heex
+++ b/lib/aprs_web/controllers/page_html/map.html.heex
@@ -1,25 +1,50 @@
-
-
-
-
-
-
+
+
+
diff --git a/lib/aprs_web/helpers/aprs_symbols.ex b/lib/aprs_web/helpers/aprs_symbols.ex
new file mode 100644
index 0000000..f06b934
--- /dev/null
+++ b/lib/aprs_web/helpers/aprs_symbols.ex
@@ -0,0 +1,181 @@
+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"
+ _ -> "aprs-symbols-24-0.png" # Default to primary table
+ 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 <> " " <> extra_style, else: base_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
diff --git a/lib/aprs_web/live/map_live/enhanced.ex b/lib/aprs_web/live/map_live/enhanced.ex
index ab858c8..c0e1681 100644
--- a/lib/aprs_web/live/map_live/enhanced.ex
+++ b/lib/aprs_web/live/map_live/enhanced.ex
@@ -262,8 +262,12 @@ defmodule AprsWeb.MapLive.Enhanced do
.historical-marker {
opacity: 0.7;
}
+
+
+
+
bounds}, socket) do
+ handle_bounds_update(bounds, socket)
+ end
+
@impl true
def handle_event("update_bounds", %{"bounds" => bounds}, socket) do
- # Update the map bounds from the client
- map_bounds = %{
- north: bounds["north"],
- south: bounds["south"],
- east: bounds["east"],
- west: bounds["west"]
- }
-
- # Don't clear markers - let the client preserve those still in view
- # Recalculate packet count based on new bounds
- visible_packets =
- socket.assigns.visible_packets
- |> Enum.filter(fn {_callsign, packet} ->
- within_bounds?(packet, map_bounds)
- end)
- |> Map.new()
-
- # If replay is not active, update the replay packets based on the new bounds
- socket =
- if socket.assigns.replay_active do
- socket
- else
- # Clear any existing replay data when bounds change
- socket =
- assign(socket,
- replay_packets: [],
- replay_index: 0,
- historical_packets: %{},
- map_ready: true
- )
-
- # Don't automatically start replay on every bounds change
- # We'll handle this in initialize_replay to avoid too many restarts
- socket
- end
-
- {:noreply, assign(socket, map_bounds: map_bounds, visible_packets: visible_packets)}
+ handle_bounds_update(bounds, socket)
end
@impl true
@@ -247,6 +217,46 @@ defmodule AprsWeb.MapLive.Index do
{:noreply, socket}
end
+ defp handle_bounds_update(bounds, socket) do
+ # Update the map bounds from the client
+ map_bounds = %{
+ north: bounds["north"],
+ south: bounds["south"],
+ east: bounds["east"],
+ west: bounds["west"]
+ }
+
+ # Don't clear markers - let the client preserve those still in view
+ # Recalculate packet count based on new bounds
+ visible_packets =
+ socket.assigns.visible_packets
+ |> Enum.filter(fn {_callsign, packet} ->
+ within_bounds?(packet, map_bounds)
+ end)
+ |> Map.new()
+
+ # If replay is not active, update the replay packets based on the new bounds
+ socket =
+ if socket.assigns.replay_active do
+ socket
+ else
+ # Clear any existing replay data when bounds change
+ socket =
+ assign(socket,
+ replay_packets: [],
+ replay_index: 0,
+ historical_packets: %{},
+ map_ready: true
+ )
+
+ # Don't automatically start replay on every bounds change
+ # We'll handle this in initialize_replay to avoid too many restarts
+ socket
+ end
+
+ {:noreply, assign(socket, map_bounds: map_bounds, visible_packets: visible_packets)}
+ end
+
@impl true
def handle_info(msg, socket) do
case msg do
@@ -364,36 +374,40 @@ defmodule AprsWeb.MapLive.Index do
# Convert to a simple map structure for JSON encoding
packet_data = build_packet_data(packet)
- # Add is_historical flag and timestamp
- packet_data =
- Map.merge(packet_data, %{
- "is_historical" => true,
- "timestamp" => if(Map.has_key?(packet, :received_at), do: DateTime.to_iso8601(packet.received_at))
- })
+ # Only process packets with valid position data
+ socket =
+ if packet_data do
+ # Add is_historical flag and timestamp
+ packet_data =
+ Map.merge(packet_data, %{
+ "is_historical" => true,
+ "timestamp" => if(Map.has_key?(packet, :received_at), do: DateTime.to_iso8601(packet.received_at))
+ })
- # Generate a unique key for this packet
- packet_id = "hist_#{if Map.has_key?(packet, :id), do: packet.id, else: System.unique_integer([:positive])}"
+ # Generate a unique key for this packet
+ packet_id = "hist_#{if Map.has_key?(packet, :id), do: packet.id, else: System.unique_integer([:positive])}"
- # Update historical packets tracking
- new_historical_packets = Map.put(historical_packets, packet_id, packet)
+ # Update historical packets tracking
+ new_historical_packets = Map.put(historical_packets, packet_id, packet)
+
+ # Send packet data to client
+ socket
+ |> push_event("historical_packet", packet_data)
+ |> assign(
+ replay_index: next_index,
+ historical_packets: new_historical_packets
+ )
+ else
+ # Skip packets without position data, just advance the index
+ assign(socket, replay_index: next_index)
+ end
# Calculate delay for next packet (faster based on replay speed)
delay_ms = trunc(1000 / speed)
# Schedule the next packet
timer_ref = Process.send_after(self(), :replay_next_packet, delay_ms)
-
- # Push the packet to the client-side JavaScript
- socket =
- socket
- |> push_event("historical_packet", packet_data)
- |> assign(
- replay_index: next_index,
- replay_timer_ref: timer_ref,
- historical_packets: new_historical_packets
- )
-
- {:noreply, socket}
+ {:noreply, assign(socket, replay_timer_ref: timer_ref)}
else
# All packets replayed, start over with a short delay
Process.send_after(self(), :initialize_replay, 10_000)
@@ -448,6 +462,8 @@ defmodule AprsWeb.MapLive.Index do
z-index: 1;
}
+
+
.locate-button {
position: absolute;
left: 10px;
@@ -472,8 +488,69 @@ defmodule AprsWeb.MapLive.Index do
.historical-marker {
opacity: 0.7;
}
+
+ .aprs-popup {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ font-size: 12px;
+ line-height: 1.4;
+ max-width: 200px;
+ }
+
+ .aprs-callsign {
+ font-size: 14px;
+ font-weight: bold;
+ color: #1e40af;
+ margin-bottom: 4px;
+ }
+
+ .aprs-symbol-info {
+ color: #6b7280;
+ font-style: italic;
+ margin-bottom: 4px;
+ }
+
+ .aprs-comment {
+ color: #374151;
+ margin-bottom: 4px;
+ word-wrap: break-word;
+ }
+
+ .aprs-coords {
+ color: #6b7280;
+ font-size: 11px;
+ font-family: monospace;
+ }
+
+ /* High-DPI support for APRS symbols */
+ @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
+ .aprs-marker div[style*="aprs-symbols-24-0.png"] {
+ background-image: url('/aprs-symbols/aprs-symbols-24-0@2x.png') !important;
+ background-size: 384px 144px !important;
+ }
+
+ .aprs-marker div[style*="aprs-symbols-24-1.png"] {
+ background-image: url('/aprs-symbols/aprs-symbols-24-1@2x.png') !important;
+ background-size: 384px 144px !important;
+ }
+
+ .aprs-marker div[style*="aprs-symbols-24-2.png"] {
+ background-image: url('/aprs-symbols/aprs-symbols-24-2@2x.png') !important;
+ background-size: 384px 144px !important;
+ }
+ }
+
+ /* Leaflet popup improvements for APRS data */
+ .leaflet-popup-content-wrapper {
+ border-radius: 8px;
+ }
+
+ .leaflet-popup-content {
+ margin: 8px 12px;
+ }
+
+
packet.base_callsign || "",
- "ssid" => packet.ssid || "",
- "data_type" => to_string(packet.data_type || "unknown"),
- "path" => packet.path || "",
- "data_extended" => data_extended || %{}
- }
+ # Only include packets with valid position data
+ if lat && lng do
+ # Get symbol information from data_extended
+ symbol_table_id = get_in(data_extended, ["symbol_table_id"]) ||
+ packet.symbol_table_id || "/"
+ symbol_code = get_in(data_extended, ["symbol_code"]) ||
+ packet.symbol_code || ">"
+
+ # Validate symbol
+ {final_table_id, final_symbol_code} =
+ if AprsSymbols.valid_symbol?(symbol_table_id, symbol_code) do
+ {symbol_table_id, symbol_code}
+ else
+ AprsSymbols.default_symbol()
+ end
+
+ # Generate callsign for display
+ callsign = if packet.ssid && packet.ssid != "" do
+ "#{packet.base_callsign}-#{packet.ssid}"
+ else
+ packet.base_callsign || ""
+ end
+
+ %{
+ "id" => callsign,
+ "callsign" => callsign,
+ "base_callsign" => packet.base_callsign || "",
+ "ssid" => packet.ssid || "",
+ "lat" => lat,
+ "lng" => lng,
+ "symbol_table_id" => final_table_id,
+ "symbol_code" => final_symbol_code,
+ "symbol_description" => AprsSymbols.symbol_description(final_table_id, final_symbol_code),
+ "data_type" => to_string(packet.data_type || "unknown"),
+ "path" => packet.path || "",
+ "comment" => get_in(data_extended, ["comment"]) || "",
+ "data_extended" => data_extended || %{}
+ }
+ else
+ # Return nil for packets without position data
+ nil
+ end
end
# Get IP location from external service
diff --git a/lib/aprs_web/router.ex b/lib/aprs_web/router.ex
index b4ccfd1..0318dd6 100644
--- a/lib/aprs_web/router.ex
+++ b/lib/aprs_web/router.ex
@@ -29,7 +29,7 @@ defmodule AprsWeb.Router do
live "/", MapLive.Index, :index
live "/enhanced", MapLive.Enhanced, :index
- get "/map", PageController, :map
+ get "/old", PageController, :map
live "/status", StatusLive.Index, :index
live "/packets", PacketsLive.Index, :index
diff --git a/priv/static/aprs-symbols/aprs-symbols-24-0.png b/priv/static/aprs-symbols/aprs-symbols-24-0.png
new file mode 100644
index 0000000..8a2713f
Binary files /dev/null and b/priv/static/aprs-symbols/aprs-symbols-24-0.png differ
diff --git a/priv/static/aprs-symbols/aprs-symbols-24-0@2x.png b/priv/static/aprs-symbols/aprs-symbols-24-0@2x.png
new file mode 100644
index 0000000..d6c15bb
Binary files /dev/null and b/priv/static/aprs-symbols/aprs-symbols-24-0@2x.png differ
diff --git a/priv/static/aprs-symbols/aprs-symbols-24-1.png b/priv/static/aprs-symbols/aprs-symbols-24-1.png
new file mode 100644
index 0000000..10126ef
Binary files /dev/null and b/priv/static/aprs-symbols/aprs-symbols-24-1.png differ
diff --git a/priv/static/aprs-symbols/aprs-symbols-24-1@2x.png b/priv/static/aprs-symbols/aprs-symbols-24-1@2x.png
new file mode 100644
index 0000000..f7e1a78
Binary files /dev/null and b/priv/static/aprs-symbols/aprs-symbols-24-1@2x.png differ
diff --git a/priv/static/aprs-symbols/aprs-symbols-24-2.png b/priv/static/aprs-symbols/aprs-symbols-24-2.png
new file mode 100644
index 0000000..c1dfa98
Binary files /dev/null and b/priv/static/aprs-symbols/aprs-symbols-24-2.png differ
diff --git a/priv/static/aprs-symbols/aprs-symbols-24-2@2x.png b/priv/static/aprs-symbols/aprs-symbols-24-2@2x.png
new file mode 100644
index 0000000..ac8918a
Binary files /dev/null and b/priv/static/aprs-symbols/aprs-symbols-24-2@2x.png differ