Files
flyradar/src/address_search_screen.cpp
Eiswolf-BG 6f3713f348 Revert Sound feature (hardware EMI noise, not fixable in firmware); keep preset auto-activate fix and QR-on-main-screen layout
Root-caused to SPI bus crosstalk from TFT/touch during radar-sweep
redraws - muting the software output did not silence the residual
hiss, confirming this is hardware EMI rather than a firmware logic
bug. All Sound-related files, settings, i18n strings, and menu
entries have been removed.

Unrelated fixes from the same session are kept: newly created
location presets (via address search, manual coordinates, or the
nearest-airport ticker) now activate immediately instead of leaving
the previous location active; the WebUI QR code moved from a
separate sub-screen onto the main info screen, centered under the
explanatory text.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 21:33:02 +02:00

623 lines
27 KiB
C++

#include "address_search_screen.h"
#include "location_presets.h"
#include "touch_input.h"
#include "menu_stars.h"
#include "settings_store.h"
#include "config.h"
#include "i18n.h"
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <cstring>
namespace AddressSearchScreen {
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 active = false, bool danger = false) {
uint16_t accent = danger ? TFT_RED : TFT_GREEN;
uint16_t bg = active ? accent : TFT_BLACK;
uint16_t fg = active ? TFT_BLACK : accent;
tft.fillRoundRect(r.x, r.y, r.w, r.h, 4, bg);
tft.drawRoundRect(r.x, r.y, r.w, r.h, 4, accent);
tft.setTextDatum(MC_DATUM);
tft.setTextColor(fg, bg);
tft.drawString(label, r.x + r.w / 2, r.y + r.h / 2);
tft.setTextDatum(TL_DATUM);
}
// Lokale Kopie (siehe Konvention in location_presets_screen.cpp - jeder
// Screen haelt seine eigenen kleinen Helfer statt eines gemeinsamen
// Moduls). Ohne Scroll-Unterstuetzung - hier immer nur kurze,
// vorab abgeschnittene Texte (siehe runConfirmScreen/showErrorRetry).
// Lokale Kopie (siehe Konvention in location_presets_screen.cpp - jeder
// Screen haelt seine eigenen kleinen Helfer statt eines gemeinsamen
// Moduls). Optionale Scroll-Unterstuetzung (scrollY/viewTop/viewBottom/
// draw) fuer runConfirmScreen() unten - bei den bestehenden Aufrufstellen
// (showErrorRetry, Kopfzeilen) bleiben die Defaults aktiv, es wird also
// wie bisher immer alles auf einmal gezeichnet.
int16_t layoutWrapped(TFT_eSPI& tft, int16_t x, int16_t startY, int16_t maxWidth,
int16_t lineHeight, const String& text,
int16_t scrollY = 0, int16_t viewTop = -32000, int16_t viewBottom = 32000,
bool draw = true) {
int16_t y = startY;
int32_t start = 0;
int32_t len = text.length();
while (start < len) {
while (start < len && text[start] == ' ') start++;
if (start >= len) break;
String line = text.substring(start, len);
while (tft.textWidth(line) > maxWidth) {
int32_t lastSpace = line.lastIndexOf(' ');
if (lastSpace <= 0) break;
line = line.substring(0, lastSpace);
}
if (draw) {
int16_t screenY = (int16_t)(y - scrollY);
if (screenY >= viewTop && screenY <= viewBottom) {
tft.setCursor(x, screenY);
tft.print(line);
}
}
y += lineHeight;
start += line.length();
}
return y;
}
// ---- UTF-8-bewusste Puffer-Helfer (fuer die Sonderzeichen-Tasten wie
// "Ä", die als 2-Byte-UTF-8-Sequenz eingefuegt werden muessen) ----
void appendUtf8(char* buf, uint8_t& len, uint8_t cap, const char* s) {
size_t sl = strlen(s);
if (len + sl >= cap) return;
memcpy(buf + len, s, sl);
len += (uint8_t)sl;
buf[len] = 0;
}
// Entfernt beim Loeschen eine ganze UTF-8-Zeichensequenz (nicht nur ein
// einzelnes Byte) - sonst bliebe bei Sonderzeichen ein kaputtes
// halbes Byte im Puffer stehen.
void backspaceUtf8(char* buf, uint8_t& len) {
if (len == 0) return;
len--;
while (len > 0 && (((uint8_t)buf[len]) & 0xC0) == 0x80) len--;
buf[len] = 0;
}
// Zeigt nur so viel vom ENDE des Puffers, wie in maxWidth passt - der
// Nutzer tippt am Ende weiter, das sichtbare Fenster soll dem folgen
// (wie bei einem gewoehnlichen einzeiligen Texteingabefeld).
String visibleTail(TFT_eSPI& tft, const char* buf, int16_t maxWidth) {
String s(buf);
while (s.length() > 0 && tft.textWidth(s) > maxWidth) {
int32_t cut = ((((uint8_t)s[0]) & 0xE0) == 0xC0) ? 2 : 1;
if (cut > (int32_t)s.length()) cut = s.length();
s = s.substring(cut);
}
return s;
}
// ---- Tastatur-Layout ----
constexpr const char* DIGITS = "1234567890";
constexpr const char* ROW1 = "QWERTYUIOP";
constexpr const char* ROW2 = "ASDFGHJKL";
// Um Komma und Bindestrich erweitert (7->9 Tasten) - Adressen wie eine
// Hausnummer "45/3" oder "Strasse 12, Ort" waren sonst gar nicht
// eintippbar, da die Tastatur bisher keinerlei Satzzeichen enthielt.
constexpr const char* ROW3 = "ZXCVBNM,-";
// Sonderzeichen-Seite: gegenueber der ersten Version wurden die seltenen
// Â/Ë/Î/Û gegen die fuer Adressen wichtigen Satzzeichen "/", ".", "'", "-"
// getauscht (Hausnummern wie "45/3", Abkuerzungen wie "Str.", Apostroph-
// Namen).
constexpr const char* SPEC0[6] = {"À", "Á", "Ä", "Ç", "É", "È"};
constexpr const char* SPEC1[6] = {"Ê", "Í", "Ñ", "Ó", "Ò", "Ô"};
constexpr const char* SPEC2[6] = {"Ö", "Ù", "Ú", "Ü", "ß", "Ğ"};
constexpr const char* SPEC3[6] = {"İ", "Ş", "/", ".", "'", "-"};
// Gibt die eingegebene Adresse zurueck, oder einen leeren String, wenn
// der Nutzer abgebrochen hat.
String runAddressKeyboard(TFT_eSPI& tft) {
MenuStars::reset();
constexpr uint8_t CAP = 64;
char buf[CAP] = {0};
uint8_t len = 0;
bool specialPage = false;
constexpr int16_t KEY_H = 30;
constexpr int16_t KEY_GAP = 3;
constexpr int16_t FIELD_H = 34;
// Kopfbereich (Titel + Format-Hinweis) einmal zeichnen, um seine
// tatsaechliche Hoehe per getCursorY() zu messen (variiert je nach
// Sprache/Uebersetzungslaenge) - Eingabefeld und Tastatur-Start
// werden daraus abgeleitet statt wie zuvor eine feste Pixelposition
// (78) anzunehmen. redraw() unten zeichnet denselben Kopf bei jedem
// Aufruf identisch neu.
tft.fillScreen(TFT_BLACK);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.setCursor(10, 14);
tft.println(I18n::t(StringId::ADDRESS_SEARCH_TITLE));
tft.setTextColor(TFT_CYAN, TFT_BLACK);
tft.println(I18n::t(StringId::ADDRESS_SEARCH_HINT));
int16_t fieldY = (int16_t)(tft.getCursorY() + 4);
int16_t ROW0_Y = (int16_t)(fieldY + FIELD_H + 8);
auto layoutRow = [&](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 digitRects[10], row1Rects[10], row2Rects[9], row3Rects[9];
layoutRow(ROW0_Y, digitRects, 10);
layoutRow((int16_t)(ROW0_Y + (KEY_H + KEY_GAP)), row1Rects, 10);
layoutRow((int16_t)(ROW0_Y + 2 * (KEY_H + KEY_GAP)), row2Rects, 9);
layoutRow((int16_t)(ROW0_Y + 3 * (KEY_H + KEY_GAP)), row3Rects, 9);
Rect specRects[4][6];
for (uint8_t r = 0; r < 4; r++) {
layoutRow((int16_t)(ROW0_Y + r * (KEY_H + KEY_GAP)), specRects[r], 6);
}
Rect spaceBtn = {4, (int16_t)(ROW0_Y + 4 * (KEY_H + KEY_GAP)), 150, KEY_H};
Rect backspaceBtn = {158, (int16_t)(ROW0_Y + 4 * (KEY_H + KEY_GAP)), (int16_t)(Config::SCREEN_WIDTH - 8 - 154), KEY_H};
int16_t BTN_ROW_Y = (int16_t)(ROW0_Y + 5 * (KEY_H + KEY_GAP));
constexpr int16_t BTN_W = (Config::SCREEN_WIDTH - 8 - 2 * KEY_GAP) / 3;
Rect toggleBtn = {4, BTN_ROW_Y, BTN_W, KEY_H};
Rect cancelBtn = {(int16_t)(4 + BTN_W + KEY_GAP), BTN_ROW_Y, BTN_W, KEY_H};
Rect searchBtn = {(int16_t)(4 + 2 * (BTN_W + KEY_GAP)), BTN_ROW_Y,
(int16_t)(Config::SCREEN_WIDTH - 8 - 2 * (BTN_W + KEY_GAP)), KEY_H};
bool done = false;
bool searched = false;
auto redraw = [&]() {
tft.fillScreen(TFT_BLACK);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.setCursor(10, 14);
tft.println(I18n::t(StringId::ADDRESS_SEARCH_TITLE));
tft.setTextColor(TFT_CYAN, TFT_BLACK);
tft.println(I18n::t(StringId::ADDRESS_SEARCH_HINT));
tft.fillRect(8, fieldY, Config::SCREEN_WIDTH - 16, FIELD_H, TFT_BLACK);
tft.drawRect(8, fieldY, Config::SCREEN_WIDTH - 16, FIELD_H, TFT_GREEN);
tft.setTextSize(2);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.setCursor(14, (int16_t)(fieldY + 26));
tft.print(visibleTail(tft, buf, (int16_t)(Config::SCREEN_WIDTH - 28)));
tft.setTextSize(1);
if (!specialPage) {
for (uint8_t i = 0; i < 10; i++) drawButton(tft, digitRects[i], String(DIGITS[i]));
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 < 9; i++) drawButton(tft, row3Rects[i], String(ROW3[i]));
} else {
for (uint8_t i = 0; i < 6; i++) drawButton(tft, specRects[0][i], SPEC0[i]);
for (uint8_t i = 0; i < 6; i++) drawButton(tft, specRects[1][i], SPEC1[i]);
for (uint8_t i = 0; i < 6; i++) drawButton(tft, specRects[2][i], SPEC2[i]);
for (uint8_t i = 0; i < 6; i++) drawButton(tft, specRects[3][i], SPEC3[i]);
}
drawButton(tft, spaceBtn, I18n::t(StringId::WIFI_SPACE));
drawButton(tft, backspaceBtn, "<-");
drawButton(tft, toggleBtn, specialPage ? "ABC" : "ÄÖÜ");
drawButton(tft, cancelBtn, I18n::t(StringId::ADDRESS_SEARCH_CANCEL), false, true);
drawButton(tft, searchBtn, I18n::t(StringId::OK));
};
redraw();
while (!done) {
TouchInput::Point tap;
if (!TouchInput::wasTapped(tap)) { MenuStars::update(tft); delay(20); continue; }
bool handled = false;
if (!specialPage) {
for (uint8_t i = 0; i < 10 && !handled; i++) {
if (digitRects[i].contains(tap.x, tap.y)) {
char s[2] = {DIGITS[i], 0};
appendUtf8(buf, len, CAP, s);
handled = true;
}
}
for (uint8_t i = 0; i < 10 && !handled; i++) {
if (row1Rects[i].contains(tap.x, tap.y)) {
char s[2] = {ROW1[i], 0};
appendUtf8(buf, len, CAP, s);
handled = true;
}
}
for (uint8_t i = 0; i < 9 && !handled; i++) {
if (row2Rects[i].contains(tap.x, tap.y)) {
char s[2] = {ROW2[i], 0};
appendUtf8(buf, len, CAP, s);
handled = true;
}
}
for (uint8_t i = 0; i < 9 && !handled; i++) {
if (row3Rects[i].contains(tap.x, tap.y)) {
char s[2] = {ROW3[i], 0};
appendUtf8(buf, len, CAP, s);
handled = true;
}
}
} else {
for (uint8_t i = 0; i < 6 && !handled; i++) {
if (specRects[0][i].contains(tap.x, tap.y)) { appendUtf8(buf, len, CAP, SPEC0[i]); handled = true; }
}
for (uint8_t i = 0; i < 6 && !handled; i++) {
if (specRects[1][i].contains(tap.x, tap.y)) { appendUtf8(buf, len, CAP, SPEC1[i]); handled = true; }
}
for (uint8_t i = 0; i < 6 && !handled; i++) {
if (specRects[2][i].contains(tap.x, tap.y)) { appendUtf8(buf, len, CAP, SPEC2[i]); handled = true; }
}
for (uint8_t i = 0; i < 6 && !handled; i++) {
if (specRects[3][i].contains(tap.x, tap.y)) { appendUtf8(buf, len, CAP, SPEC3[i]); handled = true; }
}
}
if (!handled && spaceBtn.contains(tap.x, tap.y)) { appendUtf8(buf, len, CAP, " "); handled = true; }
if (!handled && backspaceBtn.contains(tap.x, tap.y)) { backspaceUtf8(buf, len); handled = true; }
if (!handled && toggleBtn.contains(tap.x, tap.y)) { specialPage = !specialPage; handled = true; }
if (!handled && cancelBtn.contains(tap.x, tap.y)) {
buf[0] = 0;
len = 0;
done = true;
searched = false;
handled = true;
}
if (!handled && searchBtn.contains(tap.x, tap.y) && len > 0) {
done = true;
searched = true;
handled = true;
}
if (handled) redraw();
}
return searched ? String(buf) : String();
}
// Namens-Tastatur fuer den finalen Schritt (Preset-Name vergeben) -
// absichtlich eine eigene, einfache ASCII-Tastatur (keine
// Sonderzeichen noetig fuer einen Preset-Namen wie "Zuhause"),
// spiegelt runPresetNameKeypad() aus location_presets_screen.cpp.
String runNameKeypad(TFT_eSPI& tft) {
MenuStars::reset();
constexpr const char* NDIGITS = "1234567890";
constexpr const char* NROW1 = "QWERTYUIOP";
constexpr const char* NROW2 = "ASDFGHJKL";
constexpr const char* NROW3 = "ZXCVBNM";
char buf[17] = {0};
uint8_t len = 0;
constexpr int16_t KEY_H = 30;
constexpr int16_t KEY_GAP = 3;
constexpr int16_t FIELD_H = 34;
// Kopfbereich (Titel + Namens-Hinweis) einmal zeichnen, um seine
// tatsaechliche Hoehe per getCursorY() zu messen - selbes Muster wie
// in runAddressKeyboard() oben.
tft.fillScreen(TFT_BLACK);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.setCursor(10, 14);
tft.println(I18n::t(StringId::LOCATION_NAME_PROMPT));
tft.setTextColor(TFT_CYAN, TFT_BLACK);
tft.println(I18n::t(StringId::LOCATION_NAME_HINT));
int16_t fieldY = (int16_t)(tft.getCursorY() + 4);
int16_t ROW0_Y = (int16_t)(fieldY + FIELD_H + 8);
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 digitRects[10], row1Rects[10], row2Rects[9], row3Rects[7];
layoutRow(NDIGITS, ROW0_Y, digitRects, 10);
layoutRow(NROW1, (int16_t)(ROW0_Y + (KEY_H + KEY_GAP)), row1Rects, 10);
layoutRow(NROW2, (int16_t)(ROW0_Y + 2 * (KEY_H + KEY_GAP)), row2Rects, 9);
layoutRow(NROW3, (int16_t)(ROW0_Y + 3 * (KEY_H + KEY_GAP)), row3Rects, 7);
Rect spaceBtn = {4, (int16_t)(ROW0_Y + 4 * (KEY_H + KEY_GAP)), 150, KEY_H};
Rect backspaceBtn = {158, (int16_t)(ROW0_Y + 4 * (KEY_H + KEY_GAP)), (int16_t)(Config::SCREEN_WIDTH - 8 - 154), KEY_H};
Rect skipBtn = {4, (int16_t)(ROW0_Y + 5 * (KEY_H + KEY_GAP)), 110, KEY_H};
Rect confirmBtn = {118, (int16_t)(ROW0_Y + 5 * (KEY_H + KEY_GAP)), (int16_t)(Config::SCREEN_WIDTH - 8 - 114), KEY_H};
bool done = false;
auto redraw = [&]() {
tft.fillScreen(TFT_BLACK);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.setCursor(10, 14);
tft.println(I18n::t(StringId::LOCATION_NAME_PROMPT));
tft.setTextColor(TFT_CYAN, TFT_BLACK);
tft.println(I18n::t(StringId::LOCATION_NAME_HINT));
tft.fillRect(8, fieldY, Config::SCREEN_WIDTH - 16, FIELD_H, TFT_BLACK);
tft.drawRect(8, fieldY, Config::SCREEN_WIDTH - 16, FIELD_H, TFT_GREEN);
tft.setTextSize(2);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.setCursor(14, (int16_t)(fieldY + 26));
tft.print(buf);
tft.setTextSize(1);
for (uint8_t i = 0; i < 10; i++) drawButton(tft, digitRects[i], String(NDIGITS[i]));
for (uint8_t i = 0; i < 10; i++) drawButton(tft, row1Rects[i], String(NROW1[i]));
for (uint8_t i = 0; i < 9; i++) drawButton(tft, row2Rects[i], String(NROW2[i]));
for (uint8_t i = 0; i < 7; i++) drawButton(tft, row3Rects[i], String(NROW3[i]));
drawButton(tft, spaceBtn, I18n::t(StringId::WIFI_SPACE));
drawButton(tft, backspaceBtn, "<-");
drawButton(tft, skipBtn, I18n::t(StringId::LOCATION_NAME_SKIP));
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 (digitRects[i].contains(tap.x, tap.y) && len < sizeof(buf) - 1) { buf[len++] = NDIGITS[i]; buf[len] = 0; handled = true; }
}
for (uint8_t i = 0; i < 10 && !handled; i++) {
if (row1Rects[i].contains(tap.x, tap.y) && len < sizeof(buf) - 1) { buf[len++] = NROW1[i]; buf[len] = 0; handled = true; }
}
for (uint8_t i = 0; i < 9 && !handled; i++) {
if (row2Rects[i].contains(tap.x, tap.y) && len < sizeof(buf) - 1) { buf[len++] = NROW2[i]; buf[len] = 0; handled = true; }
}
for (uint8_t i = 0; i < 7 && !handled; i++) {
if (row3Rects[i].contains(tap.x, tap.y) && len < sizeof(buf) - 1) { buf[len++] = NROW3[i]; buf[len] = 0; handled = true; }
}
if (!handled && spaceBtn.contains(tap.x, tap.y) && len < sizeof(buf) - 1) { buf[len++] = ' '; buf[len] = 0; handled = true; }
if (!handled && backspaceBtn.contains(tap.x, tap.y)) {
if (len > 0) { len--; buf[len] = 0; }
handled = true;
}
if (!handled && skipBtn.contains(tap.x, tap.y)) { buf[0] = 0; len = 0; done = true; handled = true; }
if (!handled && confirmBtn.contains(tap.x, tap.y)) { done = true; handled = true; }
if (handled) redraw();
}
return String(buf);
}
// Prozentkodiert einen UTF-8-String fuer die Nominatim-Query (jedes
// Roh-Byte einzeln - funktioniert damit auch fuer die mehrbytigen
// Sonderzeichen).
String urlEncode(const String& s) {
String out;
char hex[4];
for (size_t i = 0; i < s.length(); i++) {
uint8_t c = (uint8_t)s[i];
bool unreserved = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
(c >= '0' && c <= '9') || c == '-' || c == '_' || c == '.' || c == '~';
if (unreserved) {
out += (char)c;
} else {
snprintf(hex, sizeof(hex), "%%%02X", c);
out += hex;
}
}
return out;
}
// ISO-639-1-Codes in derselben Reihenfolge wie I18n::TABLES (siehe
// i18n.cpp) - fuer staerker lokalisierte display_name-Ergebnisse
// (Nominatim accept-language-Parameter).
constexpr const char* NOMINATIM_LANG_CODES[6] = {"en", "de", "fr", "tr", "es", "it"};
enum class GeocodeResult { Ok, NoResults, NetworkError };
// Kostenloser, anmeldefreier Geokodierungs-Dienst (OpenStreetMap
// Nominatim, siehe Config::NOMINATIM_HOST). Blockierender Aufruf,
// ausgeloest durch Nutzer-Interaktion (Tap auf "Suchen") - laeuft im
// Vordergrund auf Core 1, exakt wie der bestehende Flugzeug-Detail-
// Abruf in aircraft_details.cpp.
GeocodeResult geocode(const String& query, double& lat, double& lon, String& displayName) {
if (WiFi.status() != WL_CONNECTED) return GeocodeResult::NetworkError;
WiFiClientSecure client;
client.setInsecure();
client.setTimeout(Config::HTTP_TIMEOUT_MS);
String url = "https://" + String(Config::NOMINATIM_HOST) +
"/search?format=json&limit=1&q=" + urlEncode(query);
uint8_t lang = SettingsStore::language();
if (lang < 6) url += "&accept-language=" + String(NOMINATIM_LANG_CODES[lang]);
HTTPClient http;
http.setTimeout(Config::HTTP_TIMEOUT_MS);
if (!http.begin(client, url)) return GeocodeResult::NetworkError;
http.addHeader("User-Agent", Config::NOMINATIM_USER_AGENT);
int code = http.GET();
if (code != HTTP_CODE_OK) {
http.end();
return GeocodeResult::NetworkError;
}
// Body erst komplett einsammeln statt direkt aus http.getStream()
// zu parsen - siehe weather.cpp, gleicher Grund (Chunked-Transfer-
// Encoding fuehrte dort sonst zu ArduinoJson-"InvalidInput").
String body = http.getString();
http.end();
JsonDocument doc;
DeserializationError err = deserializeJson(doc, body);
if (err) return GeocodeResult::NetworkError;
JsonArray arr = doc.as<JsonArray>();
if (arr.isNull() || arr.size() == 0) return GeocodeResult::NoResults;
JsonObject first = arr[0];
const char* latStr = first["lat"] | "";
const char* lonStr = first["lon"] | "";
if (!latStr[0] || !lonStr[0]) return GeocodeResult::NoResults;
lat = atof(latStr);
lon = atof(lonStr);
const char* dn = first["display_name"] | "";
displayName = String(dn);
return GeocodeResult::Ok;
}
// Fehler-/Kein-Ergebnis-Screen mit "Erneut versuchen"/"Abbrechen".
// Gibt true zurueck, wenn der Nutzer es erneut versuchen will.
bool showErrorRetry(TFT_eSPI& tft, StringId msgId) {
MenuStars::reset();
tft.fillScreen(TFT_BLACK);
tft.setTextColor(TFT_RED, TFT_BLACK);
layoutWrapped(tft, 10, 40, (int16_t)(Config::SCREEN_WIDTH - 20), 18, I18n::t(msgId));
Rect retryBtn = {10, (int16_t)(Config::SCREEN_HEIGHT - 96), (int16_t)(Config::SCREEN_WIDTH - 20), 40};
Rect cancelBtn = {10, (int16_t)(Config::SCREEN_HEIGHT - 50), (int16_t)(Config::SCREEN_WIDTH - 20), 40};
drawButton(tft, retryBtn, I18n::t(StringId::ADDRESS_SEARCH_TRY_AGAIN));
drawButton(tft, cancelBtn, I18n::t(StringId::ADDRESS_SEARCH_CANCEL), false, true);
while (true) {
TouchInput::Point tap;
if (!TouchInput::wasTapped(tap)) { MenuStars::update(tft); delay(20); continue; }
if (retryBtn.contains(tap.x, tap.y)) return true;
if (cancelBtn.contains(tap.x, tap.y)) return false;
}
}
// Bestaetigungs-Screen mit dem von Nominatim gefundenen Ort. Gibt 1
// zurueck ("verwenden"), 0 fuer "erneut versuchen" (zurueck zur
// Adress-Tastatur).
int runConfirmScreen(TFT_eSPI& tft, const String& displayName) {
MenuStars::reset();
constexpr int16_t textMaxWidth = Config::SCREEN_WIDTH - 20;
constexpr int16_t LINE_H = 18;
constexpr int16_t VIEW_TOP = 40;
constexpr int16_t TRY_AGAIN_Y = (int16_t)(Config::SCREEN_HEIGHT - 96);
constexpr int16_t USE_Y = (int16_t)(Config::SCREEN_HEIGHT - 50);
constexpr int16_t VIEW_BOTTOM = (int16_t)(TRY_AGAIN_Y - 10);
// Volle Adresse OHNE Kuerzung anzeigen (frueher wurde bei > 150
// Zeichen mit "..." abgeschnitten) - stattdessen wird der Absatz bei
// Bedarf vertikal scrollbar, exakt dasselbe Muster wie beim "Wie
// funktionieren Presets"-Infoscreen (location_presets_screen.cpp
// ::runInfoScreen). Bewusst KEIN Marquee hier: das ist ein
// mehrzeiliger, wortumgebrochener Absatz, keine einzelne Zeile, die
// in der Breite nicht passt - horizontales Scrollen waere hier die
// falsche Loesung.
int16_t totalH = layoutWrapped(tft, 10, VIEW_TOP, textMaxWidth, LINE_H, displayName, 0, 0, 0, false);
int16_t maxScroll = (int16_t)(totalH - VIEW_BOTTOM);
if (maxScroll < 0) maxScroll = 0;
bool scrollable = maxScroll > 0;
int16_t scrollY = 0;
Rect tryAgainBtn = {10, TRY_AGAIN_Y, (int16_t)(Config::SCREEN_WIDTH - 20), 40};
Rect useBtn = scrollable
? Rect{10, USE_Y, 130, 40}
: Rect{10, USE_Y, (int16_t)(Config::SCREEN_WIDTH - 20), 40};
Rect upBtn = {146, USE_Y, 38, 40};
Rect downBtn = {190, USE_Y, 38, 40};
constexpr int16_t SCROLL_STEP = 48;
auto redraw = [&]() {
tft.fillScreen(TFT_BLACK);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.setCursor(10, 14);
tft.println(I18n::t(StringId::ADDRESS_SEARCH_CONFIRM_TITLE));
tft.setTextColor(TFT_WHITE, TFT_BLACK);
layoutWrapped(tft, 10, VIEW_TOP, textMaxWidth, LINE_H, displayName, scrollY, VIEW_TOP, VIEW_BOTTOM, true);
drawButton(tft, tryAgainBtn, I18n::t(StringId::ADDRESS_SEARCH_TRY_AGAIN));
drawButton(tft, useBtn, I18n::t(StringId::ADDRESS_SEARCH_USE_THIS));
if (scrollable) {
drawButton(tft, upBtn, "^");
drawButton(tft, downBtn, "v");
}
};
redraw();
while (true) {
TouchInput::Point tap;
if (TouchInput::wasTapped(tap)) {
if (tryAgainBtn.contains(tap.x, tap.y)) return 0;
if (useBtn.contains(tap.x, tap.y)) return 1;
if (scrollable && upBtn.contains(tap.x, tap.y) && scrollY > 0) {
scrollY -= SCROLL_STEP;
if (scrollY < 0) scrollY = 0;
redraw();
} else if (scrollable && downBtn.contains(tap.x, tap.y) && scrollY < maxScroll) {
scrollY += SCROLL_STEP;
if (scrollY > maxScroll) scrollY = maxScroll;
redraw();
}
}
MenuStars::update(tft);
delay(20);
}
}
}
bool run(TFT_eSPI& tft) {
while (true) {
String address = runAddressKeyboard(tft);
if (address.length() == 0) return false;
tft.fillScreen(TFT_BLACK);
tft.setTextDatum(MC_DATUM);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.drawString(I18n::t(StringId::ADDRESS_SEARCH_SEARCHING), Config::SCREEN_WIDTH / 2, Config::SCREEN_HEIGHT / 2);
tft.setTextDatum(TL_DATUM);
double lat = 0, lon = 0;
String displayName;
GeocodeResult result = geocode(address, lat, lon, displayName);
if (result == GeocodeResult::NetworkError) {
if (!showErrorRetry(tft, StringId::ADDRESS_SEARCH_ERROR)) return false;
continue;
}
if (result == GeocodeResult::NoResults) {
if (!showErrorRetry(tft, StringId::ADDRESS_SEARCH_NO_RESULTS)) return false;
continue;
}
int choice = runConfirmScreen(tft, displayName);
if (choice == 0) continue;
String name = runNameKeypad(tft);
if (!LocationPresets::addPreset(lat, lon, name)) return false;
// Neu angelegtes Preset sofort aktivieren - sonst blieb z.B. der
// automatische IP-Standort aktiv, obwohl gerade extra eine genauere
// Adresse eingegeben wurde (siehe Alex' Feedback).
LocationPresets::setActiveIndex((int8_t)(LocationPresets::count() - 1));
return true;
}
}
}