Airplane Scatter – Documentation v2.2

Airplane Scatter

Plugin Documentation
Client Plugin: airplanescatter.js
Server Plugin: airplanescatter_server.js
Version: 2.2
Author: Highpoint
Date: April 2026
v2.2

Table of Contents

1What is Airplane Scatter?3
2Why a Plugin?3
3Architecture – Data Sources and Components4
4Simple Explanation – How the Plugin Works5
4.1The Reflection Geometry5
4.2Score – What does the Percentage Mean?6
4.3ETA – The Countdown Timer7
4.4Transmitter Database (fmdx.org)7
4.5Aircraft Data (ADS-B)8
4.6Elevation Profile8
4.7The Sweet Spot – Ideal Scatter Corridor9
4.8The Map Panel – Display Elements11
5Technical Deep Dive13
5.1Scatter Score Calculation13
5.2Geometric Helper Functions15
5.3Radio Horizon and Line-of-Sight Gate16
5.4Persistent Crossing Tracker17
5.5Dead Reckoning and Aircraft State18
5.6Elevation Data and Topographic Profile19
5.7Sweet Spot Corridor Computation20
5.8Elevation Profile Hover Tooltip22
5.9TX Spatial Grid Index, Envelope Cache & Anti-Stutter23
5.10ADS-B Source Failover25
5.11fmdx.org TX Database – Server-Side Cache25
5.12GPS & PST Rotator WebSocket Listener26
5.13Country Flag Lookup27
5.14Compass and Frequency Filters27
5.15Update Check28
5.16Audio Stream Integration (Web Radio)28
5.17Local Node Proxy Server29
5.18fmscan.org Userlist Integration (userlist1.csv) NEW in v2.230
5.19Transmitter Beam Direction Score CHANGED in v2.231
5.20Bug Fixes in v2.2 FIX33
6Configuration Parameters34
7User Settings35
7.1Blacklist and Whitelist Configuration36
7.2userlist1.csv – fmscan.org Beam & RDS Data NEW in v2.236
8Recommended Companion Plugins37
9Tips, Tricks & Best Practices38
10Changelog39

1 · What is Airplane Scatter?

Airplane Scatter (also known as Aircraft Scatter) is a radio propagation phenomenon in which a passing aircraft reflects or scatters FM radio signals between a transmitter and a receiver that would otherwise not be in direct line of sight of each other. The effect is real and well documented in the DX community.

When a large commercial aircraft — typically at cruise altitude between 8,000 m and 12,000 m — passes near the geometric midpoint of the great-circle path between a transmitting antenna and a receiving antenna, it can act as a mirror. The signal is scattered off the fuselage, wings, and engines and redirected toward the receiver. The scatter event is short-lived (typically 10–60 seconds) but can produce signals strong enough to decode RDS data or hear audio from a distant station.

Key factors that determine whether a scatter event will be detectable:

2 · Why a Plugin?

Without prediction tools, a DX operator has no way of knowing in advance that a scatter opportunity is about to occur. The window is narrow — typically under one minute — and the aircraft moves continuously. By the time a DX signal is noticed and identified, the aircraft may have already moved away from the optimal reflection geometry.

Plugin goal: Automatically fetch live ADS-B aircraft positions and the full FM transmitter database for the area around the receiver, then continuously compute which aircraft are geometrically positioned to produce a scatter event between any known high-power transmitter and the receiver. Events are predicted up to several minutes in advance, displayed on an interactive map with a live countdown, and annotated with a quality score so the operator can tune to the right frequency at the right moment.
System Requirements & Notice for Version 2.2: This plugin strictly requires FM-DX-Webserver version 1.4.0 or higher to function. It uses the dedicated local proxy backend (airplanescatter_server.js) which relies on the Node.js Plugin API introduced in FM-DX-Webserver v1.4.0. Version 2.2 adds optional integration of the fmscan.org userlist1.csv file which provides transmitter beam directions and supplementary RDS metadata (PI code, PS name, programme name, polarisation), enhancing both the scoring accuracy and the information displayed in the TX detail panel. It also fixes two bugs present in v2.1a: incorrect compass filtering for the E and NE sectors, and a WebSocket connection counter discrepancy caused by the plugin registering its own connections.

3 · Architecture – Data Sources and Components

