mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-07-26 23:08:11 +00:00
feat(map): north + scale on home mini-map; avoid overlapping labels
Home Map page mini-preview gains a north mini-icon and a fillRect scale bar with a round-distance label (metric/imperial), and its status line now shows the distance to the nearest tracked contact alongside fix/track. Trail map labels no longer smear in dense clusters: each placed label reserves a rect and a new one tries four offsets around its marker, skipping (marker still shown) when none fit without overlap. Verified: WioTrackerL1_companion_solo_dual builds clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -81,14 +81,14 @@ Because it is the same list, all the same keys apply — **UP/DOWN** to navigate
|
||||
Records your route in a RAM ring buffer (up to 512 points, sampled every 1 s). Tracking runs in the background — a blinking **G** appears in the status bar. The trail survives display auto-off but is lost on reboot unless saved to flash first.
|
||||
|
||||
> [!TIP]
|
||||
> The **Map** view is also reachable directly from the home carousel — the **Map** page shows a live mini-preview (your position, trail, and tracked contacts); press **Enter** to open the full Trail Map, **Back** returns home.
|
||||
> The **Map** view is also reachable directly from the home carousel — the **Map** page shows a live mini-preview (your position, trail, and tracked contacts) with a **north marker** and a **scale bar**, and a status line with the GPS-fix state, tracked-node count and distance to the **nearest** tracked contact. Press **Enter** to open the full Trail Map; **Hold Enter** shares your position (see **Live Share**); **Back** returns home.
|
||||
|
||||
Cycle views with **LEFT / RIGHT**:
|
||||
|
||||
| View | Content |
|
||||
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Summary** | Distance, elapsed time, avg speed or pace, point count, tracking status |
|
||||
| **Map** | Auto-fit dot-and-line plot with cos(lat) aspect correction; segment breaks marked; north arrow; square scale grid fitted to the map frame (toggle under **Hold Enter → Settings → Grid**, Map view only). Your **current GPS position**, all **waypoints**, and any **live-tracked contacts** (positions shared via Live Share) are always drawn — even with no trail recording — so the map is useful standalone |
|
||||
| **Map** | Auto-fit dot-and-line plot with cos(lat) aspect correction; segment breaks marked; north arrow; square scale grid fitted to the map frame (toggle under **Hold Enter → Settings → Grid**, Map view only). Your **current GPS position**, all **waypoints**, and any **live-tracked contacts** (positions shared via Live Share) are always drawn — even with no trail recording — so the map is useful standalone. Point labels are auto-placed to avoid overlapping (a crowded cluster drops some labels rather than smearing them) |
|
||||
| **List** | Per-point rows showing local time (HH:MM) and delta distance from the previous point; segment-start rows show `start`; scroll with **UP/DOWN** |
|
||||
|
||||
| OLED | E-Ink |
|
||||
|
||||
@@ -577,6 +577,9 @@ private:
|
||||
const int area_w = proj.area_w, area_h = proj.area_h;
|
||||
WaypointStore& wp = _task->waypoints();
|
||||
const int wp_x_max = area_x + area_w - 1, wp_y_max = area_y + area_h - 1;
|
||||
// Reserved rectangles of labels already placed this frame, so a dense
|
||||
// cluster of points doesn't smear their labels on top of one another.
|
||||
LblRect occ[40]; int nocc = 0;
|
||||
for (int i = 0; i < wp.count(); i++) {
|
||||
const Waypoint& w = wp.at(i);
|
||||
int wx, wy; proj.project(w.lat_1e6, w.lon_1e6, wx, wy);
|
||||
@@ -584,7 +587,7 @@ private:
|
||||
if (wy < area_y) wy = area_y; else if (wy > wp_y_max) wy = wp_y_max;
|
||||
drawWaypointMarker(display, wx, wy);
|
||||
char s[3] = { w.label[0], w.label[0] ? w.label[1] : (char)0, 0 };
|
||||
drawWaypointLabel(display, s, wx, wy, area_x, area_y, area_w, area_h);
|
||||
drawLabelAvoiding(display, s, wx, wy, area_x, area_y, area_w, area_h, occ, nocc, 40);
|
||||
}
|
||||
// Live-tracked contacts ([LOC] shares): filled diamond + name initial,
|
||||
// clamped to the frame like waypoints.
|
||||
@@ -599,7 +602,7 @@ private:
|
||||
if (cy < area_y) cy = area_y; else if (cy > wp_y_max) cy = wp_y_max;
|
||||
drawContactMarker(display, cx, cy);
|
||||
char s2[3] = { s.name[0], s.name[0] ? s.name[1] : (char)0, 0 };
|
||||
drawWaypointLabel(display, s2, cx, cy, area_x, area_y, area_w, area_h);
|
||||
drawLabelAvoiding(display, s2, cx, cy, area_x, area_y, area_w, area_h, occ, nocc, 40);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -848,6 +851,38 @@ private:
|
||||
d.print(s);
|
||||
}
|
||||
|
||||
// Map label collision avoidance. Each placed label reserves a rectangle; a new
|
||||
// label tries up to four offsets around its marker (upper-right, upper-left,
|
||||
// lower-right, lower-left) and is skipped — the marker still shows — if none
|
||||
// fit on-frame without overlapping an already-placed one. On a dense map this
|
||||
// drops a few labels instead of smearing them into an unreadable blob.
|
||||
struct LblRect { int x, y, w, h; };
|
||||
static bool rectsOverlap(int ax, int ay, int aw, int ah, int bx, int by, int bw, int bh) {
|
||||
return ax < bx + bw && bx < ax + aw && ay < by + bh && by < ay + ah;
|
||||
}
|
||||
static void drawLabelAvoiding(DisplayDriver& d, const char* s, int wx, int wy,
|
||||
int ax, int ay, int aw, int ah,
|
||||
LblRect* occ, int& nocc, int occ_max) {
|
||||
if (!s[0]) return;
|
||||
int tw = (int)d.getTextWidth(s);
|
||||
int ch = d.getLineHeight();
|
||||
const int cand[4][2] = { { wx + 4, wy - 3 }, { wx - 4 - tw, wy - 3 },
|
||||
{ wx + 4, wy + 3 }, { wx - 4 - tw, wy + 3 } };
|
||||
for (int c = 0; c < 4; c++) {
|
||||
int lx = cand[c][0], ly = cand[c][1];
|
||||
if (lx < ax || lx + tw > ax + aw || ly < ay || ly + ch > ay + ah) continue;
|
||||
bool clash = false;
|
||||
for (int k = 0; k < nocc; k++)
|
||||
if (rectsOverlap(lx, ly, tw, ch, occ[k].x, occ[k].y, occ[k].w, occ[k].h)) { clash = true; break; }
|
||||
if (clash) continue;
|
||||
d.setCursor(lx, ly);
|
||||
d.print(s);
|
||||
if (nocc < occ_max) occ[nocc++] = { lx, ly, tw, ch };
|
||||
return;
|
||||
}
|
||||
// No free slot — skip the label; the marker alone still conveys the point.
|
||||
}
|
||||
|
||||
static void drawFilledDot(DisplayDriver& d, int cx, int cy) {
|
||||
miniIconDrawCentered(d, cx, cy, ICON_MAP_DOT);
|
||||
}
|
||||
|
||||
@@ -554,6 +554,13 @@ public:
|
||||
if (have_gps) fold(mla, mlo);
|
||||
if (!init) return false;
|
||||
|
||||
// North marker — top-right, the same mini-icon as the full Trail map.
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
{
|
||||
const int ns = miniIconScale(display);
|
||||
miniIconDrawTop(display, ax + aw - ICON_MAP_NORTH.w * ns - 1, ay + 1, ICON_MAP_NORTH);
|
||||
}
|
||||
|
||||
int cx = ax + aw / 2, cy = ay + ah / 2;
|
||||
// Degenerate: one coincident point — just centre the markers.
|
||||
if (mnla == mxla && mnlo == mxlo) {
|
||||
@@ -586,6 +593,37 @@ public:
|
||||
miniIconDrawCentered(display, px, py, ICON_MAP_CONTACT);
|
||||
}
|
||||
if (have_gps) { int px, py; project(mla, mlo, px, py); miniIconDrawCentered(display, px, py, ICON_MAP_CURRENT); }
|
||||
|
||||
// Scale bar (bottom-left): a round distance ~1/3 of the width, drawn with
|
||||
// fillRect ticks (gfx line isn't available this early in the TU) + a label,
|
||||
// so the preview conveys rough scale / distance.
|
||||
{
|
||||
static const float M_PER_1E6 = 0.11132f; // metres per 1e-6° lat
|
||||
float ppm = scale / M_PER_1E6; // pixels per metre
|
||||
if (ppm > 0.0f) {
|
||||
bool imp = _task->useImperial();
|
||||
static const float MET_M[] = { 5,10,25,50,100,250,500,1000,2000,5000,10000,25000,50000 };
|
||||
static const char* MET_L[] = { "5m","10m","25m","50m","100m","250m","500m","1km","2km","5km","10km","25km","50km" };
|
||||
static const float IMP_M[] = { 4.572f,15.24f,30.48f,76.2f,152.4f,402.34f,804.67f,1609.34f,4828.0f,16093.4f,80467.2f };
|
||||
static const char* IMP_L[] = { "15ft","50ft","100ft","250ft","500ft","1/4mi","1/2mi","1mi","3mi","10mi","50mi" };
|
||||
const float* M = imp ? IMP_M : MET_M;
|
||||
const char* const* L = imp ? IMP_L : MET_L;
|
||||
int N = imp ? (int)(sizeof(IMP_M) / sizeof(IMP_M[0])) : (int)(sizeof(MET_M) / sizeof(MET_M[0]));
|
||||
float target = (aw / 3.0f) / ppm; // ~1/3 of the width, in metres
|
||||
int sel = 0;
|
||||
for (int i = N - 1; i >= 0; i--) if (M[i] <= target) { sel = i; break; }
|
||||
int barpx = (int)(M[sel] * ppm + 0.5f);
|
||||
if (barpx < 6) barpx = 6;
|
||||
if (barpx > aw - 2) barpx = aw - 2;
|
||||
int bx = ax + 1, by = ay + ah - 1;
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.fillRect(bx, by, barpx, 1); // bar
|
||||
display.fillRect(bx, by - 2, 1, 3); // left tick
|
||||
display.fillRect(bx + barpx - 1, by - 2, 1, 3); // right tick
|
||||
display.setCursor(bx, by - 2 - display.getLineHeight());
|
||||
display.print(L[sel]);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -963,8 +1001,26 @@ public:
|
||||
char info[40];
|
||||
int32_t la, lo;
|
||||
bool fix = _task->currentLocation(la, lo);
|
||||
int trk = _task->liveTrack().active(rtc_clock.getCurrentTime());
|
||||
snprintf(info, sizeof(info), "Fix:%s Track:%d", fix ? "y" : "n", trk);
|
||||
uint32_t now_m = rtc_clock.getCurrentTime();
|
||||
LiveTrackStore& lt = _task->liveTrack();
|
||||
int trk = lt.active(now_m);
|
||||
// Distance to the nearest tracked contact, so the page also answers
|
||||
// "how far is the closest shared point".
|
||||
float nearest = -1.0f;
|
||||
if (fix) {
|
||||
for (int i = 0; i < LiveTrackStore::CAPACITY; i++) {
|
||||
if (!lt.isActive(i, now_m)) continue;
|
||||
float d = geo::haversineKm(la, lo, lt.slotAt(i).lat_1e6, lt.slotAt(i).lon_1e6);
|
||||
if (nearest < 0 || d < nearest) nearest = d;
|
||||
}
|
||||
}
|
||||
if (nearest >= 0) {
|
||||
char ds[12];
|
||||
geo::fmtDist(ds, sizeof(ds), nearest, _task->useImperial());
|
||||
snprintf(info, sizeof(info), "Fix:%s Trk:%d %s", fix ? "y" : "n", trk, ds);
|
||||
} else {
|
||||
snprintf(info, sizeof(info), "Fix:%s Track:%d", fix ? "y" : "n", trk);
|
||||
}
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
if (!drew)
|
||||
display.drawTextCentered(display.width() / 2, content_y + area_h / 2, "No GPS / no trail");
|
||||
|
||||
Reference in New Issue
Block a user