Skip to content

Bug: Label_10_LDR, Label_10_POS, Label_1L_070, and Label_1L_3Line report position (0, 0) for empty lat/lon fields — silently places aircraft in the Gulf of Guinea #497

Description

@kevinelliott

Summary

Four label plugins parse the "N/S/E/W hemisphere + magnitude" coordinate form with the shape

(lat[0] === 'N' ? 1 : -1) * Number(lat.substring(1).trim())

Every one of them has the same silent-(0, 0) failure as #487 (Label_2P_FM3) and #495 (Label_2P_FM4/FM5): when the field is empty ("") — or holds only a hemisphere letter with no magnitude (e.g. "N") — Number(...) returns 0, the ? 1 : -1 ternary defaults to -1, and the plugin emits position: { latitude: -0, longitude: -0 }. ResultFormatter.position only rejects NaN, so the (0, 0) result flows through unchecked and renders as "0.000 S, 0.000 W" — the middle of the Gulf of Guinea.

ResultFormatter.position also stores it into raw.position, so any downstream consumer that maps or track-plots the message drops a marker on the ocean instead of dropping the field entirely.

Affected code

lib/plugins/Label_10_LDR.ts:32-38

const lat = parts[5];
const lon = parts[6];
const position = {
  latitude: (lat[0] === 'N' ? 1 : -1) * Number(lat.substring(1).trim()),
  longitude: (lon[0] === 'E' ? 1 : -1) * Number(lon.substring(1).trim()),
};
ResultFormatter.position(decodeResult, position);

parts.length < 17 is guarded (line 22), so parts[5] and parts[6] exist, but they can still be "".

lib/plugins/Label_10_POS.ts:24-32

const lat = parts[1].trim();
const lon = parts[2].trim();
const position = {
  latitude: ((lat[0] === 'N' ? 1 : -1) * Number(lat.substring(1).trim())) / 100,
  longitude: ((lon[0] === 'E' ? 1 : -1) * Number(lon.substring(1).trim())) / 100,
};
ResultFormatter.position(decodeResult, position);

Same failure mode: empty parts[1] or parts[2]-0 / 100 = -0.

lib/plugins/Label_1L_070.ts:55-62

ResultFormatter.position(decodeResult, {
  latitude:
    CoordinateUtils.getDirection(parts[4][0]) *
    Number(parts[4].substring(1)),
  longitude:
    CoordinateUtils.getDirection(parts[5][0]) *
    Number(parts[5].substring(1)),
});

If parts[4] is "N" (hemisphere-only) getDirection('N') * Number("") === 1 * 0 === 0. If parts[4] is "", parts[4][0] is undefined and getDirection(undefined) returns NaN — that path is caught by ResultFormatter.position's isNaN guard, so only the "hemisphere with no magnitude" variant leaks through as (0, 0).

lib/plugins/Label_1L_3-line.ts:77-86

if (lat && lon) {                                // ← lat="N" is truthy → enters branch
  ResultFormatter.position(decodeResult, {
    latitude:
      CoordinateUtils.getDirection(lat[0]) * Number(lat.substring(1)),
    longitude:
      CoordinateUtils.getDirection(lon[0]) * Number(lon.substring(1)),
  });
  data.delete('LAT');
  data.delete('LON');
}

The if (lat && lon) guard rejects undefined / "" but happily accepts LAT N / LON W with no numeric magnitude — same 0 * 1 = 0 silent (0, 0).

Reproduction

Label_10_LDR with an empty lat field (16 well-formed fields plus an empty parts[5]):

const decoder = new MessageDecoder();
const r = decoder.decode({
  label: '10',
  text: 'LDR,f1,f2,f3,f4,,W121.187,35000,f8,KJFK,KLAX,KSAN,04L/,,,f15,f16',
});
// r.raw.position.latitude === -0
// r.raw.position.longitude === -121.187
// r.formatted.items[0].value === '0.000 S, 121.187 W'

Label_10_POS with both lat and lon empty:

const r = decoder.decode({
  label: '10',
  text: 'POS,,,f3,f4,f5,f6,35000,f8,f9,f10,f11',
});
// r.raw.position.latitude === -0, r.raw.position.longitude === -0
// r.formatted.items[0].value === '0.000 S, 0.000 W'

Label_1L_3Line with LAT N\r\nLON W and no magnitudes:

const r = decoder.decode({
  label: '1L',
  text: 'line1\r\n/LAT N\r\n/LON W',
});
// r.raw.position === { latitude: 0, longitude: 0 }
// r.formatted.items includes { code: 'POS', value: '0.000 S, 0.000 W' }

Suggested fix

Two orthogonal changes:

  1. Per-plugin fix — treat "no digits after the hemisphere" the same as "missing coordinate". A simple regex check works: reject if parts[X].substring(1).trim() is empty or Number(...) returns NaN:

    function parseHemisphereCoord(s: string, positive: string): number | null {
      if (!s || s.length < 2) return null;
      const magnitude = Number(s.substring(1).trim());
      if (!Number.isFinite(magnitude)) return null;
      return (s[0] === positive ? 1 : -1) * magnitude;
    }
    
    const lat = parseHemisphereCoord(parts[5], 'N');
    const lon = parseHemisphereCoord(parts[6], 'E');
    if (lat !== null && lon !== null) {
      ResultFormatter.position(decodeResult, { latitude: lat, longitude: lon });
    }
  2. ResultFormatter.position hardening — extend the isNaN guard to also drop the (0, 0) case since it's never a valid ACARS position (the exact intersection of the equator and the prime meridian is not a real aircraft fix and shouldn't be reported as one):

    static position(decodeResult, value) {
      if (!value ||
          !Number.isFinite(value.latitude) ||
          !Number.isFinite(value.longitude) ||
          (value.latitude === 0 && value.longitude === 0)) {
        return;
      }
      // …existing push
    }

    Change Some position decoding improvement #2 also closes the same failure surface for Bug: Label_2P_FM3 else-branch reports position (0, 0) for empty lat/lon fields — silently places aircraft in the Gulf of Guinea #487 and Bug: Label_2P_FM4 and Label_2P_FM5 report position (0, 0) for empty lat/lon fields — same silent Gulf-of-Guinea failure as Label_2P_FM3 (#487) #495 without needing per-plugin fixes.

Test coverage

For each of the four plugins, add regression tests where the lat/lon fields are "" and "N" / "W" (hemisphere-only), asserting no POS item is emitted and raw.position is not set.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions