Files
MeshCore-Solo/examples/companion_radio/ui-new/CompassScreen.h
Jakub 3bcb94ec10 fix(compass): move "move to set heading" hint below the ring
The no-heading hint was centred on the compass circle, overlapping the
ring. Draw it in the bottom readout band instead so the ring stays clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 09:37:37 +02:00

108 lines
3.9 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#pragma once
// Tools Compass. A standalone heading view: shows the device's course over
// ground (derived from GPS movement — there is no magnetometer) as a rotating
// arrow plus a numeric degrees + cardinal readout. When standing still the
// heading is undefined, so it shows a hint to move.
//
// Reuses UITask's COG ring (currentCourse) — works whether or not a trail is
// being recorded.
#include "../GeoUtils.h"
#include "GfxUtils.h"
#include <math.h>
class CompassScreen : public UIScreen {
UITask* _task;
bool gpsValid() const { int32_t lat, lon; return _task->currentLocation(lat, lon); }
public:
CompassScreen(UITask* task) : _task(task) {}
void enter() {}
int render(DisplayDriver& display) override {
display.setTextSize(1);
display.setColor(DisplayDriver::LIGHT);
display.drawTextCentered(display.width() / 2, 0, "COMPASS");
display.fillRect(0, display.headerH() - 1, display.width(), display.sepH());
const int hdr = display.headerH();
const int cx = display.width() / 2;
// Reserve a size-2 row at the bottom for the numeric readout.
display.setTextSize(2);
const int bigH = display.getLineHeight();
display.setTextSize(1);
const int top = hdr + 2;
const int bottom = display.height() - bigH - 2;
const int cy = (top + bottom) / 2;
int r = ((bottom - top) / 2);
int rx = (display.width() / 2) - 4;
if (rx < r) r = rx;
if (r < 6) r = 6;
if (!gpsValid()) {
display.drawTextCentered(cx, cy - display.getLineHeight() / 2, "No GPS fix");
return 1000;
}
int cog;
bool have = _task->currentCourse(cog);
gfx::drawCircle(display, cx, cy, r); // compass ring
// Fixed forward index at the top: the direction you are travelling is
// always "up". The compass card (North) rotates underneath it.
display.fillRect(cx - 1, cy - r - 2, 3, 1);
display.fillRect(cx, cy - r - 1, 1, 2);
if (!have) {
// No stable heading yet — keep the ring as context but put the hint in
// the bottom readout band so it doesn't overlap the circle.
int lh = display.getLineHeight();
display.drawTextCentered(cx, bottom + 1, "move to");
display.drawTextCentered(cx, bottom + 1 + lh, "set heading");
return 1000;
}
// Heads-up compass: with "up" = course, an absolute bearing B sits at
// screen angle (B - cog). Draw the rotating card — a North needle plus the
// other three cardinals as letters around the ring.
auto cardPos = [&](int bearing, int rad_px, int& px, int& py) {
float a = (bearing - cog) * (float)M_PI / 180.0f;
px = cx + (int)(sinf(a) * rad_px);
py = cy - (int)(cosf(a) * rad_px);
};
// North needle from centre.
int nx, ny; cardPos(0, r - 2, nx, ny);
gfx::drawLine(display, cx, cy, nx, ny);
float nrad = (0 - cog) * (float)M_PI / 180.0f;
for (int da = -25; da <= 25; da += 50) {
float a = nrad + (float)M_PI + da * (float)M_PI / 180.0f;
gfx::drawLine(display, nx, ny, nx + (int)(sinf(a) * 4), ny - (int)(cosf(a) * 4));
}
// Cardinal letters around the rim (N at the needle tip).
const int cw = display.getCharWidth(), ch = display.getLineHeight();
struct { int b; const char* s; } card[4] = { {0,"N"},{90,"E"},{180,"S"},{270,"W"} };
for (int i = 0; i < 4; i++) {
int lx, ly; cardPos(card[i].b, r - 2, lx, ly);
display.setCursor(lx - cw / 2, ly - ch / 2);
display.print(card[i].s);
}
// Numeric readout below the ring — the absolute course you're travelling.
char buf[16];
snprintf(buf, sizeof(buf), "%d %s", cog, geo::bearingCardinal(cog));
display.setTextSize(2);
display.drawTextCentered(cx, bottom + 1, buf);
display.setTextSize(1);
return 1000;
}
bool handleInput(char c) override {
if (c == KEY_CANCEL || c == KEY_CONTEXT_MENU) { _task->gotoToolsScreen(); return true; }
return true;
}
};