┌──────────────────────────────────────────────────────────────────────┐ │ User's Browser │ │ │ │ ┌──────────────────────────────────────────────────────────────┐ │ │ │ airplanescatter.js v2.2 │ │ │ │ │ │ │ │ ┌─────────────────┐ ┌──────────────────────────────────┐ │ │ │ │ │ Data Fetching │ │ Geometry Engine │ │ │ │ │ │ │ │ │ │ │ │ │ │ ADS-B APIs: │ │ • haversineKm() │ │ │ │ │ │ adsb.one ──┐ │ │ • bearingDeg() │ │ │ │ │ │ adsb.lol ──┼──┼──▶│ • gcIntersectionPoint() │ │ │ │ │ │ adsb.fi ──┘ │ │ • deadReckon() │ │ │ │ │ │ (failover) │ │ • crossAlongTrack() │ │ │ │ │ │ │ │ • radioHorizonKm() │ │ │ │ │ │ /api/fmdx ────┼──▶│ • reflectionGeometryScore() │ │ │ │ │ │ (server RAM) │ │ • fuselageAlignmentScore() │ │ │ │ │ │ │ │ • calcScatter() │ │ │ │ │ │ opentopodata ─┼──▶│ • computePersistentCrossings() │ │ │ │ │ │ Elevation API │ │ • computeSweetSpotCorridor() │ │ │ │ │ │ │ │ • getTxEnvelope() │ │ │ │ │ │ userlist1.csv ─┼──▶│ • getTxBeamScore() NEW v2.2 │ │ │ │ │ │ (fmscan.org) │ │ • loadUserlist() NEW v2.2 │ │ │ │ │ └─────────────────┘ │ • enrichTxBeamData() NEW v2.2 │ │ │ │ │ └──────────────┬───────────────────┘ │ │ │ │ │ │ │ │ │ ┌────────────────────────────────────▼─────────────────┐ │ │ │ │ │ State Management │ │ │ │ │ │ │ │ │ │ │ │ _activeAircraft{} – live aircraft registry │ │ │ │ │ │ _persistentCrossings{} – icao24 → txKey → event │ │ │ │ │ │ txStations[] – loaded TX database │ │ │ │ │ │ txStationGrid{} – 10°×10° spatial grid │ │ │ │ │ │ _elevCache{} – terrain elevation cache │ │ │ │ │ │ _pathElevCache{} – profile elevation cache │ │ │ │ │ │ _txEnvelopeCache – TX horizon envelope cache │ │ │ │ │ │ _userlistDb{} – fmscan userlist NEW v2.2 │ │ │ │ │ └──────────────────────────────────────────────────────┘ │ │ │ │ │ │ │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ │ │ UI Layer (Leaflet.js Map) │ │ │ │ │ │ │ │ │ │ │ │ • Draggable/resizable floating panel │ │ │ │ │ │ • Left list panel – sorted candidate list │ │ │ │ │ │ • TX detail panel – frequencies, PI, PS, audio │ │ │ │ │ │ ↳ Extended RDS metadata from userlist NEW v2.2 │ │ │ │ │ │ ↳ Beam direction display NEW v2.2 │ │ │ │ │ │ • Elevation profile canvas (interactive) │ │ │ │ │ │ ↳ Sweet Spot corridor (green band) │ │ │ │ │ │ ↳ Hover tooltip with elevation angles │ │ │ │ │ │ ↳ Hash-based redraw guard │ │ │ │ │ │ • Compass filter – E/NE bug fixed NEW v2.2 │ │ │ │ │ │ • Status bar (aircraft count, candidates, DB size) │ │ │ │ │ └──────────────────────────────────────────────────────┘ │ │ │ └──────────────────────────────────────────────────────────────┘ │ │ │ │ ◀── WebSocket /data_plugins ─── FM-DX-Webserver (GPS + PST Rotator) │ │ ◀── WebSocket (main) ─── FM-DX-Webserver (tune + turns) │ │ ◀── /api/airplanescatter/fmdx airplanescatter_server.js (RAM) │ │ ◀── /api/airplanescatter/proxy airplanescatter_server.js (proxy) │ │ ◀── /plugins/AirplaneScatter/userlist1.csv (optional, fmscan.org) │ └──────────────────────────────────────────────────────────────────────┘ Server side (airplanescatter_server.js v2.2): ┌──────────────────────────────────────────────────────────────────┐ │ Priority 1: tx_search.js RAM ← zero download, shared DB │ │ Priority 2: Own RAM cache ← valid for 24 h / 100 km QTH │ │ Priority 3: Disk cache ← survives server restart │ │ Priority 4: Upstream fetch ← maps.fmdx.org (last resort) │ └──────────────────────────────────────────────────────────────────┘

4 · Simple Explanation – How the Plugin Works

Imagine you are trying to shine a laser pointer at a mirror to hit a distant target. If the mirror (the aircraft) is positioned at exactly the right angle between the laser source (the transmitter) and the target (your receiver), the beam bounces directly to you. The plugin computes this geometry for every aircraft currently in the sky, for every known FM transmitter in a 750 km radius, and tells you which combinations are close to the ideal reflection angle — before they occur.

4.1 · The Reflection Geometry

For a scatter event to occur, three points must be approximately collinear in a specific angular relationship: the transmitter, the aircraft, and the receiver. The aircraft should be positioned roughly on the great-circle path between transmitter and receiver, at a point where the angle of incidence (TX→AC) approximately equals the angle of reflection (AC→RX).

Transmitter (TX) │ Receiver (RX) │ signal path │ ▼ ▼ TX ─────────────────────────── RX ← 800 km apart (below radio horizon) ↗ scattered signal ↘ TX ─────── Aircraft ─────────── RX ← aircraft at 10 000 m altitude ↗ incidence angle ↘ ≈ 40° optimal Great-circle path TX → RX: ┌──────────────────────────────────────────────────────────┐ │ TX · · · · [AC] ←— cross-track offset ←— path │ │ │ │ [AC optimal] — on the great-circle │ │ │ │ · · · · · · · · · · · · · · · · RX │ └──────────────────────────────────────────────────────────┘ cross-track offset (⊥ distance from path): 0 km → max reflection probability 25 km → ~37% of maximum (e^-1) 50 km → ~14% of maximum

The plugin computes the great-circle intersection point — the point on the TX–RX path closest to the aircraft's current track — and evaluates the reflection quality based on cross-track distance, reflection angle, aircraft altitude, transmitter ERP, and aircraft fuselage alignment. From v2.2 onward, the transmitter's beam direction (if known from the userlist) is also factored in as a hard gate. The result is a single scatter score from 0–100%.

4.2 · Score – What does the Percentage Mean?

The scatter score is a weighted composite of physical factors. The score is designed so that aircraft flying in the Sweet Spot — just barely above both the TX and RX horizon envelopes with the flattest possible elevation angle — receive the highest scores. This directly reflects the physical reality of airplane scatter: the most efficient scatter occurs when the signal grazes the aircraft at the lowest possible elevation angle from both ends simultaneously.

FactorMax pointsDescription
Sweet Spot (elevation angles)40Gaussian reward for flat elevation angles from both TX and RX simultaneously. TX ideal: ~0° (grazing horizon), RX ideal: ~0° (just above horizon). Score drops rapidly as elevation angle increases.
Horizon margin20Extra reward for flying just barely above both horizon envelopes (inside the purple zone but close to its floor). Exponential decay with σ ≈ 4000 m above the envelope.
Cross-track distance20How close the aircraft is to the great-circle path. Gaussian decay with σ = 25 km.
Reflection geometry10How close the TX→AC→RX angle is to the 40° optimum. Gaussian decay with σ = 25°.
Fuselage alignment5Whether the aircraft's longitudinal axis is perpendicular to the bisector of the TX–AC–RX angle.
Transmitter ERP3Logarithmic scale: a 500 kW transmitter scores higher than a 100 kW transmitter.
Frequency2Lower FM frequencies (87.5 MHz) scatter slightly better than higher ones (108 MHz).

