diff --git a/examples/companion_radio/ui-core/HopRoute.h b/examples/companion_radio/ui-core/HopRoute.h new file mode 100644 index 00000000..b8a796c5 --- /dev/null +++ b/examples/companion_radio/ui-core/HopRoute.h @@ -0,0 +1,132 @@ +#pragma once +// A route written in a message -- a bot's {path} / !path reply ("3f:a1:c8", +// "3fa1:c802") -- placed on the map: this device, then each hop's repeater, +// then the node the message came from when it has a position. The route a +// reply carries is the way our own message went to the bot, so it starts +// here. +// +// Finding one: two or more hex groups of one width (2 or 4 characters, i.e. +// 1- or 2-byte hashes) joined by ':', standing alone. A clock time (12:30, +// 12:30:45) reads the same, so all-decimal groups that make a time aren't +// one. A single hop ("3f") is too like an ordinary word to tell. +// +// Placing a hop: a repeater with a position whose key starts with the hash +// (PacketFlow::placeHash); of several, the nearest the point before it. A hop +// that can't be placed is left out of the line, still counted and named. +// +// Header-only Core, single TU (see UiCore.h). + +class HopRoute { +public: + static const int MAX_HOPS = 16; + static const int MAX_PTS = MAX_HOPS + 2; // here, the hops, the sender + + struct Pt { + int32_t lat, lon; + int16_t node; // the contact (getContactByIdx), -1 this device + int8_t hop; // its hop (0-based), -1 here, -2 the sender + }; + + // The first route in `text`: its hashes (w bytes each) into `hashes`, the + // count into `n`. + static bool find(const char* text, uint8_t* hashes, uint8_t& n, uint8_t& w) { + for (const char* p = text; p && *p; p++) { + if (p != text && (isalnum((unsigned char)p[-1]) || p[-1] == ':')) continue; + int gw = 0; + while (isxdigit((unsigned char)p[gw])) gw++; + if (gw != 2 && gw != 4) continue; + uint8_t k = 0; + bool decimal = true, ok = true; + int vals[3] = {0, 0, 0}; + const char* q = p; + for (;;) { + for (int i = 0; i < gw; i++) if (!isxdigit((unsigned char)q[i])) { ok = false; break; } + if (!ok || isalnum((unsigned char)q[gw])) { ok = false; break; } + if (k < MAX_HOPS) { + for (int b = 0; b < gw / 2; b++) hashes[k * (gw / 2) + b] = (uint8_t)((hexv(q[2 * b]) << 4) | hexv(q[2 * b + 1])); + } + for (int i = 0; i < gw; i++) if (!isdigit((unsigned char)q[i])) decimal = false; + if (k < 3 && gw == 2) vals[k] = (q[0] - '0') * 10 + (q[1] - '0'); + k++; + q += gw; + if (*q != ':') break; + q++; + } + if (!ok || k < 2) continue; + if (decimal && gw == 2 && k <= 3 && vals[0] <= 23 && vals[1] <= 59 && (k < 3 || vals[2] <= 59)) continue; // a time + n = k > MAX_HOPS ? MAX_HOPS : k; + w = (uint8_t)(gw / 2); + return true; + } + return false; + } + static bool has(const char* text) { + uint8_t h[MAX_HOPS * 2], n, w; + return find(text, h, n, w); + } + + // Places the route in `text` (see find()), from (own_lat, own_lon) when + // `own`, to the contact keyed `sender_key` (prefix of 4+ bytes) or named + // `sender_name` when one has a position. False when the text holds none. + bool show(const char* text, bool own, int32_t own_lat, int32_t own_lon, + const uint8_t* sender_key, const char* sender_name) { + if (!find(text, _hash, _n, _w)) return false; + _pts = 0; + _placed = 0; + if (own) addPt(own_lat, own_lon, -1, -1); + for (uint8_t i = 0; i < _n; i++) { + const bool near = _pts > 0; + int32_t la, lo; + int16_t idx; + if (PacketFlow::placeHash(&_hash[i * _w], _w, true, near ? _pt[_pts - 1].lat : 0, near ? _pt[_pts - 1].lon : 0, + near, la, lo, idx)) { + addPt(la, lo, idx, (int8_t)i); + _placed++; + } + } + ContactInfo* c = sender_key ? the_mesh.lookupContactByPubKey(sender_key, 4) : nullptr; + if (!c && sender_name && sender_name[0]) c = byName(sender_name); + if (c && (c->gps_lat || c->gps_lon)) addPt(c->gps_lat, c->gps_lon, -1, -2); + _active = true; + return true; + } + void close() { _active = false; } + bool active() const { return _active; } + + uint8_t hops() const { return _n; } + uint8_t placed() const { return _placed; } + int points() const { return _pts; } + const Pt& point(int i) const { return _pt[i]; } + + // A hop's name: its repeater's (as contactctl::hopName), else "?3F". + void hopName(int i, char* out, size_t len) const { + contactctl::hopName((uint8_t)((_w - 1) << 6 | _n), _hash, i, out, len); + } + + // "3 of 4 hops on the map" (or "4 hops on the map"). + void summary(char* out, size_t len) const { + if (_placed == _n) snprintf(out, len, "%u hop%s on the map", (unsigned)_n, _n == 1 ? "" : "s"); + else snprintf(out, len, "%u of %u hops on the map", (unsigned)_placed, (unsigned)_n); + } + +private: + static int hexv(char c) { return c <= '9' ? c - '0' : (c | 0x20) - 'a' + 10; } + + void addPt(int32_t lat, int32_t lon, int16_t node, int8_t hop) { + if (_pts < MAX_PTS) _pt[_pts++] = { lat, lon, node, hop }; + } + + static ContactInfo* byName(const char* name) { + for (int idx = MAX_ANON_CONTACTS; ; idx++) { + ContactInfo c; + if (!the_mesh.getContactByIdx(idx, c)) return nullptr; + if (strcmp(c.name, name) == 0) return the_mesh.lookupContactByPubKey(c.id.pub_key, PUB_KEY_SIZE); + } + } + + uint8_t _hash[MAX_HOPS * 2]; + uint8_t _n = 0, _w = 1, _placed = 0; + Pt _pt[MAX_PTS]; + int _pts = 0; + bool _active = false; +}; diff --git a/examples/companion_radio/ui-core/PacketFlow.h b/examples/companion_radio/ui-core/PacketFlow.h index 3cc242d4..c7a59c4c 100644 --- a/examples/companion_radio/ui-core/PacketFlow.h +++ b/examples/companion_radio/ui-core/PacketFlow.h @@ -248,6 +248,7 @@ private: return false; } +public: // HopRoute places a route's hops the same way // A contact with a position whose key starts with `hash` (`size` bytes): // repeaters only when `repeaters`; with several, the nearest to (nlat, nlon) // when `near`, else the first. @@ -268,6 +269,7 @@ private: return found; } +private: // The contact's index (getContactByIdx) by its key; -1 none. static int16_t indexOf(const uint8_t* key) { for (int idx = MAX_ANON_CONTACTS; ; idx++) { diff --git a/examples/companion_radio/ui-core/UiCore.h b/examples/companion_radio/ui-core/UiCore.h index 200b5733..f15c4af2 100644 --- a/examples/companion_radio/ui-core/UiCore.h +++ b/examples/companion_radio/ui-core/UiCore.h @@ -30,6 +30,7 @@ #include "AdminSession.h" #include "RoomSessions.h" #include "ContactControl.h" +#include "HopRoute.h" // after ContactControl (hopName) class UiCore : public MyMesh::Listener { public: @@ -71,6 +72,7 @@ public: PathEditor route; // route to a node: edit, trace out and back, save CourseEngine course; // GPS position + course-over-ground ring PacketFlow flow; // packets heard, as lines between nodes for the map + HopRoute hop_route; // a route written in a message ({path}), placed for the map LiveShareEngine live_share; // [LOC] auto-share session + peers' shared positions LocatorEngine locator; // active target, geofence crossings, proximity beeper TrailEngine trail; // GPS trail store, sampling, auto-pause, shutdown save diff --git a/examples/companion_radio/ui-lvgl/ConversationScreen.h b/examples/companion_radio/ui-lvgl/ConversationScreen.h index eaf99a11..ecd882a9 100644 --- a/examples/companion_radio/ui-lvgl/ConversationScreen.h +++ b/examples/companion_radio/ui-lvgl/ConversationScreen.h @@ -17,7 +17,7 @@ namespace convview { enum : uint8_t { A_FAV, A_READ, A_LOGIN, A_LOGOUT, A_PIN }; -enum : uint8_t { M_REPLY, M_TARGET }; +enum : uint8_t { M_REPLY, M_TARGET, M_ROUTE }; static uint8_t s_key[PUB_KEY_SIZE]; // the conversation the popup is about static uint8_t s_login_key[PUB_KEY_SIZE]; @@ -368,10 +368,12 @@ void UITask::messageMenu(int idx) { } bool can_reply = !m.own && m.from[0] && _compose_ta; - if (!can_reply && m.loc < 0) return; + const bool route = HopRoute::has(body); // a bot's {path} reply: the route on the map + if (!can_reply && m.loc < 0 && !route) return; lv_obj_t* acts = buttonBar(panel); if (can_reply) barButton(acts, LV_SYMBOL_EDIT " Reply", onMsgAction, M_REPLY, false); if (m.loc >= 0) barButton(acts, UI_SYMBOL_COMPASS " Set target", onMsgAction, M_TARGET, false); + if (route) barButton(acts, UI_SYMBOL_MAP " Route", onMsgAction, M_ROUTE, false); } // The open message popup, every half second on a conversation: the age in @@ -419,6 +421,18 @@ void UITask::messageAction(uint8_t act) { char msg[48]; snprintf(msg, sizeof(msg), "Target: %s", l.label); showToast(msg); + } else if (act == M_ROUTE && m.pos >= 0) { + // The way our message went to whoever wrote this: from here, to them. + const char* text = m.channel ? s_th_ch[m.pos].text : s_th_dm[m.pos].text; + int32_t lat, lon; + const bool own = _core->course.currentLocation(lat, lon) || _core->fixedPosition(lat, lon); + if (!_core->hop_route.show(text, own, lat, lon, m.channel ? nullptr : _thread_key, m.from)) return; + if (_core->hop_route.points() == 0) { showToast("None of its hops has a position"); _core->hop_route.close(); return; } + _map_nav = true; // the nodes map (openMap(false)), with the route's bar + _map_nodes = true; + _route_show = true; + showMap(false); + routeMapFrame(); } } diff --git a/examples/companion_radio/ui-lvgl/MapScreen.h b/examples/companion_radio/ui-lvgl/MapScreen.h index d9c7f793..ec998c85 100644 --- a/examples/companion_radio/ui-lvgl/MapScreen.h +++ b/examples/companion_radio/ui-lvgl/MapScreen.h @@ -200,10 +200,12 @@ static int s_sig_n = 0; static lv_obj_t* s_mark_layer = nullptr; // The route being picked (Route > Map): its points as lat/lon, and in layer px // from the last layout, drawn as a line under the markers. -static const int ROUTE_PTS = 10; +static const int ROUTE_PTS = HopRoute::MAX_PTS; // a picked route has 10 at most static int32_t s_route_lat[ROUTE_PTS], s_route_lon[ROUTE_PTS]; static int s_route_n = 0; static lv_point_precise_t s_route_px[ROUTE_PTS]; +static int8_t s_route_hop[ROUTE_PTS]; // a shown message route's: each point's hop (-1 none) +static uint8_t s_route_key[ROUTE_PTS][4]; // and its repeater's key prefix static bool s_route_on = false; static int s_mark_count = 0; static int s_mark_prev = 0; // during a rebuild: how many the last one left, reused where unchanged @@ -693,7 +695,7 @@ double UITask::mapCenterBias() const { // The bar along the map's bottom edge: the target's (Navigation map, while // there is one) or the route pick's. int UITask::mapBottomH() const { - if (_route_pick) return navmap::BAR_H; + if (_route_pick || _route_show) return navmap::BAR_H; return _map_nav && _nav_bar && !lv_obj_has_flag(_nav_bar, LV_OBJ_FLAG_HIDDEN) ? navmap::BAR_H : 0; } @@ -736,6 +738,8 @@ void UITask::openMap(bool nav) { // One map everywhere (trail, target, tools); from Nodes it adds the nodes. _map_nav = true; _map_nodes = !nav; + _route_show = false; // a message's route is shown by its own way in (messageAction) + _core->hop_route.close(); showMap(); } @@ -903,7 +907,7 @@ void UITask::buildMap() { lv_obj_add_flag(_map_hint, LV_OBJ_FLAG_HIDDEN); if (_map_nav) buildNavControls(body); _route_bar = _route_lbl = nullptr; - if (_route_pick) buildRouteBar(body); + if (_route_pick || _route_show) buildRouteBar(body); mapPlaceBottom(); layoutNow(body); @@ -1177,6 +1181,11 @@ void UITask::layoutMapMarks(double left, double top, int w, int h) { int at = ne.has_key ? _core->route.indexOf(ne.pub_key) : -1; if (at >= 0) m.order = (uint8_t)(at + 1); } + if (_route_show && m.vis && m.kind == MK_NODE && m.idx < _nearby->count()) { // a message's route: its hops numbered + const NearbyModel::Entry& ne = _nearby->at(m.idx); + for (int i = 0; ne.has_key && i < s_route_n; i++) + if (s_route_hop[i] >= 0 && memcmp(s_route_key[i], ne.pub_key, 4) == 0) { m.order = (uint8_t)(s_route_hop[i] + 1); break; } + } if (!m.vis) continue; m.x = (int16_t)lround(x); m.y = (int16_t)lround(y); @@ -1213,8 +1222,8 @@ void UITask::layoutMapMarks(double left, double top, int w, int h) { if (lw) { x1 = min(x1, lx); y1 = min(y1, ly); x2 = max(x2, lx + lw); y2 = max(y2, ly + lh); } a = { la.x1 + x1 - 1, la.y1 + y1 - 1, la.x1 + x2 + 1, la.y1 + y2 + 1 }; }; - s_route_on = _route_pick; - if (_route_pick) { // the route's points, in px (any distance away: the line is clipped by the layer) + s_route_on = _route_pick || _route_show; + if (s_route_on) { // the route's points, in px (any distance away: the line is clipped by the layer) for (int i = 0; i < s_route_n; i++) { double rx = lonToTileX(s_route_lon[i] / 1e6, _map_z) * TILE_PX - left; double ry = latToTileY(s_route_lat[i] / 1e6, _map_z) * TILE_PX - top; @@ -1339,7 +1348,7 @@ void UITask::mapFlowTick() { // ── Picking a route (Route > Map) ───────────────────────────────────────────── static void onRouteUndo(lv_event_t* e) { (void)e; s_ui->routeMapUndo(); } -static void onRouteDone(lv_event_t* e) { (void)e; s_ui->routeMapDone(); } +static void onRouteDone(lv_event_t* e) { (void)e; if (s_ui->routeShowing()) s_ui->routeShowDone(); else s_ui->routeMapDone(); } // A bar over the nav bar: the route so far, Undo (last hop out) and Done. void UITask::buildRouteBar(lv_obj_t* body) { @@ -1366,6 +1375,12 @@ void UITask::buildRouteBar(lv_obj_t* body) { lv_obj_add_event_cb(done, onRouteDone, LV_EVENT_CLICKED, NULL); lv_obj_center(label(done, LV_SYMBOL_OK " Done", THEME_FONT_SMALL, theme::TEXT)); stylePrimary(done); + if (_route_show) { // only looked at: the label has the room Undo would take + lv_obj_set_width(_route_lbl, lv_pct(75)); + lv_label_set_long_mode(_route_lbl, LV_LABEL_LONG_DOT); + routeMapRefresh(); + return; + } lv_obj_t* undo = lv_button_create(_route_bar); lv_obj_set_size(undo, 64, 34); lv_obj_align(undo, LV_ALIGN_RIGHT_MID, -76, 0); @@ -1377,6 +1392,16 @@ void UITask::buildRouteBar(lv_obj_t* body) { routeMapRefresh(); } +// Done (or Back) on a message's route: back to the conversation. +void UITask::routeShowDone() { + _route_show = false; + _core->hop_route.close(); + _screen = SCR_THREAD; + _nav_back = true; + buildThread(); + _nav_back = false; +} + void UITask::routeMapUndo() { _core->route.removeLast(); routeMapRefresh(); @@ -1385,8 +1410,33 @@ void UITask::routeMapUndo() { // The route changed: its points for the line, the text, the markers. void UITask::routeMapRefresh() { using namespace mapview; - PathEditor& R = _core->route; s_route_n = 0; + if (_route_show) { // a message's route: here, its hops placed, the sender + const HopRoute& H = _core->hop_route; + for (int i = 0; i < H.points() && s_route_n < ROUTE_PTS; i++) { + const HopRoute::Pt& p = H.point(i); + s_route_lat[s_route_n] = p.lat; s_route_lon[s_route_n] = p.lon; + s_route_hop[s_route_n] = p.hop; + ContactInfo c; + if (p.node >= 0 && the_mesh.getContactByIdx(p.node, c)) memcpy(s_route_key[s_route_n], c.id.pub_key, 4); + else s_route_hop[s_route_n] = -1; + s_route_n++; + } + if (_route_lbl) { + char t[128], nm[33]; + int o = 0; + H.summary(t, sizeof(t)); + o = strlen(t); + for (int i = 0; i < H.hops() && o < (int)sizeof(t) - 8; i++) { + H.hopName(i, nm, sizeof(nm)); + o += snprintf(t + o, sizeof(t) - o, "%s%.10s", i ? " > " : ": ", nm); + } + lv_label_set_text(_route_lbl, t); + } + if (_map_area) layoutMap(); + return; + } + PathEditor& R = _core->route; auto add = [&](int32_t lat, int32_t lon) { if ((lat != 0 || lon != 0) && s_route_n < ROUTE_PTS) { s_route_lat[s_route_n] = lat; s_route_lon[s_route_n] = lon; s_route_n++; } }; diff --git a/examples/companion_radio/ui-lvgl/UITask.cpp b/examples/companion_radio/ui-lvgl/UITask.cpp index f63858f7..8d28a71f 100644 --- a/examples/companion_radio/ui-lvgl/UITask.cpp +++ b/examples/companion_radio/ui-lvgl/UITask.cpp @@ -1313,6 +1313,7 @@ void UITask::back() { break; case SCR_MAP: if (_route_pick) { routeMapDone(); break; } + if (_route_show) { routeShowDone(); break; } if (_nav_overlay) navClosePopup(); else if (areaSelecting()) areaSelectEnd(); else if (_dl_overlay) mapDownloadClose(); diff --git a/examples/companion_radio/ui-lvgl/UITask.h b/examples/companion_radio/ui-lvgl/UITask.h index 7fb76150..45eed060 100644 --- a/examples/companion_radio/ui-lvgl/UITask.h +++ b/examples/companion_radio/ui-lvgl/UITask.h @@ -135,6 +135,8 @@ public: void routePick(int row); void routeMapTap(const uint8_t* pub_key, uint8_t type); void routeMapDone(); + void routeShowDone(); + bool routeShowing() const { return _route_show; } void routeMapRefresh(); void routeMapUndo(); void routeMapFrame(); @@ -665,6 +667,7 @@ private: uint32_t _next_flow_ms = 0; // the map's packet flow repaint uint8_t _flow_legend_tick = 0; // its legend, every 10th repaint bool _route_pick = false; // the nodes map taps repeaters into the route + bool _route_show = false; // the nodes map shows a message's route (_core->hop_route) lv_obj_t* _route_bar = nullptr; lv_obj_t* _route_lbl = nullptr; diff --git a/examples/companion_radio/ui-new/MessagesScreen.h b/examples/companion_radio/ui-new/MessagesScreen.h index c3727970..90ca1849 100644 --- a/examples/companion_radio/ui-new/MessagesScreen.h +++ b/examples/companion_radio/ui-new/MessagesScreen.h @@ -91,7 +91,7 @@ class MessagesScreen : public UIScreen { // location (a {loc} string or a [WAY] share), plus Path/Relayed by when the // entry has hop data recorded. _fs_act maps each visible row back to an // action so the index math survives the conditional layout. - enum FsAct : uint8_t { FS_REPLY, FS_NAV, FS_SAVE, FS_TARGET, FS_PATH }; + enum FsAct : uint8_t { FS_REPLY, FS_NAV, FS_SAVE, FS_TARGET, FS_PATH, FS_ROUTE }; uint8_t _fs_act[5]; int _fs_act_n = 0; // Which history entry the currently-open Options menu (and, after FS_PATH is @@ -366,8 +366,9 @@ class MessagesScreen : public UIScreen { if (is_channel) path_is_relay = (_history.chAtPos(ring_pos).relay_seq != 0); } bool has_path_item = hop_count > 0; + const bool has_route = HopRoute::has(body); // a bot's {path} reply: the route on a map - int n = (reply_allowed ? 1 : 0) + (has_loc ? 3 : 0) + (has_path_item ? 1 : 0); + int n = (reply_allowed ? 1 : 0) + (has_loc ? 3 : 0) + (has_path_item ? 1 : 0) + (has_route ? 1 : 0); if (n == 0) return; _fs_act_n = 0; _ctx_menu.begin("Options", n); @@ -381,6 +382,82 @@ class MessagesScreen : public UIScreen { _ctx_menu.addItem(_fs_path_item); _fs_act[_fs_act_n++] = FS_PATH; } + if (has_route) { _ctx_menu.addItem("Show route"); _fs_act[_fs_act_n++] = FS_ROUTE; } + } + + // The route in the message stashed by buildFsMenu(), placed: the way our + // message went to whoever wrote this, so from here to them. + void showRoute() { + if (_fs_menu_pos < 0) return; + const char* text; + const uint8_t* key = nullptr; + char name[32] = ""; + if (_fs_menu_is_channel) { // "Name: text" + text = _history.chAtPos(_fs_menu_pos).text; + const char* sep = strstr(text, ": "); + if (sep && sep - text < (int)sizeof(name)) { memcpy(name, text, sep - text); name[sep - text] = '\0'; } + } else { + text = _history.dmAtPos(_fs_menu_pos).text; + key = _history.dmAtPos(_fs_menu_pos).prefix; + } + int32_t lat, lon; + const bool own = _task->currentLocation(lat, lon) || _task->core().fixedPosition(lat, lon); + HopRoute& R = _task->core().hop_route; + if (!R.show(text, own, lat, lon, key, name)) return; + if (R.points() == 0) { R.close(); _task->showAlert("No hop has a position", 1200); } + } + + // The route shown (showRoute()): the points fitted to the screen, joined; + // you, each hop's number, the sender; under it how many hops are placed. + void renderRoute(DisplayDriver& d) { + const HopRoute& R = _task->core().hop_route; + drawScreenHeader(d, "Route"); + const int W = d.width(), H = d.height(), lh = d.getLineHeight(), s = miniIconScale(d); + const int top = d.listStart(), bar_y = H - lh - 1; + const int ax = 1, ay = top, aw = W - 2, ah = bar_y - top - 2; + d.setColor(UIColor::primary_txt); + int32_t mnla = 0, mxla = 0, mnlo = 0, mxlo = 0; + for (int i = 0; i < R.points(); i++) { + const HopRoute::Pt& p = R.point(i); + if (i == 0 || p.lat < mnla) mnla = p.lat; if (i == 0 || p.lat > mxla) mxla = p.lat; + if (i == 0 || p.lon < mnlo) mnlo = p.lon; if (i == 0 || p.lon > mxlo) mxlo = p.lon; + } + float kx = cosf(((mnla + mxla) / 2.0e6f) * (float)M_PI / 180.0f); + if (kx < 0.05f) kx = 0.05f; + float span_la = (float)(mxla - mnla), span_lo = (float)(mxlo - mnlo) * kx; + if (span_la < 2000.0f) span_la = 2000.0f; + if (span_lo < 2000.0f) span_lo = 2000.0f; + const int m = 4 * s + d.getCharWidth(); // the marks and a hop's number clear of the edge + const float fw = (float)(aw - 2 * m), fh = (float)(ah - 2 * m); + const float scale = fh / span_la < fw / span_lo ? fh / span_la : fw / span_lo; + const float cla = (mnla + mxla) / 2.0f, clo = (mnlo + mxlo) / 2.0f; + auto proj = [&](const HopRoute::Pt& p, float& x, float& y) { + x = ax + aw / 2 + ((float)p.lon - clo) * kx * scale; + y = ay + ah / 2 - ((float)p.lat - cla) * scale; + }; + float x0 = 0, y0 = 0; + for (int i = 0; i < R.points(); i++) { + float x, y; proj(R.point(i), x, y); + if (i > 0) gfx::drawLineClipped(d, x0, y0, x, y, ax, ay, aw, ah); + x0 = x; y0 = y; + } + for (int i = 0; i < R.points(); i++) { + const HopRoute::Pt& p = R.point(i); + float fx, fy; proj(p, fx, fy); + const int x = (int)lroundf(fx), y = (int)lroundf(fy); + if (p.hop == -1) miniIconDrawCentered(d, x, y, ICON_MAP_CURRENT); + else if (p.hop == -2) miniIconDrawCentered(d, x, y, ICON_MAP_CONTACT); + else { + miniIconDrawCentered(d, x, y, ICON_MAP_DOT); + char t[4]; + snprintf(t, sizeof(t), "%d", p.hop + 1); + d.setCursor(x + 2 * s + 1, y - lh / 2); + d.print(t); + } + } + char t[40]; + R.summary(t, sizeof(t)); + d.drawTextCentered(W / 2, bar_y, t); } // Opens the path/relay detail popup for the entry stashed by buildFsMenu(). @@ -433,6 +510,8 @@ class MessagesScreen : public UIScreen { _nav_label[0] ? _nav_label : "Msg loc"); } else if (a == FS_PATH) { showPathDetail(); + } else if (a == FS_ROUTE) { + showRoute(); } } @@ -1143,6 +1222,7 @@ public: _ctx_menu.active = false; _ctx_dirty = false; _nav_active = false; + _task->core().hop_route.close(); _share_mode = false; _pick_target = false; _pick_bot_channel = false; @@ -1329,6 +1409,7 @@ public: // Navigate-to-location view sits over everything else while active. if (_nav_active) { renderNav(display); return 1000; } + if (_task->core().hop_route.active()) { renderRoute(display); return 5000; } int lh = display.getLineHeight(); int item_h = display.lineStep(); @@ -1934,6 +2015,11 @@ public: if (c == KEY_CANCEL) _nav_active = false; return true; } + // A message's route on the map: Back returns to the message. + if (_task->core().hop_route.active()) { + if (c == KEY_CANCEL) _task->core().hop_route.close(); + return true; + } if (_phase == MODE_SELECT) { // Context menu (Mark-all-read) takes precedence while active. if (_ctx_menu.active) {