Skip to content

Commit 95cf113

Browse files
authored
Merge pull request #71 from MrSuttonmann/silhouette-fallback-chain
Extend silhouette lookup with tar1090's full fallback chain
2 parents e538200 + ff152d0 commit 95cf113

17 files changed

Lines changed: 350 additions & 21 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ jobs:
130130
# a complete page. Python interpreter is preinstalled on GitHub runners.
131131
env:
132132
TAR1090_SHAPES_DEST: ${{ github.workspace }}/app/static/tar1090_shapes.js
133+
TAR1090_TYPES_DEST: ${{ github.workspace }}/app/static/icao_aircraft_types.js
133134
run: python3 scripts/fetch_plane_shapes.py
134135

135136
- name: Publish backend for Playwright

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ app/aircraft_db.csv.gz
4242
app/airports.csv
4343
app/navaids.csv
4444
app/static/tar1090_shapes.js
45+
app/static/icao_aircraft_types.js
4546

4647
# Editors / OS
4748
.vscode/

app/static/detail_panel.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -884,7 +884,8 @@ export function openDetailPanel(icao) {
884884
if (entry) {
885885
entry.marker.setIcon(
886886
planeIcon(a.track, altColor(a.altitude), true, !!a.emergency,
887-
!!a.position_source && a.position_source !== 'adsb', a.type_icao),
887+
!!a.position_source && a.position_source !== 'adsb',
888+
a.type_icao, a.category, a.category_set),
888889
);
889890
}
890891
document.querySelectorAll('.ac-item').forEach(el => {
@@ -924,7 +925,8 @@ export function closeDetailPanel() {
924925
const a = entry.data;
925926
entry.marker.setIcon(
926927
planeIcon(a.track, altColor(a.altitude), false, !!a.emergency,
927-
!!a.position_source && a.position_source !== 'adsb', a.type_icao),
928+
!!a.position_source && a.position_source !== 'adsb',
929+
a.type_icao, a.category, a.category_set),
928930
);
929931
}
930932
}

app/static/icons.js

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,45 @@
1-
// Marker icon rendering. Prefers tar1090's per-type silhouette when the
2-
// bundle is loaded and the aircraft's ICAO type code has a match;
3-
// otherwise falls back to a generic triangular arrow.
1+
// Marker icon rendering. Prefers tar1090's per-type silhouette via the
2+
// four-tier fallback in `resolveSilhouette` (designator → description →
3+
// category); falls back to a generic triangular arrow when none match
4+
// or the bundles haven't loaded yet.
45
//
5-
// tar1090_shapes.js is loaded asynchronously (it's a big static file
6-
// and failing to fetch it shouldn't stop the app from running). Until
7-
// it resolves we render the generic arrow for everyone; once it does,
8-
// subsequent planeIcon() calls light up.
6+
// tar1090_shapes.js and icao_aircraft_types.js are loaded asynchronously
7+
// (they're big static files and failing to fetch them shouldn't stop the
8+
// app from running). Until they resolve we render the generic arrow for
9+
// everyone; once they do, subsequent planeIcon() calls light up.
10+
11+
import { resolveSilhouette } from './silhouette.js';
912

1013
let tar1090Shapes = null;
1114
let tar1090TypeIcons = null;
15+
let tar1090TypeDescIcons = null;
16+
let tar1090CategoryIcons = null;
17+
let typeDesignatorMeta = null;
1218

1319
import('./tar1090_shapes.js').then(mod => {
1420
tar1090Shapes = mod.shapes;
1521
tar1090TypeIcons = mod.TypeDesignatorIcons;
22+
tar1090TypeDescIcons = mod.TypeDescriptionIcons;
23+
tar1090CategoryIcons = mod.CategoryIcons;
1624
}).catch(e => {
1725
console.warn('tar1090 shapes unavailable — using generic arrow', e);
1826
});
1927

