fix(map): tighten edge cases in heatmap, info map, and trail color

- Heatmap: filter points with invalid coords or out-of-range intensity
  so malformed rows can't distort the layer.
- InfoMap: validate coords for range (not just NaN) and use an epsilon
  when comparing positions, so float noise doesn't cause needless
  re-renders or accept bogus data.
- TrailManager: getTrailCenter returns null for empty input instead of
  the ambiguous (0, 0) null-island; callers guard accordingly.
This commit is contained in:
Graham McIntire 2026-04-20 18:05:30 -05:00
parent 52418a7b41
commit e90226dc21
No known key found for this signature in database
GPG key ID: F4ABF488E6029E59
3 changed files with 51 additions and 18 deletions

View file

@ -304,12 +304,13 @@ export class TrailManager {
return unwrapped;
}
// Get the average position of a trail
// 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[]): {
lat: number;
lng: number;
} {
if (positions.length === 0) return { lat: 0, lng: 0 };
} | null {
if (positions.length === 0) return null;
const sum = positions.reduce(
(acc, pos) => ({
@ -341,6 +342,8 @@ export class TrailManager {
return;
const otherCenter = this.getTrailCenter(trailState.positions);
if (!otherCenter) return;
const distance = this.calculateDistance(
center.lat,
center.lng,
@ -377,8 +380,8 @@ export class TrailManager {
}
// All 15 colors in use — pick one not used by nearby trails
if (positions.length > 0) {
const center = this.getTrailCenter(positions);
const center = this.getTrailCenter(positions);
if (center) {
const nearbyColors = this.getNearbyTrailColors(baseCallsign, center);
for (const color of this.colorPalette) {
if (!nearbyColors.has(color)) {

View file

@ -2,6 +2,23 @@
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;
@ -29,7 +46,7 @@ export const InfoMap = {
const symbolHtml = this.el.dataset.symbolHtml || null;
const callsign = this.el.dataset.callsign || "unknown";
if (isNaN(lat) || isNaN(lon)) {
if (!isValidLatLng(lat, lon)) {
console.warn(
`InfoMap: Invalid coordinates lat=${lat}, lon=${lon} for ${callsign} during update`,
);
@ -47,7 +64,10 @@ export const InfoMap = {
if (this.marker) {
const currentPos = this.marker.getLatLng();
if (currentPos.lat !== lat || currentPos.lng !== lon) {
if (
Math.abs(currentPos.lat - lat) > POSITION_EPSILON ||
Math.abs(currentPos.lng - lon) > POSITION_EPSILON
) {
this.marker.setLatLng([lat, lon]);
this.marker.setPopupContent(
@ -97,7 +117,7 @@ function initializeMap(this: InfoMapContext) {
const callsign = this.el.dataset.callsign || "unknown";
const symbolHtml = this.el.dataset.symbolHtml || null;
if (isNaN(lat) || isNaN(lon)) {
if (!isValidLatLng(lat, lon)) {
console.warn(
`InfoMap: Invalid coordinates lat=${lat}, lon=${lon} for ${callsign}`,
);

View file

@ -1575,16 +1575,26 @@ let MapAPRSMap = {
}
}
// Convert heat points to format expected by Leaflet.heat
// Intensity is pre-normalized to 0-1 range by the server
const heatData = data.heat_points.map(
(point) =>
[point.lat, point.lng, point.intensity] as [
number,
number,
number,
],
);
// Convert heat points to format expected by Leaflet.heat.
// Server pre-normalizes intensity to [0, 1] but validate defensively
// so malformed rows can't distort the whole heat layer.
const heatData = data.heat_points
.filter(
(point) =>
isValidCoordinate(point.lat, point.lng) &&
typeof point.intensity === "number" &&
isFinite(point.intensity) &&
point.intensity >= 0 &&
point.intensity <= 1,
)
.map(
(point) =>
[point.lat, point.lng, point.intensity] as [
number,
number,
number,
],
);
// Update heat layer data
self.heatLayer.setLatLngs(heatData);