mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-08-02 10:16:11 +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:
co-authored by
Claude Sonnet 4.6
parent
26278e433d
commit
0c4e2e3f47
@@ -12,9 +12,10 @@ class TrailScreen : public UIScreen {
|
|||||||
UITask* _task;
|
UITask* _task;
|
||||||
TrailStore* _store;
|
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;
|
uint8_t _view = V_SUMMARY;
|
||||||
int _summary_scroll = 0; // top visible item index in Summary view
|
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;
|
static const int SUMMARY_ITEM_COUNT = 5;
|
||||||
|
|
||||||
@@ -26,12 +27,15 @@ public:
|
|||||||
int render(DisplayDriver& display) override {
|
int render(DisplayDriver& display) override {
|
||||||
display.setTextSize(1);
|
display.setTextSize(1);
|
||||||
display.setColor(DisplayDriver::LIGHT);
|
display.setColor(DisplayDriver::LIGHT);
|
||||||
display.drawTextCentered(display.width() / 2, 0,
|
const char* title = (_view == V_MAP) ? "TRAIL MAP"
|
||||||
_view == V_MAP ? "TRAIL MAP" : "TRAIL");
|
: (_view == V_LIST) ? "TRAIL LIST"
|
||||||
|
: "TRAIL";
|
||||||
|
display.drawTextCentered(display.width() / 2, 0, title);
|
||||||
display.fillRect(0, display.headerH() - 1, display.width(), display.sepH());
|
display.fillRect(0, display.headerH() - 1, display.width(), display.sepH());
|
||||||
|
|
||||||
if (_view == V_MAP) renderMap(display);
|
if (_view == V_MAP) renderMap(display);
|
||||||
else renderSummary(display);
|
else if (_view == V_LIST) renderList(display);
|
||||||
|
else renderSummary(display);
|
||||||
|
|
||||||
// Bottom hint — current view indicator + control reminders.
|
// Bottom hint — current view indicator + control reminders.
|
||||||
display.setColor(DisplayDriver::LIGHT);
|
display.setColor(DisplayDriver::LIGHT);
|
||||||
@@ -55,6 +59,8 @@ public:
|
|||||||
if (c == KEY_RIGHT || c == KEY_NEXT) { _view = (_view + 1) % V_COUNT; return true; }
|
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_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_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) {
|
if (c == KEY_ENTER) {
|
||||||
_store->setActive(!_store->isActive());
|
_store->setActive(!_store->isActive());
|
||||||
_task->showAlert(_store->isActive() ? "Tracking started" : "Tracking stopped", 800);
|
_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
|
// 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.
|
// longitude is scaled by cos(avg_lat) so high-latitude trails don't stretch.
|
||||||
void renderMap(DisplayDriver& display) {
|
void renderMap(DisplayDriver& display) {
|
||||||
|
|||||||
Reference in New Issue
Block a user