20-
function tar1090ShapeFor(typeIcao) {
21-
if (!tar1090Shapes || !tar1090TypeIcons || !typeIcao) return null;
22-
const entry = tar1090TypeIcons[typeIcao.toUpperCase()];
28+
import('./icao_aircraft_types.js').then(mod => {
29+
typeDesignatorMeta = mod.TypeDesignatorMeta;
30+
}).catch(e => {
31+
console.warn('ICAO type metadata unavailable — silhouette fallback narrowed', e);
32+
});
33+
34+
function tar1090ShapeFor(typeIcao, category, categorySet) {
35+
if (!tar1090Shapes) return null;
36+
const entry = resolveSilhouette({
37+
typeIcao, category, categorySet,
38+
typeDesignatorIcons: tar1090TypeIcons,
39+
typeDescriptionIcons: tar1090TypeDescIcons,
40+
categoryIcons: tar1090CategoryIcons,
41+
typeDesignatorMeta,
42+
});
2343
if (!entry) return null;
2444
const [shapeName, scaleFactor] = entry;
2545
const shape = tar1090Shapes[shapeName];
@@ -86,8 +106,8 @@ function tar1090Icon(track, color, selected, emergency, relayed, shape, scaleFac
86106
const GENERIC_ARROW_PATH = 'M0,-10 L7,8 L0,4 L-7,8 Z';
87107
const GENERIC_ARROW_SIZE = 26;
88108

89-
export function planeIcon(track, color, selected, emergency, relayed, typeIcao) {
90-
const tar = tar1090ShapeFor(typeIcao);
109+
export function planeIcon(track, color, selected, emergency, relayed, typeIcao, category, categorySet) {
110+
const tar = tar1090ShapeFor(typeIcao, category, categorySet);
91111
if (tar) return tar1090Icon(track, color, selected, emergency, relayed, tar.shape, tar.scaleFactor);
92112

93113
const rot = track == null ? 0 : track;

app/static/silhouette.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Pure resolver for the tar1090 silhouette fallback chain. Lives apart
2+
// from icons.js so it can be unit-tested without a Leaflet stub.
3+
//
4+
// `resolveSilhouette` mirrors tar1090's getBaseMarker fallback (minus the
5+
// halloween / ATC-style branches we don't carry). Given an aircraft's
6+
// type designator, ADS-B category + set letter, and the four lookup
7+
// tables, returns the [shapeName, scaleFactor] entry that the marker
8+
// renderer should use, or null when nothing matches and the caller
9+
// should draw the generic arrow.
10+
//
11+
// Tables (all four are optional — pass null when one isn't loaded yet):
12+
// - typeDesignatorIcons: map<TYPE, [shape, scale]> (e.g. "B738")
13+
// - typeDescriptionIcons: map<DESC | DESC-WTC | KIND, [shape, scale]>
14+
// (e.g. "L1P", "L2J-M", "H")
15+
// - categoryIcons: map<SET+CAT, [shape, scale]> (e.g. "A1")
16+
// - typeDesignatorMeta: map<TYPE, { desc, wtc }> (e.g. C172 -> { desc:"L1P", wtc:"L" })
17+
18+
export function resolveSilhouette({
19+
typeIcao,
20+
category,
21+
categorySet,
22+
typeDesignatorIcons,
23+
typeDescriptionIcons,
24+
categoryIcons,
25+
typeDesignatorMeta,
26+
}) {
27+
// 1. Most specific: ICAO type designator.
28+
if (typeDesignatorIcons && typeIcao) {
29+
const entry = typeDesignatorIcons[typeIcao.toUpperCase()];
30+
if (entry) return entry;
31+
}
32+
// 2. ICAO type description ("L1P", "L2J", "H1T", ...) ± wake turbulence.
33+
if (typeDescriptionIcons && typeDesignatorMeta && typeIcao) {
34+
const meta = typeDesignatorMeta[typeIcao.toUpperCase()];
35+
const desc = meta?.desc;
36+
const wtc = meta?.wtc;
37+
if (desc && desc.length === 3) {
38+
if (wtc && wtc.length === 1) {
39+
const withWtc = typeDescriptionIcons[`${desc}-${wtc}`];
40+
if (withWtc) return withWtc;
41+
}
42+
const bare = typeDescriptionIcons[desc];
43+
if (bare) return bare;
44+
const basic = typeDescriptionIcons[desc.charAt(0)];
45+
if (basic) return basic;
46+
}
47+
}
48+
// 3. ADS-B emitter category. The set letter ("A"/"B"/"C") came from the
49+
// BDS-08 typecode in the backend; default to "A" when the backend
50+
// didn't capture it (older snapshots, peer aircraft from a pre-field
51+
// build) since A is by far the most common set.
52+
if (categoryIcons && category != null && category > 0) {
53+
const setLetter = (categorySet || 'A').toString().toUpperCase();
54+
const entry = categoryIcons[`${setLetter}${category}`];
55+
if (entry) return entry;
56+
}
57+
return null;
58+
}

app/static/update_loop.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,10 @@ export function update(snap) {
8989
// Any non-direct source — MLAT / TIS-B / ADS-R — gets the same
9090
// dashed amber marker. The detail panel chip distinguishes them.
9191
const isRelayed = a.position_source && a.position_source !== 'adsb';
92-
const iconFp = `${altBand}|${isSelected ? 1 : 0}|${a.emergency ? 1 : 0}|${isRelayed ? 1 : 0}|${a.type_icao || ''}`;
92+
const iconFp = `${altBand}|${isSelected ? 1 : 0}|${a.emergency ? 1 : 0}|${isRelayed ? 1 : 0}|${a.type_icao || ''}|${a.category || ''}${a.category_set || ''}`;
9393
let entry = state.aircraft.get(a.icao);
9494
if (!entry) {
95-
const icon = planeIcon(a.track, color, isSelected, !!a.emergency, isRelayed, a.type_icao);
95+
const icon = planeIcon(a.track, color, isSelected, !!a.emergency, isRelayed, a.type_icao, a.category, a.category_set);
9696
const marker = L.marker([a.lat, a.lon], { icon }).addTo(state.map);
9797
const trail = L.layerGroup().addTo(state.map);
9898
marker.on('click', () => selectAircraft(a.icao));
@@ -140,7 +140,7 @@ export function update(snap) {
140140
// updates, rotate the existing element in place.
141141
if (entry.iconFp !== iconFp) {
142142
entry.marker.setIcon(
143-
planeIcon(a.track, color, isSelected, !!a.emergency, isRelayed, a.type_icao)
143+
planeIcon(a.track, color, isSelected, !!a.emergency, isRelayed, a.type_icao, a.category, a.category_set)
144144
);
145145
entry.iconFp = iconFp;
146146
entry.lastTrack = a.track;

dotnet/Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ COPY app/static /out/static
9393
COPY scripts/fetch_plane_shapes.py /tmp/fetch_plane_shapes.py
9494
RUN echo "data fetch ${DATA_CACHEBUST}" \
9595
&& TAR1090_SHAPES_DEST=/out/static/tar1090_shapes.js \
96+
TAR1090_TYPES_DEST=/out/static/icao_aircraft_types.js \
9697
python3 /tmp/fetch_plane_shapes.py
9798

9899
# esbuild is a standalone Go binary; we pull it straight from npm's

dotnet/src/FlightJar.Api/Hosting/RegistryWorker.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,8 @@ private RegistrySnapshot EnrichSnapshot(RegistrySnapshot snap)
406406
string? operatorCountry = null;
407407
string? countryIso = null;
408408
string? manufacturer = null;
409+
string? typeIcao = ac.TypeIcao;
410+
string? typeLong = ac.TypeLong;
409411

410412
if (_adsbdb is not null)
411413
{
@@ -430,6 +432,12 @@ private RegistrySnapshot EnrichSnapshot(RegistrySnapshot snap)
430432
operatorCountry = acCached.Data.OperatorCountry;
431433
countryIso = acCached.Data.OperatorCountryIso;
432434
manufacturer = acCached.Data.Manufacturer;
435+
// Tar1090-db is the primary source for type designator
436+
// because it's the most consistent (uppercase ICAO codes);
437+
// adsbdb fills the gap for tails it doesn't ship — recent
438+
// registrations, GA, military.
439+
typeIcao ??= acCached.Data.IcaoType;
440+
typeLong ??= acCached.Data.Type;
433441
}
434442
else if (!acCached.Known)
435443
{
@@ -480,6 +488,8 @@ private RegistrySnapshot EnrichSnapshot(RegistrySnapshot snap)
480488

481489
aircraft.Add(ac with
482490
{
491+
TypeIcao = typeIcao,
492+
TypeLong = typeLong,
483493
Origin = origin,
484494
Destination = destination,
485495
OriginInfo = originInfo,

dotnet/src/FlightJar.Core/State/Aircraft.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@ public sealed class Aircraft
1212
public string? Callsign { get; set; }
1313
public int? Category { get; set; }
1414

15+
/// <summary>ADS-B emitter category set letter — 'A' / 'B' / 'C' / 'D',
16+
/// derived from the BDS-08 typecode that delivered <see cref="Category"/>.
17+
/// Pairs with the 3-bit subcategory: e.g. typecode 4 + subcategory 1
18+
/// → "A1" (Light). Needed by the frontend silhouette fallback to
19+
/// distinguish Set B (gliders, balloons, UAVs) and Set C (surface
20+
/// vehicles) from the default Set A.</summary>
21+
public char? CategorySet { get; set; }
22+
1523
public double? Lat { get; set; }
1624
public double? Lon { get; set; }
1725
public int? AltitudeBaro { get; set; }

dotnet/src/FlightJar.Core/State/AircraftRegistry.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,19 @@ private bool IngestAdsb(DecodedMessage r, double now, long? mlatTicks)
157157
if (r.Category is int cat)
158158
{
159159
ac.Category = cat;
160+
// Typecode 1-4 maps to set D/C/B/A. Set A is "powered
161+
// aircraft" (the vast majority of ADS-B traffic); Set B
162+
// covers gliders / balloons / UAVs; Set C covers surface
163+
// vehicles. The frontend uses the set letter to pick the
164+
// right silhouette when no specific designator matches.
165+
ac.CategorySet = tc switch
166+
{
167+
4 => 'A',
168+
3 => 'B',
169+
2 => 'C',
170+
1 => 'D',
171+
_ => ac.CategorySet,
172+
};
160173
}
161174
}
162175
else if (tc >= 5 && tc <= 8)
@@ -623,6 +636,7 @@ public RegistrySnapshot Snapshot(double? now = null)
623636
Icao = ac.Icao,
624637
Callsign = ac.Callsign,
625638
Category = ac.Category,
639+
CategorySet = ac.CategorySet?.ToString(),
626640
Registration = dbInfo?.Registration,
627641
TypeIcao = dbInfo?.TypeIcao,
628642
TypeLong = dbInfo?.TypeLong,

0 commit comments

Comments
 (0)