Fix aircraft categorization misclassifying Cessnas as helicopters

N-prefix callsigns (US general aviation) were incorrectly matching the
helicopter detection rules due to 'N' in the helicopter callsign list
and a broken .some() condition. Speed/altitude heuristics were also too
broad, and zero-value speed data (not yet received) triggered false
matches. Types are now re-evaluated as better data arrives instead of
being locked to the first guess.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
root
2026-06-08 09:00:15 -07:00
parent 2a2c29de29
commit c996ade142
+76 -40
View File
@@ -749,8 +749,9 @@ class ADSBit {
flights.forEach(flight => { flights.forEach(flight => {
if (flight.lat && flight.lon && flight.altitude) { if (flight.lat && flight.lon && flight.altitude) {
this.flights.set(flight.icao, flight); this.flights.set(flight.icao, flight);
// Categorize aircraft type if not already done // Categorize aircraft type - re-evaluate when speed data arrives
if (!this.aircraftTypes.has(flight.icao)) { // since early messages often lack speed, leading to wrong guesses
if (!this.aircraftTypes.has(flight.icao) || (flight.speed > 0 && flight.callsign)) {
this.categorizeAircraft(flight); this.categorizeAircraft(flight);
} }
} }
@@ -760,58 +761,93 @@ class ADSBit {
categorizeAircraft(flight) { categorizeAircraft(flight) {
// Categorize aircraft based on available data // Categorize aircraft based on available data
// Priority: 1. Helicopter detection, 2. Callsign patterns, 3. Altitude/Speed heuristics // Priority: 1. Callsign-based, 2. Altitude/Speed heuristics, 3. Default
//
// NOTE: speed=0 or altitude=0 means data not yet received, so we
// avoid making assumptions from missing data. The caller should
// re-categorize once better data arrives.
let category = 'narrowBody'; // Default let category = 'narrowBody'; // Default
const callsign = (flight.callsign || '').trim(); const callsign = (flight.callsign || '').trim();
const altitude = flight.altitude || 0; const altitude = flight.altitude || 0;
const speed = flight.speed || 0; const speed = flight.speed || 0;
const hasSpeed = speed > 0;
const hasAlt = altitude > 0;
// Helicopter detection with multiple criteria // --- Callsign-based detection (most reliable) ---
// Known helicopter callsign patterns (medical, news, police, tours, etc.) const airline = callsign.length >= 3 ? callsign.substring(0, 3) : '';
const helicopterCallsigns = ['LIFE', 'MED', 'STAR', 'AIR', 'CARE', 'MERCY', 'REACH', 'CHP', 'COPTER', 'HELO', 'N', 'PHI'];
const isHelicopterCallsign = helicopterCallsigns.some(pattern =>
callsign.includes(pattern) || callsign.startsWith('N') && callsign.length <= 6
);
// Helicopter speed/altitude heuristics: // Known helicopter operator callsign prefixes
// - Very low speed at any altitude (< 100 knots) const heliOperators = ['LFE', 'MED', 'CHP', 'PHI', 'ERA', 'BHS'];
// - Low altitude with moderate speed (< 3000 ft and < 180 knots) // Keywords that appear anywhere in helicopter callsigns
// - Known helicopter callsign with reasonable parameters (< 10000 ft and < 200 knots) const heliKeywords = ['LIFE', 'MERCY', 'REACH', 'COPTER', 'HELO', 'MEDEVAC', 'CARE'];
if (isHelicopterCallsign && altitude < 10000 && speed < 200) { const isHeliCallsign = heliOperators.includes(airline) ||
category = 'helicopter'; heliKeywords.some(kw => callsign.includes(kw));
} else if (speed < 100 && altitude < 15000) {
category = 'helicopter'; const heavyCallsigns = ['CPA', 'UAE', 'ETH', 'QTR', 'SIA', 'KLM', 'AFL', 'BAW', 'AAL', 'DAL', 'UAL', 'FDX', 'UPS'];
} else if (altitude < 3000 && speed < 180) { const regionalCallsigns = ['SKW', 'RPA', 'ASH', 'PDT', 'CHQ', 'ENY', 'JIA', 'CPZ'];
// N-number = US general aviation registration (Cessna, Piper, etc.)
const isNNumber = callsign.startsWith('N') && callsign.length >= 4 && callsign.length <= 6 && /^N\d/.test(callsign);
// --- Apply rules ---
// 1. Helicopter: callsign match + reasonable flight envelope
if (isHeliCallsign) {
category = 'helicopter'; category = 'helicopter';
} }
// Heavy/jumbo aircraft callsigns (cargo and passenger) // 2. Heavy / Wide body
else if (callsign.length > 0) { else if (heavyCallsigns.includes(airline)) {
const heavyCallsigns = ['CPA', 'UAE', 'ETH', 'QTR', 'SIA', 'KLM', 'AFL', 'BAW', 'AAL', 'DAL', 'UAL', 'FDX', 'UPS']; category = (hasAlt && altitude > 42000) || (hasSpeed && speed > 550) ? 'heavy' : 'wideBody';
// Regional jet callsigns }
const regionalCallsigns = ['SKW', 'RPA', 'ASH', 'PDT', 'CHQ', 'ENY']; else if ((hasAlt && altitude > 42000) || (hasSpeed && speed > 550)) {
// Small prop patterns (typically GA aircraft with N-numbers or short callsigns) category = 'heavy';
const isSmallProp = callsign.length <= 4 && (callsign.startsWith('N') || !callsign.match(/[0-9]/)); }
else if ((hasAlt && altitude > 40000) || (hasSpeed && speed > 500)) {
// Check callsign patterns category = 'wideBody';
const airline = callsign.substring(0, 3); }
if (heavyCallsigns.includes(airline) || altitude > 40000 || speed > 500) { // 3. N-number = general aviation (small prop unless data says otherwise)
// High altitude or speed suggests wide body or heavy else if (isNNumber) {
if (altitude > 42000 || speed > 550) { // N-numbers at high altitude/speed are likely turboprops or light jets
category = 'heavy'; if (hasAlt && altitude > 25000) {
} else { category = 'narrowBody';
category = 'wideBody'; } else {
} category = 'smallProp';
} else if (regionalCallsigns.includes(airline) || (altitude < 25000 && speed < 350)) { }
category = 'regionalJet'; }
} else if (isSmallProp || (altitude < 10000 && speed < 200)) { // 4. Regional jets
category = 'smallProp'; else if (regionalCallsigns.includes(airline)) {
category = 'regionalJet';
}
// 5. Speed/altitude heuristics (only when we have real data)
else if (hasSpeed && hasAlt) {
if (speed < 60 && altitude < 5000) {
// Very slow + very low = likely helicopter
category = 'helicopter';
} else if (altitude < 18000 && speed < 300) {
// Below FL180 at moderate speed
if (speed < 170 && altitude < 12000) {
category = 'smallProp';
} else {
category = 'regionalJet';
}
} else { } else {
// Default narrow body (737, A320 family)
category = 'narrowBody'; category = 'narrowBody';
} }
} }
// 6. Altitude-only heuristic
else if (hasAlt && !hasSpeed) {
if (altitude < 10000) {
category = 'smallProp'; // Low altitude, unknown speed - guess GA
} else if (altitude < 25000) {
category = 'regionalJet';
} else {
category = 'narrowBody';
}
}
// 7. No useful data yet - stay with default
// (will be re-categorized on next update)
this.aircraftTypes.set(flight.icao, category); this.aircraftTypes.set(flight.icao, category);
} }