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,
|
PolylineOptions,
|
||||||
} from "leaflet";
|
} from "leaflet";
|
||||||
|
|
||||||
|
import { isValidCoordinate, unwrapLongitudes } from "../map_helpers";
|
||||||
|
|
||||||
// Declare Leaflet as a global
|
// Declare Leaflet as a global
|
||||||
declare const L: typeof import("leaflet");
|
declare const L: typeof import("leaflet");
|
||||||
|
|
||||||
|
|
@ -137,19 +139,7 @@ export class TrailManager {
|
||||||
) {
|
) {
|
||||||
if (!this.showTrails) return;
|
if (!this.showTrails) return;
|
||||||
|
|
||||||
// Validate coordinates before processing
|
if (!isValidCoordinate(lat, lng)) {
|
||||||
if (
|
|
||||||
typeof lat !== "number" ||
|
|
||||||
typeof lng !== "number" ||
|
|
||||||
isNaN(lat) ||
|
|
||||||
isNaN(lng) ||
|
|
||||||
!isFinite(lat) ||
|
|
||||||
!isFinite(lng) ||
|
|
||||||
lat < -90 ||
|
|
||||||
lat > 90 ||
|
|
||||||
lng < -180 ||
|
|
||||||
lng > 180
|
|
||||||
) {
|
|
||||||
console.warn("Invalid coordinates provided to addPosition:", {
|
console.warn("Invalid coordinates provided to addPosition:", {
|
||||||
markerId,
|
markerId,
|
||||||
lat,
|
lat,
|
||||||
|
|
@ -283,27 +273,6 @@ 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. Returns null for empty arrays so
|
// 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.
|
// callers can't accidentally treat null island (0, 0) as a real center.
|
||||||
private getTrailCenter(positions: PositionHistory[]): {
|
private getTrailCenter(positions: PositionHistory[]): {
|
||||||
|
|
@ -415,21 +384,7 @@ export class TrailManager {
|
||||||
if (trailState.positions.length >= 2) {
|
if (trailState.positions.length >= 2) {
|
||||||
// Filter out positions with invalid coordinates and create coordinate pairs
|
// Filter out positions with invalid coordinates and create coordinate pairs
|
||||||
const validPositions: [number, number][] = trailState.positions
|
const validPositions: [number, number][] = trailState.positions
|
||||||
.filter((pos) => {
|
.filter((pos) => pos && isValidCoordinate(pos.lat, pos.lng))
|
||||||
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
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.map((pos) => [pos.lat, pos.lng]);
|
.map((pos) => [pos.lat, pos.lng]);
|
||||||
|
|
||||||
// Calculate total path distance to determine if station is actually moving
|
// 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
|
// Unwrap longitudes so segments that cross the antimeridian are drawn as
|
||||||
// a short local line rather than a polyline stretched across the world.
|
// 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).
|
// Break trail into segments at giant jumps (bad data / different igates).
|
||||||
// Hop distance uses the Haversine calculator which normalizes the lng
|
// 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
|
// Simple map hook for displaying a single station on the info page
|
||||||
|
|
||||||
|
import { isValidCoordinate } from "../map_helpers";
|
||||||
|
|
||||||
declare const L: typeof import("leaflet");
|
declare const L: typeof import("leaflet");
|
||||||
|
|
||||||
// ~1.1 m at the equator — enough to ignore float noise but still notice any
|
// ~1.1 m at the equator — enough to ignore float noise but still notice any
|
||||||
// real position change from the server.
|
// real position change from the server.
|
||||||
const POSITION_EPSILON = 0.00001;
|
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 {
|
interface InfoMapContext {
|
||||||
el: HTMLElement;
|
el: HTMLElement;
|
||||||
map: import("leaflet").Map | null;
|
map: import("leaflet").Map | null;
|
||||||
|
|
@ -46,7 +35,7 @@ export const InfoMap = {
|
||||||
const symbolHtml = this.el.dataset.symbolHtml || null;
|
const symbolHtml = this.el.dataset.symbolHtml || null;
|
||||||
const callsign = this.el.dataset.callsign || "unknown";
|
const callsign = this.el.dataset.callsign || "unknown";
|
||||||
|
|
||||||
if (!isValidLatLng(lat, lon)) {
|
if (!isValidCoordinate(lat, lon)) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`InfoMap: Invalid coordinates lat=${lat}, lon=${lon} for ${callsign} during update`,
|
`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 callsign = this.el.dataset.callsign || "unknown";
|
||||||
const symbolHtml = this.el.dataset.symbolHtml || null;
|
const symbolHtml = this.el.dataset.symbolHtml || null;
|
||||||
|
|
||||||
if (!isValidLatLng(lat, lon)) {
|
if (!isValidCoordinate(lat, lon)) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`InfoMap: Invalid coordinates lat=${lat}, lon=${lon} for ${callsign}`,
|
`InfoMap: Invalid coordinates lat=${lat}, lon=${lon} for ${callsign}`,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,8 @@ import {
|
||||||
safePushEvent,
|
safePushEvent,
|
||||||
getLiveSocket,
|
getLiveSocket,
|
||||||
escapeHtml,
|
escapeHtml,
|
||||||
|
isValidCoordinate,
|
||||||
|
unwrapLongitudes,
|
||||||
} from "./map_helpers";
|
} from "./map_helpers";
|
||||||
|
|
||||||
// APRS Map Hook - handles only basic map interaction
|
// 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
|
// Unwrap longitudes so a trail that crosses the antimeridian draws as a
|
||||||
// short hop instead of a polyline that spans the whole world.
|
// short hop instead of a polyline that spans the whole world.
|
||||||
const latlngs: [number, number][] = [
|
const latlngs = unwrapLongitudes(
|
||||||
[validPoints[0].lat, validPoints[0].lng],
|
validPoints.map((p) => [p.lat, p.lng] as [number, number]),
|
||||||
];
|
);
|
||||||
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;
|
||||||
|
|
@ -2650,19 +2644,6 @@ function extractCoordinate(value: any): number {
|
||||||
return NaN;
|
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
|
// Helper to create divIcon with common defaults
|
||||||
function createDivIcon(
|
function createDivIcon(
|
||||||
|
|
|
||||||
|
|
@ -149,3 +149,46 @@ export function isLiveViewConnected(): boolean {
|
||||||
export function getLiveSocket(): LiveSocket | null {
|
export function getLiveSocket(): LiveSocket | null {
|
||||||
return typeof window !== 'undefined' ? window.liveSocket || null : 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