mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-07-26 14:58:12 +00:00
feat(ui): Trail phase 3 — per-point list view
Third view in TrailScreen, reached by LEFT/RIGHT from Summary/Map. Newest-first, each row shows `HH:MM +N m` (or `+N.N km` past 1 km). The first point of each segment shows `start` instead of a delta — matches the map renderer's segment-aware drawing. UP/DOWN scrolls within the list; ^/v arrows on the right edge appear when more rows hide above/below. Time is local (tz_offset_hours applied via gmtime on (ts + offset)). Empty trail shows "No trail yet" centred in the content area. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -12,9 +12,10 @@ class TrailScreen : public UIScreen {
|
||||
UITask* _task;
|
||||
TrailStore* _store;
|
||||
|
||||
enum View { V_SUMMARY = 0, V_MAP = 1, V_COUNT };
|
||||
enum View { V_SUMMARY = 0, V_MAP = 1, V_LIST = 2, V_COUNT };
|
||||
uint8_t _view = V_SUMMARY;
|
||||
int _summary_scroll = 0; // top visible item index in Summary view
|
||||
int _list_scroll = 0; // top visible row in List view (newest = row 0)
|
||||
|
||||
static const int SUMMARY_ITEM_COUNT = 5;
|
||||
|
||||
@@ -26,12 +27,15 @@ public:
|
||||
int render(DisplayDriver& display) override {
|
||||
display.setTextSize(1);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.drawTextCentered(display.width() / 2, 0,
|
||||
_view == V_MAP ? "TRAIL MAP" : "TRAIL");
|
||||
const char* title = (_view == V_MAP) ? "TRAIL MAP"
|
||||
: (_view == V_LIST) ? "TRAIL LIST"
|
||||
: "TRAIL";
|
||||
display.drawTextCentered(display.width() / 2, 0, title);
|
||||
display.fillRect(0, display.headerH() - 1, display.width(), display.sepH());
|
||||
|
||||
if (_view == V_MAP) renderMap(display);
|
||||
else renderSummary(display);
|
||||
if (_view == V_MAP) renderMap(display);
|
||||
else if (_view == V_LIST) renderList(display);
|
||||
else renderSummary(display);
|
||||
|
||||
// Bottom hint — current view indicator + control reminders.
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
@@ -55,6 +59,8 @@ public:
|
||||
if (c == KEY_RIGHT || c == KEY_NEXT) { _view = (_view + 1) % V_COUNT; return true; }
|
||||
if (_view == V_SUMMARY && c == KEY_UP && _summary_scroll > 0) { _summary_scroll--; return true; }
|
||||
if (_view == V_SUMMARY && c == KEY_DOWN) { _summary_scroll++; return true; } // clamped on render
|
||||
if (_view == V_LIST && c == KEY_UP && _list_scroll > 0) { _list_scroll--; return true; }
|
||||
if (_view == V_LIST && c == KEY_DOWN) { _list_scroll++; return true; } // clamped on render
|
||||
if (c == KEY_ENTER) {
|
||||
_store->setActive(!_store->isActive());
|
||||
_task->showAlert(_store->isActive() ? "Tracking started" : "Tracking stopped", 800);
|
||||
@@ -133,6 +139,68 @@ private:
|
||||
}
|
||||
}
|
||||
|
||||
// Per-point list, newest first. Each row: HH:MM (local) + delta from the
|
||||
// previous point in the same segment, or "start" for segment-start rows.
|
||||
void renderList(DisplayDriver& display) {
|
||||
const int top = display.listStart();
|
||||
const int step = display.lineStep();
|
||||
const int hint_h = step;
|
||||
const int avail = display.height() - top - hint_h - 2;
|
||||
|
||||
if (_store->empty()) {
|
||||
display.drawTextCentered(display.width() / 2, top + avail / 2, "No trail yet");
|
||||
return;
|
||||
}
|
||||
|
||||
int visible = avail / step;
|
||||
if (visible < 1) visible = 1;
|
||||
const int total = _store->count();
|
||||
if (visible > total) visible = total;
|
||||
|
||||
int max_scroll = total - visible;
|
||||
if (_list_scroll > max_scroll) _list_scroll = max_scroll;
|
||||
if (_list_scroll < 0) _list_scroll = 0;
|
||||
|
||||
NodePrefs* p = _task->getNodePrefs();
|
||||
int32_t tz_off = (int32_t)(p ? p->tz_offset_hours : 0) * 3600;
|
||||
|
||||
for (int i = 0; i < visible; i++) {
|
||||
int idx = total - 1 - (_list_scroll + i); // newest first
|
||||
if (idx < 0) break;
|
||||
const TrailPoint& pt = _store->at(idx);
|
||||
|
||||
time_t lt = (time_t)((int32_t)pt.ts + tz_off);
|
||||
struct tm* ti = gmtime(<);
|
||||
|
||||
char dist[12];
|
||||
if (idx == 0 || (pt.flags & TRAIL_FLAG_SEG_START)) {
|
||||
snprintf(dist, sizeof(dist), "start");
|
||||
} else {
|
||||
const TrailPoint& prev = _store->at(idx - 1);
|
||||
float d = TrailStore::haversineMeters(prev.lat_1e6, prev.lon_1e6,
|
||||
pt.lat_1e6, pt.lon_1e6);
|
||||
if (d < 1000.0f) snprintf(dist, sizeof(dist), "+%d m", (int)d);
|
||||
else snprintf(dist, sizeof(dist), "+%.1f km", d / 1000.0f);
|
||||
}
|
||||
|
||||
char row[28];
|
||||
snprintf(row, sizeof(row), "%02d:%02d %s",
|
||||
ti->tm_hour, ti->tm_min, dist);
|
||||
display.setCursor(2, top + i * step);
|
||||
display.print(row);
|
||||
}
|
||||
|
||||
int cw = display.getCharWidth();
|
||||
if (_list_scroll > 0) {
|
||||
display.setCursor(display.width() - cw, top);
|
||||
display.print("^");
|
||||
}
|
||||
if (_list_scroll + visible < total) {
|
||||
display.setCursor(display.width() - cw, top + (visible - 1) * step);
|
||||
display.print("v");
|
||||
}
|
||||
}
|
||||
|
||||
// Pixel-by-pixel polyline. Bounds are auto-fitted to the available area and
|
||||
// longitude is scaled by cos(avg_lat) so high-latitude trails don't stretch.
|
||||
void renderMap(DisplayDriver& display) {
|
||||
|
||||
Reference in New Issue
Block a user