mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-07-26 14:58:12 +00:00
feat(ui): Trail phase 2 — auto-fit map view + view switching
LEFT/RIGHT cycles between Summary (existing) and the new Map view. Pixel-by-pixel polyline auto-fits the bounding box of the points to the visible area, with longitude scaled by cos(avg_lat) so trails at higher latitudes don't smear east/west. Picks the limiting axis and centres the non-limiting dimension, so the trail fills as much screen as possible without distorting. Markers: - Start point: a 5×5 plus sign - Current/last point: a 5×5 diagonal cross Edge cases: - Empty trail → "No trail yet" centred - Single point or all samples colocated → single current marker at centre - Degenerate area_w/h (very small screen) → no draw Lines are Bresenham via 1×1 fillRect — a few-tens-of-ms render which is fine for an on-demand screen. A drawPixel override on the two drivers would speed this up, deferred until the cost becomes visible. Bottom hint shows `<>` for view nav, "N/2" current view, and the Enter-toggle label. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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 <math.h>
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user