Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions html/airlines.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// airlines.js
// Loads the IATA -> ICAO callsign prefix table from OpenTravelData at
// runtime and caches it in localStorage, so it's fetched at most once
// per AIRLINE_CACHE_MAX_AGE_MS instead of on every page load.
"use strict";

const AIRLINE_CSV_URL = "https://raw.githubusercontent.com/opentraveldata/opentraveldata/master/opentraveldata/optd_airline_best_known_so_far.csv";
Comment thread
jaluebbe marked this conversation as resolved.
const AIRLINE_CACHE_KEY = "tar1090_iata_to_icao_v1";
const AIRLINE_CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 1 week

let iata_to_icao = {};

function parseAirlineCsv(text) {
// Columns: pk^env_id^validity_from^validity_to^3char_code^2char_code^...^type^...
// Only currently valid (validity_to empty), non-cargo-only (type != "C")
// entries are kept, to avoid ambiguity between passenger and cargo
// airlines sharing an IATA code (see the Lufthansa special case in
// iataToIcao() in planeObject.js).
const map = {};
const lines = text.split('\n');
for (let i = 1; i < lines.length; i++) {
const f = lines[i].split('^');
if (f.length < 12) {
continue;
}
const validityTo = (f[3] || '').trim();
const icao3 = (f[4] || '').trim();
const iata2 = (f[5] || '').trim();
const type = (f[11] || '').trim();
if (!/^[A-Z0-9]{2}$/.test(iata2) || !/^[A-Z]{3}$/.test(icao3) || validityTo || type === 'C') {
continue;
}
map[iata2] = icao3;
}
Comment thread
jaluebbe marked this conversation as resolved.
return map;
}

// Re-run setFlight() on already-tracked aircraft so their callsigns pick
// up the table once it's loaded (planes seen before that point were left
// with their raw, unconverted callsign).
function refreshTrackedCallsigns() {
if (typeof g === 'undefined' || !g.planes) {
return;
}
for (const hex in g.planes) {
const plane = g.planes[hex];
if (plane.flight) {
const oldTs = plane.flightTs;
plane.setFlight(plane.flight);
plane.flightTs = oldTs;
}
}
Comment thread
jaluebbe marked this conversation as resolved.
}

function loadAirlineTable() {
try {
const cached = JSON.parse(localStorage.getItem(AIRLINE_CACHE_KEY));
if (cached && typeof cached.ts === 'number' && cached.data && typeof cached.data === 'object'
&& Date.now() - cached.ts < AIRLINE_CACHE_MAX_AGE_MS) {
Comment thread
jaluebbe marked this conversation as resolved.
iata_to_icao = cached.data;
refreshTrackedCallsigns();
return;
}
Comment thread
Copilot marked this conversation as resolved.
Comment thread
Copilot marked this conversation as resolved.
} catch (e) {
// ignore missing/corrupt cache, fall through to fetch
}

fetch(AIRLINE_CSV_URL)
.then(res => {
if (!res.ok) {
throw new Error(`HTTP ${res.status} ${res.statusText}`);
}
return res.text();
})
.then(text => {
Comment thread
Copilot marked this conversation as resolved.
iata_to_icao = parseAirlineCsv(text);
refreshTrackedCallsigns();
try {
localStorage.setItem(AIRLINE_CACHE_KEY, JSON.stringify({ ts: Date.now(), data: iata_to_icao }));
} catch (e) {
// localStorage full or unavailable, not fatal
}
})
.catch(err => {
console.error("airlines.js: failed to load IATA->ICAO table", err);
});
}

loadAirlineTable();
1 change: 1 addition & 0 deletions html/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -1103,6 +1103,7 @@
<script src="layers.js"></script>
<script src="geomag2020.js"></script>
<script src="markers.js"></script>
<script src="airlines.js"></script>

<!-- JS_ANCHOR3 -->
<script>let spriteSrc = 'images/sprites.png';</script>
Expand Down
79 changes: 68 additions & 11 deletions html/planeObject.js
Original file line number Diff line number Diff line change
Expand Up @@ -2881,18 +2881,69 @@ PlaneObject.prototype.setProjection = function(arg) {
}
}

// Converts an IATA airline code + flight number to the ICAO code.
// Returns null if there's no known mapping.
function iataToIcao(iataCode, numPart) {
// LH is the IATA code for both Lufthansa passenger (ICAO DLH) and
// Lufthansa Cargo (ICAO GEC). Cargo flights use 4-digit numbers 8000-8999.
if (iataCode === 'LH') {
if (/^[0-9]{4}$/.test(numPart)) {
const n = parseInt(numPart, 10);
if (n >= 8000 && n <= 8999) {
return 'GEC';
}
}
return 'DLH';
}

if (iata_to_icao && iata_to_icao[iataCode]) {
return iata_to_icao[iataCode];
}
Comment thread
Copilot marked this conversation as resolved.
Outdated

return null;
}

function normalized_callsign(flight) {
const re = /^([A-Z]*)([0-9]*)([A-Z]*)$/;
const match = flight.match(re);
if (!match) {
return flight;
}
let alpha = match[1];
let num = match[2];
let alpha2 = match[3];
while(num[0] == '0' && num.length > 1) {
// Distinguish ICAO (3 letters) from IATA (2 alphanumeric, or digit+letter)
// prefixes so IATA callsigns can be converted to ICAO below.
const prefixRe = /^(?:([A-Z]{3})|([A-Z][A-Z0-9])|([0-9][A-Z]))([0-9]+)([A-Z]*)$/;
let match = flight.match(prefixRe);

let alpha, num, alpha2, prefixType;
if (match) {
if (match[1]) {
alpha = match[1];
prefixType = 'ICAO';
} else {
alpha = match[2] || match[3];
prefixType = 'IATA';
}
num = match[4];
alpha2 = match[5];
} else {
// Fallback for callsigns that don't fit the pattern above (e.g. registrations).
const re = /^([A-Z]*)([0-9]*)([A-Z]*)$/;
match = flight.match(re);
if (!match) {
return flight;
}
alpha = match[1];
num = match[2];
alpha2 = match[3];
prefixType = 'unknown';
}

while (num[0] == '0' && num.length > 1) {
num = num.slice(1);
}

if (prefixType === 'IATA') {
const icao = iataToIcao(alpha, num);
if (icao) {
return icao + num + alpha2;
}
}
Comment thread
jaluebbe marked this conversation as resolved.

return alpha + num + alpha2;
}

Expand Down Expand Up @@ -3095,8 +3146,14 @@ PlaneObject.prototype.setFlight = function(flight) {
this.flight = null;
this.name ='no callsign';
} else {
this.flight = `${flight}`;
this.name = this.flight.trim() || 'empty callsign';
let trimmed = `${flight}`.trim();
if (trimmed) {
// Normalize here so the table, map label, and route lookup
// (routeCheck() uses this.name) all show the same callsign.
trimmed = normalized_callsign(trimmed);
}
this.flight = trimmed;
this.name = trimmed || 'empty callsign';
this.flightTs = now;
}
}
Expand Down