The raw score is then multiplied by an aircraft size multiplier based on the ADS-B category code, and additionally by the beam multiplier from the TX antenna beam direction (new in v2.2):

CategoryDescriptionMultiplier
A5Super Heavy (A380, B747-8)1.30×
A4Heavy jet (B777, A330, B744)1.15×
A3Large jet (B737, A320) – default1.00×
A6High-performance aircraft0.90×
A2Small aircraft0.85×
A1Light aircraft0.70×
B1–B4Rotorcraft0.60–0.80×
Key principle: A high score requires the aircraft to be above both horizon envelopes (inside the purple zone) and flying with the flattest possible elevation angle from both TX and RX simultaneously. An aircraft flying high above the horizon envelopes — even with perfect reflection geometry — will score low because the elevation angles become too steep for efficient scatter coupling into both antennas. Aircraft below either horizon envelope are rejected entirely and receive no score. From v2.2 onward, aircraft are additionally rejected if they fall outside the transmitter's antenna beam arc (hard gate using the beam data from the userlist).

Hard rejection gates (score = 0, aircraft not shown)

Score colour scale in the UI

score ≥ 80% (Excellent)
Red
score ≥ 60% (High)
Orange
score ≥ 40% (Medium)
Yellow
score < 40% (Low)
Green

4.3 · ETA – The Countdown Timer

The ETA (Estimated Time of Arrival at the crossing point) is displayed as a countdown timer in the format −M:SS (approaching) or +M:SS (receding). When the aircraft is within 5 seconds of the optimal crossing point, the display switches to NOW ✓ in green.

ETA display states: −3:00 → approaching, 3 minutes to go (blue) −0:30 → approaching, 30 seconds to go (blue) NOW ✓ → aircraft at crossing point ±5 sec (green) +0:45 → past crossing, 45 seconds ago (red) +1:00 → past crossing, 1 minute ago (red) Candidate is visible in the panel from: −leadTimeSec (default: 3 minutes before) to: +trailTimeSec (default: 1 minute after) Outside this window the candidate is silently removed.

4.4 · Transmitter Database (fmdx.org)

The TX database originates from maps.fmdx.org/api/ and contains all known FM transmitters worldwide. This database is loaded and managed entirely on the server side by airplanescatter_server.js. Only the pre-filtered result (typically under 200 KB) is sent to the browser, instead of the full 50 MB raw file.

Each transmitter entry provides:

Starting with v2.2, supplementary data (beam direction, RDS PI code, PS name, programme name, language) can optionally be loaded from the userlist1.csv file obtained from fmscan.org (see §5.18 and §7.2).

Cache invalidation: The client-side localStorage cache is considered stale if it is older than 24 hours or if the receiver's current location has moved more than 100 km from the location at which the cache was built. On the server side, the full database is refreshed at most once every 24 hours, and additionally if the QTH moves more than 100 km (relevant for mobile use).

4.5 · Aircraft Data (ADS-B)

Live aircraft positions are fetched every 15 seconds from ADS-B REST APIs. The plugin supports three public API endpoints with automatic failover (see §5.10). Each aircraft record provides:

Aircraft on the ground (alt_baro = "ground") or with a speed below 50 knots or an altitude below 1000 ft are silently excluded from all scatter calculations.

4.6 · Elevation Profile

When a transmitter is selected from the list or map, the plugin renders an interactive terrain elevation profile along the great-circle path from the receiver to the transmitter. The profile shows:

The profile is fully interactive: mouse-wheel to zoom horizontally, click-drag to pan, a vertical slider to scale the altitude axis, and a hover tooltip that displays terrain height, elevation angles from both ends, and aircraft elevation angles at the cursor position.

4.7 · The Sweet Spot – Ideal Scatter Corridor

The Sweet Spot is displayed as a semi-transparent green band in the elevation profile. This is the single most important visual indicator for understanding whether a scatter event is likely to be strong and audible.

What is the Sweet Spot?

For airplane scatter to work optimally, the aircraft must simultaneously satisfy two angular constraints — one seen from the receiver side and one seen from the transmitter side:

The Sweet Spot is the altitude band along the TX–RX path where both conditions are satisfied at the same time. An aircraft flying through this green corridor is in the ideal geometric position to produce a strong, detectable scatter signal.

Elevation profile cross-section (schematic): Altitude ▲ │ ┌──────────────────────┐ │ Purple zone │ ★ SWEET SPOT (green)│ │ (both above LOS │ RX elev ≤ 5° │ │ ceilings) │ TX elev ≤ 1° │ │ └──────────────────────┘ │ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ← RX 5° ceiling (upper bound from RX) │ · · · · · · · · · · · · ← TX 1° ceiling (upper bound from TX) │ ────────────────────────────────── ← Purple zone floor (max of LOS envelopes) │ ████████████████████████████████ ← Terrain │ └──────────────────────────────────────────────────────▶ Distance (RX → TX) RX TX

How to use the Sweet Spot in practice

4.8 · The Map Panel – Display Elements

