v1.3: Screenshot, double-tap zoom, statistics screen, flight trail

This commit is contained in:
Eiswolf-BG
2026-08-03 04:47:53 +02:00
parent b913ca2f44
commit 7b19726b95
11 changed files with 323 additions and 44 deletions

View File

@@ -58,6 +58,12 @@ namespace {
char selectedHex[7] = {0};
uint32_t lastEmptyTapMs = 0;
int16_t lastEmptyTapX = -1000;
int16_t lastEmptyTapY = -1000;
constexpr uint32_t DOUBLE_TAP_MS = 400;
constexpr int16_t DOUBLE_TAP_RADIUS = 25;
bool ledBlinkOn = true;
bool isEmergencySquawk(const char* squawk) {
@@ -78,6 +84,79 @@ namespace {
return TFT_RED;
}
// Gedaempfte Version der Hoehenfarbe fuer die Flugbahn-Linie (Trail) -
// deutlich dunkler als der Punkt selbst, damit der Trail nicht mit dem
// aktuellen Punkt/Label verwechselt wird.
uint16_t dimColorForAltitude(int32_t altFt) {
if (altFt < Config::COLOR_LOW_ALT_THRESHOLD_FT) return TFT_DARKGREEN;
if (altFt < Config::COLOR_MID_ALT_THRESHOLD_FT) return TFT_OLIVE;
return TFT_MAROON;
}
// --- Flugbahn-Trail: merkt sich die letzten paar Bildschirm-Positionen
// pro Flugzeug (per Hex-Code), damit man die grobe Flugrichtung ueber die
// letzten Datenupdates hinweg sieht. Wird nur bei render() (Vollbild-
// Redraw) aktualisiert/gezeichnet, nicht bei jedem Sweep-Tick.
constexpr uint8_t TRAIL_LEN = 4;
constexpr uint32_t TRAIL_STALE_MS = Config::FETCH_INTERVAL_MS * 3;
struct TrailEntry {
char hex[7] = {0};
bool active = false;
int16_t xs[TRAIL_LEN] = {0};
int16_t ys[TRAIL_LEN] = {0};
uint8_t count = 0;
uint32_t lastUpdateMs = 0;
};
constexpr uint8_t MAX_TRAILS = Config::MAX_TRACKED_AIRCRAFT;
TrailEntry trails[MAX_TRAILS];
void pruneStaleTrails() {
uint32_t now = millis();
for (uint8_t i = 0; i < MAX_TRAILS; i++) {
if (trails[i].active && now - trails[i].lastUpdateMs > TRAIL_STALE_MS) {
trails[i] = TrailEntry{};
}
}
}
TrailEntry* findOrCreateTrail(const char* hex) {
int16_t freeIdx = -1;
for (uint8_t i = 0; i < MAX_TRAILS; i++) {
if (trails[i].active && strcmp(trails[i].hex, hex) == 0) return &trails[i];
if (!trails[i].active && freeIdx < 0) freeIdx = (int16_t)i;
}
if (freeIdx < 0) return nullptr;
trails[freeIdx] = TrailEntry{};
strncpy(trails[freeIdx].hex, hex, sizeof(trails[freeIdx].hex) - 1);
trails[freeIdx].active = true;
return &trails[freeIdx];
}
void pushTrailPoint(TrailEntry* t, int16_t x, int16_t y) {
if (!t) return;
if (t->count < TRAIL_LEN) {
t->xs[t->count] = x;
t->ys[t->count] = y;
t->count++;
} else {
for (uint8_t i = 1; i < TRAIL_LEN; i++) {
t->xs[i - 1] = t->xs[i];
t->ys[i - 1] = t->ys[i];
}
t->xs[TRAIL_LEN - 1] = x;
t->ys[TRAIL_LEN - 1] = y;
}
t->lastUpdateMs = millis();
}
void drawTrail(TFT_eSPI& gfx, const TrailEntry* t, uint16_t color) {
if (!t || t->count < 2) return;
for (uint8_t i = 1; i < t->count; i++) {
gfx.drawLine(t->xs[i - 1], t->ys[i - 1], t->xs[i], t->ys[i], color);
}
}
void printLineTruncated(TFT_eSPI& gfx, int16_t x, int16_t y, int16_t maxWidth, const String& text) {
String s = text;
if (gfx.textWidth(s) > maxWidth) {
@@ -99,9 +178,6 @@ namespace {
gfx.setTextDatum(TL_DATUM);
}
// Kleine Farb-Legende (Hoehenband -> Farbe), damit auf den ersten Blick
// klar ist, was Gruen/Gelb/Rot bei den Flugzeug-Punkten bedeuten. Zeigt
// Meter statt Fuss, wenn die Region (per Geolocation) metrisch ist.
void drawLegend(TFT_eSPI& gfx, int16_t y) {
bool metric = LocationManager::useMetricUnits();
@@ -309,6 +385,8 @@ void render(TFT_eSPI& tft, int16_t top) {
for (uint8_t i = 0; i < MAX_HIT_POINTS; i++) hitPoints[i].valid = false;
pruneStaleTrails();
for (uint8_t i = 0; i < count && i < MAX_HIT_POINTS; i++) {
Aircraft& a = snapshot[i];
if (a.distanceKm > rangeKm * 1.05f) continue;
@@ -320,6 +398,12 @@ void render(TFT_eSPI& tft, int16_t top) {
bool isSelected = selectedHex[0] && strcmp(a.hex, selectedHex) == 0;
bool isEmergency = SettingsStore::emergencyAlertEnabled() && isEmergencySquawk(a.squawk);
// Flugbahn-Trail: erst die bisherige Spur zeichnen, DANN den neuen
// Punkt anhaengen und den Punkt/Ring/Label darueber legen.
TrailEntry* trail = findOrCreateTrail(a.hex);
drawTrail(tft, trail, dimColorForAltitude(a.altBaroFt));
pushTrailPoint(trail, pt.x, pt.y);
if (isSelected) {
tft.drawCircle(pt.x, pt.y, 9, TFT_WHITE);
selectionStillPresent = true;
@@ -462,6 +546,21 @@ bool handleTap(int16_t x, int16_t y, int16_t top) {
}
}
uint32_t nowMs = millis();
bool isDoubleTap = (nowMs - lastEmptyTapMs <= DOUBLE_TAP_MS) &&
(abs((int)x - (int)lastEmptyTapX) <= DOUBLE_TAP_RADIUS) &&
(abs((int)y - (int)lastEmptyTapY) <= DOUBLE_TAP_RADIUS);
lastEmptyTapMs = nowMs;
lastEmptyTapX = x;
lastEmptyTapY = y;
if (isDoubleTap) {
uint8_t idx = SettingsStore::rangeIndex();
idx = (idx == 0) ? (Config::RANGE_STEP_COUNT - 1) : (idx - 1);
SettingsStore::setRangeIndex(idx);
lastEmptyTapMs = 0;
}
return true;
}