From bcbb39f21ceff3f729e3c93a525ce926f1653992 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Fri, 27 Feb 2026 13:25:00 -0600 Subject: [PATCH] feat: add trail line display to map frontend - Add trailLine state to MapHook - Implement show_trail_line event handler with Leaflet polyline - Implement clear_trail_line event handler - Clear trail line when switching to heat map or markers - Style trail line with blue color, 3px weight, 0.8 opacity --- assets/js/map.ts | 81 ++++++++++++++++++++++++++++++++++++++++ assets/js/types/map.d.ts | 1 + 2 files changed, 82 insertions(+) diff --git a/assets/js/map.ts b/assets/js/map.ts index 09d2593..ec3386c 100644 --- a/assets/js/map.ts +++ b/assets/js/map.ts @@ -1520,6 +1520,12 @@ let MapAPRSMap = { // Handle clear_all_markers event self.handleEvent("clear_all_markers", () => { + // Clear trail line if present + if (self.trailLine) { + self.map?.removeLayer(self.trailLine); + self.trailLine = undefined; + } + // Modified to preserve historical trails - only removes non-historical markers self.clearAllMarkers(); @@ -1537,6 +1543,12 @@ let MapAPRSMap = { return; } + // Clear trail line if present + if (self.trailLine) { + self.map.removeLayer(self.trailLine); + self.trailLine = undefined; + } + if (!L.heatLayer) { console.error("Leaflet.heat plugin not loaded!"); return; @@ -1595,6 +1607,12 @@ let MapAPRSMap = { return; } + // Clear trail line if present + if (self.trailLine) { + self.map.removeLayer(self.trailLine); + self.trailLine = undefined; + } + // Hide heat layer and show marker layer if (self.heatLayer && self.map.hasLayer(self.heatLayer)) { self.map.removeLayer(self.heatLayer); @@ -1606,6 +1624,69 @@ let MapAPRSMap = { console.error("Error in show_markers handler:", error); } }); + + // Handle trail line display for tracked callsigns at low zoom + self.handleEvent("show_trail_line", (payload: BaseEventPayload) => { + const data = payload as { callsign: string; points: Array<{ lat: number; lng: number; timestamp: string }> }; + + if (!self.map) { + console.warn("Map not initialized, cannot show trail line"); + return; + } + + // Clear existing trail line if present + if (self.trailLine) { + self.map.removeLayer(self.trailLine); + self.trailLine = undefined; + } + + // Clear heat map if present (switching from heat map to trail line) + if (self.heatLayer) { + self.map.removeLayer(self.heatLayer); + self.heatLayer = undefined; + } + + // If we have less than 2 points, don't draw a line + if (data.points.length < 2) { + console.debug(`Not enough points for trail line: ${data.points.length}`); + return; + } + + // Extract coordinates for polyline + const latlngs = data.points.map(point => [point.lat, point.lng] as [number, number]); + + // Access Leaflet from window + const L = (window as any).L; + + // Create polyline + self.trailLine = L.polyline(latlngs, { + color: '#3B82F6', // blue-500 + weight: 3, + opacity: 0.8, + lineCap: 'round', + lineJoin: 'round', + interactive: true + }); + + // Add to map + self.trailLine.addTo(self.map); + + console.debug(`Trail line added for ${data.callsign} with ${data.points.length} points`); + }); + + // Handle clearing trail line + self.handleEvent("clear_trail_line", (_payload: BaseEventPayload) => { + if (!self.map) { + return; + } + + // Remove trail line if present + if (self.trailLine) { + self.map.removeLayer(self.trailLine); + self.trailLine = undefined; + console.debug("Trail line cleared"); + } + }); }, sendBoundsToServer() { diff --git a/assets/js/types/map.d.ts b/assets/js/types/map.d.ts index 7f2c48f..77844ca 100644 --- a/assets/js/types/map.d.ts +++ b/assets/js/types/map.d.ts @@ -34,6 +34,7 @@ export interface LiveViewHookContext { zoomEndHandler?: () => void; rfPathLines?: Array; // Array of Leaflet polylines and markers for RF path visualization trailLayer?: LayerGroup; + trailLine?: Polyline; // Polyline for callsign trail display at low zoom markerClusterGroup?: MarkerClusterGroup; cleanupTimeouts?: ReturnType[]; pendingMarkers?: MarkerData[];