┌──────────────────────────────────────────────────────────────────────┐ │ 📡 Scatter Candidates │ ✈ Airplane Scatter ⚙ ↺ ✕ │ ├──────────────────────────────┤ │ │ ✈ DLH123 [83%] │ │ │ → Munich [DEU] −1:24 │ ┌───────────────────────────────┐ │ │ │ │ │ │ │ ✈ EZY456 [67%] │ │ [Leaflet Map] │ │ │ → Lyon [FRA] −2:51 │ │ │ │ │ │ │ 🔵 Receiver QTH │ │ │ ✈ BAW789 [44%] │ │ 🟡/🟠/🔴 TX dots │ │ │ → Barcelona [ESP] −0:08 │ │ ✈ Aircraft icons + ETA │ │ │ │ │ ─ ─ Scatter path lines │ │ │ │ │ │ │ │ │ │ [🔓][NW][N ][NO] Frequency: │ │ │ │ │ [W ][✕][O ] [____][🔓] │ │ │ │ │ [SW][S ][SO] │ │ │ │ └───────────────────────────────┘ │ │ │ │ │ │ ┌───────────────────────────────┐ │ │ │ │ RX ──── TX [Elev. Profile] │ │ │ │ │ Purple zone + Green Sweet Spot│ │ │ │ │ Aircraft dots + hover tooltip │ │ │ │ └───────────────────────────────┘ │ ├──────────────────────────────┴───────────────────────────────────────┤ │ ✈ 312 aircraft 📡 3 active DB: 4872 TX 12:34:56 │ └──────────────────────────────────────────────────────────────────────┘
ElementMeaning
Candidate list (left panel)All currently active scatter candidates sorted by |ETA|. Each row shows callsign, target TX city with country flag, scatter score (colour-coded), and ETA countdown.
TX detail panelOpens when a candidate or TX dot is clicked. Shows all frequencies at that transmitter site with clickable tune buttons, bearing and distance from RX, terrain height, and list of crossing aircraft. In v2.2, also shows beam direction, RDS PI code, PS name, programme name and language if the userlist1.csv is available. Includes Play/Stop icons for live audio streaming.
Clickable AzimuthClicking the Azimuth degree in the TX detail panel automatically turns your PST Rotator antenna to that station (requires Admin/Tune permissions).
Tune buttonClicking a frequency in the TX detail panel sends a tune command (T{freq_kHz}) over the main FM-DX-Webserver WebSocket, instantly changing the receiver frequency.
Audio Stream Play/StopClicking the play icon next to a frequency will fetch and start the live audio webstream for that station directly inside your browser.
Aircraft iconPlane symbol rotated to match heading. Colour follows score scale. Turns bright green when ETA is within ±5 seconds (NOW ✓).
ETA labelCountdown rendered next to each aircraft icon. Updates every second. Blue = approaching, green = NOW ✓, red = past.
TX dots on mapColoured circles sized by ERP. The connecting dashed line from TX to RX is drawn for each active scatter candidate. Both dot and line turn green when the associated aircraft is at NOW ✓.
Receiver dotBlue circle at receiver QTH. Tooltip shows coordinates, terrain elevation, and antenna AGL height.
Compass filterNine-button directional filter. Supports Multi-Select (click multiple directions simultaneously). Click ✕ to clear. E/NE sector boundary bug fixed in v2.2.
Rotor Sync LockClick the 🔓/🔒 icon next to the compass. When locked, the compass filter automatically updates to track your physical PST Rotator azimuth.
Frequency filterType a frequency to restrict the map and candidate list to TX stations on that exact frequency. Also tunes the receiver. ✕ clears the filter.
Frequency Sync LockWhen locked, the map automatically filters to whatever frequency your FM-DX-Webserver receiver is currently tuned to.
Elevation profileShown below the map when a TX is selected. Interactive terrain cross-section with LOS ceilings, purple scatter zone, green Sweet Spot corridor, and aircraft dots. Mouse wheel = horizontal zoom, drag = pan, right slider = vertical zoom.
Profile hover tooltipMoving the mouse over the elevation profile canvas shows a floating tooltip with: distance from RX and TX at cursor, terrain height at cursor, elevation angle from RX to terrain (red), elevation angle from TX to terrain (yellow), and if an aircraft is within ±30 km of the cursor, its individual elevation angles from both RX and TX.
Status barAircraft count (total tracked), active candidate count (passing filter), TX database size, last update time or error message.
⚙ Settings buttonOpens the settings panel (see §7).
↺ Reload buttonClears the TX database cache and forces a fresh download and a new ADS-B fetch.

5 · Technical Deep Dive

5.1 · Scatter Score Calculation

The core scoring function calcScatter(ac, rxLat, rxLon, rxElevM, tx) is called for every (aircraft, transmitter) pair that passes the spatial pre-filter. It performs three steps: compute the ETA to the crossing point, evaluate the score at a series of forecast positions, and return the best score found.

Step 1 – Crossing point and ETA

const crossPt = gcIntersectionPoint(txLat,txLon,rxLat,rxLon,ac.lat,ac.lon,ac.track);
const speedKmS = (ac.speed * 1.852) / 3600;
const distToCross = haversineKm(ac.lat,ac.lon,crossPt.lat,crossPt.lon);
let etaSec = distToCross / speedKmS;
// Negate if aircraft is flying away from crossing point
const brgToCross = bearingDeg(ac.lat,ac.lon,crossPt.lat,crossPt.lon);
if (Math.abs(normalizeAngle180(ac.track - brgToCross)) > 90) etaSec = -etaSec;

Step 2 – Forecast evaluation

The aircraft position is dead-reckoned to 5 forecast time steps: 0, 60, 120, 180, 300 seconds. For each step the inner evaluation function _evalScatterAt() is called and the best score is retained. An early-exit stops the loop as soon as a score of 95 or higher is found.

Step 3 – Inner evaluation: beam gate and terrain-aware horizon gate

Before any score is computed, two hard gates are applied in sequence. First, the beam gate (new in v2.2): if userlist data is available for this TX, the bearing to the aircraft and the bearing to the RX are both checked against the antenna beam arc. If either lies outside the beam, the candidate is rejected immediately without computing the terrain envelope. Second, the terrain-aware horizon check using the cached TX envelope:

// ── Beam hard gate (v2.2) ─────────────────────────────────────────
const beamMultAc = getTxBeamScore(tx, acLat, acLon);
if (beamMultAc === 0.0) return null;  // aircraft outside beam → hard reject

const beamMultRx = getTxBeamScore(tx, rxLat, rxLon);
if (beamMultRx === 0.0) return null;  // RX outside beam → hard reject

// Combined beam multiplier applied to final score
const beamMult = beamMultAc * beamMultRx;

// ── Terrain-aware horizon check (envelope cache) ──────────────────
const env    = getTxEnvelope(tx, rxLat, rxLon, rxElevM);
if (altM < hrxAtAc || altM < htxAtAc) return null;

Score formula

let baseScore = sweetSpotScore * 40
              + marginScore    * 20
              + distScore      * 20
              + reflScore      * 10
              + fuseScore      *  5
              + erpScoreVal    *  3
              + (freqFactor - 0.97) / 0.06 * 2;

// beamMult: product of aircraft-side and RX-side beam scores (v2.2)
const finalScore = baseScore * sizeMult * beamMult;

