From cf78298f460e8a8db531858a034670cabf9b3a3f Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Mon, 21 Jul 2025 08:31:16 -0500 Subject: [PATCH] Fix trail lines not connecting historical positions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When changing trail duration and historical data settings, historical positions would show up on the map but trail lines wouldn't connect them. This was caused by a mismatch between trail ID generation and callsign extraction. ## Changes - Fix getTrailId() to properly extract base callsigns from historical marker IDs - Add setTrailDuration() method to TrailManager for dynamic duration updates - Add LiveView event to synchronize trail duration changes with client - Add comprehensive tests for historical ID handling ## Problem Historical markers have IDs like "hist_CALLSIGN_123" but getTrailId() was returning the full ID while TrailManager.extractBaseCallsign() extracted just "CALLSIGN", creating separate trails instead of connected ones. ## Solution Updated getTrailId() to extract base callsigns from historical IDs using the same logic as TrailManager, ensuring all positions from the same station use the same trail ID and connect properly. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- assets/js/features/trail_manager.ts | 18 ++++++++++++++++ assets/js/map.ts | 7 +++++++ assets/js/map_helpers.ts | 21 ++++++++++++++++++- lib/aprsme_web/live/map_live/index.ex | 3 +++ test/assets/js/map_helpers_test.ts | 30 +++++++++++++++++++++++++++ 5 files changed, 78 insertions(+), 1 deletion(-) diff --git a/assets/js/features/trail_manager.ts b/assets/js/features/trail_manager.ts index 925d5b0..4810807 100644 --- a/assets/js/features/trail_manager.ts +++ b/assets/js/features/trail_manager.ts @@ -78,6 +78,24 @@ export class TrailManager { } } + setTrailDuration(durationHours: number) { + this.trailDuration = durationHours * 60 * 60 * 1000; // Convert hours to milliseconds + + // Clean up existing trails based on new duration + const cutoffTime = Date.now() - this.trailDuration; + + this.trails.forEach((trailState, baseCallsign) => { + // Filter positions based on new duration (skip historical dots) + trailState.positions = trailState.positions.filter((pos) => { + const posTimestamp = typeof pos.timestamp === "string" ? new Date(pos.timestamp).getTime() : pos.timestamp; + return posTimestamp >= cutoffTime; + }); + + // Update trail visualization + this.updateTrailVisualization(baseCallsign, trailState, true); + }); + } + addPosition( markerId: string, lat: number, diff --git a/assets/js/map.ts b/assets/js/map.ts index 73e1815..ea8d23a 100644 --- a/assets/js/map.ts +++ b/assets/js/map.ts @@ -663,6 +663,13 @@ let MapAPRSMap = { } }); + // Handle trail duration updates from LiveView + self.handleEvent("update_trail_duration", (data: { duration_hours: number }) => { + if (self.trailManager) { + self.trailManager.setTrailDuration(data.duration_hours); + } + }); + // Handle new packets from LiveView self.handleEvent("new_packet", (data: MarkerData) => { try { diff --git a/assets/js/map_helpers.ts b/assets/js/map_helpers.ts index 04b7232..1c690a0 100644 --- a/assets/js/map_helpers.ts +++ b/assets/js/map_helpers.ts @@ -35,7 +35,26 @@ export function parseTimestamp(timestamp: string | number | Date | undefined): n * Get trail ID from marker data */ export function getTrailId(data: { callsign_group?: string; callsign?: string; id: string }): string { - return data.callsign_group || data.callsign || data.id; + // Prioritize callsign_group and callsign over extracting from ID + if (data.callsign_group) { + return data.callsign_group; + } + + if (data.callsign) { + return data.callsign; + } + + // For historical markers like "hist_CALLSIGN_123", extract the base callsign + const id = String(data.id); + if (id.startsWith("hist_")) { + // Remove hist_ prefix and any trailing numeric index + const withoutPrefix = id.replace(/^hist_/, ""); + // Remove trailing _digits pattern + return withoutPrefix.replace(/_\d+$/, ""); + } + + // For regular IDs, return as-is + return data.id; } /** diff --git a/lib/aprsme_web/live/map_live/index.ex b/lib/aprsme_web/live/map_live/index.ex index d5e74e8..9fb3f9f 100644 --- a/lib/aprsme_web/live/map_live/index.ex +++ b/lib/aprsme_web/live/map_live/index.ex @@ -360,6 +360,9 @@ defmodule AprsmeWeb.MapLive.Index do socket = assign(socket, trail_duration: duration, packet_age_threshold: new_threshold) + # Update client-side TrailManager with new duration + socket = push_event(socket, "update_trail_duration", %{duration_hours: hours}) + # Trigger cleanup to remove packets that are now outside the new duration send(self(), :cleanup_old_packets) diff --git a/test/assets/js/map_helpers_test.ts b/test/assets/js/map_helpers_test.ts index 3eae93d..72e2a78 100644 --- a/test/assets/js/map_helpers_test.ts +++ b/test/assets/js/map_helpers_test.ts @@ -62,6 +62,36 @@ describe('map_helpers', () => { }; expect(getTrailId(data)).toBe('ID-1'); }); + + test('extracts base callsign from historical ID format', () => { + const data = { + id: 'hist_MYCALL_123' + }; + expect(getTrailId(data)).toBe('MYCALL'); + }); + + test('handles historical ID with complex callsign', () => { + const data = { + id: 'hist_KD8ABC-9_456' + }; + expect(getTrailId(data)).toBe('KD8ABC-9'); + }); + + test('prioritizes callsign_group over historical ID extraction', () => { + const data = { + callsign_group: 'GROUP-1', + id: 'hist_MYCALL_123' + }; + expect(getTrailId(data)).toBe('GROUP-1'); + }); + + test('prioritizes callsign over historical ID extraction', () => { + const data = { + callsign: 'REALCALL', + id: 'hist_MYCALL_123' + }; + expect(getTrailId(data)).toBe('REALCALL'); + }); }); describe('saveMapState', () => {