From 52812d8235162f0a20543fc66b35ef1b02f0dff6 Mon Sep 17 00:00:00 2001 From: Jakub <106778416+MarekZegare4@users.noreply.github.com> Date: Sun, 25 Oct 2026 14:47:51 +0100 Subject: [PATCH] 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 --- examples/companion_radio/ui-new/InfoKit.h | 12 +++ examples/companion_radio/ui-new/NavView.h | 73 ++++++++++++++++--- examples/companion_radio/ui-new/TrailScreen.h | 1 + examples/companion_radio/ui-new/UITask.cpp | 9 +++ examples/companion_radio/ui-new/UITask.h | 1 + .../companion_radio/ui-new/WaypointsView.h | 1 + 6 files changed, 88 insertions(+), 9 deletions(-) diff --git a/examples/companion_radio/ui-new/InfoKit.h b/examples/companion_radio/ui-new/InfoKit.h index d15745fa..7b8b54e1 100644 --- a/examples/companion_radio/ui-new/InfoKit.h +++ b/examples/companion_radio/ui-new/InfoKit.h @@ -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. diff --git a/examples/companion_radio/ui-new/NavView.h b/examples/companion_radio/ui-new/NavView.h index 61c01a53..e729b630 100644 --- a/examples/companion_radio/ui-new/NavView.h +++ b/examples/companion_radio/ui-new/NavView.h @@ -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. diff --git a/examples/companion_radio/ui-new/TrailScreen.h b/examples/companion_radio/ui-new/TrailScreen.h index 5e1b2d43..6adaf999 100644 --- a/examples/companion_radio/ui-new/TrailScreen.h +++ b/examples/companion_radio/ui-new/TrailScreen.h @@ -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); diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index de68fe9e..3129315e 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -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(); } diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index 5e3ed06b..cb616d29 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -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); diff --git a/examples/companion_radio/ui-new/WaypointsView.h b/examples/companion_radio/ui-new/WaypointsView.h index 149022a9..78ebcd27 100644 --- a/examples/companion_radio/ui-new/WaypointsView.h +++ b/examples/companion_radio/ui-new/WaypointsView.h @@ -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; }