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:
@@ -275,6 +275,13 @@ enum class StringId : uint8_t {
|
|||||||
// gemeint ist (Preset-Name wie "Zuhause", nicht z.B. ein Ort/Adresse).
|
// gemeint ist (Preset-Name wie "Zuhause", nicht z.B. ein Ort/Adresse).
|
||||||
LOCATION_NAME_HINT,
|
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
|
COUNT
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -229,6 +229,8 @@ static const char* const I18N_DE[] = {
|
|||||||
" abgespeichert. Ab dem nächsten Start verbindet sich das WLAN automatisch.",
|
" abgespeichert. Ab dem nächsten Start verbindet sich das WLAN automatisch.",
|
||||||
|
|
||||||
"z.B. Zuhause, Arbeit, Ferienhaus",
|
"z.B. Zuhause, Arbeit, Ferienhaus",
|
||||||
|
|
||||||
|
"Bodenfahrzeug",
|
||||||
};
|
};
|
||||||
|
|
||||||
static_assert(sizeof(I18N_DE) / sizeof(I18N_DE[0]) == (size_t)StringId::COUNT,
|
static_assert(sizeof(I18N_DE) / sizeof(I18N_DE[0]) == (size_t)StringId::COUNT,
|
||||||
|
|||||||
@@ -230,6 +230,8 @@ static const char* const I18N_EN[] = {
|
|||||||
". From the next boot, WiFi will connect automatically.",
|
". From the next boot, WiFi will connect automatically.",
|
||||||
|
|
||||||
"e.g. Home, Work, Holiday home",
|
"e.g. Home, Work, Holiday home",
|
||||||
|
|
||||||
|
"Ground vehicle",
|
||||||
};
|
};
|
||||||
|
|
||||||
static_assert(sizeof(I18N_EN) / sizeof(I18N_EN[0]) == (size_t)StringId::COUNT,
|
static_assert(sizeof(I18N_EN) / sizeof(I18N_EN[0]) == (size_t)StringId::COUNT,
|
||||||
|
|||||||
@@ -229,6 +229,8 @@ static const char* const I18N_ES[] = {
|
|||||||
". A partir del próximo inicio, el WiFi se conectará automáticamente.",
|
". A partir del próximo inicio, el WiFi se conectará automáticamente.",
|
||||||
|
|
||||||
"ej. Casa, Trabajo, Casa de vacaciones",
|
"ej. Casa, Trabajo, Casa de vacaciones",
|
||||||
|
|
||||||
|
"Vehículo terrestre",
|
||||||
};
|
};
|
||||||
|
|
||||||
static_assert(sizeof(I18N_ES) / sizeof(I18N_ES[0]) == (size_t)StringId::COUNT,
|
static_assert(sizeof(I18N_ES) / sizeof(I18N_ES[0]) == (size_t)StringId::COUNT,
|
||||||
|
|||||||
@@ -229,6 +229,8 @@ static const char* const I18N_FR[] = {
|
|||||||
". À partir du prochain démarrage, le WiFi se connectera automatiquement.",
|
". À partir du prochain démarrage, le WiFi se connectera automatiquement.",
|
||||||
|
|
||||||
"ex. Maison, Travail, Maison de vacances",
|
"ex. Maison, Travail, Maison de vacances",
|
||||||
|
|
||||||
|
"Véhicule au sol",
|
||||||
};
|
};
|
||||||
|
|
||||||
static_assert(sizeof(I18N_FR) / sizeof(I18N_FR[0]) == (size_t)StringId::COUNT,
|
static_assert(sizeof(I18N_FR) / sizeof(I18N_FR[0]) == (size_t)StringId::COUNT,
|
||||||
|
|||||||
@@ -229,6 +229,8 @@ static const char* const I18N_IT[] = {
|
|||||||
". Dal prossimo avvio, il WiFi si connetterà automaticamente.",
|
". Dal prossimo avvio, il WiFi si connetterà automaticamente.",
|
||||||
|
|
||||||
"es. Casa, Lavoro, Casa vacanze",
|
"es. Casa, Lavoro, Casa vacanze",
|
||||||
|
|
||||||
|
"Veicolo a terra",
|
||||||
};
|
};
|
||||||
|
|
||||||
static_assert(sizeof(I18N_IT) / sizeof(I18N_IT[0]) == (size_t)StringId::COUNT,
|
static_assert(sizeof(I18N_IT) / sizeof(I18N_IT[0]) == (size_t)StringId::COUNT,
|
||||||
|
|||||||
@@ -229,6 +229,8 @@ static const char* const I18N_TR[] = {
|
|||||||
". Bir sonraki başlatmadan itibaren WiFi otomatik olarak bağlanacak.",
|
". Bir sonraki başlatmadan itibaren WiFi otomatik olarak bağlanacak.",
|
||||||
|
|
||||||
"örn. Ev, İş, Tatil evi",
|
"örn. Ev, İş, Tatil evi",
|
||||||
|
|
||||||
|
"Kara aracı",
|
||||||
};
|
};
|
||||||
|
|
||||||
static_assert(sizeof(I18N_TR) / sizeof(I18N_TR[0]) == (size_t)StringId::COUNT,
|
static_assert(sizeof(I18N_TR) / sizeof(I18N_TR[0]) == (size_t)StringId::COUNT,
|
||||||
|
|||||||
@@ -33,10 +33,20 @@ namespace {
|
|||||||
};
|
};
|
||||||
|
|
||||||
constexpr int16_t INFO_BAR_H = 64;
|
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 computeLayout(int16_t top) {
|
||||||
Layout L;
|
Layout L;
|
||||||
L.infoTop = Config::SCREEN_HEIGHT - INFO_BAR_H;
|
L.infoTop = Config::SCREEN_HEIGHT - infoBarHeight();
|
||||||
constexpr int16_t TOP_LABEL_MARGIN = 10;
|
constexpr int16_t TOP_LABEL_MARGIN = 10;
|
||||||
int16_t maxRadiusByWidth = Config::SCREEN_WIDTH / 2 - 6;
|
int16_t maxRadiusByWidth = Config::SCREEN_WIDTH / 2 - 6;
|
||||||
int16_t maxRadiusByHeight = (L.infoTop - top - TOP_LABEL_MARGIN) / 2 - 6;
|
int16_t maxRadiusByHeight = (L.infoTop - top - TOP_LABEL_MARGIN) / 2 - 6;
|
||||||
@@ -59,6 +69,7 @@ namespace {
|
|||||||
float distanceKm;
|
float distanceKm;
|
||||||
bool isEmergency;
|
bool isEmergency;
|
||||||
bool isWatched;
|
bool isWatched;
|
||||||
|
bool isGroundVehicle;
|
||||||
};
|
};
|
||||||
constexpr uint8_t MAX_HIT_POINTS = Config::MAX_TRACKED_AIRCRAFT;
|
constexpr uint8_t MAX_HIT_POINTS = Config::MAX_TRACKED_AIRCRAFT;
|
||||||
HitPoint hitPoints[MAX_HIT_POINTS];
|
HitPoint hitPoints[MAX_HIT_POINTS];
|
||||||
@@ -228,6 +239,17 @@ namespace {
|
|||||||
return nightDimActiveNow() ? gfx.color565(0, 110, 0) : TFT_GREEN;
|
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)
|
// Zeigt die PEILUNG (nicht zu verwechseln mit der Flugrichtung/heading)
|
||||||
// zum aktuell ausgewaehlten Flugzeug: eine duenne, gepunktete Linie vom
|
// zum aktuell ausgewaehlten Flugzeug: eine duenne, gepunktete Linie vom
|
||||||
// Radar-Zentrum zum Kreisrand in Richtung bearingDeg, plus die Gradzahl
|
// Radar-Zentrum zum Kreisrand in Richtung bearingDeg, plus die Gradzahl
|
||||||
@@ -302,6 +324,15 @@ namespace {
|
|||||||
gfx.drawLine(tipX, tipY, w2x, w2y, color);
|
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) {
|
void printLineTruncated(TFT_eSPI& gfx, int16_t x, int16_t y, int16_t maxWidth, const String& text) {
|
||||||
String s = text;
|
String s = text;
|
||||||
if (gfx.textWidth(s) > maxWidth) {
|
if (gfx.textWidth(s) > maxWidth) {
|
||||||
@@ -358,6 +389,18 @@ namespace {
|
|||||||
gfx.setCursor(x0 + 7, y);
|
gfx.setCursor(x0 + 7, y);
|
||||||
gfx.print(items[i].label);
|
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) {
|
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::PolarCoord polar{a.distanceKm, a.bearingDeg};
|
||||||
RadarMath::ScreenPoint pt = RadarMath::toScreen(polar, L.cx, L.cy, L.radius, rangeKm);
|
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 isSelected = selectedHex[0] && strcmp(a.hex, selectedHex) == 0;
|
||||||
bool isEmergency = SettingsStore::emergencyAlertEnabled() && isEmergencySquawk(a.squawk);
|
bool isEmergency = SettingsStore::emergencyAlertEnabled() && isEmergencySquawk(a.squawk);
|
||||||
bool isWatched = SettingsStore::watchlistAlertEnabled() && AircraftWatchlist::isWatched(a.callsign);
|
bool isWatched = SettingsStore::watchlistAlertEnabled() && AircraftWatchlist::isWatched(a.callsign);
|
||||||
@@ -640,7 +688,11 @@ void render(TFT_eSPI& tft, int16_t top) {
|
|||||||
selected = a;
|
selected = a;
|
||||||
drawBearingIndicator(tft, L, a.bearingDeg);
|
drawBearingIndicator(tft, L, a.bearingDeg);
|
||||||
}
|
}
|
||||||
|
if (isGroundVehicle) {
|
||||||
|
drawGroundVehicleMarker(tft, pt.x, pt.y, color);
|
||||||
|
} else {
|
||||||
drawAircraftMarker(tft, pt.x, pt.y, a.headingDeg, color);
|
drawAircraftMarker(tft, pt.x, pt.y, a.headingDeg, color);
|
||||||
|
}
|
||||||
|
|
||||||
if (isEmergency) {
|
if (isEmergency) {
|
||||||
tft.drawCircle(pt.x, pt.y, 12, TFT_RED);
|
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].distanceKm = a.distanceKm;
|
||||||
hitPoints[i].isEmergency = isEmergency;
|
hitPoints[i].isEmergency = isEmergency;
|
||||||
hitPoints[i].isWatched = isWatched;
|
hitPoints[i].isWatched = isWatched;
|
||||||
|
hitPoints[i].isGroundVehicle = isGroundVehicle;
|
||||||
strncpy(hitPoints[i].hex, a.hex, sizeof(hitPoints[i].hex) - 1);
|
strncpy(hitPoints[i].hex, a.hex, sizeof(hitPoints[i].hex) - 1);
|
||||||
strncpy(hitPoints[i].callsign, a.callsign, sizeof(hitPoints[i].callsign) - 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
|
// einfach in Schwarz nachgezeichnet - das macht sie pixelgenau
|
||||||
// wieder unsichtbar, ohne irgendetwas ausserhalb dieser Formen zu
|
// wieder unsichtbar, ohne irgendetwas ausserhalb dieser Formen zu
|
||||||
// beruehren.
|
// beruehren.
|
||||||
|
if (hp.isGroundVehicle) {
|
||||||
|
drawGroundVehicleMarker(tft, hp.x, hp.y, TFT_BLACK);
|
||||||
|
} else {
|
||||||
drawAircraftMarker(tft, hp.x, hp.y, hp.headingDeg, TFT_BLACK);
|
drawAircraftMarker(tft, hp.x, hp.y, hp.headingDeg, TFT_BLACK);
|
||||||
|
}
|
||||||
if (hp.isEmergency || hp.isWatched) {
|
if (hp.isEmergency || hp.isWatched) {
|
||||||
tft.drawCircle(hp.x, hp.y, 12, TFT_BLACK);
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hp.isGroundVehicle) {
|
||||||
|
drawGroundVehicleMarker(tft, hp.x, hp.y, hp.color);
|
||||||
|
} else {
|
||||||
drawAircraftMarker(tft, hp.x, hp.y, hp.headingDeg, hp.color);
|
drawAircraftMarker(tft, hp.x, hp.y, hp.headingDeg, hp.color);
|
||||||
|
}
|
||||||
|
|
||||||
if (hp.isEmergency) {
|
if (hp.isEmergency) {
|
||||||
tft.drawCircle(hp.x, hp.y, 12, TFT_RED);
|
tft.drawCircle(hp.x, hp.y, 12, TFT_RED);
|
||||||
|
|||||||
Reference in New Issue
Block a user