refactor(map): share isValidCoordinate and unwrapLongitudes helpers
The coord-validation and antimeridian-unwrap logic added in the last pass had been duplicated across map.ts, trail_manager.ts, and info_map.ts. Move both into map_helpers.ts and import from there so there is one authoritative implementation.
This commit is contained in:
parent
c41e47ed95
commit
9aa836e462
4 changed files with 57 additions and 89 deletions
|
|
@ -7,6 +7,8 @@ import type {
|
|||
PolylineOptions,
|
||||
} from "leaflet";
|
||||
|
||||
import { isValidCoordinate, unwrapLongitudes } from "../map_helpers";
|
||||
|
||||
// Declare Leaflet as a global
|
||||
declare const L: typeof import("leaflet");
|
||||
|
||||
|
|
@ -137,19 +139,7 @@ export class TrailManager {
|
|||
) {
|
||||
if (!this.showTrails) return;
|
||||
|
||||
// Validate coordinates before processing
|
||||
if (
|
||||
typeof lat !== "number" ||
|
||||
typeof lng !== "number" ||
|
||||
isNaN(lat) ||
|
||||
isNaN(lng) ||
|
||||
!isFinite(lat) ||
|
||||
!isFinite(lng) ||
|
||||
lat < -90 ||
|
||||
lat > 90 ||
|
||||
lng < -180 ||
|
||||
lng > 180
|
||||
) {
|
||||
if (!isValidCoordinate(lat, lng)) {
|
||||
console.warn("Invalid coordinates provided to addPosition:", {
|
||||
markerId,
|
||||
lat,
|
||||
|
|
@ -283,27 +273,6 @@ export class TrailManager {
|
|||
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. Returns null for empty arrays so
|
||||
// callers can't accidentally treat null island (0, 0) as a real center.
|
||||
private getTrailCenter(positions: PositionHistory[]): {
|
||||
|
|
@ -415,21 +384,7 @@ export class TrailManager {
|
|||
if (trailState.positions.length >= 2) {
|
||||
// Filter out positions with invalid coordinates and create coordinate pairs
|
||||
const validPositions: [number, number][] = trailState.positions
|
||||
.filter((pos) => {
|
||||
return (
|
||||
pos &&
|
||||
typeof pos.lat === "number" &&
|
||||
typeof pos.lng === "number" &&
|
||||
!isNaN(pos.lat) &&
|
||||
!isNaN(pos.lng) &&
|
||||
isFinite(pos.lat) &&
|
||||
isFinite(pos.lng) &&
|
||||
pos.lat >= -90 &&
|
||||
pos.lat <= 90 &&
|
||||
pos.lng >= -180 &&
|
||||
pos.lng <= 180
|
||||
);
|
||||
})
|
||||
.filter((pos) => pos && isValidCoordinate(pos.lat, pos.lng))
|
||||
.map((pos) => [pos.lat, pos.lng]);
|
||||
|
||||
// Calculate total path distance to determine if station is actually moving
|
||||
|
|
@ -451,7 +406,7 @@ export class TrailManager {
|
|||
|
||||
// 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);
|
||||
const unwrappedPositions = unwrapLongitudes(validPositions);
|
||||
|
||||
// Break trail into segments at giant jumps (bad data / different igates).
|
||||
// Hop distance uses the Haversine calculator which normalizes the lng
|
||||
|
|
|
|||
|
|
@ -1,24 +1,13 @@
|
|||
// Simple map hook for displaying a single station on the info page
|
||||
|
||||
import { isValidCoordinate } from "../map_helpers";
|
||||
|
||||
declare const L: typeof import("leaflet");
|
||||
|
||||
// ~1.1 m at the equator — enough to ignore float noise but still notice any
|
||||
// real position change from the server.
|
||||
const POSITION_EPSILON = 0.00001;
|
||||
|
||||
function isValidLatLng(lat: number, lng: number): boolean {
|
||||
return (
|
||||
!isNaN(lat) &&
|
||||
!isNaN(lng) &&
|
||||
isFinite(lat) &&
|
||||
isFinite(lng) &&
|
||||
lat >= -90 &&
|
||||
lat <= 90 &&
|
||||
lng >= -180 &&
|
||||
lng <= 180
|
||||
);
|
||||
}
|
||||
|
||||
interface InfoMapContext {
|
||||
el: HTMLElement;
|
||||
map: import("leaflet").Map | null;
|
||||
|
|
@ -46,7 +35,7 @@ export const InfoMap = {
|
|||
const symbolHtml = this.el.dataset.symbolHtml || null;
|
||||
const callsign = this.el.dataset.callsign || "unknown";
|
||||
|
||||
if (!isValidLatLng(lat, lon)) {
|
||||
if (!isValidCoordinate(lat, lon)) {
|
||||
console.warn(
|
||||
`InfoMap: Invalid coordinates lat=${lat}, lon=${lon} for ${callsign} during update`,
|
||||
);
|
||||
|
|
@ -117,7 +106,7 @@ function initializeMap(this: InfoMapContext) {
|
|||
const callsign = this.el.dataset.callsign || "unknown";
|
||||
const symbolHtml = this.el.dataset.symbolHtml || null;
|
||||
|
||||
if (!isValidLatLng(lat, lon)) {
|
||||
if (!isValidCoordinate(lat, lon)) {
|
||||
console.warn(
|
||||
`InfoMap: Invalid coordinates lat=${lat}, lon=${lon} for ${callsign}`,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -65,6 +65,8 @@ import {
|
|||
safePushEvent,
|
||||
getLiveSocket,
|
||||
escapeHtml,
|
||||
isValidCoordinate,
|
||||
unwrapLongitudes,
|
||||
} from "./map_helpers";
|
||||
|
||||
// APRS Map Hook - handles only basic map interaction
|
||||
|
|
@ -1674,17 +1676,9 @@ let MapAPRSMap = {
|
|||
|
||||
// Unwrap longitudes so a trail that crosses the antimeridian draws as a
|
||||
// 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]);
|
||||
}
|
||||
const latlngs = unwrapLongitudes(
|
||||
validPoints.map((p) => [p.lat, p.lng] as [number, number]),
|
||||
);
|
||||
|
||||
// Access Leaflet from window
|
||||
const L = (window as any).L;
|
||||
|
|
@ -2650,19 +2644,6 @@ function extractCoordinate(value: any): number {
|
|||
return NaN;
|
||||
}
|
||||
|
||||
// Helper to validate coordinates
|
||||
function isValidCoordinate(lat: number, lng: number): boolean {
|
||||
return (
|
||||
!isNaN(lat) &&
|
||||
!isNaN(lng) &&
|
||||
isFinite(lat) &&
|
||||
isFinite(lng) &&
|
||||
lat >= -90 &&
|
||||
lat <= 90 &&
|
||||
lng >= -180 &&
|
||||
lng <= 180
|
||||
);
|
||||
}
|
||||
|
||||
// Helper to create divIcon with common defaults
|
||||
function createDivIcon(
|
||||
|
|
|
|||
|
|
@ -149,3 +149,46 @@ export function isLiveViewConnected(): boolean {
|
|||
export function getLiveSocket(): LiveSocket | null {
|
||||
return typeof window !== 'undefined' ? window.liveSocket || null : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that a lat/lng pair is a real, in-range, finite coordinate.
|
||||
* Used everywhere we receive coordinates from the server or the DOM, so
|
||||
* NaN / Infinity / out-of-range values never reach Leaflet.
|
||||
*/
|
||||
export function isValidCoordinate(lat: number, lng: number): boolean {
|
||||
return (
|
||||
typeof lat === "number" &&
|
||||
typeof lng === "number" &&
|
||||
!isNaN(lat) &&
|
||||
!isNaN(lng) &&
|
||||
isFinite(lat) &&
|
||||
isFinite(lng) &&
|
||||
lat >= -90 &&
|
||||
lat <= 90 &&
|
||||
lng >= -180 &&
|
||||
lng <= 180
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unwrap a sequence of [lat, lng] points so a polyline that crosses the
|
||||
* antimeridian renders as a short hop instead of a line looping around the
|
||||
* world. Each point's lng is shifted by a cumulative multiple of 360 so the
|
||||
* delta between consecutive points is always the short-way difference.
|
||||
*/
|
||||
export function 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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue