feat(nav): add a waypoint by typing coordinates

New Trail action "Add by coords" creates a waypoint without a GPS fix: the
keyboard takes a "lat,lon" string (decimal degrees), parsed by the shared
geo::parseLatLon; on success it falls through to the normal label keyboard
(blank → auto WP<n>). Useful for a meeting point or a spot read off a map.
Invalid input reports "Bad coordinates".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-06-04 13:31:53 +02:00
parent 5c1ec5ee28
commit 80c14a75ed
2 changed files with 41 additions and 5 deletions

View File

@@ -92,6 +92,7 @@ Cycle views with **LEFT / RIGHT**:
| Readout | LEFT/RIGHT or Enter | Summary shows Speed or Pace (in the global unit system) |
| Grid | LEFT/RIGHT or Enter | Toggle scale grid on the map |
| Mark here | Enter | Drop a waypoint at the current GPS fix (see below) |
| Add by coords | Enter | Add a waypoint by typing `lat,lon` — no GPS fix needed |
| Waypoints | Enter | Open the waypoint list / navigation |
| Start / Stop tracking | Enter | Begin or end a recording session |
| Save trail | Enter | Write RAM ring to flash (`/trail`) |
@@ -109,6 +110,8 @@ A waypoint is a saved spot — your car, camp, a water source — that you can n
**Dropping a waypoint****Hold Enter → Mark here**. This captures the current GPS fix and opens the on-screen keyboard for a short label (up to 11 characters — e.g. `CAR`, `CAMP`, `H2O`). Leaving it blank auto-names it `WP1`, `WP2`, … Marking works whether or not the trail is being recorded; it needs a GPS fix (otherwise it reports *No GPS fix*).
**Adding by coordinates****Hold Enter → Add by coords** lets you create a waypoint without being there: type `lat,lon` in decimal degrees (e.g. `37.42123,-122.08456`), then a label. No GPS fix is required — handy for entering a meeting point or a spot read off a map. Invalid input reports *Bad coordinates*.
**On the map** — saved waypoints show on the Trail Map view as a hollow diamond with the label's first two characters beside it (enough to tell nearby waypoints apart). Waypoints and your current GPS position are drawn continuously — even with no trail recording in progress — so the Map view doubles as a live "you + your marks" view, not just a recorded-track plot. With **no trail**, the view auto-fits to your waypoints and position. **While a trail exists**, the view frames the recorded route instead, and any waypoint that falls outside it is clamped to the nearest map edge — a distant mark can't blow up the scale and squash the trail.
**Navigating****Hold Enter → Waypoints** opens the list (each row shows the label and live distance). The list always begins with a synthetic **Trail start** row whenever a trail exists, so you can backtrack to where you began without having marked it. Select a row and press **Enter** to open the navigation view:

View File

