Airplane Scatter – Documentation v2.4b

Airplane Scatter

Plugin Documentation
Client Plugin: airplanescatter.js
Server Plugin: airplanescatter_server.js
Version: 2.4a
Author: Highpoint
Date: May 2026
v2.4a

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)30
5.19Transmitter Beam Direction Score31
6Configuration Parameters34
7User Settings35
7.1Blacklist and Whitelist Configuration36
7.2userlist1.csv – fmscan.org Beam & RDS Data36
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 Versions 2.2 and above: 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. The plugin also supports 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.

3 · Architecture – Data Sources and Components

┌──────────────────────────────────────────────────────────────────────┐ │ User's Browser │ │ │ │ ┌──────────────────────────────────────────────────────────────┐ │ │ │ airplanescatter.js │ │ │ │ │ │ │ │ ┌─────────────────┐ ┌──────────────────────────────────┐ │ │ │ │ │ Data Fetching │ │ Geometry Engine │ │ │ │ │ │ │ │ │ │ │ │ │ │ ADS-B APIs: │ │ • haversineKm() │ │ │ │ │ │ adsb.one ──┐ │ │ • bearingDeg() │ │ │ │ │ │ adsb.lol ──┤ │ │ • gcIntersectionPoint() │ │ │ │ │ │ adsb.fi ──┼──┼──▶│ • deadReckon() │ │ │ │ │ │ airplanes ──┤ │ │ • crossAlongTrack() │ │ │ │ │ │ theairtrf ──┘ │ │ • radioHorizonKm() │ │ │ │ │ │ (parallel) │ │ • reflectionGeometryScore() │ │ │ │ │ │ │ │ • radioHorizonKm() │ │ │ │ │ │ /api/fmdx ────┼──▶│ • reflectionGeometryScore() │ │ │ │ │ │ (server RAM) │ │ • fuselageAlignmentScore() │ │ │ │ │ │ │ │ • calcScatter() │ │ │ │ │ │ opentopodata ─┼──▶│ • computePersistentCrossings() │ │ │ │ │ │ Elevation API │ │ • computeSweetSpotCorridor() │ │ │ │ │ │ │ │ • getTxEnvelope() │ │ │ │ │ │ userlist1.csv ─┼──▶│ • getTxBeamScore() │ │ │ │ │ │ (fmscan.org) │ │ • loadUserlist() │ │ │ │ │ └─────────────────┘ │ • enrichTxBeamData() │ │ │ │ │ └──────────────┬───────────────────┘ │ │ │ │ │ │ │ │ │ ┌────────────────────────────────────▼─────────────────┐ │ │ │ │ │ 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 │ │ │ │ │ └──────────────────────────────────────────────────────┘ │ │ │ │ │ │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ │ │ 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 │ │ │ │ │ │ ↳ Beam direction display │ │ │ │ │ │ • Elevation profile canvas (interactive) │ │ │ │ │ │ ↳ Sweet Spot corridor (green band) │ │ │ │ │ │ ↳ Hover tooltip with elevation angles │ │ │ │ │ │ ↳ Hash-based redraw guard │ │ │ │ │ │ • Compass filter │ │ │ │ │ │ • 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): ┌──────────────────────────────────────────────────────────────────┐ │ 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. The transmitter's beam direction (if known from the userlist) is also factored in as a hard gate. Finally, a two-pass topography check ensures the aircraft isn't physically blocked by mountains. 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).
Diffraction PenaltyMultiplierIf an aircraft is shadowed by mountains (up to a 500m tolerance), its sweet spot score is penalized proportionally.

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:

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. 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:

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 simultaneously queries five public API endpoints in parallel to ensure maximum coverage and automatic error handling (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. 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 icon / PhotoPlane symbol rotated to match heading, or airplane photo if enabled in settings. Colour follows score scale. Turns bright green when ETA is within ±5 seconds (NOW ✓).
Sweet spot markerAdditional marker on the map indicating the sweet spot.
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.
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, diffraction tolerance, and terrain-aware horizon check

Before any score is computed, two hard gates are applied in sequence. First, the beam gate: 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. In version 2.4a, a 500m diffraction tolerance zone is implemented to model knife-edge diffraction and troposcatter.

// ── Beam hard gate ─────────────────────────────────────────
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);

