Add route filter screen with airport name lookup

This commit is contained in:
2026-08-15 22:09:09 +02:00
parent d89fba5200
commit aae8890942
12 changed files with 441 additions and 20 deletions

View File

@@ -45,9 +45,32 @@ Nearest findNearest(double lat, double lon) {
strncpy(result.name, name.c_str(), sizeof(result.name) - 1);
}
}
f.close();
f.close();
return result;
}
String findName(const char* icao) {
if (!SdStorage::isMounted() || !icao[0]) return String();
File f = SD.open(Config::SD_AIRPORTS_CSV);
if (!f) return String();
f.readStringUntil('\n'); // skip header
while (f.available()) {
String line = f.readStringUntil('\n');
line.trim();
if (line.length() == 0) continue;
int c1 = line.indexOf(',');
if (c1 < 0) continue;
String fileIcao = line.substring(0, c1);
if (fileIcao.equalsIgnoreCase(icao)) {
int c2 = line.indexOf(',', c1 + 1);
f.close();
if (c2 < 0) return String();
return line.substring(c1 + 1, c2);
}
}
f.close();
return String();
}
}

View File

@@ -13,5 +13,9 @@ namespace AirportLookup {
double lat = 0;
double lon = 0;
};
Nearest findNearest(double lat, double lon);
Nearest findNearest(double lat, double lon);
// Looks up an airport name by ICAO code (e.g. "EDDF" -> "Frankfurt Main")
// from the airports CSV on the SD card. Returns empty string if not found.
String findName(const char* icao);
}

View File

@@ -352,7 +352,10 @@ enum class StringId : uint8_t {
// eng mit dem Timeout zusammenhaengt und hier genug Platz fuer eine
// kurze Erklaerung ist.
TIMEOUT_SCREEN_TITLE,
TIMEOUT_SCREENSAVER_DESC,
TIMEOUT_SCREENSAVER_DESC,
MENU_ROUTE_FILTER,
ROUTE_FILTER_PROMPT,
COUNT
};

View File

