list view
This commit is contained in:
@@ -696,7 +696,7 @@ void loop() {
|
|||||||
updateStatusLine();
|
updateStatusLine();
|
||||||
forceRedraw = true;
|
forceRedraw = true;
|
||||||
} else if (tap.y >= CONTENT_TOP) {
|
} else if (tap.y >= CONTENT_TOP) {
|
||||||
if (RadarScreen::handleTap(tap.x, tap.y, CONTENT_TOP)) {
|
if (RadarScreen::handleTap(tft, tap.x, tap.y, CONTENT_TOP)) {
|
||||||
forceRedraw = true;
|
forceRedraw = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,11 @@
|
|||||||
#include "airline_filter.h"
|
#include "airline_filter.h"
|
||||||
#include "aircraft_watchlist.h"
|
#include "aircraft_watchlist.h"
|
||||||
#include "i18n.h"
|
#include "i18n.h"
|
||||||
|
#include "menu_stars.h"
|
||||||
|
#include "touch_input.h"
|
||||||
#include <math.h>
|
#include <math.h>
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
namespace RadarScreen {
|
namespace RadarScreen {
|
||||||
|
|
||||||
@@ -27,9 +30,10 @@ namespace {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Layout {
|
struct Layout {
|
||||||
int16_t cx, cy, radius;
|
int16_t cx, cy, radius;
|
||||||
Rect rangeBtn;
|
Rect rangeBtn;
|
||||||
|
Rect listBtn;
|
||||||
int16_t infoTop;
|
int16_t infoTop;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -45,7 +49,7 @@ namespace {
|
|||||||
return SettingsStore::hideGroundVehicles() ? INFO_BAR_H : (int16_t)(INFO_BAR_H + INFO_BAR_GROUND_ROW_H);
|
return SettingsStore::hideGroundVehicles() ? INFO_BAR_H : (int16_t)(INFO_BAR_H + INFO_BAR_GROUND_ROW_H);
|
||||||
}
|
}
|
||||||
|
|
||||||
Layout computeLayout(int16_t top) {
|
Layout computeLayout(int16_t top) {
|
||||||
Layout L;
|
Layout L;
|
||||||
L.infoTop = Config::SCREEN_HEIGHT - infoBarHeight();
|
L.infoTop = Config::SCREEN_HEIGHT - infoBarHeight();
|
||||||
constexpr int16_t TOP_LABEL_MARGIN = 10;
|
constexpr int16_t TOP_LABEL_MARGIN = 10;
|
||||||
@@ -55,6 +59,7 @@ namespace {
|
|||||||
L.cx = Config::SCREEN_WIDTH / 2;
|
L.cx = Config::SCREEN_WIDTH / 2;
|
||||||
L.cy = top + L.radius + 6 + TOP_LABEL_MARGIN;
|
L.cy = top + L.radius + 6 + TOP_LABEL_MARGIN;
|
||||||
L.rangeBtn = {(int16_t)(Config::SCREEN_WIDTH - 70), (int16_t)(L.infoTop + 4), 62, 22};
|
L.rangeBtn = {(int16_t)(Config::SCREEN_WIDTH - 70), (int16_t)(L.infoTop + 4), 62, 22};
|
||||||
|
L.listBtn = {(int16_t)(Config::SCREEN_WIDTH - 110), (int16_t)(L.infoTop + 4), 36, 22};
|
||||||
return L;
|
return L;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1025,10 +1030,10 @@ void render(TFT_eSPI& tft, int16_t top) {
|
|||||||
// Heartbeat-Blinken) - ohne dieses sofortige Nachzeichnen blieb die
|
// Heartbeat-Blinken) - ohne dieses sofortige Nachzeichnen blieb die
|
||||||
// Zeile bis zu 80ms lang leer, sichtbar als kurzes Blinken im
|
// Zeile bis zu 80ms lang leer, sichtbar als kurzes Blinken im
|
||||||
// Heartbeat-Takt.
|
// Heartbeat-Takt.
|
||||||
constexpr int16_t INFO_TEXT_X = 8;
|
constexpr int16_t INFO_TEXT_X = 8;
|
||||||
constexpr int16_t INFO_TEXT_GAP = 6;
|
constexpr int16_t INFO_TEXT_GAP = 6;
|
||||||
int16_t infoTextY = infoTop + 20;
|
int16_t infoTextY = infoTop + 20;
|
||||||
int16_t infoTextW = L.rangeBtn.x - INFO_TEXT_X - INFO_TEXT_GAP;
|
int16_t infoTextW = L.listBtn.x - INFO_TEXT_X - INFO_TEXT_GAP;
|
||||||
drawInfoMarquee(tft, INFO_TEXT_X, infoTextY, infoTextW);
|
drawInfoMarquee(tft, INFO_TEXT_X, infoTextY, infoTextW);
|
||||||
|
|
||||||
// Respektiert jetzt die Einheiten-Einstellung (Menue > Einheiten) -
|
// Respektiert jetzt die Einheiten-Einstellung (Menue > Einheiten) -
|
||||||
@@ -1041,6 +1046,7 @@ void render(TFT_eSPI& tft, int16_t top) {
|
|||||||
snprintf(rangeLabel, sizeof(rangeLabel), "%.0fnm", Units::kmToNm(rangeKm));
|
snprintf(rangeLabel, sizeof(rangeLabel), "%.0fnm", Units::kmToNm(rangeKm));
|
||||||
}
|
}
|
||||||
drawButton(tft, L.rangeBtn, rangeLabel);
|
drawButton(tft, L.rangeBtn, rangeLabel);
|
||||||
|
drawButton(tft, L.listBtn, "L");
|
||||||
|
|
||||||
drawLegend(tft, infoTop + 44);
|
drawLegend(tft, infoTop + 44);
|
||||||
}
|
}
|
||||||
@@ -1184,7 +1190,7 @@ void tick(TFT_eSPI& tft, int16_t top, uint32_t deltaMs) {
|
|||||||
constexpr int16_t INFO_TEXT_X = 8;
|
constexpr int16_t INFO_TEXT_X = 8;
|
||||||
constexpr int16_t INFO_TEXT_GAP = 6;
|
constexpr int16_t INFO_TEXT_GAP = 6;
|
||||||
int16_t infoTextY = L.infoTop + 20;
|
int16_t infoTextY = L.infoTop + 20;
|
||||||
int16_t infoTextW = L.rangeBtn.x - INFO_TEXT_X - INFO_TEXT_GAP;
|
int16_t infoTextW = L.listBtn.x - INFO_TEXT_X - INFO_TEXT_GAP;
|
||||||
|
|
||||||
if (kind != infoMarqueeKind) {
|
if (kind != infoMarqueeKind) {
|
||||||
// Die ART der Nachricht hat sich geaendert (z.B. letztes Flugzeug
|
// Die ART der Nachricht hat sich geaendert (z.B. letztes Flugzeug
|
||||||
@@ -1205,7 +1211,7 @@ void tick(TFT_eSPI& tft, int16_t top, uint32_t deltaMs) {
|
|||||||
tft.endWrite();
|
tft.endWrite();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool handleTap(int16_t x, int16_t y, int16_t top) {
|
bool handleTap(TFT_eSPI& tft, int16_t x, int16_t y, int16_t top) {
|
||||||
Layout L = computeLayout(top);
|
Layout L = computeLayout(top);
|
||||||
|
|
||||||
if (selectedHex[0]) {
|
if (selectedHex[0]) {
|
||||||
@@ -1224,6 +1230,11 @@ bool handleTap(int16_t x, int16_t y, int16_t top) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (L.listBtn.contains(x, y)) {
|
||||||
|
bool hadSelection = runTable(tft, top);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (L.rangeBtn.contains(x, y)) {
|
if (L.rangeBtn.contains(x, y)) {
|
||||||
uint8_t idx = (SettingsStore::rangeIndex() + 1) % Config::RANGE_STEP_COUNT;
|
uint8_t idx = (SettingsStore::rangeIndex() + 1) % Config::RANGE_STEP_COUNT;
|
||||||
SettingsStore::setRangeIndex(idx);
|
SettingsStore::setRangeIndex(idx);
|
||||||
@@ -1256,7 +1267,155 @@ bool handleTap(int16_t x, int16_t y, int16_t top) {
|
|||||||
lastEmptyTapMs = 0;
|
lastEmptyTapMs = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool runTable(TFT_eSPI& tft, int16_t top) {
|
||||||
|
constexpr int16_t ROW_H = 22;
|
||||||
|
constexpr int16_t ROW_GAP = 3;
|
||||||
|
constexpr int16_t PADDING = 6;
|
||||||
|
constexpr int16_t TABLE_LEFT = PADDING;
|
||||||
|
constexpr int16_t TABLE_RIGHT = Config::SCREEN_WIDTH - PADDING;
|
||||||
|
constexpr int16_t TABLE_W = TABLE_RIGHT - TABLE_LEFT;
|
||||||
|
int16_t TABLE_TOP = top + 4;
|
||||||
|
constexpr int16_t NAV_H = 24;
|
||||||
|
constexpr int16_t BTN_H = 30;
|
||||||
|
constexpr int16_t BTN_Y = Config::SCREEN_HEIGHT - BTN_H - 4;
|
||||||
|
constexpr int16_t NAV_Y = BTN_Y - ROW_H - ROW_GAP - 2;
|
||||||
|
|
||||||
|
int16_t tableBottom = NAV_Y - 4;
|
||||||
|
uint8_t rowsVisible = (tableBottom - TABLE_TOP) / (ROW_H + ROW_GAP);
|
||||||
|
if (rowsVisible < 1) rowsVisible = 1;
|
||||||
|
|
||||||
|
uint8_t scrollTop = 0;
|
||||||
|
bool done = false;
|
||||||
|
bool selected = false;
|
||||||
|
MenuStars::reset();
|
||||||
|
|
||||||
|
while (!done && !selected) {
|
||||||
|
static Aircraft snapshot[Config::MAX_TRACKED_AIRCRAFT];
|
||||||
|
uint8_t count = 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;
|
||||||
|
snapshot[count++] = table[i];
|
||||||
|
}
|
||||||
|
AircraftTable::unlock();
|
||||||
|
|
||||||
|
std::sort(snapshot, snapshot + count, [](const Aircraft& a, const Aircraft& b) {
|
||||||
|
return a.distanceKm < b.distanceKm;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (scrollTop > 0 && scrollTop + rowsVisible > count) {
|
||||||
|
scrollTop = (count > rowsVisible) ? (uint8_t)(count - rowsVisible) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
tft.fillRect(TABLE_LEFT, TABLE_TOP, TABLE_W, tableBottom - TABLE_TOP, TFT_BLACK);
|
||||||
|
|
||||||
|
bool metric = LocationManager::useMetricUnits();
|
||||||
|
int16_t y = TABLE_TOP;
|
||||||
|
uint8_t drawn = 0;
|
||||||
|
|
||||||
|
for (uint8_t i = scrollTop; i < count && drawn < rowsVisible; i++) {
|
||||||
|
Aircraft& a = snapshot[i];
|
||||||
|
Rect r = {TABLE_LEFT, y, TABLE_W, ROW_H};
|
||||||
|
|
||||||
|
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, 3, borderColor);
|
||||||
|
|
||||||
|
int16_t midY = r.y + r.h / 2;
|
||||||
|
|
||||||
|
const char* label = a.callsign[0] ? a.callsign : a.hex;
|
||||||
|
tft.setTextColor(TFT_GREEN, TFT_BLACK);
|
||||||
|
tft.setTextDatum(ML_DATUM);
|
||||||
|
tft.drawString(label, r.x + 4, midY);
|
||||||
|
|
||||||
|
char buf[16];
|
||||||
|
if (metric) {
|
||||||
|
snprintf(buf, sizeof(buf), "%.0fm", Units::feetToMeters((float)a.altBaroFt));
|
||||||
|
} else {
|
||||||
|
snprintf(buf, sizeof(buf), "%.0fft", (float)a.altBaroFt);
|
||||||
|
}
|
||||||
|
tft.setTextColor(colorForAltitude(tft, a.altBaroFt), TFT_BLACK);
|
||||||
|
tft.setTextDatum(MC_DATUM);
|
||||||
|
tft.drawString(buf, r.x + r.w / 2, midY);
|
||||||
|
|
||||||
|
if (metric) {
|
||||||
|
snprintf(buf, sizeof(buf), "%.1fkm", a.distanceKm);
|
||||||
|
} else {
|
||||||
|
snprintf(buf, sizeof(buf), "%.1fnm", Units::kmToNm(a.distanceKm));
|
||||||
|
}
|
||||||
|
tft.setTextColor(TFT_GREEN, TFT_BLACK);
|
||||||
|
tft.setTextDatum(MR_DATUM);
|
||||||
|
tft.drawString(buf, r.x + r.w - 4, midY);
|
||||||
|
|
||||||
|
tft.setTextDatum(TL_DATUM);
|
||||||
|
|
||||||
|
y += ROW_H + ROW_GAP;
|
||||||
|
drawn++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count == 0) {
|
||||||
|
tft.setTextColor(TFT_DARKGREY, TFT_BLACK);
|
||||||
|
tft.setTextDatum(MC_DATUM);
|
||||||
|
tft.drawString(I18n::t(StringId::AIRCRAFT_LIST_EMPTY),
|
||||||
|
Config::SCREEN_WIDTH / 2, (int16_t)(TABLE_TOP + ROW_H + 10));
|
||||||
|
tft.setTextDatum(TL_DATUM);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool canUp = scrollTop > 0;
|
||||||
|
bool canDown = (uint16_t)(scrollTop + rowsVisible) < count;
|
||||||
|
|
||||||
|
tft.fillRect(0, NAV_Y, Config::SCREEN_WIDTH, Config::SCREEN_HEIGHT - NAV_Y, TFT_BLACK);
|
||||||
|
|
||||||
|
Rect upBtn = {TABLE_LEFT, NAV_Y, 44, NAV_H};
|
||||||
|
Rect downBtn = {(int16_t)(TABLE_LEFT + 48), NAV_Y, 44, NAV_H};
|
||||||
|
Rect radarBtn = {(int16_t)(TABLE_LEFT + 100), NAV_Y, (int16_t)(TABLE_W - 100), NAV_H};
|
||||||
|
|
||||||
|
drawButton(tft, upBtn, "^");
|
||||||
|
drawButton(tft, downBtn, "v");
|
||||||
|
drawButton(tft, radarBtn, I18n::t(StringId::BACK));
|
||||||
|
|
||||||
|
TouchInput::Point tap;
|
||||||
|
while (true) {
|
||||||
|
if (TouchInput::wasTapped(tap)) break;
|
||||||
|
MenuStars::update(tft);
|
||||||
|
delay(20);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canUp && upBtn.contains(tap.x, tap.y)) {
|
||||||
|
scrollTop--;
|
||||||
|
} else if (canDown && downBtn.contains(tap.x, tap.y)) {
|
||||||
|
scrollTop++;
|
||||||
|
} else if (radarBtn.contains(tap.x, tap.y)) {
|
||||||
|
done = true;
|
||||||
|
} else {
|
||||||
|
y = TABLE_TOP;
|
||||||
|
for (uint8_t i = scrollTop; i < count; i++) {
|
||||||
|
uint8_t idx = (uint8_t)(i - scrollTop);
|
||||||
|
if (idx >= rowsVisible) break;
|
||||||
|
Rect r = {TABLE_LEFT, y, TABLE_W, ROW_H};
|
||||||
|
if (r.contains(tap.x, tap.y)) {
|
||||||
|
selectAircraft(snapshot[i].hex, snapshot[i].callsign);
|
||||||
|
selected = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
y += ROW_H + ROW_GAP;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait briefly to let the tap release propagate before render() runs
|
||||||
|
delay(50);
|
||||||
|
return selected;
|
||||||
}
|
}
|
||||||
|
|
||||||
void selectAircraft(const char* hex, const char* callsign) {
|
void selectAircraft(const char* hex, const char* callsign) {
|
||||||
|
|||||||
@@ -3,11 +3,16 @@
|
|||||||
#include <TFT_eSPI.h>
|
#include <TFT_eSPI.h>
|
||||||
|
|
||||||
namespace RadarScreen {
|
namespace RadarScreen {
|
||||||
void render(TFT_eSPI& tft, int16_t top);
|
void render(TFT_eSPI& tft, int16_t top);
|
||||||
void tick(TFT_eSPI& tft, int16_t top, uint32_t deltaMs);
|
void tick(TFT_eSPI& tft, int16_t top, uint32_t deltaMs);
|
||||||
bool handleTap(int16_t x, int16_t y, int16_t top);
|
bool handleTap(TFT_eSPI& tft, int16_t x, int16_t y, int16_t top);
|
||||||
void updateProximityAlert(uint32_t nowMs);
|
void updateProximityAlert(uint32_t nowMs);
|
||||||
|
|
||||||
|
// Switchable table view: shows a sortable list of aircraft directly
|
||||||
|
// on the radar screen (toggled via a "L" button in the info bar).
|
||||||
|
// Returns true if user selected an aircraft (opens detail panel).
|
||||||
|
bool runTable(TFT_eSPI& tft, int16_t top);
|
||||||
|
|
||||||
// Waehlt ein Flugzeug programmgesteuert aus (z.B. von der Flugzeugliste
|
// Waehlt ein Flugzeug programmgesteuert aus (z.B. von der Flugzeugliste
|
||||||
// aus, nicht per Antippen auf dem Radar) - damit beim naechsten render()
|
// aus, nicht per Antippen auf dem Radar) - damit beim naechsten render()
|
||||||
// sofort das Detail-Panel fuer dieses Flugzeug erscheint, so als haette
|
// sofort das Detail-Panel fuer dieses Flugzeug erscheint, so als haette
|
||||||
|
|||||||
Reference in New Issue
Block a user