return {
    score: Math.max(0, Math.min(100, Math.round(finalScore))),
    crossTrackKm
};

5.2 · Geometric Helper Functions

Great-circle intersection point

const nTxRx = normalize(cross(vTx, vRx));
const nAc   = normalize(cross(vAcPos, vAcAhead));
const inter = normalize(cross(nTxRx, nAc));
// Two antipodal solutions – pick closest to aircraft
const best = haversineKm(acLat,acLon,p1.lat,p1.lon) <
             haversineKm(acLat,acLon,p2.lat,p2.lon) ? p1 : p2;
// Safety: if outside TX–RX segment, fall back to midpoint
if (haversineKm(best,txLat,txLon) > d_txrx ||
    haversineKm(best,rxLat,rxLon) > d_txrx)
    return midpointGreatCircle(txLat,txLon,rxLat,rxLon);

Cross-track and along-track decomposition

const d13 = haversineKm(aLat,aLon,pLat,pLon);
const angleRad = toRad(normalizeAngle180(t13 - t12));
crossTrackKm  = Math.abs(d13 * Math.sin(angleRad));
alongTrackKm  = d13 * Math.cos(angleRad);

Fuselage alignment score

const optimalTrack =
    ((bTx + normalizeAngle180(bRx - bTx) / 2 + 360) % 360 + 90) % 360;
let trackDiff = Math.abs(normalizeAngle180(acTrackDeg - optimalTrack));
if (trackDiff > 90) trackDiff = 180 - trackDiff;
return 0.3 + 0.7 * Math.max(0, Math.cos(toRad(trackDiff)));

Reflection geometry score

const angleDeg = toDeg(Math.acos(
    Math.max(-1, Math.min(1, dot(i, o)))));
const diff = angleDeg - 40;
reflScore = Math.exp(-(diff * diff) / (2 * 25 * 25));

5.3 · Radio Horizon and Line-of-Sight Gate

Airplane scatter can only occur when neither the transmitter nor the receiver can "see" the other directly, but both can "see" the aircraft. The radio horizon formula uses the standard 4/3-earth approximation:

function radioHorizonKm(h1m, h2m) {
  return 4.12 * (Math.sqrt(Math.max(0, h1m))
               + Math.sqrt(Math.max(0, h2m)));
}

The dynamic ellipse gate grows slightly with altitude:

const dynamicEllipseFactor = 1.02 + Math.min(1.0, altM / 12000) * 0.06;
if (d_tx + d_rx > dynamicEllipseFactor * d_txrx) return null;

5.4 · Persistent Crossing Tracker

Each (aircraft, transmitter) pair that reaches the score threshold is stored in the two-level dictionary _persistentCrossings[icao24][txKey]. This persistence layer ensures that a candidate first detected at −3 minutes remains visible throughout its approach, peak, and recession.

_persistentCrossings structure: { "4b1902": { ← ICAO 24 (aircraft) "47.8_11.2_103.4": { ← txKey (lat_lon_freq) tx: { freq, city, erp, lat, lon, ... }, ac: { lat, lon, speed, track, alt_ft, ... }, etaSec: -92.4, score: 83, calcTime: 1743500000000 ← timestamp of last calculation } } } Live ETA (updated every second): const elapsed = (Date.now() - cr.calcTime) / 1000; const liveEta = cr.etaSec - elapsed; Expiry: liveEta < −trailTimeSec → entry deleted icao not in ADS-B > 180s → _activeAircraft + crossings deleted

5.5 · Dead Reckoning and Aircraft State

Between 15-second ADS-B updates the plugin uses dead reckoning to estimate each aircraft's current position:

function deadReckonRad(lat, lon, trackDeg, distKm) {
  const d = distKm / 6371;
  const f = toRad(lat), l = toRad(lon), t = toRad(trackDeg);
  const lat2 = Math.asin(Math.sin(f)*Math.cos(d)
                       + Math.cos(f)*Math.sin(d)*Math.cos(t));
  const lon2 = l + Math.atan2(
                       Math.sin(t)*Math.sin(d)*Math.cos(f),
                       Math.cos(d)-Math.sin(f)*Math.sin(lat2));
  return { lat: toDeg(lat2), lon: toDeg(lon2) };
}

Altitude is also extrapolated using the reported vertical speed:

const fAltFt = ac.alt_ft + ((ac.vspeed || 0) / 60) * dtSec;
if (fAltFt < 1000) continue;

5.6 · Elevation Data and Topographic Profile

Terrain elevation data is fetched from two APIs:

APIUsageResolution
opentopodata.org/v1/srtm90mBatch profile points (up to 100 per request), TX site elevationsSRTM 90 m
open-elevation.com/api/v1/lookupFallback for individual points if batch API failsSRTM 30 m

The profile is built from 100 equidistant great-circle sample points. Two running envelopes are computed — one from RX and one from TX — representing the minimum flight altitude to be simultaneously visible from both ends.

The purple scatter zone floor at each profile point is max(hrx_arr[i], htx_arr[i]) — the aircraft must be above this to be visible from both ends.

5.7 · Sweet Spot Corridor Computation

The Sweet Spot corridor is computed by the function computeSweetSpotCorridor(elevs, d_txrx, rxAltM, txAltM, hrx_arr, htx_arr). It adds two angular ceiling constraints on top of the existing purple zone floor.

The two angle ceilings

const RX_ANGLE_DEG = 5.0;   // aircraft must be ≤ 5° elevation from RX
const TX_ANGLE_DEG = 1.0;   // aircraft must be ≤ 1° elevation from TX

// Ceiling imposed by the RX elevation angle limit
const rxAngleCeil = rxAltM + x    * 1000 * tanRx;

// Ceiling imposed by the TX elevation angle limit
const txAngleCeil = txAltM + d_tx * 1000 * tanTx;

// Sweet spot ceiling = the lower of the two limits
const sweetCeil = Math.min(rxAngleCeil, txAngleCeil);

5.8 · Elevation Profile Hover Tooltip

The function initProfileCanvasHover() attaches a mousemove listener to the profile canvas. When the cursor is inside the plot area, it creates a floating tooltip showing detailed information for the path point directly below the cursor.

