diff --git a/src/airport_lookup.cpp b/src/airport_lookup.cpp index 3c4bebc..af2288d 100644 --- a/src/airport_lookup.cpp +++ b/src/airport_lookup.cpp @@ -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(); +} + } diff --git a/src/airport_lookup.h b/src/airport_lookup.h index 9b0b905..f422533 100644 --- a/src/airport_lookup.h +++ b/src/airport_lookup.h @@ -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); } diff --git a/src/i18n.h b/src/i18n.h index 24b4c2d..860e90f 100644 --- a/src/i18n.h +++ b/src/i18n.h @@ -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 }; diff --git a/src/i18n_de.h b/src/i18n_de.h index 916d436..de6234f 100644 --- a/src/i18n_de.h +++ b/src/i18n_de.h @@ -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, diff --git a/src/i18n_en.h b/src/i18n_en.h index 7af1774..4f94de5 100644 --- a/src/i18n_en.h +++ b/src/i18n_en.h @@ -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, diff --git a/src/i18n_es.h b/src/i18n_es.h index 9e086b2..5a201e0 100644 --- a/src/i18n_es.h +++ b/src/i18n_es.h @@ -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, diff --git a/src/i18n_fr.h b/src/i18n_fr.h index 024b116..83d2e02 100644 --- a/src/i18n_fr.h +++ b/src/i18n_fr.h @@ -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, diff --git a/src/i18n_it.h b/src/i18n_it.h index 91b1ce0..7f4c0b7 100644 --- a/src/i18n_it.h +++ b/src/i18n_it.h @@ -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, diff --git a/src/i18n_tr.h b/src/i18n_tr.h index 0e36c3b..d94dbe5 100644 --- a/src/i18n_tr.h +++ b/src/i18n_tr.h @@ -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, diff --git a/src/menu_screen.cpp b/src/menu_screen.cpp index 8c9d54a..adb649c 100644 --- a/src/menu_screen.cpp +++ b/src/menu_screen.cpp @@ -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)) { diff --git a/src/route_filter_screen.cpp b/src/route_filter_screen.cpp new file mode 100644 index 0000000..76cfdf2 --- /dev/null +++ b/src/route_filter_screen.cpp @@ -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 +#include + +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((static_cast(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; +} + +} \ No newline at end of file diff --git a/src/route_filter_screen.h b/src/route_filter_screen.h new file mode 100644 index 0000000..d3632a4 --- /dev/null +++ b/src/route_filter_screen.h @@ -0,0 +1,7 @@ +#pragma once +#include +#include + +namespace RouteFilterScreen { + bool run(TFT_eSPI& tft); +} \ No newline at end of file