mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-11-01 10:21:59 +00:00
feat(eink): Nodes map uses the room - names, trail, waypoints, side pane with the flow legend, a scale bar
- Every e-ink build: the nodes' names beside their dots, placed where they cover no other name, dot or mark; the trail dotted and the waypoints. - 4.2" and 4.26": the map on the left 3/5, beside it the selected node (distance and direction, type, last heard, path, a compass rose) and, with Flow on, a legend counting each kind over the last minute; the kinds are told apart by their dashes (gfx::Dash). - E-ink scale: a framed bar, half filled, ticks at the ends, the distance over its middle. The OLED map is unchanged. - The distance shows with the position set in Privacy too. - Sim: sim_trail() lays a trail and two waypoints. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,14 +11,20 @@
|
||||
|
||||
namespace gfx {
|
||||
|
||||
// A dash pattern: bit i of `bits` says whether step i of every `len` is
|
||||
// drawn (steps of a line's width, so a doubled panel doubles the dashes).
|
||||
// len 0: solid.
|
||||
struct Dash { uint16_t bits; uint8_t len; };
|
||||
static const Dash DASH_SOLID = { 0, 0 };
|
||||
|
||||
// Bresenham line between two points.
|
||||
static inline void drawLine(DisplayDriver& d, int x0, int y0, int x1, int y1) {
|
||||
static inline void drawLine(DisplayDriver& d, int x0, int y0, int x1, int y1, Dash dash = DASH_SOLID) {
|
||||
int dx = abs(x1 - x0), sx = x0 < x1 ? 1 : -1;
|
||||
int dy = -abs(y1 - y0), sy = y0 < y1 ? 1 : -1;
|
||||
int err = dx + dy;
|
||||
const int t = d.sepH(); // 2px on a doubled panel
|
||||
while (true) {
|
||||
d.fillRect(x0 - t / 2, y0 - t / 2, t, t);
|
||||
for (int i = 0; ; i++) {
|
||||
if (!dash.len || (dash.bits >> ((i / t) % dash.len) & 1)) d.fillRect(x0 - t / 2, y0 - t / 2, t, t);
|
||||
if (x0 == x1 && y0 == y1) break;
|
||||
int e2 = err * 2;
|
||||
if (e2 >= dy) { err += dy; x0 += sx; }
|
||||
@@ -30,7 +36,8 @@ static inline void drawLine(DisplayDriver& d, int x0, int y0, int x1, int y1) {
|
||||
// it misses the box. For lines whose ends may lie far off the screen: the
|
||||
// ends are floats, so a point projected thousands of screens away (a map
|
||||
// zoomed in) neither overflows an int nor turns the line around.
|
||||
static inline void drawLineClipped(DisplayDriver& d, float x0, float y0, float x1, float y1, int bx, int by, int bw, int bh) {
|
||||
static inline void drawLineClipped(DisplayDriver& d, float x0, float y0, float x1, float y1, int bx, int by, int bw, int bh,
|
||||
Dash dash = DASH_SOLID) {
|
||||
float t0 = 0, t1 = 1;
|
||||
const float dx = x1 - x0, dy = y1 - y0;
|
||||
const float p[4] = { -dx, dx, -dy, dy };
|
||||
@@ -41,7 +48,7 @@ static inline void drawLineClipped(DisplayDriver& d, float x0, float y0, float x
|
||||
if (p[i] < 0) { if (r > t1) return; if (r > t0) t0 = r; }
|
||||
else { if (r < t0) return; if (r < t1) t1 = r; }
|
||||
}
|
||||
drawLine(d, (int)lroundf(x0 + t0 * dx), (int)lroundf(y0 + t0 * dy), (int)lroundf(x0 + t1 * dx), (int)lroundf(y0 + t1 * dy));
|
||||
drawLine(d, (int)lroundf(x0 + t0 * dx), (int)lroundf(y0 + t0 * dy), (int)lroundf(x0 + t1 * dx), (int)lroundf(y0 + t1 * dy), dash);
|
||||
}
|
||||
|
||||
// Midpoint-circle outline.
|
||||
|
||||
@@ -893,9 +893,15 @@ class NearbyScreen : public UIScreen, protected NearbyModel {
|
||||
// Every node with a position (and you) fitted in; zoomed in, centred on the
|
||||
// selected one. Dots, the selected one boxed, live shares as diamonds;
|
||||
// under the map its name and distance, "+n" for others under the same spot.
|
||||
// E-ink has the room for more: the nodes' names beside their dots, your
|
||||
// trail (dotted) and waypoints; a screen 40 characters wide (4.2", 4.26")
|
||||
// keeps the map to the left and shows the selected node and the packet
|
||||
// flow's legend beside it, the flow's kinds then told apart by their dashes.
|
||||
void renderMap(DisplayDriver& d) {
|
||||
const int W = d.width(), H = d.height(), lh = d.getLineHeight(), s = miniIconScale(d);
|
||||
const int bar_y = H - lh - 1, ax = 1, ay = 1, aw = W - 2, ah = bar_y - 3;
|
||||
const int W = d.width(), H = d.height(), lh = d.getLineHeight(), cw = d.getCharWidth(), s = miniIconScale(d);
|
||||
const bool eink = Features::IS_EINK, wide = eink && W >= 40 * cw;
|
||||
const int bar_y = H - lh - 1, mw = wide ? W * 3 / 5 : W; // the map's right edge
|
||||
const int ax = 1, ay = 1, aw = mw - 2, ah = wide ? H - 2 : bar_y - 3;
|
||||
d.setColor(UIColor::primary_txt);
|
||||
int32_t tla = 0, tlo = 0;
|
||||
const bool target = _task->activeTargetPos(tla, tlo);
|
||||
@@ -922,8 +928,9 @@ class NearbyScreen : public UIScreen, protected NearbyModel {
|
||||
float span_la = (float)(mxla - mnla), span_lo = (float)(mxlo - mnlo) * kx;
|
||||
if (span_la < 2000.0f) span_la = 2000.0f; // ~200 m: one node alone still has a scale
|
||||
if (span_lo < 2000.0f) span_lo = 2000.0f;
|
||||
// Fitted inside a margin clear of the north mark and the scale's label.
|
||||
const int mx = ICON_MAP_NORTH.w * s + 2, my = lh + 2;
|
||||
// Fitted inside a margin clear of the north mark and the scale's label,
|
||||
// and on e-ink of a name beside a dot at the edge.
|
||||
const int mx = ICON_MAP_NORTH.w * s + 2 + (eink ? 2 * cw : 0), my = lh + 2 + (eink ? 2 * (lh / 3) + d.sepH() : 0);
|
||||
const float fw = (float)(aw - 2 * mx), fh = (float)(ah - 2 * my);
|
||||
float scale = fh / span_la < fw / span_lo ? fh / span_la : fw / span_lo;
|
||||
float cla = (mnla + mxla) / 2.0f, clo = (mnlo + mxlo) / 2.0f;
|
||||
@@ -938,6 +945,28 @@ class NearbyScreen : public UIScreen, protected NearbyModel {
|
||||
y = (int)lroundf(fy < -30000 ? -30000 : fy > 30000 ? 30000 : fy);
|
||||
};
|
||||
auto inside = [&](int x, int y) { return x >= ax && x < ax + aw && y >= ay && y < ay + ah; };
|
||||
const int t = d.sepH();
|
||||
|
||||
// E-ink: your trail, dotted, and the waypoints, under everything else.
|
||||
if (eink) {
|
||||
const TrailStore& tr = _task->core().trail.store();
|
||||
float x0 = 0, y0 = 0;
|
||||
for (int i = 0; i < tr.count(); i++) {
|
||||
float x1, y1; projectF(tr.at(i).lat_1e6, tr.at(i).lon_1e6, x1, y1);
|
||||
if (i > 0 && !(tr.at(i).flags & TRAIL_FLAG_SEG_START)) gfx::drawLineClipped(d, x0, y0, x1, y1, ax, ay, aw, ah, trailDash());
|
||||
x0 = x1; y0 = y1;
|
||||
}
|
||||
TrailPoint pend;
|
||||
if (tr.count() > 0 && tr.pendingPoint(pend)) {
|
||||
float x1, y1; projectF(pend.lat_1e6, pend.lon_1e6, x1, y1);
|
||||
gfx::drawLineClipped(d, x0, y0, x1, y1, ax, ay, aw, ah, trailDash());
|
||||
}
|
||||
const WaypointStore& wps = _task->waypoints();
|
||||
for (int i = 0; i < wps.count(); i++) {
|
||||
int x, y; project(wps.at(i).lat_1e6, wps.at(i).lon_1e6, x, y);
|
||||
if (inside(x, y)) miniIconDrawCentered(d, x, y, ICON_MAP_WAYPOINT);
|
||||
}
|
||||
}
|
||||
|
||||
int sx, sy, near = 0;
|
||||
project(sel.lat_e6, sel.lon_e6, sx, sy);
|
||||
@@ -954,7 +983,6 @@ class NearbyScreen : public UIScreen, protected NearbyModel {
|
||||
// Its points are drawn where the contacts and you are now, not where they
|
||||
// were when it was heard, so the lines meet on the marks.
|
||||
const PacketFlow& fl = _task->core().flow;
|
||||
const int t = d.sepH();
|
||||
for (int i = 0, lines = 0; fl.enabled() && i < fl.count(); i++) {
|
||||
const PacketFlow::Flow& f = fl.at(i);
|
||||
if (millis() - f.t_ms > FLOW_SHOW_MS) break; // newest first
|
||||
@@ -975,28 +1003,30 @@ class NearbyScreen : public UIScreen, protected NearbyModel {
|
||||
gfx::drawCircleClipped(d, (int)lroundf(px[0]), (int)lroundf(py[0]), 6 * s, ax, ay, aw, ah);
|
||||
continue;
|
||||
}
|
||||
const gfx::Dash dash = wide ? flowDash(f.kind) : gfx::DASH_SOLID;
|
||||
const bool bold = lines++ < 2;
|
||||
for (int k = 1; k < f.n; k++) {
|
||||
gfx::drawLineClipped(d, px[k - 1], py[k - 1], px[k], py[k], ax, ay, aw, ah);
|
||||
gfx::drawLineClipped(d, px[k - 1], py[k - 1], px[k], py[k], ax, ay, aw, ah, dash);
|
||||
if (!bold) continue;
|
||||
// a stroke either side of the first, across the line's run
|
||||
const bool flat = fabsf(px[k] - px[k - 1]) >= fabsf(py[k] - py[k - 1]);
|
||||
const float ox = flat ? 0 : t, oy = flat ? t : 0;
|
||||
gfx::drawLineClipped(d, px[k - 1] + ox, py[k - 1] + oy, px[k] + ox, py[k] + oy, ax, ay, aw, ah);
|
||||
gfx::drawLineClipped(d, px[k - 1] - ox, py[k - 1] - oy, px[k] - ox, py[k] - oy, ax, ay, aw, ah);
|
||||
gfx::drawLineClipped(d, px[k - 1] + ox, py[k - 1] + oy, px[k] + ox, py[k] + oy, ax, ay, aw, ah, dash);
|
||||
gfx::drawLineClipped(d, px[k - 1] - ox, py[k - 1] - oy, px[k] - ox, py[k] - oy, ax, ay, aw, ah, dash);
|
||||
}
|
||||
}
|
||||
if (me) { int x, y; project(mla, mlo, x, y); if (inside(x, y)) miniIconDrawCentered(d, x, y, ICON_MAP_CURRENT); }
|
||||
if (target) { int x, y; project(tla, tlo, x, y); if (inside(x, y)) miniIconDrawCentered(d, x, y, ICON_MAP_TARGET); }
|
||||
const int r = 4 * s;
|
||||
d.drawRect(sx - r, sy - r, 2 * r + 1, 2 * r + 1);
|
||||
// The north mark and the scale stay readable over the lines.
|
||||
|
||||
// The north mark and the scale, on a background so they stay readable
|
||||
// over the lines; their boxes are kept clear of the names below.
|
||||
const int nw = ICON_MAP_NORTH.w * s + 2, nh = ICON_MAP_NORTH.h * s + 1;
|
||||
d.setColor(UIColor::window_bkg);
|
||||
d.fillRect(ax + aw - ICON_MAP_NORTH.w * s - 2, ay, ICON_MAP_NORTH.w * s + 2, ICON_MAP_NORTH.h * s + 1);
|
||||
d.fillRect(ax + aw - nw, ay, nw, nh);
|
||||
d.setColor(UIColor::primary_txt);
|
||||
miniIconDrawTop(d, ax + aw - ICON_MAP_NORTH.w * s - 1, ay, ICON_MAP_NORTH);
|
||||
|
||||
// The scale: a round distance about a quarter of the width, bottom right.
|
||||
const float m_px = 0.111f / scale; // 1e-6 degree of latitude is 0.111 m
|
||||
static const float STEPS[] = { 10, 20, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 50000, 100000, 200000 };
|
||||
float m = STEPS[0];
|
||||
@@ -1004,26 +1034,176 @@ class NearbyScreen : public UIScreen, protected NearbyModel {
|
||||
const int len = (int)(m / m_px);
|
||||
char sc[12];
|
||||
geo::fmtDist(sc, sizeof(sc), m / 1000.0f, useImperial());
|
||||
if (len > 2 && len < aw) {
|
||||
const int ly = ay + ah - 2, lx = ax + aw - len - 1, tw = d.getTextWidth(sc);
|
||||
const bool scale_on = len > 2 && len < aw;
|
||||
const int ly = ay + ah - 2, lx = ax + aw - len - 1 - (wide ? 2 * t : 0), stw = d.getTextWidth(sc);
|
||||
// On e-ink a map's scale bar: a framed bar, its left half filled, ticks
|
||||
// up at the ends, the distance over its middle -- the OLED keeps a line.
|
||||
const int bh = lh / 3 + t, tick = lh / 3; // e-ink: the bar's height, the ticks above it
|
||||
if (scale_on && eink) {
|
||||
const int by = ay + ah - bh - 1, ty = by - tick - lh;
|
||||
int tx = lx + (len - stw) / 2;
|
||||
if (tx + stw > ax + aw) tx = ax + aw - stw;
|
||||
d.setColor(UIColor::window_bkg);
|
||||
d.fillRect(lx + len - tw - 1, ly - lh - 1, tw + 2, lh + 1);
|
||||
d.fillRect(tx - 2, ty - 1, stw + 4, lh + 1);
|
||||
d.fillRect(lx - t - 1, by - tick - 1, len + 2 * t + 2, bh + tick + 2);
|
||||
d.setColor(UIColor::primary_txt);
|
||||
for (int k = 0; k < t; k++) d.drawRect(lx + k, by + k, len - 2 * k, bh - 2 * k);
|
||||
d.fillRect(lx, by, len / 2, bh);
|
||||
d.fillRect(lx, by - tick, t, tick);
|
||||
d.fillRect(lx + len - t, by - tick, t, tick);
|
||||
d.setCursor(tx, ty);
|
||||
d.print(sc);
|
||||
} else if (scale_on) {
|
||||
d.setColor(UIColor::window_bkg);
|
||||
d.fillRect(lx + len - stw - 1, ly - lh - 1, stw + 2, lh + 1);
|
||||
d.fillRect(lx - 1, ly - 1, len + 2, s + 2);
|
||||
d.setColor(UIColor::primary_txt);
|
||||
d.fillRect(lx, ly, len, s);
|
||||
d.setCursor(lx + len - d.getTextWidth(sc), ly - lh);
|
||||
d.setCursor(lx + len - stw, ly - lh);
|
||||
d.print(sc);
|
||||
}
|
||||
|
||||
d.fillRect(0, bar_y - 1, W, d.sepH());
|
||||
char right[16] = "";
|
||||
if (sel.dist_km >= 0.0f) geo::fmtDist(right, sizeof(right), sel.dist_km, useImperial(), true);
|
||||
// E-ink: names beside the dots, the selected one first, then the others in
|
||||
// the list's order, each where it covers no other name, dot or the marks
|
||||
// (right of the dot, else left); one that fits nowhere is left out.
|
||||
if (eink) {
|
||||
struct Box { int16_t x, y, w, h; };
|
||||
Box taken[NEARBY_MAX * 2 + 4]; // ~0.5 KB of stack, only while drawing
|
||||
int nt = 0;
|
||||
auto hit = [&](const Box& b) {
|
||||
if (b.x < ax || b.y < ay || b.x + b.w > ax + aw || b.y + b.h > ay + ah) return true;
|
||||
for (int k = 0; k < nt; k++)
|
||||
if (b.x < taken[k].x + taken[k].w && taken[k].x < b.x + b.w && b.y < taken[k].y + taken[k].h && taken[k].y < b.y + b.h) return true;
|
||||
return false;
|
||||
};
|
||||
const int half = ICON_MAP_DOT.w * s / 2 + 1;
|
||||
auto box = [](int x, int y, int w, int h) { return Box{ (int16_t)x, (int16_t)y, (int16_t)w, (int16_t)h }; };
|
||||
taken[nt++] = box(ax + aw - nw, ay, nw, nh);
|
||||
if (scale_on) taken[nt++] = box(lx - t - 1, ay + ah - bh - tick - lh - 2, len + 2 * t + 2, bh + tick + lh + 3);
|
||||
for (int i = 0; i < _count && nt < NEARBY_MAX + 2; i++) {
|
||||
if (!hasPos(_entries[i])) continue;
|
||||
int x, y; project(_entries[i].lat_e6, _entries[i].lon_e6, x, y);
|
||||
if (inside(x, y)) taken[nt++] = box(x - half, y - half, 2 * half + 1, 2 * half + 1);
|
||||
}
|
||||
const int maxw = (wide ? 12 : 8) * cw;
|
||||
for (int n = 0; n < _count && nt < (int)(sizeof(taken) / sizeof(taken[0])); n++) {
|
||||
const int i = n == 0 ? _sel : (n <= _sel ? n - 1 : n); // the selected one first
|
||||
const Entry& e = _entries[i];
|
||||
if (!hasPos(e) || !e.name[0]) continue;
|
||||
int x, y; project(e.lat_e6, e.lon_e6, x, y);
|
||||
if (!inside(x, y)) continue;
|
||||
int tw = d.getTextWidth(e.name);
|
||||
if (tw > maxw) tw = maxw;
|
||||
const int off = i == _sel ? r + 2 : half + 1;
|
||||
Box b = box(x + off, y - lh / 2, tw + 2, lh);
|
||||
if (hit(b)) { b.x = (int16_t)(x - off - tw - 2); if (hit(b)) continue; }
|
||||
taken[nt++] = b;
|
||||
d.setColor(UIColor::window_bkg);
|
||||
d.fillRect(b.x, b.y, b.w, b.h);
|
||||
d.setColor(UIColor::primary_txt);
|
||||
d.drawTextEllipsized(b.x + 1, b.y, tw, e.name);
|
||||
}
|
||||
}
|
||||
|
||||
// The selected node: in a pane beside the map (wide), else on a bar under it.
|
||||
char dist[16] = "";
|
||||
float dkm = sel.dist_km;
|
||||
if (dkm < 0.0f && me) dkm = geo::haversineKm(mla, mlo, sel.lat_e6, sel.lon_e6);
|
||||
if (dkm >= 0.0f) geo::fmtDist(dist, sizeof(dist), dkm, useImperial(), true);
|
||||
char name[40];
|
||||
if (near > 0) snprintf(name, sizeof(name), "%s +%d", sel.name, near);
|
||||
else snprintf(name, sizeof(name), "%s", sel.name);
|
||||
const int rw = right[0] ? d.getTextWidth(right) + d.getCharWidth() : 0;
|
||||
if (wide) {
|
||||
renderMapPane(d, sel, name, dist, me ? geo::bearingDeg(mla, mlo, sel.lat_e6, sel.lon_e6) : -1, mw);
|
||||
return;
|
||||
}
|
||||
d.fillRect(0, bar_y - 1, W, d.sepH());
|
||||
const int rw = dist[0] ? d.getTextWidth(dist) + d.getCharWidth() : 0;
|
||||
d.drawTextEllipsized(1, bar_y + 1, W - 2 - rw, name);
|
||||
if (right[0]) { d.setCursor(W - 1 - d.getTextWidth(right), bar_y + 1); d.print(right); }
|
||||
if (dist[0]) { d.setCursor(W - 1 - d.getTextWidth(dist), bar_y + 1); d.print(dist); }
|
||||
}
|
||||
|
||||
// The map's dashes (wide e-ink): each kind of packet its own, the trail dotted.
|
||||
static gfx::Dash trailDash() { return { 0x1, 3 }; }
|
||||
static gfx::Dash flowDash(uint8_t kind) {
|
||||
switch (kind) {
|
||||
case PacketFlow::K_ADVERT: return gfx::DASH_SOLID;
|
||||
case PacketFlow::K_MSG: return { 0x3F, 9 }; // long dashes
|
||||
case PacketFlow::K_GROUP: return { 0x07, 6 }; // short dashes
|
||||
default: return { 0x5F, 9 }; // dash and dot: requests, acks, the rest
|
||||
}
|
||||
}
|
||||
|
||||
// Beside the map on a wide screen: the selected node (name, distance and
|
||||
// bearing, type, last heard, path) and, with the packet flow on, its legend:
|
||||
// a sample of each kind's dashes and how many were heard in the last minute.
|
||||
void renderMapPane(DisplayDriver& d, const Entry& e, const char* name, const char* dist, int deg, int mw) {
|
||||
const int W = d.width(), H = d.height(), lh = d.getLineHeight(), cw = d.getCharWidth(), t = d.sepH();
|
||||
const int px = mw + t + 3, pw = W - px - 2, step = d.lineStep();
|
||||
d.setColor(UIColor::primary_txt);
|
||||
d.fillRect(mw, 0, t, H);
|
||||
int y = 1;
|
||||
d.drawTextEllipsized(px, y, pw, name);
|
||||
y += lh + 1;
|
||||
d.fillRect(px, y, pw, t);
|
||||
y += t + 2;
|
||||
char buf[32];
|
||||
if (dist[0]) {
|
||||
if (deg >= 0) snprintf(buf, sizeof(buf), "%s %s", dist, geo::bearingCardinal(deg));
|
||||
else snprintf(buf, sizeof(buf), "%s", dist);
|
||||
d.drawTextEllipsized(px, y, pw, buf); y += step;
|
||||
}
|
||||
snprintf(buf, sizeof(buf), "%s", typeName(e.type));
|
||||
d.drawTextEllipsized(px, y, pw, buf); y += step;
|
||||
char age[16];
|
||||
fmtAge(age, sizeof(age), e.lastmod);
|
||||
snprintf(buf, sizeof(buf), "%s %s", e.is_live ? "Sharing" : "Seen", age);
|
||||
d.drawTextEllipsized(px, y, pw, buf); y += step;
|
||||
ContactInfo ci;
|
||||
if (e.contact_idx >= 0 && the_mesh.getContactByIdx(e.contact_idx, ci)) {
|
||||
if (ci.out_path_len == 0xFF) snprintf(buf, sizeof(buf), "Path: flood");
|
||||
else if ((ci.out_path_len & 63) == 0) snprintf(buf, sizeof(buf), "Path: direct");
|
||||
else snprintf(buf, sizeof(buf), "Path: %d hop%s", ci.out_path_len & 63, (ci.out_path_len & 63) == 1 ? "" : "s");
|
||||
d.drawTextEllipsized(px, y, pw, buf); y += step;
|
||||
}
|
||||
|
||||
// The legend, from the bottom up as far as it fits; between it and the
|
||||
// node, a compass rose pointing at the node when there is the room.
|
||||
const PacketFlow& fl = _task->core().flow;
|
||||
const bool trail = _task->core().trail.store().count() > 1;
|
||||
struct Row { const char* label; int n; gfx::Dash dash; bool ring; };
|
||||
Row rows[7];
|
||||
int nr = 0;
|
||||
if (fl.enabled()) {
|
||||
rows[nr++] = { "Advert", fl.count(PacketFlow::K_ADVERT), flowDash(PacketFlow::K_ADVERT), false };
|
||||
rows[nr++] = { "DM", fl.count(PacketFlow::K_MSG), flowDash(PacketFlow::K_MSG), false };
|
||||
rows[nr++] = { "Channel", fl.count(PacketFlow::K_GROUP), flowDash(PacketFlow::K_GROUP), false };
|
||||
rows[nr++] = { "Other", fl.count(PacketFlow::K_REQ) + fl.count(PacketFlow::K_ACK) + fl.count(PacketFlow::K_OTHER),
|
||||
flowDash(PacketFlow::K_OTHER), false };
|
||||
rows[nr++] = { "Sent", fl.count(PacketFlow::K_SENT), gfx::DASH_SOLID, true };
|
||||
}
|
||||
if (trail) rows[nr++] = { "Trail", -1, trailDash(), false };
|
||||
const int head = fl.enabled() ? 1 : 0;
|
||||
int top = nr ? H - (nr + head) * step - 1 : H;
|
||||
if (top < y + step / 2) { top = H; nr = 0; } // no room for it under the node
|
||||
const int room = top - y - lh / 2, size = room < pw ? room : pw;
|
||||
if (deg >= 0 && size >= 5 * lh) info::rose(d, px + pw / 2, y + room / 2, size / 2 - lh, deg);
|
||||
if (!nr) return;
|
||||
int ry = top;
|
||||
if (head) { d.setCursor(px, ry); d.print("Last minute"); ry += step; }
|
||||
const int sw = 3 * cw; // the sample line
|
||||
for (int i = 0; i < nr; i++, ry += step) {
|
||||
const int cy = ry + lh / 2;
|
||||
if (rows[i].ring) gfx::drawCircleClipped(d, px + sw / 2, cy, lh / 3, px, ry, sw, lh);
|
||||
else gfx::drawLine(d, px, cy, px + sw - 1, cy, rows[i].dash);
|
||||
d.setCursor(px + sw + cw, ry);
|
||||
d.print(rows[i].label);
|
||||
if (rows[i].n >= 0) {
|
||||
snprintf(buf, sizeof(buf), "%d", rows[i].n);
|
||||
d.setCursor(px + pw - d.getTextWidth(buf), ry);
|
||||
d.print(buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void renderScanDetail(DisplayDriver& display) {
|
||||
|
||||
@@ -446,6 +446,20 @@ void UITask::simNodes() {
|
||||
setCurrScreen(nearby_screen);
|
||||
}
|
||||
|
||||
// Sim tests: a walked trail ending at the GPS fix of simNodes(), and two
|
||||
// waypoints beside it, for the maps.
|
||||
void UITask::simTrail() {
|
||||
TrailStore& tr = _core->trail.store();
|
||||
const uint32_t now = rtc_clock.getCurrentTime();
|
||||
for (int i = 0; i <= 12; i++) {
|
||||
const float f = (12 - i) / 12.0f;
|
||||
tr.addPoint((int32_t)((50.0614f - 0.010f * f + 0.002f * (i % 3)) * 1e6f),
|
||||
(int32_t)((19.9372f - 0.018f * f * f) * 1e6f), now - (12 - i) * 60, 1);
|
||||
}
|
||||
waypoints().add((int32_t)(50.0560f * 1e6f), (int32_t)(19.9250f * 1e6f), now, "Car");
|
||||
waypoints().add((int32_t)(50.0680f * 1e6f), (int32_t)(19.9600f * 1e6f), now, "Hut");
|
||||
}
|
||||
|
||||
void UITask::simDisplay(bool on) {
|
||||
if (!_display) return;
|
||||
if (on) _display->turnOn(); else _display->turnOff();
|
||||
@@ -928,6 +942,10 @@ extern "C" EMSCRIPTEN_KEEPALIVE void sim_nodes() {
|
||||
if (g_sim_ui_task_for_js) g_sim_ui_task_for_js->simNodes();
|
||||
}
|
||||
|
||||
extern "C" EMSCRIPTEN_KEEPALIVE void sim_trail() {
|
||||
if (g_sim_ui_task_for_js) g_sim_ui_task_for_js->simTrail();
|
||||
}
|
||||
|
||||
// Sim tests: the display off (0) or on (1), as the auto-off and a key press do.
|
||||
extern "C" EMSCRIPTEN_KEEPALIVE void sim_display(int on) {
|
||||
if (g_sim_ui_task_for_js) g_sim_ui_task_for_js->simDisplay(on != 0);
|
||||
|
||||
@@ -312,6 +312,7 @@ public:
|
||||
void simUnread(int which); // sim tests: read and new messages, opened
|
||||
void simDisplay(bool on); // sim tests: the display off / on
|
||||
void simNodes(); // sim tests: you and nodes with positions, Nodes open
|
||||
void simTrail(); // sim tests: a trail and two waypoints for the maps
|
||||
#endif
|
||||
void openContactDM(const ContactInfo& ci);
|
||||
void openChannelHistory(uint8_t channel_idx); // Favourites dial: open a pinned channel
|
||||
|
||||
Reference in New Issue
Block a user