FieldDescription
Distance (RX)Cursor distance from the receiver along the path (km or miles)
Distance (TX)Remaining distance to the transmitter
TerrainInterpolated terrain elevation at cursor position
El. angle (RX)Elevation angle in degrees from the RX antenna to the terrain point at cursor. Displayed in red.
El. angle (TX)Same from the TX antenna side. Displayed in yellow.
Per-aircraft El. (RX) / (TX)If a crossing aircraft is within ±30 km of the cursor, its own elevation angles from RX and TX are shown.

5.9 · TX Spatial Grid Index, Envelope Cache & Anti-Stutter

The plugin builds a 10° × 10° spatial grid index over the loaded TX database. For each aircraft, only transmitters in the 9 grid cells surrounding the aircraft position are checked.

TX Terrain Envelope Cache

Each TX envelope is computed once per RX position and stored in a Map keyed by tx.lat_tx.lon_tx.freq. All subsequent evaluations for the same TX reuse the cached result. The cache is invalidated when the RX elevation is refreshed, when a new TX dataset is loaded, after elevation enrichment completes, or when the user applies new settings.

Anti-Stutter Yield

Yield interval: 8 ms trigger, 0 ms sleep (pure event-loop yield via setTimeout(r, 0)). The zero-delay return is sufficient because the envelope cache eliminates most of the computation that previously caused long synchronous stretches.

5.10 · ADS-B Source Failover

The plugin uses automatic round-robin failover across three public ADS-B aggregators:

Large radius requests are broken into hexagonal overlapping tile queries because the APIs have a strict maximum radius of 250 Nautical Miles (≈463 km). An explicit 500 ms delay between tile points prevents API rate-limiting.

5.11 · fmdx.org TX Database – Server-Side Cache

The full 50 MB fmdx.org dataset is managed entirely on the server with a four-tier priority cascade:

Priority 1 – tx_search.js RAM (zero download, shared with webserver core) Priority 2 – Own RAM cache (valid for 24 h / 100 km QTH movement) Priority 3 – Disk cache (cache/ directory, survives server restart) Priority 4 – Upstream fetch from maps.fmdx.org (last resort)

5.12 · GPS & PST Rotator WebSocket Listener

The plugin integrates with the FM-DX-Webserver ecosystem by listening to two standard WebSockets:

Bug fix in v2.2: In previous versions the plugin's own WebSocket connections were incorrectly counted towards the FM-DX-Webserver's connected-client counter. This has been corrected in v2.2 so that the plugin's internal WebSocket connections are properly excluded from the public connection count.

5.13 · Country Flag Lookup

Country flags are generated dynamically. The plugin loads a JavaScript array from tef.noobish.eu/logos/scripts/js/countryList.js mapping ITU codes to ISO 2-letter country codes, then generates HTML <img> tags pointing to flagcdn.com. The lookup is cached in localStorage for 24 hours.

5.14 · Compass and Frequency Filters

5.15 · Update Check

On startup, the plugin fetches its own source file from GitHub and compares the pluginVersion constant. If a newer version is found, a red notification dot appears on the plugin icon and a link is injected into the settings panel.

5.16 · Audio Stream Integration (Web Radio)

The TX detail panel includes Play/Stop icons next to each frequency. Clicking Play calls window._asHandleStreamClick() which queries api.fmlist.org/152/fmdxGetStreamById.php (via the local proxy) for stream URLs, selects the highest bitrate, and plays it via a hidden HTML5 <audio> element.

5.17 · Local Node Proxy Server

The airplanescatter_server.js backend attaches to the FM-DX-Webserver HTTP server and exposes two endpoints:

Key security mechanisms: Caller Guard, Domain Whitelist (only proxies to allowed domains), 10 MB response size cap, and sealResponse() to prevent "headers already sent" conflicts.

5.18 · fmscan.org Userlist Integration (userlist1.csv) NEW in v2.2

Version 2.2 introduces optional integration of the userlist1.csv file generated by the fmscan.org FMSCAN Userlist service. This file contains supplementary per-transmitter data that is not available in the standard fmdx.org database, most importantly the main transmitter beam direction and additional RDS metadata.

What is the userlist1.csv?

The fmscan.org service (login required at fmscan.org) allows registered users to generate a personalised CSV file based on their receiver location. The file contains all FM transmitters that are theoretically receivable from the configured QTH, with calculated distance, azimuth, and signal level (dB) for each. By selecting FM+ (Tropo) as the mode and CSV format with semicolon as the separator, and then downloading userlist1.csv, the user gets a file with the following column layout:

Col #Field nameDescription
1khzFrequency in kHz (e.g. 98000 = 98.000 MHz)
2ituITU country code (e.g. D, F, I, SUI)
3langLanguage code
4programProgramme name / station name
5modModulation (p = FM stereo, m = mono, u = unknown)
6cityTransmitter site city or location name
7latTransmitter latitude (decimal degrees)
8lonTransmitter longitude (decimal degrees)
9erp_to_rxERP towards receiver direction (kW)
10erp_maxMaximum ERP (kW)
11beamMain transmitter beam direction (degrees or range, e.g. "270" or "270-40")
12distDistance from receiver (km)
13azimuthAzimuth from receiver to transmitter (degrees)
14dbEstimated signal level (dBµV/m)
15psRDS PS (Programme Service) name
16piRDS PI (Programme Identification) code
17polPolarisation (H = horizontal, V = vertical, M = mixed)
18idUnique transmitter ID (used as primary key)
Account required: Downloading the userlist1.csv requires a free account at fmscan.org. After logging in, navigate to FMSCAN → Tools (userlist etc.) → Perseus / Globaltuners / SDR Console Location Search, select FM+ (Tropo), set the separator to semicolon, select CSV format, and click DOWNLOAD userlist1.csv. The file is generated for your configured receiver location on the fmscan.org account.

How to install the userlist

  1. Download userlist1.csv from https://fmscan.org/perseus.php?userlistmode=fm
  2. Place the file in the plugin directory: …/fm-dx-webserver-main/plugins/AirplaneScatter/userlist1.csv
  3. Restart the FM-DX Webserver
  4. Reload the browser — the plugin will automatically detect and load the file on startup.

