diff --git a/examples/companion_radio/ui-new/TrailScreen.h b/examples/companion_radio/ui-new/TrailScreen.h index 418ee0e6..caf1a22f 100644 --- a/examples/companion_radio/ui-new/TrailScreen.h +++ b/examples/companion_radio/ui-new/TrailScreen.h @@ -1,14 +1,20 @@ #pragma once // GPS trail viewer. Tools › Trail. -// Phase 1: Summary view only. Phases 2/3 add Map and List views. +// Summary view: counters, distance, time, speed. +// Map view: pixel-by-pixel auto-fit of the polyline with start (+) and current (×) markers. +// Phase 3 adds a per-point list view. // Included by UITask.cpp after Trail store + ToolsScreen. #include "../Trail.h" +#include class TrailScreen : public UIScreen { UITask* _task; TrailStore* _store; + enum View { V_SUMMARY = 0, V_MAP = 1, V_COUNT }; + uint8_t _view = V_SUMMARY; + public: TrailScreen(UITask* task, TrailStore* store) : _task(task), _store(store) {} @@ -17,50 +23,22 @@ public: int render(DisplayDriver& display) override { display.setTextSize(1); display.setColor(DisplayDriver::LIGHT); - display.drawTextCentered(display.width() / 2, 0, "TRAIL"); + display.drawTextCentered(display.width() / 2, 0, + _view == V_MAP ? "TRAIL MAP" : "TRAIL"); display.fillRect(0, display.headerH() - 1, display.width(), display.sepH()); - const int y0 = display.listStart(); - const int step = display.lineStep(); + if (_view == V_MAP) renderMap(display); + else renderSummary(display); - char buf[28]; - - // Status line: ON/OFF and point count. - snprintf(buf, sizeof(buf), "Status: %s", - _store->isActive() ? "tracking" : "stopped"); - display.setCursor(2, y0); - display.print(buf); - - snprintf(buf, sizeof(buf), "Points: %d / %d", _store->count(), TrailStore::CAPACITY); - display.setCursor(2, y0 + step); - display.print(buf); - - // Distance — show m below 1 km, km otherwise. - uint32_t dist = _store->totalDistanceMeters(); - if (dist < 1000) snprintf(buf, sizeof(buf), "Dist: %lu m", (unsigned long)dist); - else snprintf(buf, sizeof(buf), "Dist: %lu.%02lu km", - (unsigned long)(dist / 1000), - (unsigned long)((dist % 1000) / 10)); - display.setCursor(2, y0 + step * 2); - display.print(buf); - - // Elapsed — h:mm format. - uint32_t es = _store->elapsedSeconds(); - snprintf(buf, sizeof(buf), "Time: %lu:%02lu", - (unsigned long)(es / 3600), - (unsigned long)((es % 3600) / 60)); - display.setCursor(2, y0 + step * 3); - display.print(buf); - - // Speed — current km/h. - snprintf(buf, sizeof(buf), "Speed: %u km/h", (unsigned)_store->currentSpeedKmh()); - display.setCursor(2, y0 + step * 4); - display.print(buf); - - // Hint at the bottom. - int hint_y = display.height() - step; + // Bottom hint — current view indicator + control reminders. + display.setColor(DisplayDriver::LIGHT); + int hint_y = display.height() - display.lineStep(); + char hint[28]; + snprintf(hint, sizeof(hint), "<>%d/%d [Ent] %s", + (int)_view + 1, (int)V_COUNT, + _store->isActive() ? "stop" : "start"); display.setCursor(2, hint_y); - display.print(_store->isActive() ? "[Ent] stop [Esc] back" : "[Ent] start [Esc] back"); + display.print(hint); return _store->isActive() ? 2000 : 5000; } @@ -70,6 +48,8 @@ public: _task->gotoToolsScreen(); return true; } + if (c == KEY_LEFT || c == KEY_PREV) { _view = (_view + V_COUNT - 1) % V_COUNT; return true; } + if (c == KEY_RIGHT || c == KEY_NEXT) { _view = (_view + 1) % V_COUNT; return true; } if (c == KEY_ENTER) { _store->setActive(!_store->isActive()); _task->showAlert(_store->isActive() ? "Tracking started" : "Tracking stopped", 800); @@ -77,4 +57,137 @@ public: } return false; } + +private: + void renderSummary(DisplayDriver& display) { + const int y0 = display.listStart(); + const int step = display.lineStep(); + + char buf[28]; + + snprintf(buf, sizeof(buf), "Status: %s", + _store->isActive() ? "tracking" : "stopped"); + display.setCursor(2, y0); + display.print(buf); + + snprintf(buf, sizeof(buf), "Points: %d / %d", _store->count(), TrailStore::CAPACITY); + display.setCursor(2, y0 + step); + display.print(buf); + + uint32_t dist = _store->totalDistanceMeters(); + if (dist < 1000) snprintf(buf, sizeof(buf), "Dist: %lu m", (unsigned long)dist); + else snprintf(buf, sizeof(buf), "Dist: %lu.%02lu km", + (unsigned long)(dist / 1000), + (unsigned long)((dist % 1000) / 10)); + display.setCursor(2, y0 + step * 2); + display.print(buf); + + uint32_t es = _store->elapsedSeconds(); + snprintf(buf, sizeof(buf), "Time: %lu:%02lu", + (unsigned long)(es / 3600), + (unsigned long)((es % 3600) / 60)); + display.setCursor(2, y0 + step * 3); + display.print(buf); + + snprintf(buf, sizeof(buf), "Speed: %u km/h", (unsigned)_store->currentSpeedKmh()); + display.setCursor(2, y0 + step * 4); + display.print(buf); + } + + // 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) { + const int top = display.listStart(); + const int bottom = display.height() - display.lineStep() - 1; + + if (_store->empty()) { + display.drawTextCentered(display.width() / 2, (top + bottom) / 2, "No trail yet"); + return; + } + + const int area_x = 2; + const int area_y = top + 1; + const int area_w = display.width() - 4; + const int area_h = bottom - top - 2; + if (area_w < 6 || area_h < 6) return; // no room to draw anything sensible + + int32_t min_lat, min_lon, max_lat, max_lon; + _store->boundingBox(min_lat, min_lon, max_lat, max_lon); + + if (_store->count() == 1 || (min_lat == max_lat && min_lon == max_lon)) { + // Degenerate — single point or all samples colocated. Mark centre. + drawCurrentMarker(display, area_x + area_w / 2, area_y + area_h / 2); + return; + } + + // cos(lat) correction prevents east/west stretching at higher latitudes. + float avg_lat_rad = ((min_lat + max_lat) / 2.0e6f) * (float)M_PI / 180.0f; + float lon_scale_geo = cosf(avg_lat_rad); + if (lon_scale_geo < 0.05f) lon_scale_geo = 0.05f; // guard near poles + + float lat_span = (float)(max_lat - min_lat); + float lon_span = (float)(max_lon - min_lon) * lon_scale_geo; + + // Pick the limiting axis so the polyline fills the area without distorting. + float scale_lat = (float)area_h / (lat_span > 0 ? lat_span : 1.0f); + float scale_lon = (float)area_w / (lon_span > 0 ? lon_span : 1.0f); + float scale = (scale_lat < scale_lon) ? scale_lat : scale_lon; + + // After scaling, the used area is smaller in the non-limiting dim; centre it. + int used_w = (int)(lon_span * scale); + int used_h = (int)(lat_span * scale); + int off_x = area_x + (area_w - used_w) / 2; + int off_y = area_y + (area_h - used_h) / 2; + + auto project = [&](const TrailPoint& p, int& px, int& py) { + float dx = (float)(p.lon_1e6 - min_lon) * lon_scale_geo * scale; + float dy = (float)(max_lat - p.lat_1e6) * scale; // north = up + px = off_x + (int)dx; + py = off_y + (int)dy; + }; + + int x0, y0; + project(_store->at(0), x0, y0); + for (int i = 1; i < _store->count(); i++) { + int x1, y1; + project(_store->at(i), x1, y1); + drawLine(display, x0, y0, x1, y1); + x0 = x1; + y0 = y1; + } + + int sx, sy, ex, ey; + project(_store->first(), sx, sy); + project(_store->last(), ex, ey); + drawStartMarker(display, sx, sy); + drawCurrentMarker(display, ex, ey); + } + + // Bresenham line via 1×1 fillRect. Acceptable for occasional view renders. + static void drawLine(DisplayDriver& d, int x0, int y0, int x1, int y1) { + int dx = abs(x1 - x0), sx = x0 < x1 ? 1 : -1; + int dy = -abs(y1 - y0), sy = y0 < y1 ? 1 : -1; + int err = dx + dy; + while (true) { + d.fillRect(x0, y0, 1, 1); + if (x0 == x1 && y0 == y1) break; + int e2 = err * 2; + if (e2 >= dy) { err += dy; x0 += sx; } + if (e2 <= dx) { err += dx; y0 += sy; } + } + } + + // 5×5 plus-sign marker for the start point. + static void drawStartMarker(DisplayDriver& d, int cx, int cy) { + d.fillRect(cx - 2, cy, 5, 1); + d.fillRect(cx, cy - 2, 1, 5); + } + + // 5×5 cross marker for the current/last point. + static void drawCurrentMarker(DisplayDriver& d, int cx, int cy) { + for (int i = -2; i <= 2; i++) { + d.fillRect(cx + i, cy + i, 1, 1); + d.fillRect(cx + i, cy - i, 1, 1); + } + } };