2026-06-03 14:07:53 +02:00
|
|
|
|
#pragma once
|
|
|
|
|
|
// Tools › Compass. A standalone heading view: shows the device's course over
|
2026-06-04 10:28:53 +02:00
|
|
|
|
// ground (derived from GPS movement — there is no magnetometer) as a horizontal
|
|
|
|
|
|
// scrolling tape (N..E..S..W) under a fixed travel-direction marker, plus a
|
|
|
|
|
|
// large numeric degrees + cardinal readout. When standing still the heading is
|
|
|
|
|
|
// undefined, so it shows a hint to move.
|
|
|
|
|
|
//
|
|
|
|
|
|
// A linear tape (no trig) reads well in the short vertical space of the OLED,
|
|
|
|
|
|
// where a circular dial leaves only a few-pixel needle.
|
2026-06-03 14:07:53 +02:00
|
|
|
|
//
|
|
|
|
|
|
// Reuses UITask's COG ring (currentCourse) — works whether or not a trail is
|
|
|
|
|
|
// being recorded.
|
|
|
|
|
|
|
|
|
|
|
|
#include "../GeoUtils.h"
|
2026-06-18 18:00:14 +02:00
|
|
|
|
#include "icons.h" // miniIconScale() — keeps the tape ticks/pointer legible on e-ink
|
2026-06-03 14:07:53 +02:00
|
|
|
|
|
|
|
|
|
|
class CompassScreen : public UIScreen {
|
|
|
|
|
|
UITask* _task;
|
|
|
|
|
|
|
2026-06-04 00:12:15 +02:00
|
|
|
|
bool gpsValid() const { int32_t lat, lon; return _task->currentLocation(lat, lon); }
|
2026-06-03 14:07:53 +02:00
|
|
|
|
|
2026-06-04 10:28:53 +02:00
|
|
|
|
static const char* cardinalLetter(int deg) {
|
|
|
|
|
|
switch (deg) { case 0: return "N"; case 90: return "E";
|
|
|
|
|
|
case 180: return "S"; case 270: return "W"; }
|
|
|
|
|
|
return "";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-03 14:07:53 +02:00
|
|
|
|
public:
|
|
|
|
|
|
CompassScreen(UITask* task) : _task(task) {}
|
|
|
|
|
|
void enter() {}
|
|
|
|
|
|
|
|
|
|
|
|
int render(DisplayDriver& display) override {
|
|
|
|
|
|
display.setTextSize(1);
|
|
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
refactor(ui): shared drawList + header + key-decode helpers
- drawList(): scrollable list skeleton (visible window, keep-sel-in-view,
scroll-column reserve, per-row callback, indicator). Migrate Tools, Bot,
Nearby, Waypoints list and the three QuickMsg lists onto it; centralises the
scroll-clamp/reserve/indicator that each had open-coded (and inconsistently).
- drawCenteredHeader(): centred title + separator, replacing the duplicated
two-liner across ~10 screens (incl. QuickMsg, which used width()/2 spacing and
was missed before); unifies the separator at headerH()-sepH().
- keyIsPrev()/keyIsNext() in UIScreen.h for the LEFT||PREV / RIGHT||NEXT idiom;
applied to 6 screens, behaviour-preserving. Cancel/context-menu stay
per-screen (they mean different things on different screens).
- rotLabel() dedups the 0/90/180/270 array in Settings.
- Unify selection-row height to lineStep()-1 (Tools/Bot matched the rest).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 21:23:00 +02:00
|
|
|
|
display.drawCenteredHeader("COMPASS");
|
2026-06-03 14:07:53 +02:00
|
|
|
|
|
|
|
|
|
|
const int cx = display.width() / 2;
|
2026-06-04 09:57:03 +02:00
|
|
|
|
const int ch = display.getLineHeight();
|
|
|
|
|
|
const int cw = display.getCharWidth();
|
2026-06-03 14:07:53 +02:00
|
|
|
|
// Reserve a size-2 row at the bottom for the numeric readout.
|
|
|
|
|
|
display.setTextSize(2);
|
|
|
|
|
|
const int bigH = display.getLineHeight();
|
|
|
|
|
|
display.setTextSize(1);
|
2026-06-17 21:35:51 +02:00
|
|
|
|
const int top = display.listStart();
|
2026-06-04 10:28:53 +02:00
|
|
|
|
const int readout_y = display.height() - bigH - 1; // size-2 readout baseline
|
|
|
|
|
|
const int mid = (top + readout_y) / 2;
|
2026-06-03 14:07:53 +02:00
|
|
|
|
|
|
|
|
|
|
if (!gpsValid()) {
|
2026-06-04 10:28:53 +02:00
|
|
|
|
display.drawTextCentered(cx, mid - ch / 2, "No GPS fix");
|
2026-06-03 14:07:53 +02:00
|
|
|
|
return 1000;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
int cog;
|
|
|
|
|
|
bool have = _task->currentCourse(cog);
|
|
|
|
|
|
|
|
|
|
|
|
if (!have) {
|
2026-06-04 10:28:53 +02:00
|
|
|
|
display.drawTextCentered(cx, mid - ch, "move to");
|
|
|
|
|
|
display.drawTextCentered(cx, mid, "set heading");
|
2026-06-03 14:07:53 +02:00
|
|
|
|
return 1000;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-04 10:28:53 +02:00
|
|
|
|
// ── heading tape ─────────────────────────────────────────────────────────
|
|
|
|
|
|
// Centre of the tape = current course; bearings spread left/right at a fixed
|
|
|
|
|
|
// pixels-per-degree. HALF_SPAN degrees reach each edge.
|
|
|
|
|
|
const int area_x = 2;
|
|
|
|
|
|
const int area_w = display.width() - 4;
|
|
|
|
|
|
const float HALF_SPAN = 80.0f; // degrees visible to each side
|
|
|
|
|
|
const float ppd = (area_w / 2.0f) / HALF_SPAN;
|
|
|
|
|
|
|
2026-06-18 18:00:14 +02:00
|
|
|
|
// Tick/pointer sizes are authored at 1x and scaled by the font (same factor
|
|
|
|
|
|
// the mini-icon framework uses), so they stay legible next to the 2x
|
|
|
|
|
|
// cardinal labels and numeric readout on landscape e-ink.
|
|
|
|
|
|
const int s = miniIconScale(display);
|
|
|
|
|
|
const int sh = display.sepH(); // baseline thickness (2px landscape e-ink, else 1)
|
|
|
|
|
|
const int pointerH_units = 6;
|
|
|
|
|
|
const int pointerH = pointerH_units * s;
|
|
|
|
|
|
const int tickMajor = 6 * s;
|
|
|
|
|
|
const int tickMinor = 3 * s;
|
|
|
|
|
|
const int blockH = pointerH + sh + tickMajor + 2 + ch;
|
2026-06-04 10:28:53 +02:00
|
|
|
|
const int block_top = top + ((readout_y - top) - blockH) / 2;
|
|
|
|
|
|
const int line_y = block_top + pointerH; // scale baseline
|
2026-06-18 18:00:14 +02:00
|
|
|
|
const int label_y = line_y + sh + tickMajor + 2;
|
2026-06-04 10:28:53 +02:00
|
|
|
|
const int x_lo = area_x, x_hi = area_x + area_w;
|
|
|
|
|
|
|
|
|
|
|
|
// Scale baseline.
|
2026-06-18 18:00:14 +02:00
|
|
|
|
display.fillRect(area_x, line_y, area_w, sh);
|
2026-06-04 10:28:53 +02:00
|
|
|
|
|
|
|
|
|
|
// Ticks + cardinal labels every 30°; majors (N/E/S/W) are taller + labelled.
|
|
|
|
|
|
for (int deg = 0; deg < 360; deg += 30) {
|
|
|
|
|
|
int delta = deg - cog;
|
|
|
|
|
|
while (delta > 180) delta -= 360;
|
|
|
|
|
|
while (delta < -180) delta += 360;
|
|
|
|
|
|
int x = cx + (int)(delta * ppd);
|
|
|
|
|
|
if (x < x_lo || x > x_hi) continue;
|
|
|
|
|
|
bool major = (deg % 90 == 0);
|
2026-06-18 18:00:14 +02:00
|
|
|
|
display.fillRect(x - s / 2, line_y + sh, s, major ? tickMajor : tickMinor);
|
2026-06-04 10:28:53 +02:00
|
|
|
|
if (major) {
|
2026-06-18 18:00:14 +02:00
|
|
|
|
const char* letter = cardinalLetter(deg);
|
2026-06-04 10:28:53 +02:00
|
|
|
|
display.setCursor(x - cw / 2, label_y);
|
2026-06-18 18:00:14 +02:00
|
|
|
|
display.print(letter);
|
2026-06-04 10:28:53 +02:00
|
|
|
|
}
|
2026-06-03 16:51:40 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-04 10:28:53 +02:00
|
|
|
|
// Fixed travel-direction pointer: a downward triangle whose tip touches the
|
|
|
|
|
|
// baseline at screen centre (your current course always sits under it).
|
2026-06-18 18:00:14 +02:00
|
|
|
|
// Each of the pointerH_units logical rows is drawn as an s-pixel-tall strip
|
|
|
|
|
|
// — the same block-scaling miniIconDraw uses for bitmap icons.
|
|
|
|
|
|
for (int i = 0; i < pointerH_units; i++) {
|
|
|
|
|
|
int half = (pointerH_units - 1 - i) * s;
|
|
|
|
|
|
display.fillRect(cx - half, block_top + i * s, 2 * half + 1, s);
|
2026-06-03 14:07:53 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-04 10:28:53 +02:00
|
|
|
|
// ── numeric readout ──────────────────────────────────────────────────────
|
2026-06-03 14:07:53 +02:00
|
|
|
|
char buf[16];
|
|
|
|
|
|
snprintf(buf, sizeof(buf), "%d %s", cog, geo::bearingCardinal(cog));
|
|
|
|
|
|
display.setTextSize(2);
|
2026-06-04 10:28:53 +02:00
|
|
|
|
display.drawTextCentered(cx, readout_y, buf);
|
2026-06-03 14:07:53 +02:00
|
|
|
|
display.setTextSize(1);
|
|
|
|
|
|
return 1000;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool handleInput(char c) override {
|
|
|
|
|
|
if (c == KEY_CANCEL || c == KEY_CONTEXT_MENU) { _task->gotoToolsScreen(); return true; }
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|