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);
|
strncpy(result.name, name.c_str(), sizeof(result.name) - 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
f.close();
|
f.close();
|
||||||
|
|
||||||
return result;
|
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 lat = 0;
|
||||||
double lon = 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
|
// eng mit dem Timeout zusammenhaengt und hier genug Platz fuer eine
|
||||||
// kurze Erklaerung ist.
|
// kurze Erklaerung ist.
|
||||||
TIMEOUT_SCREEN_TITLE,
|
TIMEOUT_SCREEN_TITLE,
|
||||||
TIMEOUT_SCREENSAVER_DESC,
|
TIMEOUT_SCREENSAVER_DESC,
|
||||||
|
|
||||||
|
MENU_ROUTE_FILTER,
|
||||||
|
ROUTE_FILTER_PROMPT,
|
||||||
|
|
||||||
COUNT
|
COUNT
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -254,7 +254,9 @@ static const char* const I18N_DE[] = {
|
|||||||
"Jetzt neu starten",
|
"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.",
|
"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",
|
"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,
|
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",
|
"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.",
|
"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",
|
"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,
|
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",
|
"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.",
|
"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",
|
"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,
|
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",
|
"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.",
|
"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é",
|
"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,
|
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",
|
"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.",
|
"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",
|
"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,
|
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",
|
"Ş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.",
|
"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ı",
|
"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,
|
static_assert(sizeof(I18N_TR) / sizeof(I18N_TR[0]) == (size_t)StringId::COUNT,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
#include "airline_filter_screen.h"
|
#include "airline_filter_screen.h"
|
||||||
#include "aircraft_watchlist_screen.h"
|
#include "aircraft_watchlist_screen.h"
|
||||||
#include "aircraft_list_screen.h"
|
#include "aircraft_list_screen.h"
|
||||||
|
#include "route_filter_screen.h"
|
||||||
#include "about_screen.h"
|
#include "about_screen.h"
|
||||||
#include "brightness_screen.h"
|
#include "brightness_screen.h"
|
||||||
#include "timeout_screen.h"
|
#include "timeout_screen.h"
|
||||||
@@ -70,7 +71,7 @@ namespace {
|
|||||||
// die normale rowRect()-Hoehe (22px) liess bei 13 Eintraegen schon keinen
|
// die normale rowRect()-Hoehe (22px) liess bei 13 Eintraegen schon keinen
|
||||||
// Platz mehr fuer einen weiteren - andere Seiten (Region/System) bleiben
|
// Platz mehr fuer einen weiteren - andere Seiten (Region/System) bleiben
|
||||||
// bei der normalen Hoehe unveraendert, um dort nichts zu verschieben.
|
// 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) {
|
Rect flightRowRect(uint8_t index) {
|
||||||
return {10, (int16_t)(ROW_START_Y + index * (FLIGHT_ROW_H + ROW_GAP)),
|
return {10, (int16_t)(ROW_START_Y + index * (FLIGHT_ROW_H + ROW_GAP)),
|
||||||
(int16_t)(Config::SCREEN_WIDTH - 20), FLIGHT_ROW_H};
|
(int16_t)(Config::SCREEN_WIDTH - 20), FLIGHT_ROW_H};
|
||||||
@@ -821,14 +822,15 @@ void run(TFT_eSPI& tft) {
|
|||||||
Rect logFilesBtn = flightRowRect(3);
|
Rect logFilesBtn = flightRowRect(3);
|
||||||
Rect locationBtn = flightRowRect(4);
|
Rect locationBtn = flightRowRect(4);
|
||||||
Rect airlineBtn = flightRowRect(5);
|
Rect airlineBtn = flightRowRect(5);
|
||||||
Rect watchlistBtn = flightRowRect(6);
|
Rect watchlistBtn = flightRowRect(6);
|
||||||
Rect heartbeatBtn = flightRowRect(7);
|
Rect routeFilterBtn = flightRowRect(7);
|
||||||
Rect emergencyBtn = flightRowRect(8);
|
Rect heartbeatBtn = flightRowRect(8);
|
||||||
Rect proximityBtn = flightRowRect(9);
|
Rect emergencyBtn = flightRowRect(9);
|
||||||
Rect logbookBtn = flightRowRect(10);
|
Rect proximityBtn = flightRowRect(10);
|
||||||
Rect watchlistAlertBtn = flightRowRect(11);
|
Rect logbookBtn = flightRowRect(11);
|
||||||
Rect groundBtn = flightRowRect(12);
|
Rect watchlistAlertBtn = flightRowRect(12);
|
||||||
Rect backBtn = flightRowRect(13);
|
Rect groundBtn = flightRowRect(13);
|
||||||
|
Rect backBtn = flightRowRect(14);
|
||||||
|
|
||||||
drawButton(tft, aircraftListBtn, I18n::t(StringId::MENU_AIRCRAFT_LIST));
|
drawButton(tft, aircraftListBtn, I18n::t(StringId::MENU_AIRCRAFT_LIST));
|
||||||
drawButton(tft, statsBtn, I18n::t(StringId::MENU_STATISTICS));
|
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, logFilesBtn, I18n::t(StringId::MENU_LOGBOOK_FILES));
|
||||||
drawButton(tft, locationBtn, I18n::t(StringId::MENU_LOCATION_PRESETS));
|
drawButton(tft, locationBtn, I18n::t(StringId::MENU_LOCATION_PRESETS));
|
||||||
drawButton(tft, airlineBtn, I18n::t(StringId::MENU_AIRLINE_FILTER));
|
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, heartbeatBtn, I18n::t(StringId::MENU_LED_HEARTBEAT) + onOff(SettingsStore::ledHeartbeatEnabled()));
|
||||||
drawButton(tft, emergencyBtn, I18n::t(StringId::MENU_EMERGENCY_ALERT) + onOff(SettingsStore::emergencyAlertEnabled()));
|
drawButton(tft, emergencyBtn, I18n::t(StringId::MENU_EMERGENCY_ALERT) + onOff(SettingsStore::emergencyAlertEnabled()));
|
||||||
drawButton(tft, proximityBtn, I18n::t(StringId::MENU_PROXIMITY_LED) + onOff(SettingsStore::proximityAlertEnabled()));
|
drawButton(tft, proximityBtn, I18n::t(StringId::MENU_PROXIMITY_LED) + onOff(SettingsStore::proximityAlertEnabled()));
|
||||||
@@ -869,8 +872,10 @@ void run(TFT_eSPI& tft) {
|
|||||||
LocationPresetsScreen::run(tft);
|
LocationPresetsScreen::run(tft);
|
||||||
} else if (airlineBtn.contains(tap.x, tap.y)) {
|
} else if (airlineBtn.contains(tap.x, tap.y)) {
|
||||||
AirlineFilterScreen::run(tft);
|
AirlineFilterScreen::run(tft);
|
||||||
} else if (watchlistBtn.contains(tap.x, tap.y)) {
|
} else if (watchlistBtn.contains(tap.x, tap.y)) {
|
||||||
AircraftWatchlistScreen::run(tft);
|
AircraftWatchlistScreen::run(tft);
|
||||||
|
} else if (routeFilterBtn.contains(tap.x, tap.y)) {
|
||||||
|
RouteFilterScreen::run(tft);
|
||||||
} else if (heartbeatBtn.contains(tap.x, tap.y)) {
|
} else if (heartbeatBtn.contains(tap.x, tap.y)) {
|
||||||
SettingsStore::setLedHeartbeatEnabled(!SettingsStore::ledHeartbeatEnabled());
|
SettingsStore::setLedHeartbeatEnabled(!SettingsStore::ledHeartbeatEnabled());
|
||||||
} else if (emergencyBtn.contains(tap.x, tap.y)) {
|
} 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