The file is loaded once at startup by the asynchronous function loadUserlist() and stored in the in-memory dictionary _userlistDb, keyed by the unique transmitter ID (last column). If the file is not present, the plugin continues to work normally using only the fmdx.org data — all userlist features simply remain inactive.

How the userlist data is used

Once loaded, the userlist data enriches the TX station objects in three ways:

Matching logic

The function getUserlistEntry(tx) first tries to find a match by the numeric transmitter ID. If no ID is stored on the TX object (fmdx.org entries do not carry fmscan IDs), it falls back to a frequency + coordinate search:

function getUserlistEntry(tx) {
    if (!tx) return null;
    // 1. Try direct ID lookup
    if (tx.id && _userlistDb[String(tx.id)])
        return _userlistDb[String(tx.id)];

    // 2. Fallback: frequency + coordinate match
    const freqKhz = Math.round(tx.freq * 1000);
    return Object.values(_userlistDb).find(e =>
        parseInt(e.khz, 10) === freqKhz &&
        Math.abs(parseFloat(e.lat) - parseFloat(tx.lat)) < 0.01 &&
        Math.abs(parseFloat(e.lon) - parseFloat(tx.lon)) < 0.01
    ) || null;
}

5.19 · Transmitter Beam Direction Score CHANGED in v2.2

In v2.2 the TX beam direction is a fully functional hard gate and score multiplier, powered by the userlist1.csv data. The function getTxBeamScore(tx, targetLat, targetLon) returns a value between 0.0 (hard reject) and 1.0 (full score) for a given target direction from the transmitter.

Beam string formats

The userlist beam field (beam) can contain two formats:

FormatExampleMeaning
Single heading270Directional antenna aimed at 270° (West). Score falls off from centre with ±30° half-beamwidth; hard reject beyond 40° off-axis.
Multiple headings90 270Bi-directional or multi-directional antenna. Score computed against the nearest heading.
Range (arc)270-40Sector antenna covering an arc from 270° to 40° (crossing 0°/North). Score is 1.0 inside the arc; hard reject beyond 3° outside the arc.

Scoring logic

function getTxBeamScore(tx, targetLat, targetLon) {
    // Look up beam string: first from this tx, then from siblings
    let beamStr = (tx.beam || '').trim();
    if (!beamStr) {
        const sibs = txSiblings(tx);
        for (const sib of sibs) {
            const sibBeam = (sib.beam || '').trim();
            if (sibBeam) { beamStr = sibBeam; break; }
        }
    }
    if (!beamStr) return 1.0;  // omnidirectional – no data

    const brg = bearingDeg(tx.lat, tx.lon, targetLat, targetLon);
    const isRange = /^\d+\s*-\s*\d+$/.test(beamStr);

    if (isRange) {
        // Range beam: e.g. "270-40" means arc from 270° to 40°
        // Returns 1.0 inside arc, linear fade over 3° outside, 0.0 beyond
        const inRange = /* ... wrapping check ... */;
        if (inRange) return 1.0;
        if (outsideDeg >= 3) return 0.0;
        return 1.0 - (outsideDeg / 3);
    } else {
        // Single / multi heading: ±30° half-beamwidth, hard reject at 40°
        const outsideDeg = Math.max(0, minDist - 30);
        if (outsideDeg >= 10) return 0.0;
        return 1.0 - (outsideDeg / 10);
    }
}

How the beam gate is applied

The function is called twice per (aircraft, TX) pair inside _evalScatterAt():

  1. Aircraft direction gate: The bearing from the TX to the aircraft's current position is checked against the beam. If the aircraft is outside the illuminated arc, the TX cannot scatter energy toward it at all — hard reject.
  2. RX direction gate: The bearing from the TX to the receiver is also checked. Even if the aircraft is inside the beam, scatter can only reach the RX if the TX also illuminates the direction toward the RX — hard reject if not.

The two individual beam scores are then multiplied together and applied to the final score:

const finalScore = baseScore * sizeMult * beamMult;
// beamMult = beamMultAc × beamMultRx  (both must be > 0)

Transmitters for which no beam data is available (either not in the userlist, or beam field is empty) are treated as omnidirectional — the gate always passes and the multiplier is 1.0. This means that the userlist1.csv only ever reduces scores or hard-rejects candidates; it never increases them above the base value.

Sibling beam lookup

The fmdx.org database lists each frequency at a transmitter site as a separate entry. When beam data is looked up for a given TX entry and the entry itself has no beam field, the function also searches all co-located entries at the same geographic position (same latitude/longitude within 0.001°) — the "siblings" — and uses the first beam value found. This is important because a transmitter site may have beam data attached only to one of its frequency entries in the userlist.

5.20 · Bug Fixes in v2.2 FIX

1. Compass filter – incorrect E and NE sector boundaries

In versions 2.1 and 2.1a the compass filter incorrectly classified bearings in the E and NE sectors. The sector boundary angles for E and NE were swapped relative to the standard compass rose definition. This caused stations that were in fact in the NE direction to be shown when the E button was pressed, and vice versa.

The corrected sector definitions in v2.2 are:

DirectionBearing range (corrected)
N337.5° – 360° and 0° – 22.5° (wrapping)
NE22.5° – 67.5°
E67.5° – 112.5°
SE112.5° – 157.5°
S157.5° – 202.5°
SW202.5° – 247.5°
W247.5° – 292.5°
NW292.5° – 337.5°

This fix also corrects the rotor auto-sync behaviour: when the rotor lock is active and the antenna points between 22.5° and 112.5°, the correct NE and/or E sectors are now highlighted.

2. WebSocket connection counter

In previous versions the plugin opened its own WebSocket connections to the FM-DX-Webserver data feeds (/data_plugins and /text) which were incorrectly counted by the server as client connections. This inflated the displayed connection count in the FM-DX-Webserver admin interface and could trigger connection limits.

In v2.2 the plugin's internal WebSocket connections are properly marked so that the FM-DX-Webserver excludes them from the public connection counter. No configuration change is required; the fix is applied automatically.

6 · Configuration Parameters

The plugin contains several hardcoded parameters at the top of airplanescatter.js that determine the physical and visual bounds of the simulation.

