Add native address search with geocoding for location presets, remove itilog.com link
Adds a new AddressSearchScreen: enter an address on a dedicated keyboard (with a special-character page for common European accented letters), geocode it via the free OpenStreetMap Nominatim service, confirm the found place, optionally name it, and save it as a new location preset - no external website needed anymore. Reachable both from Location Presets > "+" (choice between "by address" and manual coordinates) and, for new users, from a one-time FirstRunLocationScreen shown right after the language picker on first boot, explaining why an exact address beats automatic IP geolocation and letting them set it up immediately (skippable). Removes the itilog.com link/tip added in the previous commit (Location Presets info screen, all 6 languages, and the README) - superseded by this native in-app flow. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -23,10 +23,6 @@ You can flash the firmware directly from your browser to your CYD display withou
|
||||
2. Insert a FAT32-formatted microSD card into your display.
|
||||
3. On first boot: Calibrate the touchscreen and configure your Wi-Fi via the built-in on-screen keyboard.
|
||||
|
||||
**Tip:** for the best experience, set your exact location as a Location Preset (Menu → Flight Options → Location Presets) instead of relying on automatic IP-based geolocation. IP geolocation can easily be off by 20-30 km - you might hear a plane outside your window while the radar screen stays empty because the device thinks you're somewhere else entirely.
|
||||
|
||||
To get your exact coordinates for free, visit [itilog.com](https://www.itilog.com/), enter your address, and copy the resulting latitude/longitude into a new Location Preset. With your precise location set, the proximity LED blinks in sync with aircraft you can actually hear overhead.
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
524
src/address_search_screen.cpp
Normal file
524
src/address_search_screen.cpp
Normal file
@@ -0,0 +1,524 @@
|
||||
#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).
|
||||
int16_t layoutWrapped(TFT_eSPI& tft, int16_t x, int16_t startY, int16_t maxWidth,
|
||||
int16_t lineHeight, const String& text) {
|
||||
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);
|
||||
}
|
||||
tft.setCursor(x, y);
|
||||
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";
|
||||
constexpr const char* ROW3 = "ZXCVBNM";
|
||||
|
||||
// Sonderzeichen-Seite: nur Zeichen, die bereits an anderer Stelle in
|
||||
// der App verwendet werden (i18n_de/fr/tr/es/it.h) - der Font
|
||||
// (UiFont11pt, deckt U+0020-U+015F ab) stellt sie garantiert korrekt
|
||||
// dar. Deckt die in DE/FR/ES/IT/TR-Adressen gaengigsten Sonderzeichen ab.
|
||||
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 ROW0_Y = 78;
|
||||
|
||||
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[7];
|
||||
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, 7);
|
||||
|
||||
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};
|
||||
|
||||
constexpr int16_t BTN_ROW_Y = 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.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(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 < 7; 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 < 7 && !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 ROW0_Y = 78;
|
||||
|
||||
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.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, 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();
|
||||
tft.fillScreen(TFT_BLACK);
|
||||
tft.setTextColor(TFT_GREEN, TFT_BLACK);
|
||||
tft.setCursor(10, 14);
|
||||
tft.println(I18n::t(StringId::ADDRESS_SEARCH_CONFIRM_TITLE));
|
||||
|
||||
String shown = displayName;
|
||||
if (shown.length() > 150) shown = shown.substring(0, 150) + "...";
|
||||
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
||||
layoutWrapped(tft, 10, 40, (int16_t)(Config::SCREEN_WIDTH - 20), 18, shown);
|
||||
|
||||
Rect tryAgainBtn = {10, (int16_t)(Config::SCREEN_HEIGHT - 96), (int16_t)(Config::SCREEN_WIDTH - 20), 40};
|
||||
Rect useBtn = {10, (int16_t)(Config::SCREEN_HEIGHT - 50), (int16_t)(Config::SCREEN_WIDTH - 20), 40};
|
||||
drawButton(tft, tryAgainBtn, I18n::t(StringId::ADDRESS_SEARCH_TRY_AGAIN));
|
||||
drawButton(tft, useBtn, I18n::t(StringId::ADDRESS_SEARCH_USE_THIS));
|
||||
|
||||
while (true) {
|
||||
TouchInput::Point tap;
|
||||
if (!TouchInput::wasTapped(tap)) { MenuStars::update(tft); delay(20); continue; }
|
||||
if (tryAgainBtn.contains(tap.x, tap.y)) return 0;
|
||||
if (useBtn.contains(tap.x, tap.y)) return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
return LocationPresets::addPreset(lat, lon, name);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
11
src/address_search_screen.h
Normal file
11
src/address_search_screen.h
Normal file
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
#include <Arduino.h>
|
||||
#include <TFT_eSPI.h>
|
||||
|
||||
namespace AddressSearchScreen {
|
||||
// Adresse eingeben (mit Sonderzeichen-Tastatur) -> per Nominatim
|
||||
// (OpenStreetMap) geokodieren -> Ergebnis bestaetigen -> Namen vergeben
|
||||
// -> als neuen Standort-Preset speichern. Gibt true zurueck, wenn ein
|
||||
// Preset hinzugefuegt wurde, false bei Abbruch durch den Nutzer.
|
||||
bool run(TFT_eSPI& tft);
|
||||
}
|
||||
@@ -24,6 +24,13 @@ namespace Config {
|
||||
constexpr const char* IP_GEO_HOST = "ip-api.com";
|
||||
constexpr const char* IP_GEO_PATH = "/json/?fields=status,lat,lon,offset,countryCode";
|
||||
|
||||
// Adresssuche (AddressSearchScreen) - kostenloser, anmeldefreier
|
||||
// Geokodierungs-Dienst (OpenStreetMap Nominatim). Deren Nutzungsregeln
|
||||
// verlangen einen aussagekraeftigen User-Agent statt des HTTPClient-
|
||||
// Standardwerts, siehe https://operations.osmfoundation.org/policies/nominatim/.
|
||||
constexpr const char* NOMINATIM_HOST = "nominatim.openstreetmap.org";
|
||||
constexpr const char* NOMINATIM_USER_AGENT = "EiswolfsFlightradarCYD (github.com/Eiswolf-BG/eiswolfs-flightradar-CYD)";
|
||||
|
||||
struct GpsPinPair { uint8_t rx; uint8_t tx; const char* label; };
|
||||
constexpr GpsPinPair GPS_PIN_CANDIDATES[] = {
|
||||
{22, 27, "G22/G27"}
|
||||
|
||||
87
src/first_run_location_screen.cpp
Normal file
87
src/first_run_location_screen.cpp
Normal file
@@ -0,0 +1,87 @@
|
||||
#include "first_run_location_screen.h"
|
||||
#include "address_search_screen.h"
|
||||
#include "location_presets.h"
|
||||
#include "touch_input.h"
|
||||
#include "menu_stars.h"
|
||||
#include "config.h"
|
||||
#include "i18n.h"
|
||||
|
||||
namespace FirstRunLocationScreen {
|
||||
|
||||
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) {
|
||||
tft.drawRoundRect(r.x, r.y, r.w, r.h, 4, TFT_GREEN);
|
||||
tft.setTextDatum(MC_DATUM);
|
||||
tft.setTextColor(TFT_GREEN, TFT_BLACK);
|
||||
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.
|
||||
int16_t layoutWrapped(TFT_eSPI& tft, int16_t x, int16_t startY, int16_t maxWidth,
|
||||
int16_t lineHeight, const String& text) {
|
||||
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);
|
||||
}
|
||||
tft.setCursor(x, y);
|
||||
tft.print(line);
|
||||
y += lineHeight;
|
||||
start += line.length();
|
||||
}
|
||||
return y;
|
||||
}
|
||||
}
|
||||
|
||||
void run(TFT_eSPI& tft) {
|
||||
MenuStars::reset();
|
||||
tft.fillScreen(TFT_BLACK);
|
||||
tft.setTextColor(TFT_GREEN, TFT_BLACK);
|
||||
tft.setCursor(10, 14);
|
||||
tft.println(I18n::t(StringId::FIRST_RUN_LOCATION_TITLE));
|
||||
|
||||
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
||||
layoutWrapped(tft, 10, 40, (int16_t)(Config::SCREEN_WIDTH - 20), 18,
|
||||
I18n::t(StringId::FIRST_RUN_LOCATION_BODY));
|
||||
|
||||
Rect setBtn = {10, (int16_t)(Config::SCREEN_HEIGHT - 96), (int16_t)(Config::SCREEN_WIDTH - 20), 40};
|
||||
Rect skipBtn = {10, (int16_t)(Config::SCREEN_HEIGHT - 50), (int16_t)(Config::SCREEN_WIDTH - 20), 40};
|
||||
drawButton(tft, setBtn, I18n::t(StringId::FIRST_RUN_LOCATION_SET_BTN));
|
||||
drawButton(tft, skipBtn, I18n::t(StringId::FIRST_RUN_LOCATION_SKIP_BTN));
|
||||
|
||||
while (true) {
|
||||
TouchInput::Point tap;
|
||||
if (!TouchInput::wasTapped(tap)) { MenuStars::update(tft); delay(20); continue; }
|
||||
|
||||
if (setBtn.contains(tap.x, tap.y)) {
|
||||
if (AddressSearchScreen::run(tft)) {
|
||||
// Direkt aktivieren - das ist der ganze Sinn dieses
|
||||
// Screens (sofort beim ersten Start den praezisen
|
||||
// Standort nutzen, statt weiter auf Auto/IP zu bleiben).
|
||||
uint8_t idx = LocationPresets::count();
|
||||
if (idx > 0) LocationPresets::setActiveIndex((int8_t)(idx - 1));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (skipBtn.contains(tap.x, tap.y)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
11
src/first_run_location_screen.h
Normal file
11
src/first_run_location_screen.h
Normal file
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
#include <Arduino.h>
|
||||
#include <TFT_eSPI.h>
|
||||
|
||||
namespace FirstRunLocationScreen {
|
||||
// Einmaliger Hinweis-Screen beim allerersten Start (nach der
|
||||
// Sprachauswahl) - erklaert den Genauigkeitsvorteil eines per Adresse
|
||||
// gesetzten Standorts, mit direktem Einstieg in die Adresssuche.
|
||||
// Ueberspringbar.
|
||||
void run(TFT_eSPI& tft);
|
||||
}
|
||||
29
src/i18n.h
29
src/i18n.h
@@ -221,11 +221,30 @@ enum class StringId : uint8_t {
|
||||
WEATHER_INFO_TITLE,
|
||||
WEATHER_INFO_BODY,
|
||||
|
||||
// Zusaetzlicher Info-Absatz im Standort-Presets-Screen (Menue >
|
||||
// Flugoptionen > Standort-Presets > "?") mit Tipp zu itilog.com fuer
|
||||
// exakte Koordinaten - deutlich genauer als die automatische
|
||||
// IP-Standortbestimmung (die leicht 20-30km danebenliegen kann).
|
||||
LOCATION_INFO_PARA6,
|
||||
// Adresssuche (AddressSearchScreen) - Standort per Adresseingabe statt
|
||||
// manueller Koordinaten, per kostenlosem Nominatim-Geokodierungsdienst.
|
||||
// Erreichbar sowohl beim Ersteinrichten (nach der Sprachauswahl) als
|
||||
// auch jederzeit ueber Standort-Presets > "+".
|
||||
LOCATION_ADD_CHOICE_TITLE,
|
||||
LOCATION_ADD_BY_COORDS,
|
||||
LOCATION_ADD_BY_ADDRESS,
|
||||
ADDRESS_SEARCH_TITLE,
|
||||
ADDRESS_SEARCH_SEARCHING,
|
||||
ADDRESS_SEARCH_NO_RESULTS,
|
||||
ADDRESS_SEARCH_ERROR,
|
||||
ADDRESS_SEARCH_CONFIRM_TITLE,
|
||||
ADDRESS_SEARCH_USE_THIS,
|
||||
ADDRESS_SEARCH_TRY_AGAIN,
|
||||
ADDRESS_SEARCH_CANCEL,
|
||||
|
||||
// Grosser Hinweis-Screen beim allerersten Start (nur einmalig, nach der
|
||||
// Sprachauswahl) - erklaert den Genauigkeitsvorteil eines per Adresse
|
||||
// gesetzten Standorts gegenueber der automatischen IP-Standort-
|
||||
// bestimmung, mit direktem Einstieg in die Adresssuche. Ueberspringbar.
|
||||
FIRST_RUN_LOCATION_TITLE,
|
||||
FIRST_RUN_LOCATION_BODY,
|
||||
FIRST_RUN_LOCATION_SET_BTN,
|
||||
FIRST_RUN_LOCATION_SKIP_BTN,
|
||||
|
||||
COUNT
|
||||
};
|
||||
|
||||
@@ -204,7 +204,21 @@ static const char* const I18N_DE[] = {
|
||||
"Wetter",
|
||||
"Das Wetter-Icon zeigt die aktuelle Wetterlage für deinen aktiven Standort. Ist ein Standort-Preset ausgewählt, wird stattdessen dessen Wetter angezeigt.",
|
||||
|
||||
"Tipp: Auf itilog.com (https://www.itilog.com/) kannst du kostenlos deine Adresse eingeben und bekommst sofort die exakten Koordinaten dafür. Das lohnt sich - die automatische IP-Standortbestimmung kann leicht 20-30 km danebenliegen, sodass du ein Flugzeug zwar hörst, der Radar aber leer bleibt. Mit den exakten Koordinaten als Preset eingetragen, blinkt die Näherungs-LED wirklich im Takt mit dem Flugzeug über dir.",
|
||||
"Standort hinzufügen",
|
||||
"Koordinaten eingeben",
|
||||
"Per Adresse suchen",
|
||||
"Adresse eingeben",
|
||||
"Suche läuft...",
|
||||
"Keine Ergebnisse. Andere Schreibweise versuchen.",
|
||||
"Suche fehlgeschlagen. WLAN-Verbindung prüfen.",
|
||||
"Ist das der richtige Ort?",
|
||||
"Diesen Standort verwenden",
|
||||
"Erneut versuchen",
|
||||
"Abbrechen",
|
||||
"Exakten Standort einrichten",
|
||||
"Die automatische IP-Standortbestimmung kann leicht 20-30 km danebenliegen - du hörst vielleicht ein Flugzeug, während der Radar leer bleibt. Trage jetzt deine Adresse für exakte Koordinaten ein, für ein deutlich besseres Erlebnis. Die Näherungs-LED blinkt dann wirklich im Takt mit dem Flugzeug über dir. Du kannst das auch später jederzeit über Menü > Flugoptionen > Standort-Presets nachholen.",
|
||||
"Adresse eingeben",
|
||||
"Überspringen",
|
||||
};
|
||||
|
||||
static_assert(sizeof(I18N_DE) / sizeof(I18N_DE[0]) == (size_t)StringId::COUNT,
|
||||
|
||||
@@ -205,7 +205,21 @@ static const char* const I18N_EN[] = {
|
||||
"Weather",
|
||||
"The weather icon shows the current weather for your active location. If a location preset is selected, its weather is shown instead.",
|
||||
|
||||
"Tip: at itilog.com (https://www.itilog.com/) you can enter your address for free and instantly get its exact coordinates. It's worth it - automatic IP-based location can easily be off by 20-30 km, so you might hear a plane overhead while the radar stays empty. With the exact coordinates entered as a preset, the proximity LED really does blink in sync with the aircraft above you.",
|
||||
"Add Location",
|
||||
"Enter coordinates",
|
||||
"Search by address",
|
||||
"Enter your address",
|
||||
"Searching...",
|
||||
"No results found. Try a different spelling.",
|
||||
"Search failed. Check your WiFi connection.",
|
||||
"Is this the right place?",
|
||||
"Use this location",
|
||||
"Try again",
|
||||
"Cancel",
|
||||
"Set Your Exact Location",
|
||||
"Automatic IP-based location can easily be 20-30 km off - you might hear a plane overhead while the radar stays empty. Enter your address now for exact coordinates and a much better experience. The proximity LED will then really blink in sync with the aircraft above you. You can also do this later anytime via Menu > Flight Options > Location Presets.",
|
||||
"Enter Address",
|
||||
"Skip",
|
||||
};
|
||||
|
||||
static_assert(sizeof(I18N_EN) / sizeof(I18N_EN[0]) == (size_t)StringId::COUNT,
|
||||
|
||||
@@ -204,7 +204,21 @@ static const char* const I18N_ES[] = {
|
||||
"Clima",
|
||||
"El icono del clima muestra el clima actual de tu ubicación activa. Si hay una ubicación guardada seleccionada, se muestra el clima de esa ubicación en su lugar.",
|
||||
|
||||
"Consejo: en itilog.com (https://www.itilog.com/) introduce tu dirección gratis y obtén al instante sus coordenadas exactas. Merece la pena - la geolocalización automática por IP puede fallar fácilmente por 20-30 km, así que podrías oír un avión mientras el radar permanece vacío. Con las coordenadas exactas guardadas como preset, el LED de proximidad realmente parpadea al ritmo del avión que tienes encima.",
|
||||
"Añadir ubicación",
|
||||
"Introducir coordenadas",
|
||||
"Buscar por dirección",
|
||||
"Introduce tu dirección",
|
||||
"Buscando...",
|
||||
"Sin resultados. Prueba otra ortografía.",
|
||||
"Error en la búsqueda. Revisa tu conexión WiFi.",
|
||||
"¿Es este el lugar correcto?",
|
||||
"Usar esta ubicación",
|
||||
"Intentar de nuevo",
|
||||
"Cancelar",
|
||||
"Configura tu ubicación exacta",
|
||||
"La geolocalización automática por IP puede fallar fácilmente por 20-30 km - podrías oír un avión mientras el radar permanece vacío. Introduce tu dirección ahora para obtener coordenadas exactas y una experiencia mucho mejor. El LED de proximidad parpadeará entonces realmente al ritmo del avión que tienes encima. También puedes hacerlo más tarde en cualquier momento desde Menú > Opciones de vuelo > Ubicaciones guardadas.",
|
||||
"Introducir dirección",
|
||||
"Omitir",
|
||||
};
|
||||
|
||||
static_assert(sizeof(I18N_ES) / sizeof(I18N_ES[0]) == (size_t)StringId::COUNT,
|
||||
|
||||
@@ -204,7 +204,21 @@ static const char* const I18N_FR[] = {
|
||||
"Météo",
|
||||
"L'icône météo indique la situation actuelle pour votre position active. Si une position enregistrée est sélectionnée, sa météo est affichée à la place.",
|
||||
|
||||
"Astuce : sur itilog.com (https://www.itilog.com/), entrez gratuitement votre adresse pour obtenir ses coordonnées exactes. Cela en vaut la peine - la localisation automatique par IP peut facilement se tromper de 20 à 30 km, si bien que vous entendez un avion alors que le radar reste vide. Avec les coordonnées exactes enregistrées comme preset, la LED de proximité clignote vraiment en rythme avec l'avion au-dessus de vous.",
|
||||
"Ajouter une position",
|
||||
"Entrer les coordonnées",
|
||||
"Rechercher par adresse",
|
||||
"Entrez votre adresse",
|
||||
"Recherche en cours...",
|
||||
"Aucun résultat. Essayez une autre orthographe.",
|
||||
"Échec de la recherche. Vérifiez votre connexion WiFi.",
|
||||
"Est-ce le bon endroit ?",
|
||||
"Utiliser cette position",
|
||||
"Réessayer",
|
||||
"Annuler",
|
||||
"Définissez votre position exacte",
|
||||
"La localisation automatique par IP peut facilement se tromper de 20 à 30 km - vous pourriez entendre un avion alors que le radar reste vide. Entrez votre adresse maintenant pour des coordonnées exactes et une bien meilleure expérience. La LED de proximité clignotera alors vraiment en rythme avec l'avion au-dessus de vous. Vous pouvez aussi le faire plus tard via Menu > Options de vol > Positions enregistrées.",
|
||||
"Entrer l'adresse",
|
||||
"Passer",
|
||||
};
|
||||
|
||||
static_assert(sizeof(I18N_FR) / sizeof(I18N_FR[0]) == (size_t)StringId::COUNT,
|
||||
|
||||
@@ -204,7 +204,21 @@ static const char* const I18N_IT[] = {
|
||||
"Meteo",
|
||||
"L'icona meteo mostra le condizioni meteo attuali per la tua posizione attiva. Se è selezionata una posizione salvata, viene mostrato invece il meteo di quella posizione.",
|
||||
|
||||
"Suggerimento: su itilog.com (https://www.itilog.com/) inserisci gratuitamente il tuo indirizzo per ottenere subito le coordinate esatte. Ne vale la pena - la geolocalizzazione IP automatica può sbagliare facilmente di 20-30 km, quindi potresti sentire un aereo mentre il radar resta vuoto. Con le coordinate esatte inserite come preset, il LED di prossimità lampeggia davvero al ritmo dell'aereo sopra di te.",
|
||||
"Aggiungi posizione",
|
||||
"Inserisci coordinate",
|
||||
"Cerca per indirizzo",
|
||||
"Inserisci il tuo indirizzo",
|
||||
"Ricerca in corso...",
|
||||
"Nessun risultato. Prova un'altra grafia.",
|
||||
"Ricerca fallita. Controlla la connessione WiFi.",
|
||||
"È il posto giusto?",
|
||||
"Usa questa posizione",
|
||||
"Riprova",
|
||||
"Annulla",
|
||||
"Imposta la tua posizione esatta",
|
||||
"La geolocalizzazione IP automatica può sbagliare facilmente di 20-30 km - potresti sentire un aereo mentre il radar resta vuoto. Inserisci ora il tuo indirizzo per coordinate esatte e un'esperienza molto migliore. Il LED di prossimità lampeggerà allora davvero al ritmo dell'aereo sopra di te. Puoi farlo anche più tardi in qualsiasi momento tramite Menu > Opzioni di volo > Posizioni salvate.",
|
||||
"Inserisci indirizzo",
|
||||
"Salta",
|
||||
};
|
||||
|
||||
static_assert(sizeof(I18N_IT) / sizeof(I18N_IT[0]) == (size_t)StringId::COUNT,
|
||||
|
||||
@@ -204,7 +204,21 @@ static const char* const I18N_TR[] = {
|
||||
"Hava Durumu",
|
||||
"Hava durumu simgesi, aktif konumunuz için güncel hava durumunu gösterir. Bir konum ön ayarı seçiliyse, bunun yerine o ön ayarın hava durumu gösterilir.",
|
||||
|
||||
"İpucu: itilog.com (https://www.itilog.com/) adresinden adresinizi ücretsiz girerek anında tam koordinatlarınızı alabilirsiniz. Buna değer - otomatik IP tabanlı konum belirleme kolayca 20-30 km sapabilir, böylece üstünüzden geçen bir uçağı duyarken radar boş kalabilir. Tam koordinatlar bir ön ayar olarak girildiğinde, yakınlık LED'i gerçekten üstünüzdeki uçakla aynı ritimde yanıp söner.",
|
||||
"Konum Ekle",
|
||||
"Koordinat gir",
|
||||
"Adresle ara",
|
||||
"Adresinizi girin",
|
||||
"Aranıyor...",
|
||||
"Sonuç bulunamadı. Farklı bir yazım deneyin.",
|
||||
"Arama başarısız. WiFi bağlantınızı kontrol edin.",
|
||||
"Doğru yer mi?",
|
||||
"Bu konumu kullan",
|
||||
"Tekrar dene",
|
||||
"İptal",
|
||||
"Tam Konumunuzu Ayarlayın",
|
||||
"Otomatik IP tabanlı konum belirleme kolayca 20-30 km sapabilir - üstünüzden geçen bir uçağı duyarken radar boş kalabilir. Tam koordinatlar ve çok daha iyi bir deneyim için şimdi adresinizi girin. Yakınlık LED'i o zaman gerçekten üstünüzdeki uçakla aynı ritimde yanıp söner. Bunu daha sonra istediğiniz zaman Menü > Uçuş Seçenekleri > Konum ön ayarları üzerinden de yapabilirsiniz.",
|
||||
"Adres Gir",
|
||||
"Atla",
|
||||
};
|
||||
|
||||
static_assert(sizeof(I18N_TR) / sizeof(I18N_TR[0]) == (size_t)StringId::COUNT,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "location_presets.h"
|
||||
#include "location_manager.h"
|
||||
#include "airport_lookup.h"
|
||||
#include "address_search_screen.h"
|
||||
#include "touch_input.h"
|
||||
#include "menu_stars.h"
|
||||
#include "config.h"
|
||||
@@ -229,7 +230,7 @@ namespace {
|
||||
return confirmed ? String(buf) : String();
|
||||
}
|
||||
|
||||
bool addPresetFlow(TFT_eSPI& tft) {
|
||||
bool addPresetByCoordsFlow(TFT_eSPI& tft) {
|
||||
String latStr = runNumericKeypad(tft, I18n::t(StringId::LOCATION_LAT_PROMPT));
|
||||
if (latStr.length() == 0) return false;
|
||||
|
||||
@@ -244,6 +245,33 @@ namespace {
|
||||
return LocationPresets::addPreset(lat, lon, name);
|
||||
}
|
||||
|
||||
// Erster Schritt beim Antippen von "+": manuelle Koordinaten oder
|
||||
// Adresssuche (AddressSearchScreen, kuemmert sich dort bereits selbst
|
||||
// um Namensvergabe + Speichern als Preset).
|
||||
bool addPresetFlow(TFT_eSPI& tft) {
|
||||
MenuStars::reset();
|
||||
tft.fillScreen(TFT_BLACK);
|
||||
tft.setTextColor(TFT_GREEN, TFT_BLACK);
|
||||
tft.setCursor(10, 14);
|
||||
tft.println(I18n::t(StringId::LOCATION_ADD_CHOICE_TITLE));
|
||||
|
||||
Rect addressBtn = {10, 90, (int16_t)(Config::SCREEN_WIDTH - 20), 44};
|
||||
Rect coordsBtn = {10, 144, (int16_t)(Config::SCREEN_WIDTH - 20), 44};
|
||||
Rect cancelBtn = {10, (int16_t)(Config::SCREEN_HEIGHT - 50), (int16_t)(Config::SCREEN_WIDTH - 20), 40};
|
||||
drawButton(tft, addressBtn, I18n::t(StringId::LOCATION_ADD_BY_ADDRESS));
|
||||
drawButton(tft, coordsBtn, I18n::t(StringId::LOCATION_ADD_BY_COORDS));
|
||||
drawButton(tft, cancelBtn, I18n::t(StringId::CANCEL), false, true);
|
||||
|
||||
while (true) {
|
||||
TouchInput::Point tap;
|
||||
if (!TouchInput::wasTapped(tap)) { MenuStars::update(tft); delay(20); continue; }
|
||||
|
||||
if (addressBtn.contains(tap.x, tap.y)) return AddressSearchScreen::run(tft);
|
||||
if (coordsBtn.contains(tap.x, tap.y)) return addPresetByCoordsFlow(tft);
|
||||
if (cancelBtn.contains(tap.x, tap.y)) return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Kurze Meldung unten am Bildschirmrand, z.B. wenn beim Antippen des
|
||||
// Naechster-Flughafen-Textes bereits alle 3 Preset-Slots belegt sind.
|
||||
void showBriefMessage(TFT_eSPI& tft, const String& msg, uint16_t color) {
|
||||
@@ -406,8 +434,6 @@ namespace {
|
||||
totalH = layoutWrapped(tft, 10, totalH, textMaxWidth, LINE_H, I18n::t(StringId::LOCATION_INFO_PARA4), 0, 0, 0, false);
|
||||
totalH += 8;
|
||||
totalH = layoutWrapped(tft, 10, totalH, textMaxWidth, LINE_H, I18n::t(StringId::LOCATION_INFO_PARA5), 0, 0, 0, false);
|
||||
totalH += 8;
|
||||
totalH = layoutWrapped(tft, 10, totalH, textMaxWidth, LINE_H, I18n::t(StringId::LOCATION_INFO_PARA6), 0, 0, 0, false);
|
||||
|
||||
int16_t maxScroll = totalH - VIEW_BOTTOM;
|
||||
if (maxScroll < 0) maxScroll = 0;
|
||||
@@ -437,9 +463,7 @@ namespace {
|
||||
y += 8;
|
||||
y = layoutWrapped(tft, 10, y, textMaxWidth, LINE_H, I18n::t(StringId::LOCATION_INFO_PARA4), scrollY, VIEW_TOP, VIEW_BOTTOM, true);
|
||||
y += 8;
|
||||
y = layoutWrapped(tft, 10, y, textMaxWidth, LINE_H, I18n::t(StringId::LOCATION_INFO_PARA5), scrollY, VIEW_TOP, VIEW_BOTTOM, true);
|
||||
y += 8;
|
||||
layoutWrapped(tft, 10, y, textMaxWidth, LINE_H, I18n::t(StringId::LOCATION_INFO_PARA6), scrollY, VIEW_TOP, VIEW_BOTTOM, true);
|
||||
layoutWrapped(tft, 10, y, textMaxWidth, LINE_H, I18n::t(StringId::LOCATION_INFO_PARA5), scrollY, VIEW_TOP, VIEW_BOTTOM, true);
|
||||
|
||||
drawButton(tft, backBtn, I18n::t(StringId::BACK));
|
||||
if (scrollable) {
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#include "weather.h"
|
||||
#include "i18n.h"
|
||||
#include "first_run_language_screen.h"
|
||||
#include "first_run_location_screen.h"
|
||||
#include "menu_stars.h"
|
||||
#include "ui_font.h"
|
||||
#include <math.h>
|
||||
@@ -490,6 +491,11 @@ void setup() {
|
||||
|
||||
if (isFirstRun) {
|
||||
FirstRunLanguageScreen::run(tft);
|
||||
// Erst NACH der Sprachauswahl (Texte erscheinen dann gleich in der
|
||||
// richtigen Sprache) und NACH dem WLAN-Verbindungsversuch weiter
|
||||
// oben (die Adresssuche braucht eine Internetverbindung) -
|
||||
// ueberspringbar, siehe first_run_location_screen.cpp.
|
||||
FirstRunLocationScreen::run(tft);
|
||||
SplashScreen::begin(tft);
|
||||
MenuStars::update(tft);
|
||||
SplashScreen::setStatusLine(tft, 0, I18n::t(StringId::SPLASH_SD_OK), TFT_WHITE);
|
||||
|
||||
Reference in New Issue
Block a user