@@ -254,7 +254,9 @@ static const char* const I18N_DE[] = {
"Jetzt neu starten",
"Das Update konnte nicht heruntergeladen oder installiert werden. Das Gerät läuft unverändert mit der bisherigen Firmware weiter. Bitte WLAN-Verbindung prüfen und es später erneut versuchen.",
"Bildschirm-Timeout",
"Nach dieser Zeit ohne Berührung schaltet sich das Display ab, um Strom zu sparen. Ist der Ruhebildschirm aktiviert, zeigt es stattdessen einen gedimmten Sternenhimmel mit Uhrzeit.",
"Nach dieser Zeit ohne Berührung schaltet sich das Display ab, um Strom zu sparen. Ist der Ruhebildschirm aktiviert, zeigt es stattdessen einen gedimmten Sternenhimmel mit Uhrzeit.",
"Streckenfilter",
"ICAO eingeben",
};
static_assert(sizeof(I18N_DE) / sizeof(I18N_DE[0]) == (size_t)StringId::COUNT,

View File

@@ -255,7 +255,9 @@ static const char* const I18N_EN[] = {
"Restart now",
"The update could not be downloaded or installed. The device is still running the previous firmware, unchanged. Please check your WiFi connection and try again later.",
"Screen Timeout",
"After this time without touch, the display turns off to save power. With the screensaver on, it dims to a starfield with a clock instead.",
"After this time without touch, the display turns off to save power. With the screensaver on, it dims to a starfield with a clock instead.",
"Route Filter",
"Enter ICAO",
};
static_assert(sizeof(I18N_EN) / sizeof(I18N_EN[0]) == (size_t)StringId::COUNT,

View File

@@ -254,7 +254,9 @@ static const char* const I18N_ES[] = {
"Reiniciar ahora",
"No se pudo descargar o instalar la actualización. El dispositivo sigue funcionando sin cambios con el firmware anterior. Comprueba tu conexión WiFi e inténtalo de nuevo más tarde.",
"Tiempo de espera de pantalla",
"Tras este tiempo sin toques, la pantalla se apaga para ahorrar energía. Con el salvapantallas activado, muestra en su lugar un cielo estrellado atenuado con la hora.",
"Tras este tiempo sin toques, la pantalla se apaga para ahorrar energía. Con el salvapantallas activado, muestra en su lugar un cielo estrellado atenuado con la hora.",
"Filtro de ruta",
"Ingresa OACI",
};
static_assert(sizeof(I18N_ES) / sizeof(I18N_ES[0]) == (size_t)StringId::COUNT,

View File

@@ -254,7 +254,9 @@ static const char* const I18N_FR[] = {
"Redémarrer maintenant",
"La mise à jour n'a pas pu être téléchargée ou installée. L'appareil continue de fonctionner normalement avec le micrologiciel précédent. Merci de vérifier votre connexion WiFi et de réessayer plus tard.",
"Délai d'inactivité",
"Après ce délai sans contact, l'écran s'éteint pour économiser l'énergie. Avec l'écran de veille activé, il affiche à la place un ciel étoilé tamisé avec l'heure.",
"Après ce délai sans contact, l'écran s'éteint pour économiser l'énergie. Avec l'écran de veille activé, il affiche à la place un ciel étoilé tamisé avec l'heure.",
"Filtre d'itinéraire",
"Entrer code OACI",
};
static_assert(sizeof(I18N_FR) / sizeof(I18N_FR[0]) == (size_t)StringId::COUNT,

View File

@@ -254,7 +254,9 @@ static const char* const I18N_IT[] = {
"Riavvia ora",
"Non è stato possibile scaricare o installare l'aggiornamento. Il dispositivo continua a funzionare senza modifiche con il firmware precedente. Controlla la connessione WiFi e riprova più tardi.",
"Timeout schermo",
"Dopo questo tempo senza tocchi, lo schermo si spegne per risparmiare energia. Con il salvaschermo attivo, mostra invece un cielo stellato attenuato con l'orario.",
"Dopo questo tempo senza tocchi, lo schermo si spegne per risparmiare energia. Con il salvaschermo attivo, mostra invece un cielo stellato attenuato con l'orario.",
"Filtro rotta",
"Inserisci ICAO",
};
static_assert(sizeof(I18N_IT) / sizeof(I18N_IT[0]) == (size_t)StringId::COUNT,

View File

@@ -254,7 +254,9 @@ static const char* const I18N_TR[] = {
"Şimdi yeniden başlat",
"Güncelleme indirilemedi veya yüklenemedi. Cihaz değişmeden önceki donanım yazılımıyla çalışmaya devam ediyor. Lütfen WiFi bağlantınızı kontrol edip daha sonra tekrar deneyin.",
"Ekran Zaman Aşımı",
"Bu süre boyunca dokunulmazsa ekran güç tasarrufu için kapanır. Ekran koruyucu açıksa, bunun yerine saatli, sönük bir yıldızlı gökyüzü gösterir.",
"Bu süre boyunca dokunulmazsa ekran güç tasarrufu için kapanır. Ekran koruyucu açıksa, bunun yerine saatli, sönük bir yıldızlı gökyüzü gösterir.",
"Rota Filtresi",
"ICAO Girin",
};
static_assert(sizeof(I18N_TR) / sizeof(I18N_TR[0]) == (size_t)StringId::COUNT,

View File

@@ -12,6 +12,7 @@
#include "airline_filter_screen.h"
#include "aircraft_watchlist_screen.h"
#include "aircraft_list_screen.h"
#include "route_filter_screen.h"
#include "about_screen.h"
#include "brightness_screen.h"
#include "timeout_screen.h"
@@ -70,7 +71,7 @@ namespace {
// die normale rowRect()-Hoehe (22px) liess bei 13 Eintraegen schon keinen
// Platz mehr fuer einen weiteren - andere Seiten (Region/System) bleiben
// bei der normalen Hoehe unveraendert, um dort nichts zu verschieben.
constexpr int16_t FLIGHT_ROW_H = 20;
constexpr int16_t FLIGHT_ROW_H = 19;
Rect flightRowRect(uint8_t index) {
return {10, (int16_t)(ROW_START_Y + index * (FLIGHT_ROW_H + ROW_GAP)),
(int16_t)(Config::SCREEN_WIDTH - 20), FLIGHT_ROW_H};
@@ -821,14 +822,15 @@ void run(TFT_eSPI& tft) {
Rect logFilesBtn = flightRowRect(3);
Rect locationBtn = flightRowRect(4);
Rect airlineBtn = flightRowRect(5);
Rect watchlistBtn = flightRowRect(6);
Rect heartbeatBtn = flightRowRect(7);
Rect emergencyBtn = flightRowRect(8);
Rect proximityBtn = flightRowRect(9);
Rect logbookBtn = flightRowRect(10);
Rect watchlistAlertBtn = flightRowRect(11);
Rect groundBtn = flightRowRect(12);
Rect backBtn = flightRowRect(13);
Rect watchlistBtn = flightRowRect(6);
Rect routeFilterBtn = flightRowRect(7);
Rect heartbeatBtn = flightRowRect(8);
Rect emergencyBtn = flightRowRect(9);
Rect proximityBtn = flightRowRect(10);
Rect logbookBtn = flightRowRect(11);
Rect watchlistAlertBtn = flightRowRect(12);
Rect groundBtn = flightRowRect(13);
Rect backBtn = flightRowRect(14);
drawButton(tft, aircraftListBtn, I18n::t(StringId::MENU_AIRCRAFT_LIST));
drawButton(tft, statsBtn, I18n::t(StringId::MENU_STATISTICS));
@@ -836,7 +838,8 @@ void run(TFT_eSPI& tft) {
drawButton(tft, logFilesBtn, I18n::t(StringId::MENU_LOGBOOK_FILES));
drawButton(tft, locationBtn, I18n::t(StringId::MENU_LOCATION_PRESETS));
drawButton(tft, airlineBtn, I18n::t(StringId::MENU_AIRLINE_FILTER));
drawButton(tft, watchlistBtn, I18n::t(StringId::MENU_WATCHLIST));
drawButton(tft, watchlistBtn, I18n::t(StringId::MENU_WATCHLIST));
drawButton(tft, routeFilterBtn, I18n::t(StringId::MENU_ROUTE_FILTER));
drawButton(tft, heartbeatBtn, I18n::t(StringId::MENU_LED_HEARTBEAT) + onOff(SettingsStore::ledHeartbeatEnabled()));
drawButton(tft, emergencyBtn, I18n::t(StringId::MENU_EMERGENCY_ALERT) + onOff(SettingsStore::emergencyAlertEnabled()));
drawButton(tft, proximityBtn, I18n::t(StringId::MENU_PROXIMITY_LED) + onOff(SettingsStore::proximityAlertEnabled()));
@@ -869,8 +872,10 @@ void run(TFT_eSPI& tft) {
LocationPresetsScreen::run(tft);
} else if (airlineBtn.contains(tap.x, tap.y)) {
AirlineFilterScreen::run(tft);
} else if (watchlistBtn.contains(tap.x, tap.y)) {
} else if (watchlistBtn.contains(tap.x, tap.y)) {
AircraftWatchlistScreen::run(tft);
} else if (routeFilterBtn.contains(tap.x, tap.y)) {
RouteFilterScreen::run(tft);
} else if (heartbeatBtn.contains(tap.x, tap.y)) {
SettingsStore::setLedHeartbeatEnabled(!SettingsStore::ledHeartbeatEnabled());
} else if (emergencyBtn.contains(tap.x, tap.y)) {

367
src/route_filter_screen.cpp Normal file
View File

@@ -0,0 +1,367 @@
#include "route_filter_screen.h"
#include "aircraft_table.h"
#include "aircraft.h"
#include "aircraft_details.h"
#include "airline_filter.h"
#include "aircraft_watchlist.h"
#include "radar_screen.h"
#include "settings_store.h"
#include "touch_input.h"
#include "menu_stars.h"
#include "config.h"
#include "i18n.h"
#include "units.h"
#include "location_manager.h"
#include "airport_lookup.h"
#include <algorithm>
#include <cstring>
namespace RouteFilterScreen {
namespace {
struct Rect {
int16_t x, y, w, h;
bool contains(int16_t px, int16_t py) const {
return px >= x && px < x + w && py >= y && py < y + h;
}
};
void drawButton(TFT_eSPI& tft, const Rect& r, const String& label, bool danger = false) {
uint16_t accent = danger ? TFT_RED : TFT_GREEN;
tft.fillRoundRect(r.x, r.y, r.w, r.h, 4, TFT_BLACK);
tft.drawRoundRect(r.x, r.y, r.w, r.h, 4, accent);
tft.setTextDatum(MC_DATUM);
tft.setTextColor(accent, TFT_BLACK);
tft.drawString(label, r.x + r.w / 2, r.y + r.h / 2);
tft.setTextDatum(TL_DATUM);
}
uint16_t colorForAltitude(int32_t altFt) {
if (altFt < Config::COLOR_LOW_ALT_THRESHOLD_FT) return TFT_GREEN;
if (altFt < Config::COLOR_MID_ALT_THRESHOLD_FT) return TFT_YELLOW;
return TFT_RED;
}
bool isEmergencySquawk(const char* squawk) {
if (!squawk[0]) return false;
for (uint8_t i = 0; i < Config::EMERGENCY_SQUAWK_COUNT; i++) {
if (strcmp(squawk, Config::EMERGENCY_SQUAWKS[i]) == 0) return true;
}
return false;
}
enum class SortMode : uint8_t { Distance, Altitude, Callsign };
SortMode sortMode = SortMode::Distance;
const char* sortModeLabel() {
switch (sortMode) {
case SortMode::Altitude: return I18n::t(StringId::AIRCRAFT_LIST_SORT_ALTITUDE);
case SortMode::Callsign: return I18n::t(StringId::AIRCRAFT_LIST_SORT_CALLSIGN);
default: return I18n::t(StringId::AIRCRAFT_LIST_SORT_DISTANCE);
}
}
String runKeypad(TFT_eSPI& tft) {
MenuStars::reset();
constexpr const char* ROW1 = "QWERTYUIOP";
constexpr const char* ROW2 = "ASDFGHJKL";
constexpr const char* ROW3 = "ZXCVBNM";
char buf[5] = {0};
uint8_t len = 0;
constexpr uint8_t MAX_LEN = 4;
constexpr int16_t KEY_H = 32;
constexpr int16_t KEY_GAP = 3;
constexpr int16_t ROW0_Y = 90;
auto layoutRow = [&](const char* row, int16_t y, Rect* outRects, uint8_t n) {
int16_t usableW = Config::SCREEN_WIDTH - 8;
int16_t keyW = (usableW - (n - 1) * KEY_GAP) / n;
int16_t x = 4;
for (uint8_t i = 0; i < n; i++) {
outRects[i] = {x, y, keyW, KEY_H};
x += keyW + KEY_GAP;
}
};
Rect row1Rects[10], row2Rects[9], row3Rects[7];
layoutRow(ROW1, ROW0_Y, row1Rects, 10);
layoutRow(ROW2, ROW0_Y + KEY_H + KEY_GAP, row2Rects, 9);
layoutRow(ROW3, ROW0_Y + 2 * (KEY_H + KEY_GAP), row3Rects, 7);
Rect backspaceBtn = {4, (int16_t)(ROW0_Y + 3 * (KEY_H + KEY_GAP)), 100, KEY_H};
Rect cancelBtn = {(int16_t)(Config::SCREEN_WIDTH - 100), (int16_t)(ROW0_Y + 3 * (KEY_H + KEY_GAP)), 96, KEY_H};
Rect confirmBtn = {4, (int16_t)(ROW0_Y + 4 * (KEY_H + KEY_GAP)), (int16_t)(Config::SCREEN_WIDTH - 8), KEY_H};
bool done = false;
bool confirmed = false;
auto redraw = [&]() {
tft.fillScreen(TFT_BLACK);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.setCursor(10, 14);
tft.print(I18n::t(StringId::ROUTE_FILTER_PROMPT));
tft.fillRect(8, 40, Config::SCREEN_WIDTH - 16, 34, TFT_BLACK);
tft.drawRect(8, 40, Config::SCREEN_WIDTH - 16, 34, TFT_GREEN);
tft.setTextSize(2);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.setCursor(14, 66);
tft.print(buf);
tft.setTextSize(1);
for (uint8_t i = 0; i < 10; i++) drawButton(tft, row1Rects[i], String(ROW1[i]));
for (uint8_t i = 0; i < 9; i++) drawButton(tft, row2Rects[i], String(ROW2[i]));
for (uint8_t i = 0; i < 7; i++) drawButton(tft, row3Rects[i], String(ROW3[i]));
drawButton(tft, backspaceBtn, "<-");
drawButton(tft, cancelBtn, I18n::t(StringId::CANCEL), true);
drawButton(tft, confirmBtn, I18n::t(StringId::OK));
};
redraw();
while (!done) {
TouchInput::Point tap;
if (!TouchInput::wasTapped(tap)) { MenuStars::update(tft); delay(20); continue; }
bool handled = false;
for (uint8_t i = 0; i < 10 && !handled; i++) {
if (row1Rects[i].contains(tap.x, tap.y) && len < MAX_LEN) { buf[len++] = ROW1[i]; handled = true; }
}
for (uint8_t i = 0; i < 9 && !handled; i++) {
if (row2Rects[i].contains(tap.x, tap.y) && len < MAX_LEN) { buf[len++] = ROW2[i]; handled = true; }
}
for (uint8_t i = 0; i < 7 && !handled; i++) {
if (row3Rects[i].contains(tap.x, tap.y) && len < MAX_LEN) { buf[len++] = ROW3[i]; handled = true; }
}
if (!handled && backspaceBtn.contains(tap.x, tap.y)) {
if (len > 0) { len--; buf[len] = 0; }
handled = true;
}
if (!handled && cancelBtn.contains(tap.x, tap.y)) {
done = true;
confirmed = false;
handled = true;
}
if (!handled && confirmBtn.contains(tap.x, tap.y) && len > 0) {
done = true;
confirmed = true;
handled = true;
}
if (handled) redraw();
}
return confirmed ? String(buf) : String();
}
}
bool run(TFT_eSPI& tft) {
String filterIcao = runKeypad(tft);
if (filterIcao.length() == 0) return false;
filterIcao.toUpperCase();
bool selected = false;
bool done = false;
constexpr int16_t LOOKUP_ROW_H = 28;
constexpr int16_t LOOKUP_ROW_GAP = 4;
constexpr int16_t LIST_TOP = 62;
constexpr int16_t LIST_BOTTOM = Config::SCREEN_HEIGHT - 56;
constexpr uint8_t ROWS_VISIBLE = (LIST_BOTTOM - LIST_TOP) / (LOOKUP_ROW_H + LOOKUP_ROW_GAP);
uint8_t scrollTop = 0;
Aircraft matches[Config::MAX_TRACKED_AIRCRAFT];
uint8_t matchCount = 0;
bool lookupDone = false;
MenuStars::reset();
while (!done && !selected) {
if (!lookupDone) {
uint8_t total = 0;
float rangeKm = Config::RANGE_STEPS_KM[SettingsStore::rangeIndex()];
AircraftTable::lock();
Aircraft* table = AircraftTable::raw();
for (uint8_t i = 0; i < AircraftTable::capacity(); i++) {
if (!table[i].valid) continue;
if (table[i].distanceKm > rangeKm * 1.05f) continue;
if (SettingsStore::hideGroundVehicles() && table[i].category[0] == 'C') continue;
if (AirlineFilter::isHidden(table[i].callsign)) continue;
total++;
}
AircraftTable::unlock();
matchCount = 0;
uint8_t doneCount = 0;
AircraftTable::lock();
Aircraft* table2 = AircraftTable::raw();
for (uint8_t i = 0; i < AircraftTable::capacity() && doneCount < total; i++) {
if (!table2[i].valid) continue;
if (table2[i].distanceKm > rangeKm * 1.05f) continue;
if (SettingsStore::hideGroundVehicles() && table2[i].category[0] == 'C') continue;
if (AirlineFilter::isHidden(table2[i].callsign)) continue;
doneCount++;
tft.fillScreen(TFT_BLACK);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.setTextDatum(MC_DATUM);
char progBuf[32];
snprintf(progBuf, sizeof(progBuf), "Looking up routes\n%d/%d", doneCount, total);
tft.drawString(progBuf, Config::SCREEN_WIDTH / 2, Config::SCREEN_HEIGHT / 2 - 10);
tft.setTextDatum(TL_DATUM);
AircraftDetails::request(table2[i].hex, table2[i].callsign);
uint32_t waitStart = millis();
while (millis() - waitStart < 8000) {
AircraftDetails::Info info = AircraftDetails::get(table2[i].hex);
if (!info.loading) {
if (info.routeOrigin[0] || info.routeDest[0]) {
if (strcasecmp(info.routeOrigin, filterIcao.c_str()) == 0 ||
strcasecmp(info.routeDest, filterIcao.c_str()) == 0) {
matches[matchCount++] = table2[i];
}
}
break;
}
MenuStars::update(tft);
delay(50);
}
}
AircraftTable::unlock();
lookupDone = true;
std::sort(matches, matches + matchCount, [](const Aircraft& a, const Aircraft& b) {
return a.distanceKm < b.distanceKm;
});
}
if (scrollTop > 0 && scrollTop + ROWS_VISIBLE > matchCount) {
scrollTop = (matchCount > ROWS_VISIBLE) ? (uint8_t)(matchCount - ROWS_VISIBLE) : 0;
}
tft.fillScreen(TFT_BLACK);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.setCursor(10, 14);
tft.print(I18n::t(StringId::ROUTE_FILTER_PROMPT));
tft.print(": ");
tft.print(filterIcao);
Rect sortBtn = {10, 30, (int16_t)(Config::SCREEN_WIDTH - 20), 22};
String sortLabel = String(I18n::t(StringId::AIRCRAFT_LIST_SORT_PREFIX)) + sortModeLabel();
drawButton(tft, sortBtn, sortLabel);
Rect rowRects[ROWS_VISIBLE];
uint8_t visibleRowCount = 0;
bool listMetric = LocationManager::useMetricUnits();
if (matchCount == 0) {
tft.setTextColor(TFT_DARKGREY, TFT_BLACK);
tft.setCursor(10, LIST_TOP + 14);
tft.print("No planes on route ");
tft.print(filterIcao);
} else {
int16_t y = LIST_TOP;
for (uint8_t i = scrollTop; i < matchCount && visibleRowCount < ROWS_VISIBLE; i++) {
Aircraft& a = matches[i];
Rect r = {10, y, (int16_t)(Config::SCREEN_WIDTH - 20), LOOKUP_ROW_H};
rowRects[visibleRowCount] = r;
bool emergency = isEmergencySquawk(a.squawk);
bool watched = AircraftWatchlist::isWatched(a.callsign);
uint16_t borderColor = emergency ? TFT_RED : (watched ? TFT_CYAN : TFT_GREEN);
tft.drawRoundRect(r.x, r.y, r.w, r.h, 4, borderColor);
int16_t midY = r.y + r.h / 2;
const char* callsignLabel = a.callsign[0] ? a.callsign : a.hex;
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.setTextDatum(ML_DATUM);
tft.drawString(callsignLabel, r.x + 6, midY);
AircraftDetails::Info info = AircraftDetails::get(a.hex);
char routeBuf[24] = {0};
if (info.routeOrigin[0] && info.routeDest[0]) {
String origName = AirportLookup::findName(info.routeOrigin);
String destName = AirportLookup::findName(info.routeDest);
if (origName.length() > 0 && destName.length() > 0) {
snprintf(routeBuf, sizeof(routeBuf), "%.5s->%.5s", origName.c_str(), destName.c_str());
} else {
snprintf(routeBuf, sizeof(routeBuf), "%s->%s", info.routeOrigin, info.routeDest);
}
} else {
snprintf(routeBuf, sizeof(routeBuf), "?->?");
}
tft.setTextColor(TFT_DARKGREY, TFT_BLACK);
tft.setTextDatum(MC_DATUM);
tft.drawString(routeBuf, Config::SCREEN_WIDTH / 2, midY);
char buf[16];
if (listMetric) {
snprintf(buf, sizeof(buf), "%.0fm", Units::feetToMeters((float)a.altBaroFt));
} else {
snprintf(buf, sizeof(buf), "%.0fft", (float)a.altBaroFt);
}
tft.setTextColor(colorForAltitude(a.altBaroFt), TFT_BLACK);
tft.setTextDatum(MR_DATUM);
tft.drawString(buf, r.x + r.w - 6, midY);
tft.setTextDatum(TL_DATUM);
y += LOOKUP_ROW_H + LOOKUP_ROW_GAP;
visibleRowCount++;
}
}
bool canUp = scrollTop > 0;
bool canDown = (uint16_t)(scrollTop + ROWS_VISIBLE) < matchCount;
Rect upBtn = {10, (int16_t)(Config::SCREEN_HEIGHT - 50), 60, 40};
Rect downBtn = {76, (int16_t)(Config::SCREEN_HEIGHT - 50), 60, 40};
Rect backBtn = {146, (int16_t)(Config::SCREEN_HEIGHT - 50), (int16_t)(Config::SCREEN_WIDTH - 156), 40};
drawButton(tft, upBtn, "^");
drawButton(tft, downBtn, "v");
drawButton(tft, backBtn, I18n::t(StringId::BACK));
TouchInput::Point tap;
while (true) {
if (TouchInput::wasTapped(tap)) break;
MenuStars::update(tft);
delay(20);
}
if (sortBtn.contains(tap.x, tap.y)) {
sortMode = static_cast<SortMode>((static_cast<uint8_t>(sortMode) + 1) % 3);
scrollTop = 0;
std::sort(matches, matches + matchCount, [](const Aircraft& a, const Aircraft& b) {
switch (sortMode) {
case SortMode::Altitude:
return a.altBaroFt < b.altBaroFt;
case SortMode::Callsign: {
const char* ca = a.callsign[0] ? a.callsign : a.hex;
const char* cb = b.callsign[0] ? b.callsign : b.hex;
return strcmp(ca, cb) < 0;
}
default:
return a.distanceKm < b.distanceKm;
}
});
} else if (canUp && upBtn.contains(tap.x, tap.y)) {
scrollTop--;
} else if (canDown && downBtn.contains(tap.x, tap.y)) {
scrollTop++;
} else if (backBtn.contains(tap.x, tap.y)) {
done = true;
} else {
for (uint8_t i = 0; i < visibleRowCount; i++) {
if (rowRects[i].contains(tap.x, tap.y)) {
RadarScreen::selectAircraft(matches[scrollTop + i].hex, matches[scrollTop + i].callsign);
selected = true;
break;
}
}
}
}
return selected;
}
}

View File

@@ -0,0 +1,7 @@
#pragma once
#include <Arduino.h>
#include <TFT_eSPI.h>
namespace RouteFilterScreen {
bool run(TFT_eSPI& tft);
}