Fix trail lines not connecting historical positions

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 <noreply@anthropic.com>
This commit is contained in:
Graham McIntire 2025-07-21 08:31:16 -05:00
parent 7db864bb64
commit cf78298f46
No known key found for this signature in database
5 changed files with 78 additions and 1 deletions

View file

@ -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,

View file

@ -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 {

View file

@ -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;
}
/**

View file

@ -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)

View file

@ -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', () => {