better seismic drawings, and adsb enrichment

This commit is contained in:
2026-06-27 15:20:21 -04:00
parent f3356ff05c
commit d34f3cfa0e
6 changed files with 276 additions and 66 deletions

View File

@@ -170,31 +170,145 @@ export function classifyAircraft(row) {
return 'unknown';
}
async function scrapeHexDatabase(icao) {
const url = `https://hexdatabase.com/h/${icao}`;
try {
const resp = await fetch(url);
if (!resp.ok) return null;
const html = await resp.text();
const rowMatch = html.match(new RegExp(`<tr[^>]*>.*?<span[^>]*>.*?${icao}.*?<\/span>.*?<\/tr>`, 's'));
if (!rowMatch) return null;
// 0: Hex (ICAO), 1: Co (Country), 2: Reg (Registration), 3: Atype (Aircraft Type), 4: Operator, 5: Source, 6: MSN (Serial number), 7: Remarks (Description)
const cells = rowMatch.match(/<td[^>]*>.*?<\/td>/g) || [] || null;
const country = cells[1]?.replace(/<[^>]*>/g, '').trim() || null;
const registration = cells[2]?.replace(/<[^>]*>/g, '').trim() || null;
const aircraft = cells[3]?.replace(/<[^>]*>/g, '').trim() || null;
const operator = cells[4]?.replace(/<[^>]*>/g, '').trim() || null;
const serialNumber = cells[6]?.replace(/<[^>]*>/g, '').trim() || null;
return {
country,
registration,
aircraft,
operator,
serialNumber
};
} catch (e) {
return null;
}
}
async function scrapeHexDatabase(icao) {
const url = `https://hexdatabase.com/h/${icao}`;
try {
const resp = await fetch(url);
if (!resp.ok) return null;
const html = await resp.text();
const rowMatch = html.match(new RegExp(`<tr[^>]*>.*?<span[^>]*>.*?${icao}.*?<\/span>.*?<\/tr>`, 's'));
if (!rowMatch) return null;
const cells = rowMatch.match(/<td[^>]*>.*?<\/td>/g) || [];
return {
country: cells[1]?.replace(/<[^>]*>/g, '').trim() || null,
registration: cells[2]?.replace(/<[^>]*>/g, '').trim() || null,
aircraft: cells[3]?.replace(/<[^>]*>/g, '').trim() || null,
operator: cells[4]?.replace(/<[^>]*>/g, '').trim() || null,
serialNumber: cells[6]?.replace(/<[^>]*>/g, '').trim() || null,
};
} catch (e) {
return null;
}
}
async function fetchHexDb(icao) {
const resp = await fetchWithTimeout(`https://hexdb.io/api/v1/aircraft/${icao}`);
if (!resp.ok) return null;
const found = await resp.json();
return {
registration: found.Registration || null,
manufacturer: found.Manufacturer || null,
aircraft: found.ICAOTypeCode || null,
model: found.Type || null,
operator: found.RegisteredOwners || null,
};
}
function backfillFromSimilar(aircraft) {
if (!aircraft) return {};
const similar = db.prepare(`
SELECT manufacturer, model, engines, categoryDescription, class
FROM aircraft
WHERE aircraft = ?
AND (manufacturer IS NOT NULL OR model IS NOT NULL OR engines IS NOT NULL)
LIMIT 1
`).get(aircraft);
if (!similar) return {};
return {
manufacturer: similar.manufacturer || null,
model: similar.model || null,
engines: similar.engines || null,
categoryDescription: similar.categoryDescription || null,
class: similar.class || null,
};
}
export async function enrichAircraft(a) {
if (!a.hex) return a;
const row = db.prepare('SELECT * FROM aircraft WHERE icao24 = ?').get(a.hex.toUpperCase());
if (!row) {
const resp = await fetch(`https://hexdb.io/api/v1/aircraft/${a.hex.toUpperCase()}`);
if (resp.ok) {
const found = await resp.json();
const insert = db.prepare(`
INSERT OR IGNORE INTO aircraft (icao24, registration, manufacturer, aircraft, model, operator)
VALUES (@icao24, @registration, @manufacturer, @aircraft, @model, @operator)
`);
const mapped = {
icao24: a.hex.toUpperCase(),
registration: found.Registration || null,
manufacturer: found.Manufacturer || null,
aircraft: found.ICAOTypeCode || null,
model: found.Type || null,
operator: found.RegisteredOwners || null,
};
insert.run(mapped);
return { ...a, ...mapped, type: classifyAircraft(mapped) };
}
return { ...a, type: isIcaoInMilitaryRange(a.hex) ? 'military' : 'unknown' };
const icao = a.hex.toUpperCase();
// 1. Check own DB first
const row = db.prepare('SELECT * FROM aircraft WHERE icao24 = ?').get(icao);
if (row?.manufacturer && row?.model) {
return { ...a, ...row, type: classifyAircraft(row) };
}
return { ...a, ...row, type: classifyAircraft(row) };
// 2. Race the two external sources
let found = await Promise.any([
fetchHexDb(icao),
scrapeHexDatabase(icao),
]).catch(() => null);
// Merge with any partial DB row we already have
const merged = { ...row, ...found };
// 3. Backfill manufacturer/model/engines from similar aircraft type in DB
if (merged.aircraft && (!merged.manufacturer || !merged.model)) {
const similar = backfillFromSimilar(merged.aircraft);
Object.assign(merged, { ...similar, ...merged }); // don't overwrite fetched data
}
// 4. Save back to DB
if (found) {
db.prepare(`
INSERT INTO aircraft (icao24, registration, manufacturer, aircraft, model, operator, country, serialNumber, engines, categoryDescription, class)
VALUES (@icao24, @registration, @manufacturer, @aircraft, @model, @operator, @country, @serialNumber, @engines, @categoryDescription, @class)
ON CONFLICT(icao24) DO UPDATE SET
registration = COALESCE(excluded.registration, registration),
manufacturer = COALESCE(excluded.manufacturer, manufacturer),
aircraft = COALESCE(excluded.aircraft, aircraft),
model = COALESCE(excluded.model, model),
operator = COALESCE(excluded.operator, operator),
country = COALESCE(excluded.country, country),
serialNumber = COALESCE(excluded.serialNumber, serialNumber),
engines = COALESCE(excluded.engines, engines),
categoryDescription = COALESCE(excluded.categoryDescription, categoryDescription),
class = COALESCE(excluded.class, class)
`).run({
icao24: icao,
registration: merged.registration || null,
manufacturer: merged.manufacturer || null,
aircraft: merged.aircraft || null,
model: merged.model || null,
operator: merged.operator || null,
country: merged.country || null,
serialNumber: merged.serialNumber || null,
engines: merged.engines || null,
categoryDescription: merged.categoryDescription || null,
class: merged.class || null,
});
}
const result = { icao24: icao, ...merged };
return { ...a, ...result, type: classifyAircraft(result) };
}
export async function getADSB() {