From cad44216eb6bd5c05814814707efd5b03f653b70 Mon Sep 17 00:00:00 2001 From: Jakub <106778416+MarekZegare4@users.noreply.github.com> Date: Thu, 4 Jun 2026 13:39:09 +0200 Subject: [PATCH] feat(nav): Add-by-coords becomes a lat/lon/label form The on-screen keyboard has no comma or minus, so a single "lat,lon" entry was unworkable. Replace it with a WP_ADD form: separate Lat / Lon / Label rows plus Save. Lat/Lon take a magnitude from the keyboard and a hemisphere (N/S, E/W) toggled with LEFT/RIGHT, so negative coordinates need no minus key. Save validates the range, auto-names a blank label, and stores the point via the normal waypoint path. Co-Authored-By: Claude Opus 4.8 --- .../tools_screen/tools_screen.md | 8 +- examples/companion_radio/ui-new/TrailScreen.h | 117 ++++++++++++++---- 2 files changed, 99 insertions(+), 26 deletions(-) diff --git a/docs/solo_features/tools_screen/tools_screen.md b/docs/solo_features/tools_screen/tools_screen.md index 44a2e965..eebb7452 100644 --- a/docs/solo_features/tools_screen/tools_screen.md +++ b/docs/solo_features/tools_screen/tools_screen.md @@ -92,7 +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 | +| Add by coords | Enter | Add a waypoint from a lat/lon/label form — 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`) | @@ -110,7 +110,11 @@ 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*. +**Adding by coordinates** — **Hold Enter → Add by coords** lets you create a waypoint without being there — no GPS fix required (handy for a meeting point or a spot read off a map). It opens a small form with three editable rows plus **Save**: + +- **Lat** / **Lon** — **Enter** opens the keyboard to type the value in decimal degrees (magnitude only; the keyboard has no minus sign), and **LEFT/RIGHT** toggles the hemisphere — N/S for latitude, E/W for longitude. +- **Label** — **Enter** to type a name (blank → auto `WP`). +- **Save** — validates the range and stores the waypoint. Missing or out-of-range values report a brief error. **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. diff --git a/examples/companion_radio/ui-new/TrailScreen.h b/examples/companion_radio/ui-new/TrailScreen.h index 8d867a76..fd88bd47 100644 --- a/examples/companion_radio/ui-new/TrailScreen.h +++ b/examples/companion_radio/ui-new/TrailScreen.h @@ -23,18 +23,26 @@ class TrailScreen : public UIScreen { bool _map_grid = true; // show scale grid in map view (Enter to toggle) // Waypoint sub-screens layered over the trail views. WP_OFF = normal trail. - enum WpMode { WP_OFF, WP_LIST, WP_NAV }; + enum WpMode { WP_OFF, WP_LIST, WP_NAV, WP_ADD }; uint8_t _wp_mode = WP_OFF; int _wp_sel = 0; int _wp_scroll = 0; 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; + // WP_ADD form — type a waypoint by coordinates. The keyboard has no comma or + // minus, so lat/lon are entered as magnitude + a hemisphere toggle (LEFT/RIGHT). + char _add_lat[12] = "", _add_lon[12] = ""; // magnitude strings + char _add_label[WAYPOINT_LABEL_LEN] = ""; + bool _add_lat_neg = false; // true = S + bool _add_lon_neg = false; // true = W + int _add_sel = 0; // 0=Lat 1=Lon 2=Label 3=Save + int _kb_field = -1; // which WP_ADD field the keyboard edits (-1 = label/rename) + // Action popup (Hold Enter / KEY_CONTEXT_MENU). Carries both settings rows // (Min dist, Units — cycled with LEFT/RIGHT while focused) and actions // (Start/Stop tracking, Reset). PopupMenu stores label pointers verbatim, so @@ -63,7 +71,7 @@ public: _wp_mode = WP_OFF; _wp_ctx.active = false; _kb_active = false; - _kb_coords = false; + _kb_field = -1; } int render(DisplayDriver& display) override { @@ -74,6 +82,7 @@ public: if (_kb_active) return _wp_kb.render(display); // Waypoint sub-screens replace the trail views. + if (_wp_mode == WP_ADD) { renderAddForm(display); return 1000; } if (_wp_mode == WP_NAV) { renderWpNav(display); return 1000; } if (_wp_mode == WP_LIST) { renderWpList(display); @@ -104,10 +113,11 @@ public: if (_kb_active) { auto r = _wp_kb.handleInput(c); 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; } + if (_kb_field >= 0) applyAddField(_wp_kb.buf); // WP_ADD field edit + else commitLabel(_wp_kb.buf); // mark / rename label + _kb_active = false; _kb_field = -1; } else if (r == KeyboardWidget::CANCELLED) { - _kb_active = false; _kb_coords = false; + _kb_active = false; _kb_field = -1; } return true; } @@ -146,6 +156,26 @@ public: return true; } + // WP_ADD form: type lat/lon (magnitude) + hemisphere toggle + label. + if (_wp_mode == WP_ADD) { + if (c == KEY_CANCEL) { _wp_mode = WP_OFF; return true; } + if (c == KEY_UP && _add_sel > 0) { _add_sel--; return true; } + if (c == KEY_DOWN && _add_sel < 3) { _add_sel++; return true; } + if (c == KEY_LEFT || c == KEY_RIGHT) { + if (_add_sel == 0) _add_lat_neg = !_add_lat_neg; // N <-> S + else if (_add_sel == 1) _add_lon_neg = !_add_lon_neg; // E <-> W + return true; + } + if (c == KEY_ENTER) { + if (_add_sel == 0) { _kb_field = 0; _wp_kb.begin(_add_lat, 11); _kb_active = true; } + else if (_add_sel == 1) { _kb_field = 1; _wp_kb.begin(_add_lon, 11); _kb_active = true; } + else if (_add_sel == 2) { _kb_field = 2; _wp_kb.begin(_add_label, WAYPOINT_LABEL_LEN - 1); _kb_active = true; } + else { commitAddForm(); } + return true; + } + return true; + } + // Waypoint navigation view — any nav key returns to the list. if (_wp_mode == WP_NAV) { if (c == KEY_CANCEL || c == KEY_LEFT || c == KEY_PREV || @@ -390,29 +420,68 @@ 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. + // Add a waypoint by coordinates (no GPS fix needed). Opens the WP_ADD form. 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; + _add_lat[0] = _add_lon[0] = _add_label[0] = '\0'; + _add_lat_neg = _add_lon_neg = false; + _add_sel = 0; + _wp_mode = WP_ADD; } - // 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; + // Store the just-typed value into the focused WP_ADD field. + void applyAddField(const char* buf) { + char* dst = (_kb_field == 0) ? _add_lat + : (_kb_field == 1) ? _add_lon : _add_label; + int cap = (_kb_field == 2) ? WAYPOINT_LABEL_LEN : (int)sizeof(_add_lat); + strncpy(dst, buf, cap - 1); + dst[cap - 1] = '\0'; + } + + // Validate the form and save the waypoint. + void commitAddForm() { + if (!_add_lat[0] || !_add_lon[0]) { _task->showAlert("Lat/Lon needed", 1200); return; } + double la = atof(_add_lat), lo = atof(_add_lon); + if (_add_lat_neg) la = -la; + if (_add_lon_neg) lo = -lo; + if (la < -90.0 || la > 90.0 || lo < -180.0 || lo > 180.0) { + _task->showAlert("Out of range", 1200); return; + } + if (_task->waypoints().full()) { _task->showAlert("Waypoints full", 1000); return; } + int32_t lat = (int32_t)(la * 1e6 + (la < 0 ? -0.5 : 0.5)); + int32_t lon = (int32_t)(lo * 1e6 + (lo < 0 ? -0.5 : 0.5)); + char label[WAYPOINT_LABEL_LEN]; + if (_add_label[0]) { strncpy(label, _add_label, sizeof(label) - 1); label[sizeof(label) - 1] = '\0'; } + else snprintf(label, sizeof(label), "WP%d", _task->waypoints().count() + 1); + if (_task->waypoints().add(lat, lon, (uint32_t)rtc_clock.getCurrentTime(), label)) { + _task->saveWaypoints(); + _task->showAlert("Waypoint saved", 800); + _wp_mode = WP_OFF; + } else { + _task->showAlert("Waypoints full", 1000); + } + } + + void renderAddForm(DisplayDriver& display) { + display.setColor(DisplayDriver::LIGHT); + display.drawTextCentered(display.width() / 2, 0, "ADD WAYPOINT"); + display.fillRect(0, display.headerH() - 1, display.width(), display.sepH()); + const int top = display.listStart(); + const int step = display.lineStep(); + const int cw = display.getCharWidth(); + for (int i = 0; i < 4; i++) { + int y = top + i * step; + bool sel = (i == _add_sel); + display.drawSelectionRow(0, y - 1, display.width(), step - 1, sel); + display.setCursor(0, y); display.print(sel ? ">" : " "); + char row[28]; + if (i == 0) snprintf(row, sizeof(row), "Lat: %c %s", _add_lat_neg ? 'S' : 'N', _add_lat[0] ? _add_lat : "--"); + else if (i == 1) snprintf(row, sizeof(row), "Lon: %c %s", _add_lon_neg ? 'W' : 'E', _add_lon[0] ? _add_lon : "--"); + else if (i == 2) snprintf(row, sizeof(row), "Label: %s", _add_label[0] ? _add_label : "(auto)"); + else snprintf(row, sizeof(row), "[Save]"); + display.setCursor(cw + 2, y); display.print(row); + display.setColor(DisplayDriver::LIGHT); } - _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) } void commitLabel(const char* buf) {