Only show movement trails for stations that are actually moving

- Add minimum movement threshold (100m) to skip stationary stations
- Add max hop distance (10km) to break trails at igate-hopping artifacts
- Require 3+ points per segment to filter noise from 2-point jumps
This commit is contained in:
Graham McIntire 2026-02-18 13:29:15 -06:00
parent b875b7ab9e
commit 33799f3f33
No known key found for this signature in database
2 changed files with 50 additions and 13 deletions

View file

@ -48,6 +48,9 @@ export class TrailManager {
"#20B2AA", // Light Sea Green
];
private proximityThreshold: number = 5.5; // kilometers
private minMovementThreshold: number = 0.1; // km - minimum total distance to show a trail
private maxHopDistance: number = 10; // km - max distance between consecutive points before breaking
private minSegmentPoints: number = 3; // minimum points in a segment to draw it
constructor(trailLayer: LayerGroup, trailDuration: number = 60 * 60 * 1000) {
this.trailLayer = trailLayer;
@ -320,9 +323,8 @@ export class TrailManager {
// Create new trail if we have at least 2 positions with valid coordinates
if (trailState.positions.length >= 2) {
// Filter out positions with invalid coordinates and create coordinate pairs
const latLngs: [number, number][] = trailState.positions
const validPositions: [number, number][] = trailState.positions
.filter((pos) => {
// Validate coordinates are finite numbers within valid ranges
return (
pos &&
typeof pos.lat === "number" &&
@ -339,9 +341,47 @@ export class TrailManager {
})
.map((pos) => [pos.lat, pos.lng]);
// Only create trail if we still have at least 2 valid positions after filtering
if (latLngs.length >= 2) {
// Assign color if not already assigned
// Calculate total path distance to determine if station is actually moving
let totalDistance = 0;
for (let i = 1; i < validPositions.length; i++) {
totalDistance += this.calculateDistance(
validPositions[i - 1][0],
validPositions[i - 1][1],
validPositions[i][0],
validPositions[i][1],
);
}
// Skip trail if station hasn't moved enough (stationary station)
if (totalDistance < this.minMovementThreshold) {
trailState.dots = [];
return;
}
// Break trail into segments at giant jumps (bad data / different igates)
const segments: [number, number][][] = [[]];
segments[0].push(validPositions[0]);
for (let i = 1; i < validPositions.length; i++) {
const hopDist = this.calculateDistance(
validPositions[i - 1][0],
validPositions[i - 1][1],
validPositions[i][0],
validPositions[i][1],
);
if (hopDist > this.maxHopDistance) {
// Start a new segment
segments.push([validPositions[i]]);
} else {
segments[segments.length - 1].push(validPositions[i]);
}
}
// Draw only segments with enough points to indicate real movement
const drawableSegments = segments.filter(
(seg) => seg.length >= this.minSegmentPoints,
);
if (drawableSegments.length > 0) {
if (!trailState.color) {
trailState.color = this.assignTrailColor(
baseCallsign,
@ -349,8 +389,6 @@ export class TrailManager {
);
}
// Create polyline with assigned color
// For historical positions (immediate=true), use higher opacity for better visibility
const polylineOptions: PolylineOptions = {
color: trailState.color,
weight: 3,
@ -362,9 +400,11 @@ export class TrailManager {
};
try {
trailState.trail = L.polyline(latLngs, polylineOptions).addTo(
this.trailLayer,
);
// L.polyline accepts an array of arrays for multi-segment lines
trailState.trail = L.polyline(
drawableSegments,
polylineOptions,
).addTo(this.trailLayer);
} catch (error) {
console.error(
"Error creating trail polyline for",
@ -372,11 +412,9 @@ export class TrailManager {
":",
error,
);
console.error("Invalid coordinates:", latLngs);
}
}
// Don't create additional dots here since historical positions are now shown as markers
trailState.dots = [];
}
}

View file

@ -88,7 +88,6 @@
}
}
/* Slideover panel responsive styles */
body.map-page .slideover-panel {
position: fixed;