Add route filter screen with airport name lookup
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
367
src/route_filter_screen.cpp
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
7
src/route_filter_screen.h
Normal file
7
src/route_filter_screen.h
Normal file
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
#include <Arduino.h>
|
||||
#include <TFT_eSPI.h>
|
||||
|
||||
namespace RouteFilterScreen {
|
||||
bool run(TFT_eSPI& tft);
|
||||
}
|
||||
Reference in New Issue
Block a user