@@ -30,6 +30,7 @@ class TrailScreen : public UIScreen {
PopupMenu _wp_ctx; // Rename / Delete on a selected waypoint
KeyboardWidget _wp_kb; // label entry (mark new / rename)
bool _kb_active = false;
bool _kb_coords = false; // keyboard is entering "lat,lon" (then asks for a label)
int _kb_rename_idx = -1; // -1 = marking new, ≥0 = renaming that index
int32_t _mark_lat = 0, _mark_lon = 0; // pending new-mark position
uint32_t _mark_ts = 0;
@@ -39,9 +40,9 @@ class TrailScreen : public UIScreen {
// (Start/Stop tracking, Reset). PopupMenu stores label pointers verbatim, so
// the labels live in member buffers that get refreshed in openActionMenu()
// and after every LEFT/RIGHT cycle.
enum ActionId { ACT_MIN_DIST, ACT_UNITS, ACT_GRID, ACT_TOGGLE, ACT_SAVE, ACT_LOAD, ACT_RESET, ACT_EXPORT, ACT_EXPORT_SAVED, ACT_MARK, ACT_WAYPOINTS, ACT_WP_CLEAR };
enum ActionId { ACT_MIN_DIST, ACT_UNITS, ACT_GRID, ACT_TOGGLE, ACT_SAVE, ACT_LOAD, ACT_RESET, ACT_EXPORT, ACT_EXPORT_SAVED, ACT_MARK, ACT_ADD_COORDS, ACT_WAYPOINTS, ACT_WP_CLEAR };
PopupMenu _action_menu;
uint8_t _act_map[14]; // 12 used today; pad so adding an action doesn't OOB
uint8_t _act_map[16]; // 13 used today; pad so adding an action doesn't OOB
uint8_t _act_count = 0;
char _act_min_dist_label[24];
char _act_units_label[24];
@@ -62,6 +63,7 @@ public:
_wp_mode = WP_OFF;
_wp_ctx.active = false;
_kb_active = false;
_kb_coords = false;
}
int render(DisplayDriver& display) override {
@@ -101,8 +103,12 @@ public:
// Label keyboard (mark new / rename) takes all input first.
if (_kb_active) {
auto r = _wp_kb.handleInput(c);
if (r == KeyboardWidget::DONE) { commitLabel(_wp_kb.buf); _kb_active = false; }
else if (r == KeyboardWidget::CANCELLED) { _kb_active = false; }
if (r == KeyboardWidget::DONE) {
if (_kb_coords) commitCoords(_wp_kb.buf); // parse → then ask for a label
else { commitLabel(_wp_kb.buf); _kb_active = false; }
} else if (r == KeyboardWidget::CANCELLED) {
_kb_active = false; _kb_coords = false;
}
return true;
}
@@ -193,6 +199,7 @@ public:
else if (act == ACT_EXPORT) { handleExport(); }
else if (act == ACT_EXPORT_SAVED) { handleExportSaved(); }
else if (act == ACT_MARK) { handleMarkHere(); }
else if (act == ACT_ADD_COORDS) { handleAddByCoords(); }
else if (act == ACT_WAYPOINTS) { _wp_mode = WP_LIST; _wp_sel = 0; _wp_scroll = 0; }
else if (act == ACT_WP_CLEAR) { _task->waypoints().clear(); _task->saveWaypoints();
_task->showAlert("Waypoints cleared", 800); }
@@ -319,7 +326,8 @@ private:
_act_map[_act_count++] = ACT_GRID; _action_menu.addItem(_act_grid_label);
_act_map[_act_count++] = ACT_TOGGLE; _action_menu.addItem(_act_toggle_label);
// Waypoints: mark the current spot, and browse/navigate saved ones.
_act_map[_act_count++] = ACT_MARK; _action_menu.addItem("Mark here");
_act_map[_act_count++] = ACT_MARK; _action_menu.addItem("Mark here");
_act_map[_act_count++] = ACT_ADD_COORDS; _action_menu.addItem("Add by coords");
// "Waypoints" opens the nav list — useful when there are saved waypoints
// OR a trail to backtrack to (the list always includes a "Trail start" row).
if (_task->waypoints().count() > 0 || !_store->empty()) {
@@ -382,6 +390,31 @@ private:
_kb_active = true;
}
// Add a waypoint by typing "lat,lon" (no GPS fix needed). Opens the keyboard
// for coordinates; on a valid parse it falls through to the label keyboard.
void handleAddByCoords() {
if (_task->waypoints().full()) { _task->showAlert("Waypoints full", 1000); return; }
_kb_coords = true;
_kb_rename_idx = -1;
_wp_kb.begin("", 30); // "-12.345678,-123.456789" fits in 30
_kb_active = true;
}
// Parse the typed coordinates; on success store the fix and switch the
// keyboard to label entry (reusing the Mark-here commit path).
void commitCoords(const char* buf) {
int32_t lat, lon;
if (!geo::parseLatLon(buf, lat, lon)) {
_task->showAlert("Bad coordinates", 1200);
_kb_active = false; _kb_coords = false;
return;
}
_mark_lat = lat; _mark_lon = lon; _mark_ts = (uint32_t)rtc_clock.getCurrentTime();
_kb_coords = false;
_kb_rename_idx = -1;
_wp_kb.begin("", WAYPOINT_LABEL_LEN - 1); // now name it (empty → auto WP<n>)
}
void commitLabel(const char* buf) {
if (_kb_rename_idx >= 0) {
_task->waypoints().rename(_kb_rename_idx, buf);