Add distinct marker and legend entry for ground vehicles on radar

Ground vehicles (ADS-B emitter category "C*") now render as filled
blue squares instead of the normal circle+arrowhead aircraft marker,
using a fixed color independent of altitude (they're practically
always at 0ft, which would otherwise collide with the low-altitude
green). A second legend line explains the marker, but only when
ground vehicles are actually visible (Flight Options > "Hide ground
vehicles" off) - the info bar height, and therefore the radar circle
size, is otherwise unchanged from before.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Eiswolf-BG
2026-08-11 01:47:07 +02:00
parent aa07acebbf
commit 9f5da6e8b5
8 changed files with 85 additions and 5 deletions

View File

@@ -33,10 +33,20 @@ namespace {
};
constexpr int16_t INFO_BAR_H = 64;
// Zusaetzliche Hoehe fuer die zweite Legenden-Zeile (Bodenfahrzeug-
// Marker-Erklaerung), nur reserviert, wenn Bodenfahrzeuge tatsaechlich
// angezeigt werden (SettingsStore::hideGroundVehicles() == false) -
// ansonsten bleibt die Infoleiste genau so hoch wie bisher und der
// Radarkreis behaelt seine gewohnte Groesse.
constexpr int16_t INFO_BAR_GROUND_ROW_H = 18;
int16_t infoBarHeight() {
return SettingsStore::hideGroundVehicles() ? INFO_BAR_H : (int16_t)(INFO_BAR_H + INFO_BAR_GROUND_ROW_H);
}
Layout computeLayout(int16_t top) {
Layout L;
L.infoTop = Config::SCREEN_HEIGHT - INFO_BAR_H;
L.infoTop = Config::SCREEN_HEIGHT - infoBarHeight();
constexpr int16_t TOP_LABEL_MARGIN = 10;
int16_t maxRadiusByWidth = Config::SCREEN_WIDTH / 2 - 6;
int16_t maxRadiusByHeight = (L.infoTop - top - TOP_LABEL_MARGIN) / 2 - 6;
@@ -59,6 +69,7 @@ namespace {
float distanceKm;
bool isEmergency;
bool isWatched;
bool isGroundVehicle;
};
constexpr uint8_t MAX_HIT_POINTS = Config::MAX_TRACKED_AIRCRAFT;
HitPoint hitPoints[MAX_HIT_POINTS];
@@ -228,6 +239,17 @@ namespace {
return nightDimActiveNow() ? gfx.color565(0, 110, 0) : TFT_GREEN;
}
// Feste Blau-Farbe fuer Bodenfahrzeug-Marker (ADS-B-Emitter-Kategorie
// "C*", z.B. Flughafen-Fahrzeuge) - bewusst unabhaengig von
// colorForAltitude(), da Bodenfahrzeuge praktisch immer auf 0ft stehen
// und sonst dieselbe Farbe wie niedrig fliegende Flugzeuge haetten,
// was die optische Unterscheidung (Quadrat vs. Kreis) unnoetig
// erschweren wuerde. Bei Nachtdimmung gedaempft, gleiches Muster wie
// colorForAltitude().
uint16_t colorForGroundVehicle(TFT_eSPI& gfx) {
return nightDimActiveNow() ? gfx.color565(0, 0, 160) : TFT_BLUE;
}
// Zeigt die PEILUNG (nicht zu verwechseln mit der Flugrichtung/heading)
// zum aktuell ausgewaehlten Flugzeug: eine duenne, gepunktete Linie vom
// Radar-Zentrum zum Kreisrand in Richtung bearingDeg, plus die Gradzahl
@@ -302,6 +324,15 @@ namespace {
gfx.drawLine(tipX, tipY, w2x, w2y, color);
}
// Eigener Marker fuer Bodenfahrzeuge (ADS-B-Kategorie "C*") - ein
// gefuelltes Quadrat statt Kreis+Pfeilkopf, damit sie auf dem Radar
// klar von echten Flugzeugen zu unterscheiden sind (Heading/Kurs ist
// bei Bodenfahrzeugen ausserdem meist nicht aussagekraeftig).
void drawGroundVehicleMarker(TFT_eSPI& gfx, int16_t x, int16_t y, uint16_t color) {
constexpr int16_t HALF = 4;
gfx.fillRect((int16_t)(x - HALF), (int16_t)(y - HALF), (int16_t)(2 * HALF), (int16_t)(2 * HALF), color);
}
void printLineTruncated(TFT_eSPI& gfx, int16_t x, int16_t y, int16_t maxWidth, const String& text) {
String s = text;
if (gfx.textWidth(s) > maxWidth) {
@@ -358,6 +389,18 @@ namespace {
gfx.setCursor(x0 + 7, y);
gfx.print(items[i].label);
}
// Zweite Legenden-Zeile fuer die blauen Bodenfahrzeug-Quadrate - nur
// wenn sie ueberhaupt sichtbar sind (Flugoptionen >
// "Bodenfahrzeuge ausblenden" == aus). infoBarHeight() reserviert
// den dafuer noetigen zusaetzlichen Platz nur in diesem Fall, siehe
// computeLayout().
if (!SettingsStore::hideGroundVehicles()) {
int16_t gy = (int16_t)(y + INFO_BAR_GROUND_ROW_H);
gfx.fillRect(3, (int16_t)(gy - 8), 6, 6, colorForGroundVehicle(gfx));
gfx.setCursor(13, gy);
gfx.print(I18n::t(StringId::LEGEND_GROUND_VEHICLE));
}
}
void drawWorldMap(TFT_eSPI& gfx, const Layout& L) {
@@ -629,7 +672,12 @@ void render(TFT_eSPI& tft, int16_t top) {
RadarMath::PolarCoord polar{a.distanceKm, a.bearingDeg};
RadarMath::ScreenPoint pt = RadarMath::toScreen(polar, L.cx, L.cy, L.radius, rangeKm);
uint16_t color = colorForAltitude(tft, a.altBaroFt);
// ADS-B-Emitter-Kategorie "C*" = Bodenfahrzeug (nur ueberhaupt
// sichtbar, wenn "Bodenfahrzeuge ausblenden" aus ist, siehe Filter
// oben) - eigene Farbe/Marker statt der hoehenbasierten Flugzeug-
// Darstellung, siehe drawGroundVehicleMarker()/colorForGroundVehicle().
bool isGroundVehicle = a.category[0] == 'C';
uint16_t color = isGroundVehicle ? colorForGroundVehicle(tft) : colorForAltitude(tft, a.altBaroFt);
bool isSelected = selectedHex[0] && strcmp(a.hex, selectedHex) == 0;
bool isEmergency = SettingsStore::emergencyAlertEnabled() && isEmergencySquawk(a.squawk);
bool isWatched = SettingsStore::watchlistAlertEnabled() && AircraftWatchlist::isWatched(a.callsign);
@@ -640,7 +688,11 @@ void render(TFT_eSPI& tft, int16_t top) {
selected = a;
drawBearingIndicator(tft, L, a.bearingDeg);
}
drawAircraftMarker(tft, pt.x, pt.y, a.headingDeg, color);
if (isGroundVehicle) {
drawGroundVehicleMarker(tft, pt.x, pt.y, color);
} else {
drawAircraftMarker(tft, pt.x, pt.y, a.headingDeg, color);
}
if (isEmergency) {
tft.drawCircle(pt.x, pt.y, 12, TFT_RED);
@@ -662,6 +714,7 @@ void render(TFT_eSPI& tft, int16_t top) {
hitPoints[i].distanceKm = a.distanceKm;
hitPoints[i].isEmergency = isEmergency;
hitPoints[i].isWatched = isWatched;
hitPoints[i].isGroundVehicle = isGroundVehicle;
strncpy(hitPoints[i].hex, a.hex, sizeof(hitPoints[i].hex) - 1);
strncpy(hitPoints[i].callsign, a.callsign, sizeof(hitPoints[i].callsign) - 1);
}
@@ -747,7 +800,11 @@ void tick(TFT_eSPI& tft, int16_t top, uint32_t deltaMs) {
// einfach in Schwarz nachgezeichnet - das macht sie pixelgenau
// wieder unsichtbar, ohne irgendetwas ausserhalb dieser Formen zu
// beruehren.
drawAircraftMarker(tft, hp.x, hp.y, hp.headingDeg, TFT_BLACK);
if (hp.isGroundVehicle) {
drawGroundVehicleMarker(tft, hp.x, hp.y, TFT_BLACK);
} else {
drawAircraftMarker(tft, hp.x, hp.y, hp.headingDeg, TFT_BLACK);
}
if (hp.isEmergency || hp.isWatched) {
tft.drawCircle(hp.x, hp.y, 12, TFT_BLACK);
}
@@ -759,7 +816,11 @@ void tick(TFT_eSPI& tft, int16_t top, uint32_t deltaMs) {
continue;
}
drawAircraftMarker(tft, hp.x, hp.y, hp.headingDeg, hp.color);
if (hp.isGroundVehicle) {
drawGroundVehicleMarker(tft, hp.x, hp.y, hp.color);
} else {
drawAircraftMarker(tft, hp.x, hp.y, hp.headingDeg, hp.color);
}
if (hp.isEmergency) {
tft.drawCircle(hp.x, hp.y, 12, TFT_RED);