feat(ui): a turn dial / compass rose on narrow navigation screens

navview::draw (Waypoints, trail back, Nearby, a message's location) shows a
dial beside the lines, or under them on a tall screen (portrait e-ink) where
that makes it larger: with a course a turn dial (the top is where you're
heading, the arrow points at the target, "Turn R 55"), without it the compass
rose. The 4.2" screens keep theirs. On a short screen (the 64 px OLED) the
gap under the distance and the pitch of the lines give way so the ETA is not
cut off at the bottom.

sim_nav_waypoint() opens a waypoint's navigate view from the simulator page.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-10-25 14:47:51 +01:00
co-authored by Claude Sonnet 5.5
parent 15ac4d8f83
commit 52812d8235
6 changed files with 88 additions and 9 deletions
+12
View File
@@ -138,6 +138,18 @@ inline void arrow(DisplayDriver& d, int cx, int cy, int r, int deg) {
}
}
// A turn dial of radius r round (cx, cy): the way you're heading is the top
// (the mark in from the ring), and an arrow points at where to go from there
// (`deg` clockwise from the top, 0 = straight on) -- turn until it points up.
// `ahead` doubles the ring: you're on course.
inline void turnDial(DisplayDriver& d, int cx, int cy, int r, int deg, bool ahead) {
const int s = miniIconScale(d);
circle(d, cx, cy, r);
if (ahead) circle(d, cx, cy, r - d.sepH() - 1);
d.fillRect(cx - s, cy - r + d.sepH() + 1, 2 * s, 3 * s + 1); // the top mark
arrow(d, cx, cy + s, r - 4 * s - 2, deg); // a touch low: clear of the mark
}
// A compass rose of radius r round (cx, cy), north up: the ring, a tick in at
// each cardinal point, N in a gap at the top, and -- for deg >= 0 -- a needle
// from the centre to the ring, towards deg.
+64 -9
View File
@@ -14,6 +14,8 @@
namespace navview {
static const int AHEAD_DEG = 12; // within this of straight on: "ahead"
// own_valid : is there a usable GPS fix for the device right now
// cog_valid : is there a stable course-over-ground (UITask::currentCourse)
// eta : optional closing-speed/ETA tracker; when supplied a third line is
@@ -41,21 +43,63 @@ inline void draw(DisplayDriver& d,
char dist[12];
geo::fmtDist(dist, sizeof(dist), dist_km, imperial);
const int step = d.lineStep();
const int lh = d.getLineHeight(), cw = d.getCharWidth();
int step = d.lineStep();
int y = d.listStart();
// A screen 40 characters wide (the 4.2"): the lines down the left, and a
// compass rose (north up) at the right with an arrow to the target and a
// mark on the rim where you're heading.
const bool wide = d.width() >= 40 * d.getCharWidth();
const bool wide = d.width() >= 40 * cw;
// The distance at size 2, then `lines` under it (turn / To or To / Hdg, and
// the ETA). On a short screen (the 64 px OLED) the last would run off the
// bottom: the gap under the distance goes first, then the pitch of the lines.
const int lines = eta ? 3 : 2;
d.setTextSize(2);
const int dist_h = d.getLineHeight(), dw = d.getTextWidth(dist);
d.setTextSize(1);
int dist_gap = 3;
int over = y + dist_h + dist_gap + (lines - 1) * step + lh - d.height();
if (over > 0) { const int g = over < dist_gap ? over : dist_gap; dist_gap -= g; over -= g; }
if (over > 0) { step -= (over + lines - 2) / (lines - 1); if (step < lh) step = lh; }
// A narrow screen: a dial, the lines down the left. With a course it's a
// turn dial (the top is where you're heading, turn until the arrow points
// up); without, the compass rose (north up). Beside the lines, or under them
// where the screen is tall (the portrait e-ink) and that makes it larger.
// Too small for one (a very short screen): the text alone, as before.
int dial_r = 0, dial_x = 0, dial_y = 0;
if (!wide) {
const int top = d.listStart();
const int tw = (dw > 9 * cw ? dw : 9 * cw) + 4; // the widest of the lines at the left
int r_side = (d.height() - top) / 2 - lh / 2 - 1; // room over the ring for the rose's N
if (r_side > (d.width() - tw) / 2 - 1) r_side = (d.width() - tw) / 2 - 1;
const int text_bottom = y + dist_h + dist_gap + lines * step;
int r_below = (d.height() - text_bottom) / 2 - lh / 2 - 2;
if (r_below > d.width() / 2 - 3) r_below = d.width() / 2 - 3;
if (r_below > 50) r_below = 50;
if (r_below >= 14 && r_below > r_side) {
dial_r = r_below;
dial_x = d.width() / 2;
dial_y = text_bottom + (d.height() - text_bottom) / 2 + lh / 4;
} else if (r_side >= 14) {
dial_r = r_side;
dial_x = d.width() - r_side - 2;
dial_y = top + (d.height() - top) / 2 + lh / 4;
}
}
int turn = 0; // -180..179 degrees to turn, right positive; with a course
if (cog_valid) turn = ((to_deg - cog_deg + 540) % 360) - 180;
const bool ahead = cog_valid && turn <= AHEAD_DEG && turn >= -AHEAD_DEG;
// Distance — emphasised at size 2.
d.setTextSize(2);
if (wide) { d.setCursor(2, y); d.print(dist); }
else d.drawTextCentered(cx, y, dist);
y += d.getLineHeight() + 3;
if (wide || dial_r) { d.setCursor(2, y); d.print(dist); }
else d.drawTextCentered(cx, y, dist);
y += dist_h + dist_gap;
d.setTextSize(1);
if (wide) {
const int lh = d.getLineHeight(), top = d.listStart();
const int top = d.listStart();
const int r = (d.height() - top) / 2 - lh;
const int rx = d.width() - r - lh, ry = top + (d.height() - top) / 2 + lh / 4;
info::rose(d, rx, ry, r, to_deg);
@@ -72,12 +116,23 @@ inline void draw(DisplayDriver& d,
}
char line[20];
if (dial_r) {
if (cog_valid) info::turnDial(d, dial_x, dial_y, dial_r, turn, ahead);
else info::rose(d, dial_x, dial_y, dial_r, to_deg);
}
if (dial_r && cog_valid) { // how far to turn says what "To" and "Hdg" leave to be subtracted
if (ahead) snprintf(line, sizeof(line), "Straight on");
else snprintf(line, sizeof(line), "Turn %c %d", turn > 0 ? 'R' : 'L', turn > 0 ? turn : -turn);
d.drawTextLeftAlign(2, y, line); y += step;
}
snprintf(line, sizeof(line), "To: %d %s", to_deg, geo::bearingCardinal(to_deg));
d.drawTextLeftAlign(2, y, line); y += step;
if (cog_valid) snprintf(line, sizeof(line), "Hdg: %d %s", cog_deg, geo::bearingCardinal(cog_deg));
else snprintf(line, sizeof(line), "Hdg: --");
d.drawTextLeftAlign(2, y, line); y += step;
if (!(dial_r && cog_valid)) {
if (cog_valid) snprintf(line, sizeof(line), "Hdg: %d %s", cog_deg, geo::bearingCardinal(cog_deg));
else snprintf(line, sizeof(line), "Hdg: --");
d.drawTextLeftAlign(2, y, line); y += step;
}
// Optional ETA / closing-speed line. Approaching only — a receding or
// stationary target shows "ETA: --" rather than a misleading time.
@@ -117,6 +117,7 @@ public:
// Forwarded to UITask's GPS duty-cycle hold — see WaypointsView::needsLiveGps().
bool wpNeedsLiveGps() const { return _wp.needsLiveGps(); }
void simWaypointNav(int row) { _wp.simNav(row); } // sim tests
int render(DisplayDriver& display) override {
display.setTextSize(1);
@@ -472,6 +472,11 @@ void UITask::simNodes() {
// Sim tests: a walked trail ending at the GPS fix of simNodes(), and two
// waypoints beside it, for the maps.
void UITask::simNavWaypoint(int row) {
setCurrScreen(trail_screen);
((TrailScreen*)trail_screen)->simWaypointNav(row);
}
void UITask::simTrail() {
TrailStore& tr = _core->trail.store();
const uint32_t now = rtc_clock.getCurrentTime();
@@ -966,6 +971,10 @@ extern "C" EMSCRIPTEN_KEEPALIVE void sim_nodes() {
if (g_sim_ui_task_for_js) g_sim_ui_task_for_js->simNodes();
}
extern "C" EMSCRIPTEN_KEEPALIVE void sim_nav_waypoint(int row) {
if (g_sim_ui_task_for_js) g_sim_ui_task_for_js->simNavWaypoint(row);
}
extern "C" EMSCRIPTEN_KEEPALIVE void sim_trail() {
if (g_sim_ui_task_for_js) g_sim_ui_task_for_js->simTrail();
}
+1
View File
@@ -326,6 +326,7 @@ public:
void simUnread(int which); // sim tests: read and new messages, opened
void simDisplay(bool on); // sim tests: the display off / on
void simNodes(); // sim tests: you and nodes with positions, Nodes open
void simNavWaypoint(int row); // sim tests: the navigate view of a waypoint (after simTrail)
void simTrail(); // sim tests: a trail and two waypoints for the maps
#endif
void openContactDM(const ContactInfo& ci);
@@ -284,6 +284,7 @@ public:
// Entry points called from TrailScreen's action menu.
void openList() { _mode = LIST; _sel = 0; _scroll = 0; }
void simNav(int row) { _mode = NAV; _sel = row; _scroll = 0; _wp_eta.reset(); } // sim tests: its navigate view
void markHere() {
int32_t lat, lon;
if (!ownPos(lat, lon)) { _task->showAlert("No GPS fix", 1000); return; }