symbols
This commit is contained in:
parent
846cf8a74b
commit
fe96abca59
16 changed files with 735 additions and 117 deletions
|
|
@ -20,7 +20,8 @@ let MinimalAPRSMap = {
|
||||||
|
|
||||||
// Add OpenStreetMap tile layer
|
// Add OpenStreetMap tile layer
|
||||||
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors | APRS.me',
|
attribution:
|
||||||
|
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors | APRS.me',
|
||||||
maxZoom: 19,
|
maxZoom: 19,
|
||||||
}).addTo(this.map);
|
}).addTo(this.map);
|
||||||
|
|
||||||
|
|
@ -39,7 +40,7 @@ let MinimalAPRSMap = {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Send bounds to LiveView when map moves
|
// Send bounds to LiveView when map moves
|
||||||
this.map.on('moveend', () => {
|
this.map.on("moveend", () => {
|
||||||
if (this.boundsTimer) clearTimeout(this.boundsTimer);
|
if (this.boundsTimer) clearTimeout(this.boundsTimer);
|
||||||
this.boundsTimer = setTimeout(() => {
|
this.boundsTimer = setTimeout(() => {
|
||||||
this.sendBoundsToServer();
|
this.sendBoundsToServer();
|
||||||
|
|
@ -64,7 +65,7 @@ let MinimalAPRSMap = {
|
||||||
// Add multiple markers at once
|
// Add multiple markers at once
|
||||||
this.handleEvent("add_markers", (data) => {
|
this.handleEvent("add_markers", (data) => {
|
||||||
if (data.markers && Array.isArray(data.markers)) {
|
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, {
|
this.map.setView([lat, lng], zoom, {
|
||||||
animate: true,
|
animate: true,
|
||||||
duration: 1
|
duration: 1,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -108,13 +109,50 @@ let MinimalAPRSMap = {
|
||||||
(error) => {
|
(error) => {
|
||||||
console.warn("Geolocation error:", error.message);
|
console.warn("Geolocation error:", error.message);
|
||||||
this.pushEvent("geolocation_error", { error: error.message });
|
this.pushEvent("geolocation_error", { error: error.message });
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
console.warn("Geolocation not available");
|
console.warn("Geolocation not available");
|
||||||
this.pushEvent("geolocation_error", { error: "Geolocation not supported" });
|
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() {
|
sendBoundsToServer() {
|
||||||
|
|
@ -133,9 +171,9 @@ let MinimalAPRSMap = {
|
||||||
},
|
},
|
||||||
center: {
|
center: {
|
||||||
lat: center.lat,
|
lat: center.lat,
|
||||||
lng: center.lng
|
lng: center.lng,
|
||||||
},
|
},
|
||||||
zoom: zoom
|
zoom: zoom,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -169,15 +207,20 @@ let MinimalAPRSMap = {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle marker click
|
// Handle marker click
|
||||||
marker.on('click', () => {
|
marker.on("click", () => {
|
||||||
this.pushEvent("marker_clicked", {
|
this.pushEvent("marker_clicked", {
|
||||||
id: data.id,
|
id: data.id,
|
||||||
callsign: data.callsign,
|
callsign: data.callsign,
|
||||||
lat: lat,
|
lat: lat,
|
||||||
lng: lng
|
lng: lng,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Mark historical markers for identification
|
||||||
|
if (data.historical) {
|
||||||
|
marker._isHistorical = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Add to map and store reference
|
// Add to map and store reference
|
||||||
marker.addTo(this.markerLayer);
|
marker.addTo(this.markerLayer);
|
||||||
this.markers.set(data.id, marker);
|
this.markers.set(data.id, marker);
|
||||||
|
|
@ -227,29 +270,74 @@ let MinimalAPRSMap = {
|
||||||
},
|
},
|
||||||
|
|
||||||
createMarkerIcon(data) {
|
createMarkerIcon(data) {
|
||||||
// Determine color based on symbol table or provided color
|
// Get symbol information
|
||||||
let color = data.color || (data.symbol_table === "/" ? "#2563eb" : "#dc2626");
|
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
|
// 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({
|
return L.divIcon({
|
||||||
html: `<div style="
|
html: `<div style="
|
||||||
background-color: ${color};
|
background-image: url('${spriteFile}');
|
||||||
width: 12px;
|
background-position: ${x}px ${y}px;
|
||||||
height: 12px;
|
background-repeat: no-repeat;
|
||||||
border-radius: 50%;
|
width: 24px;
|
||||||
border: 2px solid white;
|
height: 24px;
|
||||||
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
|
|
||||||
opacity: ${opacity};
|
opacity: ${opacity};
|
||||||
|
background-size: auto;
|
||||||
"></div>`,
|
"></div>`,
|
||||||
className: data.historical ? "aprs-marker historical-marker" : "aprs-marker",
|
className: data.historical ? "aprs-marker historical-marker" : "aprs-marker",
|
||||||
iconSize: [16, 16],
|
iconSize: [24, 24],
|
||||||
iconAnchor: [8, 8],
|
iconAnchor: [12, 12],
|
||||||
popupAnchor: [0, -8],
|
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 = `<div class="aprs-popup">
|
||||||
|
<div class="aprs-callsign"><strong>${callsign}</strong></div>
|
||||||
|
<div class="aprs-symbol-info">${symbolDesc}</div>`;
|
||||||
|
|
||||||
|
if (comment) {
|
||||||
|
content += `<div class="aprs-comment">${comment}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.lat && data.lng) {
|
||||||
|
content += `<div class="aprs-coords">
|
||||||
|
${data.lat.toFixed(4)}, ${data.lng.toFixed(4)}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
content += `</div>`;
|
||||||
|
return content;
|
||||||
|
},
|
||||||
|
|
||||||
destroyed() {
|
destroyed() {
|
||||||
console.log("MinimalAPRSMap hook destroyed");
|
console.log("MinimalAPRSMap hook destroyed");
|
||||||
|
|
||||||
|
|
@ -272,7 +360,7 @@ let MinimalAPRSMap = {
|
||||||
this.map.remove();
|
this.map.remove();
|
||||||
this.map = null;
|
this.map = null;
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default MinimalAPRSMap;
|
export default MinimalAPRSMap;
|
||||||
|
|
|
||||||
173
docs/APRS_SYMBOLS.md
Normal file
173
docs/APRS_SYMBOLS.md
Normal file
|
|
@ -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
|
||||||
|
|
@ -275,7 +275,7 @@ defmodule Aprs.Is do
|
||||||
packet_data = Map.put(parsed_message, :received_at, current_time)
|
packet_data = Map.put(parsed_message, :received_at, current_time)
|
||||||
|
|
||||||
# Convert to map before storing to avoid struct conversion issues
|
# 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
|
# Extract additional data from the parsed packet including raw packet
|
||||||
attrs = Aprs.Packet.extract_additional_data(attrs, message)
|
attrs = Aprs.Packet.extract_additional_data(attrs, message)
|
||||||
|
|
@ -462,4 +462,23 @@ defmodule Aprs.Is do
|
||||||
last_second_timestamp: System.system_time(:second)
|
last_second_timestamp: System.system_time(:second)
|
||||||
}
|
}
|
||||||
end
|
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
|
end
|
||||||
|
|
|
||||||
|
|
@ -158,6 +158,9 @@ defmodule Aprs.Packet do
|
||||||
# Extract data based on the type of data_extended
|
# Extract data based on the type of data_extended
|
||||||
additional_data =
|
additional_data =
|
||||||
case data_extended do
|
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) ->
|
%{} when is_map(data_extended) ->
|
||||||
extract_from_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(:equipment_type, mic_e.equipment_type)
|
||||||
|> maybe_put(:course, mic_e.course)
|
|> maybe_put(:course, mic_e.course)
|
||||||
|> maybe_put(:speed, mic_e.speed)
|
|> maybe_put(:speed, mic_e.speed)
|
||||||
|> maybe_put(:symbol_code, mic_e.symbol_code)
|
|> maybe_put(:symbol_code, ">") # Default car symbol for MicE
|
||||||
|> maybe_put(:symbol_table_id, mic_e.symbol_table_id)
|
|> 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
|
end
|
||||||
|
|
||||||
# Extract weather data from various formats
|
# Extract weather data from various formats
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ defmodule AprsWeb.PageController do
|
||||||
end
|
end
|
||||||
|
|
||||||
def map(conn, _params) do
|
def map(conn, _params) do
|
||||||
|
# This serves the legacy JavaScript-based map at /old
|
||||||
render(conn, :map)
|
render(conn, :map)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,50 @@
|
||||||
<link
|
<div class="legacy-map-wrapper">
|
||||||
rel="stylesheet"
|
<div class="legacy-map-header">
|
||||||
href="https://unpkg.com/leaflet@1.9.3/dist/leaflet.css"
|
<h1 class="text-xl font-bold text-gray-800 mb-2">Legacy APRS Map</h1>
|
||||||
integrity="sha256-kLaT2GOSpHechhsozzB+flnD+zUyjE2LlfWPgU04xyI="
|
<p class="text-sm text-gray-600 mb-4">
|
||||||
crossorigin=""
|
This is the legacy JavaScript-based map.
|
||||||
/>
|
<a href="/" class="text-blue-600 hover:text-blue-800 underline">Try the new LiveView-based map</a> or
|
||||||
<link
|
<a href="/enhanced" class="text-blue-600 hover:text-blue-800 underline">enhanced version</a>.
|
||||||
rel="stylesheet"
|
</p>
|
||||||
href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.css"
|
</div>
|
||||||
/>
|
|
||||||
<link
|
|
||||||
rel="stylesheet"
|
|
||||||
href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.Default.css"
|
|
||||||
/>
|
|
||||||
<!-- Make sure you put this AFTER Leaflet's CSS -->
|
|
||||||
<script
|
|
||||||
src="https://unpkg.com/leaflet@1.9.3/dist/leaflet.js"
|
|
||||||
integrity="sha256-WBkoXOwTeyKclOHuWtc+i2uENFpDZ9YPdf5Hf+D7ewM="
|
|
||||||
crossorigin=""
|
|
||||||
>
|
|
||||||
</script>
|
|
||||||
<script src="https://unpkg.com/leaflet.markercluster@1.5.3/dist/leaflet.markercluster.js">
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div id="map"></div>
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://unpkg.com/leaflet@1.9.3/dist/leaflet.css"
|
||||||
|
integrity="sha256-kLaT2GOSpHechhsozzB+flnD+zUyjE2LlfWPgU04xyI="
|
||||||
|
crossorigin=""
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.css"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.Default.css"
|
||||||
|
/>
|
||||||
|
<!-- Make sure you put this AFTER Leaflet's CSS -->
|
||||||
|
<script
|
||||||
|
src="https://unpkg.com/leaflet@1.9.3/dist/leaflet.js"
|
||||||
|
integrity="sha256-WBkoXOwTeyKclOHuWtc+i2uENFpDZ9YPdf5Hf+D7ewM="
|
||||||
|
crossorigin=""
|
||||||
|
>
|
||||||
|
</script>
|
||||||
|
<script src="https://unpkg.com/leaflet.markercluster@1.5.3/dist/leaflet.markercluster.js">
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div id="map" style="height: 80vh; width: 100%;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.legacy-map-wrapper {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legacy-map-header {
|
||||||
|
background: #f9fafb;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
||||||
181
lib/aprs_web/helpers/aprs_symbols.ex
Normal file
181
lib/aprs_web/helpers/aprs_symbols.ex
Normal file
|
|
@ -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(<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.
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
|
@ -262,8 +262,12 @@ defmodule AprsWeb.MapLive.Enhanced do
|
||||||
.historical-marker {
|
.historical-marker {
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Map Container -->
|
<!-- Map Container -->
|
||||||
<div
|
<div
|
||||||
id="aprs-map"
|
id="aprs-map"
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ defmodule AprsWeb.MapLive.Index do
|
||||||
|
|
||||||
alias Aprs.EncodingUtils
|
alias Aprs.EncodingUtils
|
||||||
alias AprsWeb.Endpoint
|
alias AprsWeb.Endpoint
|
||||||
|
alias AprsWeb.Helpers.AprsSymbols
|
||||||
alias Parser.Types.MicE
|
alias Parser.Types.MicE
|
||||||
|
|
||||||
@default_center %{lat: 39.8283, lng: -98.5795}
|
@default_center %{lat: 39.8283, lng: -98.5795}
|
||||||
|
|
@ -97,45 +98,14 @@ defmodule AprsWeb.MapLive.Index do
|
||||||
{:ok, socket}
|
{:ok, socket}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_event("bounds_changed", %{"bounds" => bounds}, socket) do
|
||||||
|
handle_bounds_update(bounds, socket)
|
||||||
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_event("update_bounds", %{"bounds" => bounds}, socket) do
|
def handle_event("update_bounds", %{"bounds" => bounds}, socket) do
|
||||||
# Update the map bounds from the client
|
handle_bounds_update(bounds, socket)
|
||||||
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
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
|
|
@ -247,6 +217,46 @@ defmodule AprsWeb.MapLive.Index do
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
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
|
@impl true
|
||||||
def handle_info(msg, socket) do
|
def handle_info(msg, socket) do
|
||||||
case msg do
|
case msg do
|
||||||
|
|
@ -364,36 +374,40 @@ defmodule AprsWeb.MapLive.Index do
|
||||||
# Convert to a simple map structure for JSON encoding
|
# Convert to a simple map structure for JSON encoding
|
||||||
packet_data = build_packet_data(packet)
|
packet_data = build_packet_data(packet)
|
||||||
|
|
||||||
# Add is_historical flag and timestamp
|
# Only process packets with valid position data
|
||||||
packet_data =
|
socket =
|
||||||
Map.merge(packet_data, %{
|
if packet_data do
|
||||||
"is_historical" => true,
|
# Add is_historical flag and timestamp
|
||||||
"timestamp" => if(Map.has_key?(packet, :received_at), do: DateTime.to_iso8601(packet.received_at))
|
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
|
# Generate a unique key for this packet
|
||||||
packet_id = "hist_#{if Map.has_key?(packet, :id), do: packet.id, else: System.unique_integer([:positive])}"
|
packet_id = "hist_#{if Map.has_key?(packet, :id), do: packet.id, else: System.unique_integer([:positive])}"
|
||||||
|
|
||||||
# Update historical packets tracking
|
# Update historical packets tracking
|
||||||
new_historical_packets = Map.put(historical_packets, packet_id, packet)
|
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)
|
# Calculate delay for next packet (faster based on replay speed)
|
||||||
delay_ms = trunc(1000 / speed)
|
delay_ms = trunc(1000 / speed)
|
||||||
|
|
||||||
# Schedule the next packet
|
# Schedule the next packet
|
||||||
timer_ref = Process.send_after(self(), :replay_next_packet, delay_ms)
|
timer_ref = Process.send_after(self(), :replay_next_packet, delay_ms)
|
||||||
|
{:noreply, assign(socket, replay_timer_ref: timer_ref)}
|
||||||
# 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}
|
|
||||||
else
|
else
|
||||||
# All packets replayed, start over with a short delay
|
# All packets replayed, start over with a short delay
|
||||||
Process.send_after(self(), :initialize_replay, 10_000)
|
Process.send_after(self(), :initialize_replay, 10_000)
|
||||||
|
|
@ -448,6 +462,8 @@ defmodule AprsWeb.MapLive.Index do
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.locate-button {
|
.locate-button {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 10px;
|
left: 10px;
|
||||||
|
|
@ -472,8 +488,69 @@ defmodule AprsWeb.MapLive.Index do
|
||||||
.historical-marker {
|
.historical-marker {
|
||||||
opacity: 0.7;
|
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;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
id="aprs-map"
|
id="aprs-map"
|
||||||
phx-hook="APRSMap"
|
phx-hook="APRSMap"
|
||||||
|
|
@ -667,15 +744,50 @@ defmodule AprsWeb.MapLive.Index do
|
||||||
|
|
||||||
defp build_packet_data(packet) do
|
defp build_packet_data(packet) do
|
||||||
data_extended = build_data_extended(packet.data_extended)
|
data_extended = build_data_extended(packet.data_extended)
|
||||||
|
{lat, lng} = get_coordinates(packet)
|
||||||
|
|
||||||
# Always return a map, even when data_extended is nil
|
# Only include packets with valid position data
|
||||||
%{
|
if lat && lng do
|
||||||
"base_callsign" => packet.base_callsign || "",
|
# Get symbol information from data_extended
|
||||||
"ssid" => packet.ssid || "",
|
symbol_table_id = get_in(data_extended, ["symbol_table_id"]) ||
|
||||||
"data_type" => to_string(packet.data_type || "unknown"),
|
packet.symbol_table_id || "/"
|
||||||
"path" => packet.path || "",
|
symbol_code = get_in(data_extended, ["symbol_code"]) ||
|
||||||
"data_extended" => data_extended || %{}
|
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
|
end
|
||||||
|
|
||||||
# Get IP location from external service
|
# Get IP location from external service
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ defmodule AprsWeb.Router do
|
||||||
|
|
||||||
live "/", MapLive.Index, :index
|
live "/", MapLive.Index, :index
|
||||||
live "/enhanced", MapLive.Enhanced, :index
|
live "/enhanced", MapLive.Enhanced, :index
|
||||||
get "/map", PageController, :map
|
get "/old", PageController, :map
|
||||||
live "/status", StatusLive.Index, :index
|
live "/status", StatusLive.Index, :index
|
||||||
|
|
||||||
live "/packets", PacketsLive.Index, :index
|
live "/packets", PacketsLive.Index, :index
|
||||||
|
|
|
||||||
BIN
priv/static/aprs-symbols/aprs-symbols-24-0.png
Normal file
BIN
priv/static/aprs-symbols/aprs-symbols-24-0.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
BIN
priv/static/aprs-symbols/aprs-symbols-24-0@2x.png
Normal file
BIN
priv/static/aprs-symbols/aprs-symbols-24-0@2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 105 KiB |
BIN
priv/static/aprs-symbols/aprs-symbols-24-1.png
Normal file
BIN
priv/static/aprs-symbols/aprs-symbols-24-1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
BIN
priv/static/aprs-symbols/aprs-symbols-24-1@2x.png
Normal file
BIN
priv/static/aprs-symbols/aprs-symbols-24-1@2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 93 KiB |
BIN
priv/static/aprs-symbols/aprs-symbols-24-2.png
Normal file
BIN
priv/static/aprs-symbols/aprs-symbols-24-2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
BIN
priv/static/aprs-symbols/aprs-symbols-24-2@2x.png
Normal file
BIN
priv/static/aprs-symbols/aprs-symbols-24-2@2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
Loading…
Add table
Reference in a new issue