const hrxAtAc = env.hrx_env[idxLo] * (1 - frac) + env.hrx_env[idxHi] * frac;
const htxAtAc = env.htx_env[idxLo] * (1 - frac) + env.htx_env[idxHi] * frac;

// --- NEW: DIFFRACTION TOLERANCE ---
const DIFFRACTION_TOLERANCE_M = 500; // 500m tolerance for knife-edge diffraction/scatter

// Hard cut-off is now pushed 500m below the calculated radio horizon
if (altM < (hrxAtAc - DIFFRACTION_TOLERANCE_M) || altM < (htxAtAc - DIFFRACTION_TOLERANCE_M)) {
    return null;
}

// Calculate how deep the aircraft is in the "shadow" (0 if in direct line of sight)
const shadowDepthTx = Math.max(0, htxAtAc - altM);
const shadowDepthRx = Math.max(0, hrxAtAc - altM);
const maxShadowDepth = Math.max(shadowDepthTx, shadowDepthRx);

// Apply a penalty if the plane relies on diffraction (shadow zone)
// 0m shadow = multiplier 1.0 (no penalty)
// 500m shadow = multiplier 0.2 (heavy penalty, but still valid for the score)
const diffractionMultiplier = 1.0 - (maxShadowDepth / DIFFRACTION_TOLERANCE_M) * 0.8;
// ----------------------------------

const c_factor = 16.974;
const bulgeTx  = (d_tx * d_tx) / c_factor;
const bulgeRx  = (d_rx * d_rx) / c_factor;

// Zurück zur klassischen Winkel-Berechnung
const elevAngleTxDeg = toDeg(Math.atan2((altM - bulgeTx) - env.txEffM, d_tx * 1000));
const elevAngleRxDeg = toDeg(Math.atan2((altM - bulgeRx) - rxElevM,    d_rx * 1000));

if (elevAngleTxDeg > 15 || elevAngleRxDeg > 20) return null;

const txSigma = 2.0, rxSigma = 8.0;
const txElevScore  = Math.exp(-(elevAngleTxDeg * elevAngleTxDeg) / (2 * txSigma * txSigma));
const rxElevScore  = Math.exp(-(elevAngleRxDeg * elevAngleRxDeg) / (2 * rxSigma * rxSigma));

// Apply the new diffraction multiplier to the base sweetSpotScore
const sweetSpotScore = txElevScore * rxElevScore * diffractionMultiplier;

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
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 and additionally cached on the server via the new elevation_cache.json feature introduced in v2.4, preventing excessive upstream calls.

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. In version 2.4a, a two-pass topography check uses this exact array to penalize or hard-reject aircraft that are physically blocked by terrain during the scoring process (allowing a 500m diffraction zone).

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 Parallel Fetching & Redundancy

The plugin simultaneously queries five public ADS-B aggregators in parallel to ensure maximum coverage and elegantly bypass individual Cloudflare blocks or server outages:

Large radius requests are broken into hexagonal overlapping tile queries because the APIs have a strict maximum radius of 250 Nautical Miles (≈463 km). All five sources are queried simultaneously per tile (using Promise.all). The JSON responses are automatically merged, and any duplicate aircraft are filtered out using their unique ICAO 24-bit addresses. If a source is offline or returns an HTML Cloudflare challenge instead of JSON, the plugin safely ignores it without crashing and continues processing the remaining sources. 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:

WebSocket connections: The plugin's internal WebSocket connections are properly excluded from the public connection count to prevent connection limits.

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)

The plugin optionally integrates 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

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.

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. Values below 1 kW (e.g., 0.1) are now supported.
TX Search Radius1200 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.
Airplane Photo DisplayEnabledEnable or disable the display of airplane photos in the UI.
Auto-move WebserverDisabledAutomatically move the web server interface to the right side of the screen.
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

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.4a – May 2026 Current


Version 2.4 – May 2026


Version 2.3b – April 2026


Version 2.3a – April 2026


Version 2.3 – April 2026


Version 2.2 – April 2026

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


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