fix(map): stop trail spidering across antimeridian and close gaps
- Normalize longitude delta in trail_manager Haversine so wraps across 180/-180 no longer compute a near-global distance and spuriously split the trail into short-segment-filtered pieces. - Unwrap longitudes before building segments and drawing polylines in both the per-station TrailManager and the show_trail_line handler so a date-line crossing renders as a short hop instead of a line that wraps around the world. - Raise maxHopDistance (10 -> 100 km) and drop minSegmentPoints (3 -> 2) so legitimate fast movers (aircraft, balloons) and short valid hops are drawn instead of being dropped. - Unbind mousemove/mouseout handlers on a trail before removing it so queued events can't fire against discarded state. - Fix self.isValidCoordinate TypeError in updateMarker (function is module-level, not on the hook) and add coord validation to show_trail_line. - Null boundsTimer after clearTimeout for cleanup consistency.
This commit is contained in:
parent
0ed2dd3806
commit
f777dcbb35
2 changed files with 80 additions and 20 deletions
|
|
@ -54,8 +54,11 @@ export class TrailManager {
|
||||||
];
|
];
|
||||||
private proximityThreshold: number = 5.5; // kilometers
|
private proximityThreshold: number = 5.5; // kilometers
|
||||||
private minMovementThreshold: number = 0.1; // km - minimum total distance to show a trail
|
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
|
// km - max distance between consecutive points before breaking the trail.
|
||||||
private minSegmentPoints: number = 3; // minimum points in a segment to draw it
|
// Must tolerate legitimately fast movers (aircraft, balloons, high-altitude)
|
||||||
|
// reporting ~1/min — too-tight values leave gaps between real movements.
|
||||||
|
private maxHopDistance: number = 100;
|
||||||
|
private minSegmentPoints: number = 2; // minimum points in a segment to draw it
|
||||||
private onTrailHover?: (lat: number, lng: number, path: string) => void;
|
private onTrailHover?: (lat: number, lng: number, path: string) => void;
|
||||||
private onTrailHoverEnd?: () => void;
|
private onTrailHoverEnd?: () => void;
|
||||||
private trailHoverDebounceTimer?: ReturnType<typeof setTimeout>;
|
private trailHoverDebounceTimer?: ReturnType<typeof setTimeout>;
|
||||||
|
|
@ -263,7 +266,13 @@ export class TrailManager {
|
||||||
): number {
|
): number {
|
||||||
const R = 6371; // Earth's radius in km
|
const R = 6371; // Earth's radius in km
|
||||||
const dLat = ((lat2 - lat1) * Math.PI) / 180;
|
const dLat = ((lat2 - lat1) * Math.PI) / 180;
|
||||||
const dLng = ((lng2 - lng1) * Math.PI) / 180;
|
// Normalize longitude delta to [-180, 180] so antimeridian-crossing pairs
|
||||||
|
// (e.g. 179.9 → -179.9) compute the real short-way distance instead of a
|
||||||
|
// bogus near-global one that would spuriously split the trail.
|
||||||
|
let lngDiff = lng2 - lng1;
|
||||||
|
if (lngDiff > 180) lngDiff -= 360;
|
||||||
|
else if (lngDiff < -180) lngDiff += 360;
|
||||||
|
const dLng = (lngDiff * Math.PI) / 180;
|
||||||
const a =
|
const a =
|
||||||
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
||||||
Math.cos((lat1 * Math.PI) / 180) *
|
Math.cos((lat1 * Math.PI) / 180) *
|
||||||
|
|
@ -274,6 +283,27 @@ export class TrailManager {
|
||||||
return R * c;
|
return R * c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unwrap a longitude sequence so polylines drawn across the antimeridian
|
||||||
|
// render as a short hop rather than a line wrapping around the world.
|
||||||
|
// Each point's longitude is shifted by a cumulative multiple of 360 so the
|
||||||
|
// delta between consecutive points is always the short-way difference.
|
||||||
|
private unwrapLongitudes(
|
||||||
|
positions: [number, number][],
|
||||||
|
): [number, number][] {
|
||||||
|
if (positions.length === 0) return [];
|
||||||
|
const unwrapped: [number, number][] = [[positions[0][0], positions[0][1]]];
|
||||||
|
let offset = 0;
|
||||||
|
for (let i = 1; i < positions.length; i++) {
|
||||||
|
const [lat, lng] = positions[i];
|
||||||
|
const prevRawLng = positions[i - 1][1];
|
||||||
|
const delta = lng - prevRawLng;
|
||||||
|
if (delta > 180) offset -= 360;
|
||||||
|
else if (delta < -180) offset += 360;
|
||||||
|
unwrapped.push([lat, lng + offset]);
|
||||||
|
}
|
||||||
|
return unwrapped;
|
||||||
|
}
|
||||||
|
|
||||||
// Get the average position of a trail
|
// Get the average position of a trail
|
||||||
private getTrailCenter(positions: PositionHistory[]): {
|
private getTrailCenter(positions: PositionHistory[]): {
|
||||||
lat: number;
|
lat: number;
|
||||||
|
|
@ -365,8 +395,12 @@ export class TrailManager {
|
||||||
trailState: TrailState,
|
trailState: TrailState,
|
||||||
immediate: boolean = false,
|
immediate: boolean = false,
|
||||||
) {
|
) {
|
||||||
// Remove old trail and dots
|
// Remove old trail and dots. Unbind listeners first so any queued mouse
|
||||||
|
// events on the outgoing polyline can't fire after we've dropped the
|
||||||
|
// state they close over.
|
||||||
if (trailState.trail) {
|
if (trailState.trail) {
|
||||||
|
trailState.trail.off("mousemove");
|
||||||
|
trailState.trail.off("mouseout");
|
||||||
this.trailLayer.removeLayer(trailState.trail);
|
this.trailLayer.removeLayer(trailState.trail);
|
||||||
}
|
}
|
||||||
if (trailState.dots) {
|
if (trailState.dots) {
|
||||||
|
|
@ -412,21 +446,27 @@ export class TrailManager {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Break trail into segments at giant jumps (bad data / different igates)
|
// Unwrap longitudes so segments that cross the antimeridian are drawn as
|
||||||
|
// a short local line rather than a polyline stretched across the world.
|
||||||
|
const unwrappedPositions = this.unwrapLongitudes(validPositions);
|
||||||
|
|
||||||
|
// Break trail into segments at giant jumps (bad data / different igates).
|
||||||
|
// Hop distance uses the Haversine calculator which normalizes the lng
|
||||||
|
// delta, so antimeridian-crossing hops are measured correctly.
|
||||||
const segments: [number, number][][] = [[]];
|
const segments: [number, number][][] = [[]];
|
||||||
segments[0].push(validPositions[0]);
|
segments[0].push(unwrappedPositions[0]);
|
||||||
for (let i = 1; i < validPositions.length; i++) {
|
for (let i = 1; i < unwrappedPositions.length; i++) {
|
||||||
const hopDist = this.calculateDistance(
|
const hopDist = this.calculateDistance(
|
||||||
validPositions[i - 1][0],
|
unwrappedPositions[i - 1][0],
|
||||||
validPositions[i - 1][1],
|
unwrappedPositions[i - 1][1],
|
||||||
validPositions[i][0],
|
unwrappedPositions[i][0],
|
||||||
validPositions[i][1],
|
unwrappedPositions[i][1],
|
||||||
);
|
);
|
||||||
if (hopDist > this.maxHopDistance) {
|
if (hopDist > this.maxHopDistance) {
|
||||||
// Start a new segment
|
// Start a new segment
|
||||||
segments.push([validPositions[i]]);
|
segments.push([unwrappedPositions[i]]);
|
||||||
} else {
|
} else {
|
||||||
segments[segments.length - 1].push(validPositions[i]);
|
segments[segments.length - 1].push(unwrappedPositions[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1648,14 +1648,33 @@ let MapAPRSMap = {
|
||||||
self.heatLayer = undefined;
|
self.heatLayer = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we have less than 2 points, don't draw a line
|
// Filter out any invalid coordinates before building the polyline —
|
||||||
if (data.points.length < 2) {
|
// NaN / Infinity / out-of-range values make Leaflet render junk.
|
||||||
console.debug(`Not enough points for trail line: ${data.points.length}`);
|
const validPoints = data.points.filter((point) =>
|
||||||
|
isValidCoordinate(point.lat, point.lng),
|
||||||
|
);
|
||||||
|
|
||||||
|
// If we have less than 2 valid points, don't draw a line
|
||||||
|
if (validPoints.length < 2) {
|
||||||
|
console.debug(
|
||||||
|
`Not enough valid points for trail line: ${validPoints.length} of ${data.points.length}`,
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract coordinates for polyline
|
// Unwrap longitudes so a trail that crosses the antimeridian draws as a
|
||||||
const latlngs = data.points.map(point => [point.lat, point.lng] as [number, number]);
|
// short hop instead of a polyline that spans the whole world.
|
||||||
|
const latlngs: [number, number][] = [
|
||||||
|
[validPoints[0].lat, validPoints[0].lng],
|
||||||
|
];
|
||||||
|
let offset = 0;
|
||||||
|
for (let i = 1; i < validPoints.length; i++) {
|
||||||
|
const prevRawLng = validPoints[i - 1].lng;
|
||||||
|
const delta = validPoints[i].lng - prevRawLng;
|
||||||
|
if (delta > 180) offset -= 360;
|
||||||
|
else if (delta < -180) offset += 360;
|
||||||
|
latlngs.push([validPoints[i].lat, validPoints[i].lng + offset]);
|
||||||
|
}
|
||||||
|
|
||||||
// Access Leaflet from window
|
// Access Leaflet from window
|
||||||
const L = (window as any).L;
|
const L = (window as any).L;
|
||||||
|
|
@ -1673,7 +1692,7 @@ let MapAPRSMap = {
|
||||||
// Add to map
|
// Add to map
|
||||||
self.trailLine.addTo(self.map);
|
self.trailLine.addTo(self.map);
|
||||||
|
|
||||||
console.debug(`Trail line added for ${data.callsign} with ${data.points.length} points`);
|
console.debug(`Trail line added for ${data.callsign} with ${validPoints.length} points`);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle clearing trail line
|
// Handle clearing trail line
|
||||||
|
|
@ -2137,7 +2156,7 @@ let MapAPRSMap = {
|
||||||
if (data.lat && data.lng) {
|
if (data.lat && data.lng) {
|
||||||
const lat = parseFloat(data.lat.toString());
|
const lat = parseFloat(data.lat.toString());
|
||||||
const lng = parseFloat(data.lng.toString());
|
const lng = parseFloat(data.lng.toString());
|
||||||
if (self.isValidCoordinate(lat, lng)) {
|
if (isValidCoordinate(lat, lng)) {
|
||||||
const currentPos = existingMarker.getLatLng();
|
const currentPos = existingMarker.getLatLng();
|
||||||
const positionChanged =
|
const positionChanged =
|
||||||
Math.abs(currentPos.lat - lat) > 0.0001 ||
|
Math.abs(currentPos.lat - lat) > 0.0001 ||
|
||||||
|
|
@ -2499,6 +2518,7 @@ let MapAPRSMap = {
|
||||||
|
|
||||||
if (self.boundsTimer !== undefined) {
|
if (self.boundsTimer !== undefined) {
|
||||||
clearTimeout(self.boundsTimer);
|
clearTimeout(self.boundsTimer);
|
||||||
|
self.boundsTimer = undefined;
|
||||||
}
|
}
|
||||||
if (self.resizeHandler !== undefined) {
|
if (self.resizeHandler !== undefined) {
|
||||||
window.removeEventListener("resize", self.resizeHandler);
|
window.removeEventListener("resize", self.resizeHandler);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue