From 9f5da6e8b5cca3962f0399ad3195bc9dd67b3142 Mon Sep 17 00:00:00 2001 From: Eiswolf-BG <37225519+Eiswolf-BG@users.noreply.github.com> Date: Tue, 11 Aug 2026 01:47:07 +0200 Subject: [PATCH] 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 --- src/i18n.h | 7 +++++ src/i18n_de.h | 2 ++ src/i18n_en.h | 2 ++ src/i18n_es.h | 2 ++ src/i18n_fr.h | 2 ++ src/i18n_it.h | 2 ++ src/i18n_tr.h | 2 ++ src/radar_screen.cpp | 71 ++++++++++++++++++++++++++++++++++++++++---- 8 files changed, 85 insertions(+), 5 deletions(-) diff --git a/src/i18n.h b/src/i18n.h index 0695873..c7f2de7 100644 --- a/src/i18n.h +++ b/src/i18n.h @@ -275,6 +275,13 @@ enum class StringId : uint8_t { // gemeint ist (Preset-Name wie "Zuhause", nicht z.B. ein Ort/Adresse). LOCATION_NAME_HINT, + // Legenden-Eintrag auf dem Radar-Screen (radar_screen.cpp::drawLegend) + // fuer die blauen Quadrat-Marker der Bodenfahrzeuge - nur sichtbar, + // wenn "Bodenfahrzeuge ausblenden" AUS ist (Flugoptionen), da die + // Fahrzeuge dann als eigene Legenden-Zeile unter den drei + // Hoehen-Farbstufen erscheinen. + LEGEND_GROUND_VEHICLE, + COUNT }; diff --git a/src/i18n_de.h b/src/i18n_de.h index 9616bd3..6a82e41 100644 --- a/src/i18n_de.h +++ b/src/i18n_de.h @@ -229,6 +229,8 @@ static const char* const I18N_DE[] = { " abgespeichert. Ab dem nächsten Start verbindet sich das WLAN automatisch.", "z.B. Zuhause, Arbeit, Ferienhaus", + + "Bodenfahrzeug", }; static_assert(sizeof(I18N_DE) / sizeof(I18N_DE[0]) == (size_t)StringId::COUNT, diff --git a/src/i18n_en.h b/src/i18n_en.h index bbd06fd..203fb50 100644 --- a/src/i18n_en.h +++ b/src/i18n_en.h @@ -230,6 +230,8 @@ static const char* const I18N_EN[] = { ". From the next boot, WiFi will connect automatically.", "e.g. Home, Work, Holiday home", + + "Ground vehicle", }; static_assert(sizeof(I18N_EN) / sizeof(I18N_EN[0]) == (size_t)StringId::COUNT, diff --git a/src/i18n_es.h b/src/i18n_es.h index ad9f436..cb3225c 100644 --- a/src/i18n_es.h +++ b/src/i18n_es.h @@ -229,6 +229,8 @@ static const char* const I18N_ES[] = { ". A partir del próximo inicio, el WiFi se conectará automáticamente.", "ej. Casa, Trabajo, Casa de vacaciones", + + "Vehículo terrestre", }; static_assert(sizeof(I18N_ES) / sizeof(I18N_ES[0]) == (size_t)StringId::COUNT, diff --git a/src/i18n_fr.h b/src/i18n_fr.h index 96fa2e4..6bb8a32 100644 --- a/src/i18n_fr.h +++ b/src/i18n_fr.h @@ -229,6 +229,8 @@ static const char* const I18N_FR[] = { ". À partir du prochain démarrage, le WiFi se connectera automatiquement.", "ex. Maison, Travail, Maison de vacances", + + "Véhicule au sol", }; static_assert(sizeof(I18N_FR) / sizeof(I18N_FR[0]) == (size_t)StringId::COUNT, diff --git a/src/i18n_it.h b/src/i18n_it.h index 3074e3f..29ab84f 100644 --- a/src/i18n_it.h +++ b/src/i18n_it.h @@ -229,6 +229,8 @@ static const char* const I18N_IT[] = { ". Dal prossimo avvio, il WiFi si connetterà automaticamente.", "es. Casa, Lavoro, Casa vacanze", + + "Veicolo a terra", }; static_assert(sizeof(I18N_IT) / sizeof(I18N_IT[0]) == (size_t)StringId::COUNT, diff --git a/src/i18n_tr.h b/src/i18n_tr.h index bade050..dc40287 100644 --- a/src/i18n_tr.h +++ b/src/i18n_tr.h @@ -229,6 +229,8 @@ static const char* const I18N_TR[] = { ". Bir sonraki başlatmadan itibaren WiFi otomatik olarak bağlanacak.", "örn. Ev, İş, Tatil evi", + + "Kara aracı", }; static_assert(sizeof(I18N_TR) / sizeof(I18N_TR[0]) == (size_t)StringId::COUNT, diff --git a/src/radar_screen.cpp b/src/radar_screen.cpp index 0152a80..863bf88 100644 --- a/src/radar_screen.cpp +++ b/src/radar_screen.cpp @@ -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);