// Forecast steps in seconds – 5 steps, 5-minute look-ahead
const FORECAST_STEPS_SEC  = [0, 60, 120, 180, 300];

// Maximum time before missing aircraft is discarded
const AIRCRAFT_TIMEOUT_MS = 180000; // 3 minutes

// ADS-B fetch interval
const AIRCRAFT_UPDATE_MS  = 15000;  // 15 seconds

// Visual countdown ticker interval
const COUNTDOWN_TICK_MS   = 1000;   // 1 second

// TX database cache duration
const DB_CACHE_HOURS      = 24;

// Score colour thresholds
const SCORE_EXCELLENT     = 80;
const SCORE_HIGH          = 60;
const SCORE_MEDIUM        = 40;

// TX antenna height above terrain (default if not in DB)
const TX_HEIGHT_DEFAULT_M = 150;

// ADS-B pre-filter radius
const AC_PREFILTER_KM     = 1200;
const AC_PREFILTER_LATDEG = 11.0;

7 · User Settings

The settings panel (⚙ button) exposes the following parameters. All values are saved to localStorage and persist across browser sessions.

SettingDefaultDescription
Min TX–RX Distance400 kmMinimum great-circle distance between transmitter and receiver. Paths shorter than this are skipped (direct LOS likely).
Min TX ERP100 kWMinimum transmitter effective radiated power. Weaker stations are excluded from the database query.
TX Search Radius750 kmMaximum distance from the receiver within which transmitters are loaded from the database.
Aircraft Search Radius750 kmMaximum distance from the receiver within which ADS-B aircraft are fetched.
Min Score70Scatter candidates below this score threshold are silently discarded.
RX Antenna AGL10 mHeight of the receiver antenna above ground level. Used in LOS envelope calculations.
Unit SystemMetricSwitches all distance and altitude displays between metric (km / m) and imperial (mi / ft / kts).
Lead Time3:00How far in advance (before the crossing point) a candidate appears in the list.
Trail Time1:00How long after the crossing point a candidate remains in the list.
Frequency Filter ModeNoneActivates blacklist or whitelist filtering from server-side text files (see §7.1).
Aircraft Category FilterAllMinimum ADS-B category to consider. E.g. A3 = only large jets and heavier; A1 = all aircraft.
Note: After applying settings, the TX terrain envelope cache is automatically invalidated (invalidateTxEnvelopeCache()) to ensure that any changed parameters (e.g. RX AGL height) are correctly reflected in the next calculation cycle.

7.1 · Blacklist and Whitelist Configuration

Place plain text files in the plugins/AirplaneScatter/ directory on the server:

Lines starting with # are treated as comments and ignored. Both comma and period are accepted as decimal separators. Only values in the FM broadcast range (87.5–108.0 MHz) are accepted.

7.2 · userlist1.csv – fmscan.org Beam & RDS Data NEW in v2.2

The userlist1.csv file provides beam directions and supplementary RDS data for transmitters in the vicinity of your receiver. It is generated specifically for your receiver location by fmscan.org and must be placed in the plugin directory.

Download instructions

  1. Create a free account at fmscan.org and log in.
  2. Make sure your receiver location (QTH) is correctly configured in your fmscan.org account.
  3. Navigate to FMSCAN → Tools (userlist etc.) → Perseus / Globaltuners / SDR Console Location Search.
  4. Select: mode = FM+ (Tropo), CSV Separator = semicolon, format = CSV format.
  5. Click DOWNLOAD userlist1.csv and wait approximately one minute for the file to be generated.
  6. Place the downloaded file at: …/fm-dx-webserver-main/plugins/AirplaneScatter/userlist1.csv
  7. Reload the browser. The plugin will automatically detect and load the file.
When to update the userlist: The userlist is specific to your receiver location. If you move your QTH by more than a few kilometres, download a fresh userlist for the new location. The file does not expire automatically — it remains valid as long as your QTH is unchanged. To force a reload, simply overwrite the file and reload the browser.
File not present: If userlist1.csv is missing from the plugin directory, the plugin operates normally without beam filtering and without the additional RDS metadata. The file is entirely optional and does not affect the basic airplane scatter detection functionality.

8 · Recommended Companion Plugins

PluginBenefit with Airplane Scatter
DSP-Stereo-SpectrumReal-time FFT spectrum display. During a scatter event you can visually confirm the incoming signal before it is decodable.
PST RotatorEnables the Azimuth click-to-turn feature and the Compass Rotor Sync Lock in the scatter map.
RDS-LoggerLogs all RDS PI codes and station names received. Useful for post-event analysis of which scatter events actually produced a decodable signal. The PI codes visible in the TX detail panel (from userlist1.csv) can be cross-referenced directly.
GPS-LoggerProvides live GPS coordinates to the webserver. The Airplane Scatter plugin reads these automatically and updates the RX QTH in real time during mobile operation.
AI-DenoiseFor equalising and denoising audio signals during scatter reception. Scatter events often produce short, flutter-distorted audio that benefits from AI-based noise reduction.
RDS-AI-DecoderFor decoding RDS as quickly as possible during short-term scatter receptions. Accelerates PI/PS identification within the narrow scatter window.

9 · Tips, Tricks & Best Practices

10 · Changelog

Version 2.2 – April 2026 Current

Feature release adding optional beam-aware TX scoring via a personalised fmlist.org userlist, additional station metadata display, and two bug fixes.

New Features

Bug Fixes

Internal Changes

Privacy note: The userlist1.csv file generated by fmlist.org is personalised to your receiver location and fmlist account. It contains station distances and azimuths calculated from your QTH coordinates. Do not share this file publicly if you wish to keep your receiver location private.
Fallback behaviour: If userlist1.csv is absent, cannot be fetched (HTTP 404), or contains no valid entries, the plugin logs a single warning to the browser console and continues operating in omnidirectional mode — identical to v2.1a. No error is shown to the user and no functionality is lost.

Version 2.1a – April 2026

Performance optimisation release targeting low-power hardware (Intel Compute Stick, Raspberry Pi class devices). No functional or visual changes — all existing features and the UI remain identical to v2.1.


Version 2.1 – April 2026