mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-10-15 01:52:10 +00:00
feat(ui-lvgl): navigation map, clock tools, settings pages, radio
- Home > Map is the navigation map: waypoints (list, add at GPS / by coordinates / long-press spot), live shares, trail recording + tools (save, load, GPX export, track back), live share controls, target bar with ETA; the nodes map opens from Nearby. - Map: smooth panning (no decode during a drag, idle read-ahead, cached coarser stand-ins), overzoom limited to 2 levels, resumable downloads, map credit behind a "©" and in Settings > About; target ring and trail computed in double. - Home in swipeable pages; Clock (alarm, timer, stopwatch, ring card). - Settings rendered from the Core schema (display & power with a brightness slider, messages & contacts, trail / live share / alerts), GPS switch, Radio screen; low-battery shutdown. - WioTrackerL2Display: honour the 0-4 brightness level. - docs: L2 hardware checklist, ui-core notes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,16 @@
|
||||
#pragma once
|
||||
// Map screen: offline Web-Mercator tiles from the card (map/TileProvider.h,
|
||||
// cached by map/TileCache.h) under own position and the nodes Nearby knows a
|
||||
// position for. Drag to pan, +/- to zoom, the crosshair re-centres and
|
||||
// follows the GPS again; tap a marker for its node detail. The download
|
||||
// button fetches the visible area over WiFi (map/TileDownloader.h).
|
||||
// cached by map/TileCache.h) under own position. Two modes over the same tile
|
||||
// view: the Navigation map (Home > Map, NavMap.h) shows waypoints, live
|
||||
// shares, the trail and the active target; the Nodes map (Nearby > map
|
||||
// button) marks every node Nearby knows a position for, tap one for its
|
||||
// detail. Drag to pan, +/- to zoom, the crosshair re-centres and follows the
|
||||
// GPS again. The download button fetches the visible area over WiFi
|
||||
// (map/TileDownloader.h).
|
||||
//
|
||||
// Tiles decode one per loop pass (a PNG takes tens of ms), so panning stays
|
||||
// responsive and the radio keeps being serviced while a view fills in.
|
||||
// Tiles decode one per loop pass (a PNG takes tens of ms), never while a drag
|
||||
// is moving the map, so panning stays smooth and the radio keeps being
|
||||
// serviced; an idle map decodes the tiles around the view ahead of time.
|
||||
//
|
||||
// Single-TU fragment: included by ui-lvgl/UITask.cpp after the Nearby section.
|
||||
|
||||
@@ -36,15 +40,89 @@ static double latToTileY(double lat, int z) {
|
||||
return (1.0 - asinh(tan(r)) / M_PI) / 2.0 * (double)(1 << z);
|
||||
}
|
||||
|
||||
// Markers: one per positioned Nearby row, remembered so layout can reposition them.
|
||||
struct Mark { int32_t lat_e6, lon_e6; int row; lv_obj_t* obj; };
|
||||
static Mark s_marks[NearbyModel::MAX_NEARBY];
|
||||
// Markers, remembered so layout can reposition them. `idx` is the Nearby row
|
||||
// (MK_NODE), the waypoint index (MK_WAYPOINT) or the live-share slot (MK_LIVE).
|
||||
enum : uint8_t { MK_NODE, MK_WAYPOINT, MK_LIVE };
|
||||
struct Mark { int32_t lat_e6, lon_e6; int idx; uint8_t kind; lv_obj_t* obj; lv_obj_t* dot; };
|
||||
static const int MAX_MARKS = 48; // >= MAX_NEARBY, and >= waypoints + live shares
|
||||
static Mark s_marks[MAX_MARKS];
|
||||
static int s_mark_count = 0;
|
||||
static int s_drag = 0; // px moved in the current press: a drag isn't a tap
|
||||
static bool s_available = false; // provider has data; checked when the map opens, not per frame
|
||||
static double s_left = 0, s_top = 0; // world px (at the current zoom) of the view's top-left
|
||||
static lv_obj_t* s_cells[GRID_COLS * GRID_ROWS]; // clip each grid tile (a magnified parent overflows)
|
||||
|
||||
// Overzoom: a tile the card doesn't have at this zoom is drawn as the matching
|
||||
// part of the nearest coarser tile it does have, magnified -- so the map stays
|
||||
// usable a little past the downloaded detail. Only two levels (x4): beyond
|
||||
// that a raster tile is just big pixels, and the map says to download more.
|
||||
// Returns that tile (k = levels up) or nullptr; want_z/x/y is the nearest
|
||||
// coarser tile not looked up yet (decode it next), want_z = -1 if none.
|
||||
static const int OVERZOOM = 2;
|
||||
static TileCache::Slot* ancestorFor(int z, int x, int y, int& k, int& want_z, int& want_x, int& want_y) {
|
||||
want_z = -1;
|
||||
for (int d = 1; d <= OVERZOOM && z - d >= 0; d++) {
|
||||
int ax = x >> d, ay = y >> d;
|
||||
TileCache::Slot* s = s_cache.find(z - d, ax, ay);
|
||||
if (!s) { want_z = z - d; want_x = ax; want_y = ay; return nullptr; }
|
||||
if (s->present) { k = d; return s; }
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Stand-in while z/x/y hasn't been decoded yet: a coarser tile already in the
|
||||
// cache (typically the view you just zoomed in from), so a newly revealed part
|
||||
// of the map shows something at once instead of a blank. Never decodes.
|
||||
static TileCache::Slot* cachedAncestor(int z, int x, int y, int& k) {
|
||||
for (int d = 1; d <= 3 && z - d >= 0; d++) {
|
||||
TileCache::Slot* s = s_cache.find(z - d, x >> d, y >> d);
|
||||
if (s && s->present) { k = d; return s; }
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Decoding a tile holds the loop for tens of ms, which makes a drag stutter.
|
||||
// So nothing is decoded while the finger is moving the map; the view fills in
|
||||
// once it rests, and when idle the ring of tiles around the view is decoded
|
||||
// ahead of the next pan.
|
||||
static uint32_t s_last_pan_ms = 0;
|
||||
static const uint32_t PAN_SETTLE_MS = 150, PREFETCH_IDLE_MS = 400;
|
||||
|
||||
// Idle read-ahead: decode the nearest not-yet-looked-up tile in the ring just
|
||||
// outside the view (one per call). False when the ring is complete.
|
||||
static bool prefetchOne(double left, double top, int w, int h, int z) {
|
||||
int tx0 = (int)floor(left / TILE_PX), ty0 = (int)floor(top / TILE_PX);
|
||||
int n = 1 << z;
|
||||
int bx = 0, by = 0;
|
||||
double best = 1e18;
|
||||
for (int j = -1; j <= GRID_ROWS; j++) {
|
||||
for (int i = -1; i <= GRID_COLS; i++) {
|
||||
int tx = tx0 + i, ty = ty0 + j;
|
||||
double px = tx * (double)TILE_PX - left, py = ty * (double)TILE_PX - top;
|
||||
if (px < w && py < h && px + TILE_PX > 0 && py + TILE_PX > 0) continue; // on screen: not ours
|
||||
if (ty < 0 || ty >= n) continue;
|
||||
int wx = ((tx % n) + n) % n;
|
||||
if (s_cache.find(z, wx, ty)) continue; // also keeps a decoded neighbour from being evicted
|
||||
double d = fabs(px + TILE_PX / 2 - w / 2.0) + fabs(py + TILE_PX / 2 - h / 2.0);
|
||||
if (d < best) { best = d; bx = wx; by = ty; }
|
||||
}
|
||||
}
|
||||
if (best >= 1e18) return false;
|
||||
s_cache.load(*s_provider, z, bx, by);
|
||||
return true;
|
||||
}
|
||||
|
||||
static const int MARK_D = 12; // marker dot diameter
|
||||
|
||||
} // namespace mapview
|
||||
|
||||
namespace navmap { // shared with NavMap.h (included after this file)
|
||||
static const int BAR_H = 44; // target bar along the bottom of the Navigation map
|
||||
// Navigation targets as one int (event user data): type << 8 | index.
|
||||
enum : uint8_t { T_CLEAR, T_WAYPOINT, T_TRAILSTART, T_LIVE };
|
||||
static int code(uint8_t type, int idx) { return (type << 8) | (idx & 0xFF); }
|
||||
} // namespace navmap
|
||||
|
||||
static void onMapPress(lv_event_t* e) {
|
||||
lv_event_code_t code = lv_event_get_code(e);
|
||||
if (code == LV_EVENT_PRESSED) { mapview::s_drag = 0; return; }
|
||||
@@ -52,8 +130,17 @@ static void onMapPress(lv_event_t* e) {
|
||||
lv_indev_get_vect(lv_indev_active(), &v);
|
||||
if (v.x == 0 && v.y == 0) return;
|
||||
mapview::s_drag += abs(v.x) + abs(v.y);
|
||||
mapview::s_last_pan_ms = millis();
|
||||
s_ui->mapPan(v.x, v.y);
|
||||
}
|
||||
static void onMapLongPress(lv_event_t* e) {
|
||||
if (mapview::s_drag > 8) return; // held after a pan: not a long-press on a spot
|
||||
lv_point_t p;
|
||||
lv_indev_get_point(lv_indev_active(), &p);
|
||||
lv_area_t a;
|
||||
lv_obj_get_coords((lv_obj_t*)lv_event_get_target(e), &a);
|
||||
s_ui->mapLongPress(p.x - a.x1, p.y - a.y1);
|
||||
}
|
||||
static void onMapZoomIn(lv_event_t* e) { (void)e; s_ui->mapZoom(+1); }
|
||||
static void onMapZoomOut(lv_event_t* e) { (void)e; s_ui->mapZoom(-1); }
|
||||
static void onMapCenter(lv_event_t* e) { (void)e; s_ui->mapCenterOnMe(); }
|
||||
@@ -63,6 +150,10 @@ static void onDlStart(lv_event_t* e) { (void)e; s_ui->mapDownloadStart(); }
|
||||
static void onDlZoomMinus(lv_event_t* e) { (void)e; s_ui->mapDownloadZmax(-1); }
|
||||
static void onDlZoomPlus(lv_event_t* e) { (void)e; s_ui->mapDownloadZmax(+1); }
|
||||
static void onDlWifi(lv_event_t* e) { (void)e; s_ui->showWifi(true); }
|
||||
static void onDlResume(lv_event_t* e) { (void)e; s_ui->mapDownloadResume(); }
|
||||
static void onDlDiscard(lv_event_t* e) { (void)e; s_ui->mapDownloadDiscard(); }
|
||||
static void onNavTools(lv_event_t* e); // NavMap.h
|
||||
static void onMapCredits(lv_event_t* e) { (void)e; s_ui->showToast(mapview::s_provider->attribution(), 4000); }
|
||||
static void onMapMarker(lv_event_t* e) {
|
||||
if (mapview::s_drag > 8) return; // the press was a pan that ended on a marker
|
||||
s_ui->mapOpenMarker((int)(uintptr_t)lv_event_get_user_data(e));
|
||||
@@ -93,6 +184,17 @@ static lv_obj_t* mapPill(lv_obj_t* parent, const char* text) {
|
||||
return l;
|
||||
}
|
||||
|
||||
// Centring on a point puts it mid-way down the visible map: above the nav bar
|
||||
// on the Navigation map (the bias is in tiles, added to the view centre).
|
||||
double UITask::mapCenterBias() const {
|
||||
return _map_nav ? navmap::BAR_H / 2.0 / mapview::TILE_PX : 0.0;
|
||||
}
|
||||
|
||||
void UITask::openMap(bool nav) {
|
||||
_map_nav = nav;
|
||||
showMap();
|
||||
}
|
||||
|
||||
void UITask::showMap() {
|
||||
_screen = SCR_MAP;
|
||||
mapview::s_available = lvport::mountStorage() && mapview::s_provider->available();
|
||||
@@ -109,6 +211,7 @@ void UITask::showMap() {
|
||||
_map_cy = mapview::latToTileY(la, z);
|
||||
}
|
||||
buildMap();
|
||||
if (_map_nav && _prefs && _prefs->locator_has_target) navFrameTarget(); // show where we're going
|
||||
}
|
||||
|
||||
void UITask::buildMap() {
|
||||
@@ -120,11 +223,19 @@ void UITask::buildMap() {
|
||||
lv_obj_add_flag(body, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_add_event_cb(body, onMapPress, LV_EVENT_PRESSED, NULL);
|
||||
lv_obj_add_event_cb(body, onMapPress, LV_EVENT_PRESSING, NULL);
|
||||
lv_obj_add_event_cb(body, onMapLongPress, LV_EVENT_LONG_PRESSED, NULL);
|
||||
_map_area = body;
|
||||
|
||||
for (int i = 0; i < mapview::GRID_COLS * mapview::GRID_ROWS; i++) {
|
||||
_map_tiles[i] = lv_image_create(body);
|
||||
lv_obj_add_flag(_map_tiles[i], LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_t* cell = lv_obj_create(body);
|
||||
lv_obj_remove_style_all(cell);
|
||||
lv_obj_set_size(cell, mapview::TILE_PX, mapview::TILE_PX);
|
||||
lv_obj_remove_flag(cell, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_remove_flag(cell, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_add_flag(cell, LV_OBJ_FLAG_HIDDEN);
|
||||
mapview::s_cells[i] = cell;
|
||||
_map_tiles[i] = lv_image_create(cell);
|
||||
lv_image_set_pivot(_map_tiles[i], 0, 0); // magnify from the top-left (overzoom)
|
||||
}
|
||||
|
||||
// Marker layer: same size as the map, lets presses through to it.
|
||||
@@ -135,6 +246,8 @@ void UITask::buildMap() {
|
||||
lv_obj_remove_flag(_map_marks, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_add_flag(_map_marks, LV_OBJ_FLAG_EVENT_BUBBLE);
|
||||
|
||||
if (_map_nav) buildNavLayers(); // trail + line to the target, under the markers
|
||||
|
||||
_map_me = lv_obj_create(_map_marks);
|
||||
lv_obj_remove_style_all(_map_me);
|
||||
lv_obj_set_size(_map_me, 16, 16);
|
||||
@@ -151,25 +264,38 @@ void UITask::buildMap() {
|
||||
lv_obj_align(back, LV_ALIGN_TOP_LEFT, 6, 6);
|
||||
lv_obj_align(mapButton(body, LV_SYMBOL_PLUS, onMapZoomIn), LV_ALIGN_TOP_RIGHT, -6, 6);
|
||||
lv_obj_align(mapButton(body, LV_SYMBOL_MINUS, onMapZoomOut), LV_ALIGN_TOP_RIGHT, -6, 52);
|
||||
lv_obj_align(mapButton(body, LV_SYMBOL_DOWNLOAD, onMapDownload), LV_ALIGN_TOP_RIGHT, -6, 98);
|
||||
lv_obj_align(mapButton(body, LV_SYMBOL_GPS, onMapCenter), LV_ALIGN_BOTTOM_RIGHT, -6, -6);
|
||||
_map_dl_pill = mapPill(body, "");
|
||||
// Nav map: trail / live share / download live in its tools panel, in the
|
||||
// left column (back, pin, tools) -- the right one has no room above the bar.
|
||||
if (_map_nav) lv_obj_align(mapButton(body, LV_SYMBOL_BARS, onNavTools), LV_ALIGN_TOP_LEFT, 6, 98);
|
||||
else lv_obj_align(mapButton(body, LV_SYMBOL_DOWNLOAD, onMapDownload), LV_ALIGN_TOP_RIGHT, -6, 98);
|
||||
int bottom = _map_nav ? navmap::BAR_H : 0; // the nav bar takes the bottom edge
|
||||
lv_obj_align(mapButton(body, LV_SYMBOL_GPS, onMapCenter), LV_ALIGN_BOTTOM_RIGHT, -6, -6 - bottom);
|
||||
_map_dl_pill = mapPill(body, ""); // tap: the download popup
|
||||
lv_obj_set_style_text_color(_map_dl_pill, lv_color_hex(theme::ACCENT), 0);
|
||||
lv_obj_align(_map_dl_pill, LV_ALIGN_TOP_MID, 0, 16);
|
||||
lv_obj_set_style_pad_ver(_map_dl_pill, 3, 0);
|
||||
lv_obj_align(_map_dl_pill, LV_ALIGN_TOP_RIGHT, -52, 14); // beside +, clear of the zoom pill on the left
|
||||
lv_obj_add_flag(_map_dl_pill, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_set_ext_click_area(_map_dl_pill, 6);
|
||||
lv_obj_add_event_cb(_map_dl_pill, onMapDownload, LV_EVENT_CLICKED, NULL);
|
||||
lv_obj_add_flag(_map_dl_pill, LV_OBJ_FLAG_HIDDEN);
|
||||
_map_zoom_lbl = mapPill(body, "");
|
||||
lv_obj_align(_map_zoom_lbl, LV_ALIGN_TOP_LEFT, 52, 16);
|
||||
lv_obj_t* attr = mapPill(body, mapview::s_provider->attribution()); // required by the data licences
|
||||
lv_label_set_long_mode(attr, LV_LABEL_LONG_WRAP);
|
||||
lv_obj_set_style_max_width(attr, 250, 0);
|
||||
lv_obj_set_width(attr, LV_SIZE_CONTENT);
|
||||
lv_obj_align(attr, LV_ALIGN_BOTTOM_LEFT, 6, -6);
|
||||
// Map data credit (the licences require it on the map): a small "©" that
|
||||
// shows the full line when tapped; it is also in Settings > About.
|
||||
lv_obj_t* attr = mapPill(body, "\xC2\xA9");
|
||||
lv_obj_set_style_pad_hor(attr, 7, 0);
|
||||
lv_obj_set_style_pad_ver(attr, 3, 0);
|
||||
lv_obj_add_flag(attr, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_set_ext_click_area(attr, 8);
|
||||
lv_obj_add_event_cb(attr, onMapCredits, LV_EVENT_CLICKED, NULL);
|
||||
lv_obj_align(attr, LV_ALIGN_BOTTOM_LEFT, 6, -6 - bottom);
|
||||
_map_hint = mapPill(body, "");
|
||||
lv_label_set_long_mode(_map_hint, LV_LABEL_LONG_WRAP);
|
||||
lv_obj_set_width(_map_hint, 200);
|
||||
lv_obj_set_style_text_align(_map_hint, LV_TEXT_ALIGN_CENTER, 0);
|
||||
lv_obj_center(_map_hint);
|
||||
lv_obj_add_flag(_map_hint, LV_OBJ_FLAG_HIDDEN);
|
||||
if (_map_nav) buildNavControls(body);
|
||||
|
||||
lv_obj_update_layout(body);
|
||||
mapview::s_mark_count = 0; // the previous markers went with the previous screen
|
||||
@@ -184,32 +310,49 @@ void UITask::layoutMap() {
|
||||
int w = lv_obj_get_width(_map_area), h = lv_obj_get_height(_map_area);
|
||||
double left = _map_cx * mapview::TILE_PX - w / 2.0; // world px of the view's top-left
|
||||
double top = _map_cy * mapview::TILE_PX - h / 2.0;
|
||||
mapview::s_left = left; mapview::s_top = top;
|
||||
int tx0 = (int)floor(left / mapview::TILE_PX), ty0 = (int)floor(top / mapview::TILE_PX);
|
||||
int n = 1 << _map_z;
|
||||
bool have_provider = mapview::s_available;
|
||||
int shown = 0, missing = 0;
|
||||
int shown = 0, missing = 0, over = 0;
|
||||
_map_pending = false;
|
||||
|
||||
for (int j = 0; j < mapview::GRID_ROWS; j++) {
|
||||
for (int i = 0; i < mapview::GRID_COLS; i++) {
|
||||
lv_obj_t* cell = mapview::s_cells[j * mapview::GRID_COLS + i];
|
||||
lv_obj_t* img = _map_tiles[j * mapview::GRID_COLS + i];
|
||||
int tx = tx0 + i, ty = ty0 + j;
|
||||
int tx = tx0 + i, ty = ty0 + j, wx = ((tx % n) + n) % n;
|
||||
int px = (int)lround(tx * (double)mapview::TILE_PX - left);
|
||||
int py = (int)lround(ty * (double)mapview::TILE_PX - top);
|
||||
bool on_screen = px < w && py < h && px + mapview::TILE_PX > 0 && py + mapview::TILE_PX > 0;
|
||||
mapview::TileCache::Slot* s = nullptr;
|
||||
if (have_provider && on_screen && ty >= 0 && ty < n) {
|
||||
s = mapview::s_cache.find(_map_z, ((tx % n) + n) % n, ty);
|
||||
s = mapview::s_cache.find(_map_z, wx, ty);
|
||||
if (!s) _map_pending = true;
|
||||
}
|
||||
if (s && s->present) {
|
||||
if (lv_image_get_src(img) != &s->dsc) lv_image_set_src(img, &s->dsc);
|
||||
lv_obj_set_pos(img, px, py);
|
||||
lv_obj_remove_flag(img, LV_OBJ_FLAG_HIDDEN);
|
||||
mapview::TileCache::Slot* show = (s && s->present) ? s : nullptr;
|
||||
int k = 0;
|
||||
bool stand_in = false;
|
||||
if (s && !s->present) { // none at this zoom: magnify a coarser one
|
||||
missing++;
|
||||
int wz, ax, ay;
|
||||
show = mapview::ancestorFor(_map_z, wx, ty, k, wz, ax, ay);
|
||||
if (!show && wz >= 0) _map_pending = true;
|
||||
} else if (!s && have_provider && on_screen && ty >= 0 && ty < n) { // not decoded yet
|
||||
show = mapview::cachedAncestor(_map_z, wx, ty, k);
|
||||
stand_in = show != nullptr;
|
||||
}
|
||||
if (show) {
|
||||
if (lv_image_get_src(img) != &show->dsc) lv_image_set_src(img, &show->dsc);
|
||||
lv_image_set_scale(img, LV_SCALE_NONE << k);
|
||||
int m = (1 << k) - 1; // which part of the magnified tile this cell is
|
||||
lv_obj_set_pos(img, -(wx & m) * mapview::TILE_PX, -(ty & m) * mapview::TILE_PX);
|
||||
lv_obj_set_pos(cell, px, py);
|
||||
lv_obj_remove_flag(cell, LV_OBJ_FLAG_HIDDEN);
|
||||
shown++;
|
||||
if (k > over && !stand_in) over = k;
|
||||
} else {
|
||||
lv_obj_add_flag(img, LV_OBJ_FLAG_HIDDEN);
|
||||
if (s) missing++;
|
||||
lv_obj_add_flag(cell, LV_OBJ_FLAG_HIDDEN);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -224,16 +367,21 @@ void UITask::layoutMap() {
|
||||
} else {
|
||||
lv_obj_add_flag(_map_me, LV_OBJ_FLAG_HIDDEN);
|
||||
}
|
||||
const int half = mapview::MARK_D / 2;
|
||||
for (int k = 0; k < mapview::s_mark_count; k++) {
|
||||
const mapview::Mark& m = mapview::s_marks[k];
|
||||
int dy = lv_obj_get_y(m.dot); // the dot within the marker (vertically centred on the name)
|
||||
double x = mapview::lonToTileX(m.lon_e6 / 1e6, _map_z) * mapview::TILE_PX - left;
|
||||
double y = mapview::latToTileY(m.lat_e6 / 1e6, _map_z) * mapview::TILE_PX - top;
|
||||
lv_obj_set_pos(m.obj, (int)x - 6, (int)y - 6); // the dot's centre on the spot; name to its right
|
||||
lv_obj_set_pos(m.obj, (int)lround(x) - half, (int)lround(y) - half - dy); // the dot's centre on the spot
|
||||
}
|
||||
if (_map_nav) layoutNav();
|
||||
|
||||
lv_label_set_text_fmt(_map_zoom_lbl, "z%d", _map_z);
|
||||
if (over) lv_label_set_text_fmt(_map_zoom_lbl, "z%d (map z%d)", _map_z, _map_z - over); // magnified
|
||||
else lv_label_set_text_fmt(_map_zoom_lbl, "z%d", _map_z);
|
||||
const char* hint = !have_provider ? "No map on the SD card.\nPut tiles in /maps (tools/maps)."
|
||||
: (!_map_pending && shown == 0 && missing > 0) ? "No tiles here at this zoom." : nullptr;
|
||||
: (!_map_pending && shown == 0 && missing > 0)
|
||||
? "No map detail here at this zoom.\nZoom out, or download this area." : nullptr;
|
||||
if (hint) { lv_label_set_text(_map_hint, hint); lv_obj_remove_flag(_map_hint, LV_OBJ_FLAG_HIDDEN); }
|
||||
else lv_obj_add_flag(_map_hint, LV_OBJ_FLAG_HIDDEN);
|
||||
}
|
||||
@@ -246,15 +394,27 @@ void UITask::mapLoop() {
|
||||
int32_t lat, lon;
|
||||
if (_core->course.currentLocation(lat, lon)) {
|
||||
_map_cx = mapview::lonToTileX(lon / 1e6, _map_z);
|
||||
_map_cy = mapview::latToTileY(lat / 1e6, _map_z);
|
||||
_map_cy = mapview::latToTileY(lat / 1e6, _map_z) + mapCenterBias();
|
||||
}
|
||||
}
|
||||
rebuildMapMarkers();
|
||||
layoutMap();
|
||||
}
|
||||
if (!_map_pending || !_map_area) return;
|
||||
if (_map_nav) navPollAveraging();
|
||||
if (_map_nav && (int32_t)(millis() - _next_nav_bar_ms) >= 0) {
|
||||
_next_nav_bar_ms = millis() + 1000;
|
||||
refreshNavBar();
|
||||
}
|
||||
if (!_map_area) return;
|
||||
uint32_t since_pan = millis() - mapview::s_last_pan_ms;
|
||||
if (since_pan < mapview::PAN_SETTLE_MS) return; // mid-drag: keep it smooth, decode after
|
||||
int w = lv_obj_get_width(_map_area), h = lv_obj_get_height(_map_area);
|
||||
double left = _map_cx * mapview::TILE_PX - w / 2.0, top = _map_cy * mapview::TILE_PX - h / 2.0;
|
||||
if (!_map_pending) { // view complete: when idle, decode the next tile a pan would reveal
|
||||
if (since_pan >= mapview::PREFETCH_IDLE_MS && mapview::s_available)
|
||||
mapview::prefetchOne(left, top, w, h, _map_z);
|
||||
return;
|
||||
}
|
||||
int tx0 = (int)floor(left / mapview::TILE_PX), ty0 = (int)floor(top / mapview::TILE_PX);
|
||||
int n = 1 << _map_z;
|
||||
int best_x = 0, best_y = 0;
|
||||
@@ -271,7 +431,28 @@ void UITask::mapLoop() {
|
||||
if (d < best_d) { best_d = d; best_x = wx; best_y = ty; }
|
||||
}
|
||||
}
|
||||
if (best_d < 1e18) mapview::s_cache.load(*mapview::s_provider, _map_z, best_x, best_y);
|
||||
if (best_d < 1e18) {
|
||||
mapview::s_cache.load(*mapview::s_provider, _map_z, best_x, best_y);
|
||||
} else { // every tile at this zoom looked up: decode a coarser one for a missing tile
|
||||
best_d = 1e18;
|
||||
int bz = -1;
|
||||
for (int j = 0; j < mapview::GRID_ROWS; j++) {
|
||||
for (int i = 0; i < mapview::GRID_COLS; i++) {
|
||||
int tx = tx0 + i, ty = ty0 + j;
|
||||
double px = tx * (double)mapview::TILE_PX - left, py = ty * (double)mapview::TILE_PX - top;
|
||||
if (px >= w || py >= h || px + mapview::TILE_PX <= 0 || py + mapview::TILE_PX <= 0) continue;
|
||||
if (ty < 0 || ty >= n) continue;
|
||||
int wx = ((tx % n) + n) % n;
|
||||
mapview::TileCache::Slot* s = mapview::s_cache.find(_map_z, wx, ty);
|
||||
if (!s || s->present) continue;
|
||||
int k, wz, ax, ay;
|
||||
if (mapview::ancestorFor(_map_z, wx, ty, k, wz, ax, ay) || wz < 0) continue;
|
||||
double d = fabs(px + 128 - w / 2.0) + fabs(py + 128 - h / 2.0);
|
||||
if (d < best_d) { best_d = d; bz = wz; best_x = ax; best_y = ay; }
|
||||
}
|
||||
}
|
||||
if (bz >= 0) mapview::s_cache.load(*mapview::s_provider, bz, best_x, best_y);
|
||||
}
|
||||
layoutMap();
|
||||
}
|
||||
|
||||
@@ -279,38 +460,50 @@ void UITask::rebuildMapMarkers() {
|
||||
if (!_map_marks) return;
|
||||
for (int k = 0; k < mapview::s_mark_count; k++) lv_obj_delete(mapview::s_marks[k].obj);
|
||||
mapview::s_mark_count = 0;
|
||||
if (_map_nav) rebuildNavMarkers();
|
||||
else rebuildNodeMarkers();
|
||||
lv_obj_update_layout(_map_marks); // so layoutMap() knows where each dot sits in its marker
|
||||
}
|
||||
|
||||
void UITask::rebuildNodeMarkers() {
|
||||
_nearby->refreshStored();
|
||||
for (int i = 0; i < _nearby->count() && mapview::s_mark_count < NearbyModel::MAX_NEARBY; i++) {
|
||||
for (int i = 0; i < _nearby->count() && mapview::s_mark_count < mapview::MAX_MARKS; i++) {
|
||||
const NearbyModel::Entry& e = _nearby->at(i);
|
||||
if (e.lat_e6 == 0 && e.lon_e6 == 0) continue;
|
||||
uint32_t col = e.is_live ? theme::OK : e.type == ADV_TYPE_CHAT ? theme::TEXT : theme::TEXT_MUTED;
|
||||
|
||||
// One clickable object per marker: a dot plus the name beside it.
|
||||
lv_obj_t* m = lv_obj_create(_map_marks);
|
||||
lv_obj_remove_style_all(m);
|
||||
lv_obj_set_size(m, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(m, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(m, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_style_pad_column(m, 3, 0);
|
||||
lv_obj_add_flag(m, LV_OBJ_FLAG_EVENT_BUBBLE); // a drag starting here still pans
|
||||
lv_obj_add_event_cb(m, onMapMarker, LV_EVENT_CLICKED, (void*)(uintptr_t)mapview::s_mark_count);
|
||||
lv_obj_t* dot = lv_obj_create(m);
|
||||
lv_obj_remove_style_all(dot);
|
||||
lv_obj_set_size(dot, 12, 12);
|
||||
lv_obj_set_style_radius(dot, LV_RADIUS_CIRCLE, 0);
|
||||
lv_obj_set_style_bg_color(dot, lv_color_hex(col), 0);
|
||||
lv_obj_set_style_bg_opa(dot, LV_OPA_COVER, 0);
|
||||
lv_obj_set_style_border_color(dot, lv_color_hex(theme::BG), 0);
|
||||
lv_obj_set_style_border_width(dot, 2, 0);
|
||||
lv_obj_remove_flag(dot, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_t* name = mapPill(m, e.name[0] ? e.name : "?");
|
||||
lv_obj_remove_flag(name, LV_OBJ_FLAG_CLICKABLE);
|
||||
|
||||
mapview::Mark& mk = mapview::s_marks[mapview::s_mark_count++];
|
||||
mk.lat_e6 = e.lat_e6; mk.lon_e6 = e.lon_e6; mk.row = i; mk.obj = m;
|
||||
addMapMark(mapview::MK_NODE, i, e.lat_e6, e.lon_e6, col, e.name[0] ? e.name : "?");
|
||||
}
|
||||
}
|
||||
|
||||
// One clickable object per marker: a dot plus the name beside it. The object
|
||||
// is placed so the dot's centre is on the spot, from where layout actually put
|
||||
// the dot inside it (Mark::dot) -- the name may be taller than the dot.
|
||||
void UITask::addMapMark(uint8_t kind, int idx, int32_t lat_e6, int32_t lon_e6, uint32_t col, const char* text) {
|
||||
if (!_map_marks || mapview::s_mark_count >= mapview::MAX_MARKS) return;
|
||||
lv_obj_t* m = lv_obj_create(_map_marks);
|
||||
lv_obj_remove_style_all(m);
|
||||
lv_obj_set_size(m, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(m, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(m, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_style_pad_column(m, 3, 0);
|
||||
lv_obj_add_flag(m, LV_OBJ_FLAG_EVENT_BUBBLE); // a drag starting here still pans
|
||||
lv_obj_add_event_cb(m, onMapMarker, LV_EVENT_CLICKED, (void*)(uintptr_t)mapview::s_mark_count);
|
||||
lv_obj_t* dot = lv_obj_create(m);
|
||||
lv_obj_remove_style_all(dot);
|
||||
lv_obj_set_size(dot, mapview::MARK_D, mapview::MARK_D);
|
||||
lv_obj_set_style_radius(dot, LV_RADIUS_CIRCLE, 0);
|
||||
lv_obj_set_style_bg_color(dot, lv_color_hex(col), 0);
|
||||
lv_obj_set_style_bg_opa(dot, LV_OPA_COVER, 0);
|
||||
lv_obj_set_style_border_color(dot, lv_color_hex(theme::BG), 0);
|
||||
lv_obj_set_style_border_width(dot, 2, 0);
|
||||
lv_obj_remove_flag(dot, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_t* name = mapPill(m, text);
|
||||
lv_obj_remove_flag(name, LV_OBJ_FLAG_CLICKABLE);
|
||||
|
||||
mapview::Mark& mk = mapview::s_marks[mapview::s_mark_count++];
|
||||
mk.lat_e6 = lat_e6; mk.lon_e6 = lon_e6; mk.idx = idx; mk.kind = kind; mk.obj = m; mk.dot = dot;
|
||||
}
|
||||
|
||||
void UITask::mapPan(int dx, int dy) {
|
||||
_map_cx -= dx / (double)mapview::TILE_PX;
|
||||
_map_cy -= dy / (double)mapview::TILE_PX;
|
||||
@@ -332,19 +525,36 @@ void UITask::mapZoom(int delta) {
|
||||
|
||||
void UITask::mapCenterOnMe() {
|
||||
int32_t lat, lon;
|
||||
if (!_core->course.currentLocation(lat, lon)) { showToast("No GPS fix"); return; }
|
||||
if (!ensureGps() || !_core->course.currentLocation(lat, lon)) { _map_follow = true; return; } // centres once a fix comes
|
||||
_map_follow = true;
|
||||
_map_cx = mapview::lonToTileX(lon / 1e6, _map_z);
|
||||
_map_cy = mapview::latToTileY(lat / 1e6, _map_z);
|
||||
_map_cy = mapview::latToTileY(lat / 1e6, _map_z) + mapCenterBias();
|
||||
layoutMap();
|
||||
}
|
||||
|
||||
void UITask::mapOpenMarker(int idx) {
|
||||
if (idx < 0 || idx >= mapview::s_mark_count) return;
|
||||
openNode(mapview::s_marks[idx].row);
|
||||
const mapview::Mark& m = mapview::s_marks[idx];
|
||||
if (m.kind == mapview::MK_WAYPOINT) { navWaypointMenu(m.idx); return; }
|
||||
if (m.kind == mapview::MK_LIVE) { navPick(navmap::code(navmap::T_LIVE, m.idx)); return; }
|
||||
openNode(m.idx);
|
||||
_node_from_map = true;
|
||||
}
|
||||
|
||||
// Screen point (relative to the map area) -> lat/lon at the current view.
|
||||
void UITask::mapPointToLatLon(int x, int y, int32_t& lat_e6, int32_t& lon_e6) const {
|
||||
double n = (double)(1 << _map_z);
|
||||
double tx = (mapview::s_left + x) / mapview::TILE_PX, ty = (mapview::s_top + y) / mapview::TILE_PX;
|
||||
double lon = tx / n * 360.0 - 180.0;
|
||||
double lat = atan(sinh(M_PI * (1.0 - 2.0 * ty / n))) * 180.0 / M_PI;
|
||||
lat_e6 = (int32_t)lround(lat * 1e6);
|
||||
lon_e6 = (int32_t)lround(lon * 1e6);
|
||||
}
|
||||
|
||||
void UITask::mapLongPress(int x, int y) {
|
||||
if (_map_nav) navDropAt(x, y);
|
||||
}
|
||||
|
||||
|
||||
// ── Area download ─────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -405,6 +615,21 @@ void UITask::mapDownloadPopup() {
|
||||
lv_obj_align(_dl_zoom_lbl, LV_ALIGN_RIGHT_MID, -56, 0);
|
||||
headerButton(zr, LV_SYMBOL_MINUS, onDlZoomMinus, 96, NULL);
|
||||
|
||||
// Unfinished job (power-off, lost WiFi, Stop): resume or drop it.
|
||||
_dl_job_row = lv_obj_create(panel);
|
||||
styleSurface(_dl_job_row, theme::SURFACE);
|
||||
lv_obj_remove_flag(_dl_job_row, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_set_size(_dl_job_row, LV_PCT(100), 40);
|
||||
lv_obj_set_style_radius(_dl_job_row, theme::RADIUS, 0);
|
||||
_dl_job_lbl = label(_dl_job_row, "", THEME_FONT_SMALL, theme::TEXT);
|
||||
lv_obj_set_width(_dl_job_lbl, 170);
|
||||
lv_label_set_long_mode(_dl_job_lbl, LV_LABEL_LONG_WRAP);
|
||||
lv_obj_align(_dl_job_lbl, LV_ALIGN_LEFT_MID, theme::PAD, 0);
|
||||
lv_obj_t* res = headerButton(_dl_job_row, LV_SYMBOL_PLAY " Resume", onDlResume, 48, NULL);
|
||||
lv_obj_set_style_bg_color(res, lv_color_hex(theme::ACCENT_DIM), 0);
|
||||
headerButton(_dl_job_row, LV_SYMBOL_TRASH, onDlDiscard, 4, NULL);
|
||||
lv_obj_add_flag(_dl_job_row, LV_OBJ_FLAG_HIDDEN);
|
||||
|
||||
_dl_info = label(panel, "", THEME_FONT_SMALL, theme::TEXT);
|
||||
lv_label_set_long_mode(_dl_info, LV_LABEL_LONG_WRAP);
|
||||
lv_obj_set_width(_dl_info, LV_PCT(100));
|
||||
@@ -439,7 +664,7 @@ void UITask::mapDownloadPopup() {
|
||||
|
||||
void UITask::mapDownloadClose() {
|
||||
if (_dl_overlay) lv_obj_delete_async(_dl_overlay); // may be closing from its own button
|
||||
_dl_overlay = _dl_info = _dl_zoom_lbl = _dl_start_lbl = nullptr;
|
||||
_dl_overlay = _dl_info = _dl_zoom_lbl = _dl_start_lbl = _dl_job_row = _dl_job_lbl = nullptr;
|
||||
}
|
||||
|
||||
void UITask::mapDownloadZmax(int delta) {
|
||||
@@ -468,23 +693,32 @@ void UITask::refreshDownloadPopup() {
|
||||
(unsigned long)dl.processed(), (unsigned long)dl.total(),
|
||||
(unsigned long)dl.downloaded(), dl.sourceHost(), net);
|
||||
lv_label_set_text(_dl_start_lbl, LV_SYMBOL_STOP " Stop");
|
||||
if (_dl_job_row) lv_obj_add_flag(_dl_job_row, LV_OBJ_FLAG_HIDDEN);
|
||||
return;
|
||||
}
|
||||
mapview::TileArea job;
|
||||
if (_dl_job_row && dl.savedJob(job)) {
|
||||
lv_label_set_text_fmt(_dl_job_lbl, "Unfinished: z%d-%d,\n%lu tiles", job.zmin, job.zmax,
|
||||
(unsigned long)mapview::countTiles(job));
|
||||
lv_obj_remove_flag(_dl_job_row, LV_OBJ_FLAG_HIDDEN);
|
||||
} else if (_dl_job_row) {
|
||||
lv_obj_add_flag(_dl_job_row, LV_OBJ_FLAG_HIDDEN);
|
||||
}
|
||||
int w = _map_area ? lv_obj_get_width(_map_area) : 320, h = _map_area ? lv_obj_get_height(_map_area) : 218;
|
||||
mapview::TileArea a = visibleArea(_map_cx, _map_cy, _map_z, w, h, dlZmin(_map_z), _dl_zmax);
|
||||
uint32_t n = mapview::countTiles(a);
|
||||
char ssid[33], pass[65];
|
||||
bool have_wifi = lvport::loadWifi(ssid, sizeof(ssid), pass, sizeof(pass));
|
||||
char last[64] = "";
|
||||
if (dl.state() == mapview::TileDownloader::DONE || dl.state() == mapview::TileDownloader::FAILED ||
|
||||
dl.state() == mapview::TileDownloader::CANCELLED)
|
||||
bool job_shown = _dl_job_row && !lv_obj_has_flag(_dl_job_row, LV_OBJ_FLAG_HIDDEN); // it says enough
|
||||
if (!job_shown && (dl.state() == mapview::TileDownloader::DONE || dl.state() == mapview::TileDownloader::FAILED ||
|
||||
dl.state() == mapview::TileDownloader::CANCELLED))
|
||||
snprintf(last, sizeof(last), "\nLast: %s (%lu new)", dl.message(), (unsigned long)dl.downloaded());
|
||||
char size[16];
|
||||
uint64_t bytes = (uint64_t)n * mapview::AVG_TILE_BYTES;
|
||||
if (bytes < 1024 * 1024) snprintf(size, sizeof(size), "%lu KB", (unsigned long)(bytes / 1024));
|
||||
else snprintf(size, sizeof(size), "%lu MB", (unsigned long)((bytes + 512 * 1024) / (1024 * 1024)));
|
||||
lv_label_set_text_fmt(_dl_info, "z%d-%d: %lu tiles, about %s (tiles on the card are skipped)\n"
|
||||
"From %s\nWiFi: %s%s",
|
||||
lv_label_set_text_fmt(_dl_info, "z%d-%d: %lu tiles, about %s - from %s\nWiFi: %s%s",
|
||||
a.zmin, a.zmax, (unsigned long)n, size,
|
||||
dl.sourceHost(), have_wifi ? ssid : "not set - tap WiFi", last);
|
||||
lv_label_set_text(_dl_start_lbl, n > mapview::TileDownloader::MAX_TILES ? "Too large - zoom in"
|
||||
@@ -502,11 +736,28 @@ void UITask::mapDownloadStart() {
|
||||
refreshDownloadPopup();
|
||||
}
|
||||
|
||||
void UITask::mapDownloadResume() {
|
||||
mapview::TileArea a;
|
||||
if (mapview::s_dl.active() || !mapview::s_dl.savedJob(a)) return;
|
||||
char ssid[33], pass[65];
|
||||
if (!lvport::loadWifi(ssid, sizeof(ssid), pass, sizeof(pass))) { showWifi(true); return; }
|
||||
if (!lvport::mountStorage()) { showToast("No SD card"); return; }
|
||||
if (!mapview::s_dl.start(a, ssid, pass)) { showToast(mapview::s_dl.message()); return; }
|
||||
refreshDownloadPopup();
|
||||
}
|
||||
|
||||
void UITask::mapDownloadDiscard() {
|
||||
mapview::s_dl.discardJob();
|
||||
refreshDownloadPopup();
|
||||
}
|
||||
|
||||
void UITask::mapDownloadStop() {
|
||||
mapview::s_dl.cancel();
|
||||
refreshDownloadPopup();
|
||||
}
|
||||
|
||||
static mapview::TileArea s_job_tmp;
|
||||
|
||||
// Every UI loop pass, whatever the screen: a download keeps going in the background.
|
||||
void UITask::mapDownloadTick() {
|
||||
mapview::TileDownloader& dl = mapview::s_dl;
|
||||
@@ -539,6 +790,9 @@ void UITask::mapDownloadTick() {
|
||||
lv_label_set_text_fmt(_map_dl_pill, LV_SYMBOL_DOWNLOAD " %lu / %lu", (unsigned long)dl.processed(),
|
||||
(unsigned long)dl.total());
|
||||
lv_obj_remove_flag(_map_dl_pill, LV_OBJ_FLAG_HIDDEN);
|
||||
} else if (mapview::s_available && dl.savedJob(s_job_tmp)) {
|
||||
lv_label_set_text(_map_dl_pill, LV_SYMBOL_DOWNLOAD " Resume?");
|
||||
lv_obj_remove_flag(_map_dl_pill, LV_OBJ_FLAG_HIDDEN);
|
||||
} else {
|
||||
lv_obj_add_flag(_map_dl_pill, LV_OBJ_FLAG_HIDDEN);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user