mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-10-09 03:06:39 +00:00
feat(ui-lvgl): pick download areas with a frame, list of map areas
- Download an area: a frame with four corner handles on the map (kept in world coordinates, the map pans / zooms under it); a bar gives its size, zoom range, tile count and size, the trails switch and Get; areas on the card are outlined - Map tools > Map areas: the downloaded areas (map/AreaStore.h, /sdcard/maps/areas.txt) -- show (framed, outlined), rename, fill gaps, refresh (force re-download), trails on / off, delete (in steps, keeping tiles another area covers) - TileDownloader: force and per-job trails options Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -245,8 +245,12 @@ Ideas to come back to (2026-09-26):
|
||||
over the base map (Map tools > Hiking trails); fetched with an area
|
||||
download (re-running one adds just the trails) and live; empty tiles
|
||||
as 0-byte files.
|
||||
- [ ] Area manager: named list of downloaded areas (regenerate, delete),
|
||||
a draggable frame to pick an area instead of the visible screen.
|
||||
- [x] Map areas: a frame with corner handles picks the area to download
|
||||
(the map pans / zooms under it; size, zoom range, tiles, trails in a
|
||||
bar); downloaded areas are listed in /sdcard/maps/areas.txt (Map tools
|
||||
> Map areas: show, rename, fill gaps, refresh, trails on / off, delete
|
||||
-- tiles another area covers stay). Maps fetched before the list aren't
|
||||
in it (re-download them).
|
||||
- [x] A grid under the map (and the minimap), its step a round distance in
|
||||
the metric / imperial unit, with a scale bar: position and scale where
|
||||
no tile is loaded.
|
||||
|
||||
@@ -0,0 +1,489 @@
|
||||
#pragma once
|
||||
// Map areas: picking an area to download with a frame on the map, and the
|
||||
// list of downloaded areas (map/AreaStore.h) -- show, rename, fill gaps,
|
||||
// refresh, trails on / off, delete.
|
||||
//
|
||||
// The frame: four corner handles on the map, dragged to size it; the map
|
||||
// pans and zooms under it as usual, so an area can be larger than the screen.
|
||||
// It is kept in world coordinates (tiles at zoom 0), so it stays on the same
|
||||
// ground while the view moves. A bar along the bottom gives its size, the
|
||||
// zoom range, the trails switch and Download; the areas already on the card
|
||||
// are outlined meanwhile.
|
||||
//
|
||||
// Single-TU fragment: included by ui-lvgl/UITask.cpp after NavMap.h.
|
||||
|
||||
namespace areas {
|
||||
|
||||
enum : uint8_t { A_SHOW, A_RENAME, A_FILL, A_REFRESH, A_TRAILS, A_DELETE };
|
||||
|
||||
static bool s_sel = false; // the frame is up
|
||||
static double s_fx0, s_fy0, s_fx1, s_fy1; // the frame, world units (tiles at z 0)
|
||||
static int s_zmax = 16;
|
||||
static bool s_trails = false;
|
||||
static lv_obj_t* s_layer = nullptr; // draws the frame / outlines, over the tiles
|
||||
static lv_obj_t* s_handle[4]; // TL, TR, BL, BR
|
||||
static lv_obj_t* s_bar = nullptr;
|
||||
static lv_obj_t* s_info = nullptr;
|
||||
static lv_obj_t* s_zoom = nullptr;
|
||||
static lv_obj_t* s_trails_btn = nullptr;
|
||||
static int s_show = -1; // area outlined after Show
|
||||
static int s_idx = -1; // area in the open popup
|
||||
static lv_obj_t* s_del_lbl = nullptr;
|
||||
static uint32_t s_del_armed_ms = 0;
|
||||
static const int BAR_H = 84;
|
||||
static const int HANDLE = 26;
|
||||
|
||||
static double worldX(double lon) { return (lon + 180.0) / 360.0; }
|
||||
static double worldY(double lat) { double r = lat * M_PI / 180.0; return (1.0 - asinh(tan(r)) / M_PI) / 2.0; }
|
||||
static double lonOf(double wx) { return wx * 360.0 - 180.0; }
|
||||
static double latOf(double wy) { return atan(sinh(M_PI * (1.0 - 2.0 * wy))) * 180.0 / M_PI; }
|
||||
|
||||
// Deepest zoom at which a box `ww` x `wh` (world units) fits `w` x `h` px.
|
||||
static int fitZoom(double ww, double wh, int w, int h) {
|
||||
int z = mapview::MIN_Z;
|
||||
for (int k = mapview::MAX_Z; k >= mapview::MIN_Z; k--) {
|
||||
double s = (double)(1 << k) * mapview::TILE_PX;
|
||||
if (ww * s <= w && wh * s <= h) { z = k; break; }
|
||||
}
|
||||
return z;
|
||||
}
|
||||
|
||||
static mapview::TileArea frameArea(int w, int h) {
|
||||
mapview::TileArea a;
|
||||
a.lon0 = lonOf(s_fx0); a.lon1 = lonOf(s_fx1);
|
||||
a.lat1 = latOf(s_fy0); a.lat0 = latOf(s_fy1);
|
||||
int zfit = fitZoom(s_fx1 - s_fx0, s_fy1 - s_fy0, w, h);
|
||||
a.zmin = zfit - 4 < 5 ? (zfit < 5 ? zfit : 5) : zfit - 4; // a few overview levels (cheap)
|
||||
a.zmax = s_zmax < a.zmin ? a.zmin : s_zmax;
|
||||
return a;
|
||||
}
|
||||
|
||||
static void boxWorld(const mapview::TileArea& b, double& x0, double& y0, double& x1, double& y1) {
|
||||
x0 = worldX(b.lon0); x1 = worldX(b.lon1);
|
||||
y0 = worldY(b.lat1); y1 = worldY(b.lat0);
|
||||
}
|
||||
|
||||
static void fmtDate(char* out, size_t n, uint32_t t, const NodePrefs* p) {
|
||||
if (t < 1000000000UL) { out[0] = '\0'; return; }
|
||||
time_t tt = (time_t)((int64_t)t + (int64_t)(p ? p->tz_offset_hours : 0) * 3600);
|
||||
struct tm tm = *gmtime(&tt);
|
||||
static const char* const MON[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
|
||||
snprintf(out, n, "%d %s %d", tm.tm_mday, MON[tm.tm_mon], tm.tm_year + 1900);
|
||||
}
|
||||
|
||||
static void onHandle(lv_event_t* e) {
|
||||
lv_point_t v;
|
||||
lv_indev_get_vect(lv_indev_active(), &v);
|
||||
if (v.x || v.y) s_ui->areaHandleDrag((int)(uintptr_t)lv_event_get_user_data(e), v.x, v.y);
|
||||
}
|
||||
static void onCancel(lv_event_t* e) { (void)e; s_ui->areaSelectEnd(); }
|
||||
static void onGo(lv_event_t* e) { (void)e; s_ui->areaSelectDownload(); }
|
||||
static void onZMinus(lv_event_t* e) { (void)e; s_ui->areaSelectZmax(-1); }
|
||||
static void onZPlus(lv_event_t* e) { (void)e; s_ui->areaSelectZmax(+1); }
|
||||
static void onTrailsChip(lv_event_t* e) { (void)e; s_trails = !s_trails; s_ui->areaLayout(); }
|
||||
static void onArea(lv_event_t* e) { s_ui->mapAreaPopup((int)(intptr_t)lv_event_get_user_data(e)); }
|
||||
static void onNewArea(lv_event_t* e) { (void)e; s_ui->areaSelectBegin(); }
|
||||
static void onAct(lv_event_t* e) { s_ui->mapAreaAction((uint8_t)(uintptr_t)lv_event_get_user_data(e)); }
|
||||
static void onRenameKb(lv_event_t* e) { s_ui->mapAreaRenameDone(lv_event_get_code(e) == LV_EVENT_READY); }
|
||||
|
||||
// LV_EVENT_DRAW_MAIN of the layer: outlines, the frame and the dim outside it.
|
||||
static void drawLayer(lv_event_t* e) {
|
||||
if (!s_sel && s_show < 0) return;
|
||||
lv_obj_t* o = (lv_obj_t*)lv_event_get_target(e);
|
||||
lv_layer_t* layer = lv_event_get_layer(e);
|
||||
lv_area_t a;
|
||||
lv_obj_get_coords(o, &a);
|
||||
double scale = 0; // world -> px
|
||||
double left = mapview::s_left, top = mapview::s_top;
|
||||
int z = s_ui->mapZoomLevel();
|
||||
scale = (double)(1 << z) * mapview::TILE_PX;
|
||||
auto rect = [&](double x0, double y0, double x1, double y1, lv_area_t& r) {
|
||||
r.x1 = a.x1 + (int32_t)lround(x0 * scale - left);
|
||||
r.y1 = a.y1 + (int32_t)lround(y0 * scale - top);
|
||||
r.x2 = a.x1 + (int32_t)lround(x1 * scale - left);
|
||||
r.y2 = a.y1 + (int32_t)lround(y1 * scale - top);
|
||||
};
|
||||
lv_draw_rect_dsc_t d;
|
||||
// Areas on the card: thin outlines (the shown one in the accent colour).
|
||||
for (int i = 0; i < mapview::s_areas.count(); i++) {
|
||||
if (!s_sel && i != s_show) continue;
|
||||
double x0, y0, x1, y1;
|
||||
boxWorld(mapview::s_areas.at(i).box, x0, y0, x1, y1);
|
||||
lv_area_t r;
|
||||
rect(x0, y0, x1, y1, r);
|
||||
lv_draw_rect_dsc_init(&d);
|
||||
d.bg_opa = LV_OPA_TRANSP;
|
||||
d.border_width = i == s_show ? 2 : 1;
|
||||
d.border_color = lv_color_hex(i == s_show ? theme::ACCENT : 0x9A958C);
|
||||
d.border_opa = LV_OPA_COVER;
|
||||
lv_draw_rect(layer, &d, &r);
|
||||
}
|
||||
if (!s_sel) return;
|
||||
lv_area_t f;
|
||||
rect(s_fx0, s_fy0, s_fx1, s_fy1, f);
|
||||
lv_draw_rect_dsc_init(&d); // dim everything outside the frame
|
||||
d.bg_color = lv_color_hex(theme::BG);
|
||||
d.bg_opa = LV_OPA_50;
|
||||
lv_area_t r;
|
||||
r = { a.x1, a.y1, a.x2, f.y1 - 1 }; if (r.y2 >= r.y1) lv_draw_rect(layer, &d, &r);
|
||||
r = { a.x1, f.y2 + 1, a.x2, a.y2 }; if (r.y2 >= r.y1) lv_draw_rect(layer, &d, &r);
|
||||
r = { a.x1, f.y1, f.x1 - 1, f.y2 }; if (r.x2 >= r.x1) lv_draw_rect(layer, &d, &r);
|
||||
r = { f.x2 + 1, f.y1, a.x2, f.y2 }; if (r.x2 >= r.x1) lv_draw_rect(layer, &d, &r);
|
||||
lv_draw_rect_dsc_init(&d);
|
||||
d.bg_opa = LV_OPA_TRANSP;
|
||||
d.border_width = 2;
|
||||
d.border_color = lv_color_hex(theme::ACCENT);
|
||||
d.border_opa = LV_OPA_COVER;
|
||||
lv_draw_rect(layer, &d, &f);
|
||||
}
|
||||
|
||||
static lv_obj_t* barButton(lv_obj_t* row, const char* text, lv_event_cb_t cb, int w) {
|
||||
lv_obj_t* b = lv_button_create(row);
|
||||
lv_obj_set_size(b, w, 34);
|
||||
lv_obj_set_style_pad_all(b, 0, 0);
|
||||
lv_obj_set_style_radius(b, theme::RADIUS, 0);
|
||||
lv_obj_set_style_shadow_width(b, 0, 0);
|
||||
lv_obj_set_style_bg_color(b, lv_color_hex(theme::SURFACE), 0);
|
||||
lv_obj_set_style_bg_color(b, lv_color_hex(theme::SURFACE_2), LV_STATE_PRESSED);
|
||||
lv_obj_add_event_cb(b, cb, LV_EVENT_CLICKED, NULL);
|
||||
lv_obj_center(label(b, text, THEME_FONT_SMALL, theme::TEXT));
|
||||
return b;
|
||||
}
|
||||
|
||||
static lv_obj_t* actButton(lv_obj_t* row, const char* text, uint8_t act, bool accent) {
|
||||
lv_obj_t* b = lv_button_create(row);
|
||||
lv_obj_set_height(b, 36);
|
||||
lv_obj_set_flex_grow(b, 1);
|
||||
lv_obj_set_style_pad_hor(b, 4, 0);
|
||||
lv_obj_set_style_radius(b, theme::RADIUS, 0);
|
||||
lv_obj_set_style_shadow_width(b, 0, 0);
|
||||
lv_obj_set_style_bg_color(b, lv_color_hex(accent ? theme::ACCENT_DIM : theme::SURFACE), 0);
|
||||
lv_obj_set_style_bg_color(b, lv_color_hex(theme::SURFACE_2), LV_STATE_PRESSED);
|
||||
lv_obj_add_event_cb(b, onAct, LV_EVENT_CLICKED, (void*)(uintptr_t)act);
|
||||
lv_obj_t* l = label(b, text, THEME_FONT_SMALL, theme::TEXT);
|
||||
lv_obj_center(l);
|
||||
return l;
|
||||
}
|
||||
|
||||
} // namespace areas
|
||||
|
||||
int UITask::mapZoomLevel() const { return _map_z; }
|
||||
|
||||
// From buildMap(): the layer and the handles, hidden until wanted.
|
||||
void UITask::areaBuildLayer(lv_obj_t* body) {
|
||||
using namespace areas;
|
||||
s_sel = false;
|
||||
s_bar = s_info = s_zoom = s_trails_btn = nullptr;
|
||||
s_layer = lv_obj_create(body);
|
||||
lv_obj_remove_style_all(s_layer);
|
||||
lv_obj_set_size(s_layer, LV_PCT(100), LV_PCT(100));
|
||||
lv_obj_remove_flag(s_layer, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_remove_flag(s_layer, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_add_event_cb(s_layer, drawLayer, LV_EVENT_DRAW_MAIN, NULL);
|
||||
for (int i = 0; i < 4; i++) {
|
||||
lv_obj_t* h = lv_obj_create(body);
|
||||
lv_obj_remove_style_all(h);
|
||||
lv_obj_set_size(h, HANDLE, HANDLE);
|
||||
lv_obj_set_style_radius(h, LV_RADIUS_CIRCLE, 0);
|
||||
lv_obj_set_style_bg_color(h, lv_color_hex(theme::ACCENT), 0);
|
||||
lv_obj_set_style_bg_opa(h, LV_OPA_COVER, 0);
|
||||
lv_obj_set_style_border_color(h, lv_color_hex(theme::BG), 0);
|
||||
lv_obj_set_style_border_width(h, 3, 0);
|
||||
lv_obj_set_ext_click_area(h, 10);
|
||||
lv_obj_add_flag(h, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_remove_flag(h, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_remove_flag(h, LV_OBJ_FLAG_GESTURE_BUBBLE);
|
||||
lv_obj_add_event_cb(h, onHandle, LV_EVENT_PRESSING, (void*)(uintptr_t)i);
|
||||
lv_obj_add_flag(h, LV_OBJ_FLAG_HIDDEN);
|
||||
s_handle[i] = h;
|
||||
}
|
||||
}
|
||||
|
||||
// Map tools > Download an area (or the Nodes map's download button): the
|
||||
// frame over the middle of the view.
|
||||
void UITask::areaSelectBegin() {
|
||||
using namespace areas;
|
||||
if (_nav_overlay) navClosePopup();
|
||||
if (_dl_overlay) mapDownloadClose();
|
||||
if (!_map_area || !s_layer || s_sel) return;
|
||||
s_sel = true;
|
||||
s_show = -1;
|
||||
// Between the button columns and above the bar, the handles clear of the buttons.
|
||||
int w = lv_obj_get_width(_map_area), h = lv_obj_get_height(_map_area) - BAR_H;
|
||||
double scale = (double)(1 << _map_z) * mapview::TILE_PX;
|
||||
s_fx0 = (mapview::s_left + 70) / scale; s_fx1 = (mapview::s_left + w - 70) / scale;
|
||||
s_fy0 = (mapview::s_top + 30) / scale; s_fy1 = (mapview::s_top + h - 22) / scale;
|
||||
int src_max = mapview::s_dl.sourceMaxZ();
|
||||
s_zmax = _map_z + 3 > src_max ? src_max : _map_z + 3;
|
||||
s_trails = lvport::trailsOn();
|
||||
|
||||
s_bar = lv_obj_create(_map_area);
|
||||
lv_obj_remove_style_all(s_bar);
|
||||
lv_obj_remove_flag(s_bar, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_add_flag(s_bar, LV_OBJ_FLAG_CLICKABLE); // presses on it don't pan the map
|
||||
lv_obj_set_size(s_bar, LV_PCT(100), BAR_H);
|
||||
lv_obj_align(s_bar, LV_ALIGN_BOTTOM_MID, 0, 0);
|
||||
lv_obj_set_style_bg_color(s_bar, lv_color_hex(theme::BG), 0);
|
||||
lv_obj_set_style_bg_opa(s_bar, LV_OPA_COVER, 0);
|
||||
lv_obj_set_style_pad_all(s_bar, 6, 0);
|
||||
lv_obj_set_style_pad_row(s_bar, 4, 0);
|
||||
lv_obj_set_flex_flow(s_bar, LV_FLEX_FLOW_COLUMN);
|
||||
s_info = label(s_bar, "", THEME_FONT_SMALL, theme::TEXT);
|
||||
lv_label_set_long_mode(s_info, LV_LABEL_LONG_DOT);
|
||||
lv_obj_set_width(s_info, LV_PCT(100));
|
||||
lv_obj_t* row = lv_obj_create(s_bar);
|
||||
lv_obj_remove_style_all(row);
|
||||
lv_obj_set_size(row, LV_PCT(100), 34);
|
||||
lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_style_pad_column(row, 4, 0);
|
||||
barButton(row, LV_SYMBOL_CLOSE, onCancel, 34);
|
||||
barButton(row, LV_SYMBOL_MINUS, onZMinus, 30);
|
||||
s_zoom = label(row, "", THEME_FONT_SMALL, theme::ACCENT);
|
||||
lv_obj_set_width(s_zoom, 34);
|
||||
lv_obj_set_style_text_align(s_zoom, LV_TEXT_ALIGN_CENTER, 0);
|
||||
barButton(row, LV_SYMBOL_PLUS, onZPlus, 30);
|
||||
s_trails_btn = barButton(row, "Trails", onTrailsChip, 58);
|
||||
lv_obj_t* go = barButton(row, LV_SYMBOL_DOWNLOAD " Get", onGo, 10);
|
||||
lv_obj_set_flex_grow(go, 1);
|
||||
stylePrimary(go);
|
||||
areaLayout();
|
||||
}
|
||||
|
||||
void UITask::areaSelectEnd() {
|
||||
using namespace areas;
|
||||
s_sel = false;
|
||||
if (s_bar) lv_obj_delete_async(s_bar);
|
||||
s_bar = s_info = s_zoom = s_trails_btn = nullptr;
|
||||
for (lv_obj_t* h : s_handle) if (h && _map_area) lv_obj_add_flag(h, LV_OBJ_FLAG_HIDDEN);
|
||||
if (s_layer) lv_obj_invalidate(s_layer);
|
||||
}
|
||||
|
||||
bool UITask::areaSelecting() const { return areas::s_sel; }
|
||||
|
||||
void UITask::areaSelectZmax(int d) {
|
||||
using namespace areas;
|
||||
int z = s_zmax + d;
|
||||
if (z < mapview::MIN_Z || z > mapview::MAX_Z || z > mapview::s_dl.sourceMaxZ()) return;
|
||||
s_zmax = z;
|
||||
areaLayout();
|
||||
}
|
||||
|
||||
// A corner dragged by (dx, dy) px; the opposite one stays.
|
||||
void UITask::areaHandleDrag(int corner, int dx, int dy) {
|
||||
using namespace areas;
|
||||
double scale = (double)(1 << _map_z) * mapview::TILE_PX;
|
||||
double min = 30 / scale; // not smaller than that on screen
|
||||
if (corner == 0 || corner == 2) s_fx0 = fmin(s_fx0 + dx / scale, s_fx1 - min);
|
||||
else s_fx1 = fmax(s_fx1 + dx / scale, s_fx0 + min);
|
||||
if (corner == 0 || corner == 1) s_fy0 = fmin(s_fy0 + dy / scale, s_fy1 - min);
|
||||
else s_fy1 = fmax(s_fy1 + dy / scale, s_fy0 + min);
|
||||
areaLayout();
|
||||
}
|
||||
|
||||
// From layoutMap(): handles and the bar follow the view.
|
||||
void UITask::areaLayout() {
|
||||
using namespace areas;
|
||||
if (!s_layer || !_map_area) return;
|
||||
lv_obj_invalidate(s_layer);
|
||||
if (!s_sel) return;
|
||||
double scale = (double)(1 << _map_z) * mapview::TILE_PX;
|
||||
double xs[2] = { s_fx0 * scale - mapview::s_left, s_fx1 * scale - mapview::s_left };
|
||||
double ys[2] = { s_fy0 * scale - mapview::s_top, s_fy1 * scale - mapview::s_top };
|
||||
for (int i = 0; i < 4; i++) {
|
||||
lv_obj_set_pos(s_handle[i], (int)lround(xs[i & 1]) - HANDLE / 2, (int)lround(ys[i >> 1]) - HANDLE / 2);
|
||||
lv_obj_remove_flag(s_handle[i], LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_move_foreground(s_handle[i]);
|
||||
}
|
||||
if (s_bar) lv_obj_move_foreground(s_bar);
|
||||
if (!s_info) return;
|
||||
int w = lv_obj_get_width(_map_area), h = lv_obj_get_height(_map_area);
|
||||
mapview::TileArea a = frameArea(w, h);
|
||||
uint32_t n = mapview::countTiles(a);
|
||||
// Its size on the ground, at the frame's middle latitude.
|
||||
double midlat = latOf((s_fy0 + s_fy1) / 2);
|
||||
double km_w = (s_fx1 - s_fx0) * 40075.0 * cos(midlat * M_PI / 180.0);
|
||||
double km_h = (s_fy1 - s_fy0) * 40075.0 * cos(midlat * M_PI / 180.0);
|
||||
bool imperial = _prefs && _prefs->units_imperial;
|
||||
char dim[32], size[16];
|
||||
if (imperial) snprintf(dim, sizeof(dim), "%.1f x %.1f mi", km_w * 0.621371, km_h * 0.621371);
|
||||
else snprintf(dim, sizeof(dim), km_w < 10 ? "%.1f x %.1f km" : "%.0f x %.0f km", km_w, km_h);
|
||||
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) >> 20));
|
||||
bool too = n > mapview::TileDownloader::MAX_TILES;
|
||||
lv_label_set_text_fmt(s_info, "%s - z%d-%d - %lu tiles, %s%s", dim, a.zmin, a.zmax, (unsigned long)n, size,
|
||||
too ? " - too large" : "");
|
||||
lv_obj_set_style_text_color(s_info, lv_color_hex(too ? theme::FAIL : theme::TEXT), 0);
|
||||
lv_label_set_text_fmt(s_zoom, "z%d", a.zmax);
|
||||
if (s_trails_btn) {
|
||||
lv_obj_set_style_bg_color(s_trails_btn, lv_color_hex(s_trails ? theme::ACCENT_DIM : theme::SURFACE), 0);
|
||||
lv_label_set_text(lv_obj_get_child(s_trails_btn, 0), s_trails ? LV_SYMBOL_OK " Trails" : "Trails");
|
||||
}
|
||||
}
|
||||
|
||||
// Start a download of `box` (a new area, or one on the list with `idx`).
|
||||
bool UITask::areaStartDownload(const mapview::TileArea& box, bool force, bool trails) {
|
||||
char ssid[33], pass[65];
|
||||
if (mapview::s_dl.active()) { showToast("A download is running"); return false; }
|
||||
if (!lvport::wifiAllowed()) { showToast("WiFi is off - Settings > WiFi"); return false; }
|
||||
if (!lvport::loadWifi(ssid, sizeof(ssid), pass, sizeof(pass))) { showToast("Pick a WiFi network first - Settings > WiFi", 3000); return false; }
|
||||
if (!lvport::mountStorage()) { showToast("No SD card"); return false; }
|
||||
if (!mapview::s_dl.start(box, ssid, pass, force, trails ? 1 : 0)) { showToast(mapview::s_dl.message()); return false; }
|
||||
return true;
|
||||
}
|
||||
|
||||
void UITask::areaSelectDownload() {
|
||||
using namespace areas;
|
||||
if (!_map_area) return;
|
||||
mapview::TileArea a = frameArea(lv_obj_get_width(_map_area), lv_obj_get_height(_map_area));
|
||||
if (mapview::countTiles(a) > mapview::TileDownloader::MAX_TILES) { showToast("Too large - make it smaller or lower the zoom", 3000); return; }
|
||||
if (!areaStartDownload(a, false, s_trails)) return;
|
||||
int i = mapview::s_areas.add(mapview::s_dl.area(), s_trails, rtc_clock.getCurrentTime());
|
||||
char t[64];
|
||||
snprintf(t, sizeof(t), "Downloading %s - rename it in Map areas", mapview::s_areas.at(i).name);
|
||||
showToast(t, 3000);
|
||||
areaSelectEnd();
|
||||
}
|
||||
|
||||
// ── The list ──
|
||||
|
||||
void UITask::mapAreasPopup() {
|
||||
using namespace areas;
|
||||
lv_obj_t* panel = navPopupPanel("Map areas", true);
|
||||
lv_obj_t* list = scrollList(panel);
|
||||
int n = mapview::s_areas.count();
|
||||
if (mapview::s_areas.deleting()) {
|
||||
char t[48];
|
||||
snprintf(t, sizeof(t), "Deleting... %lu files", (unsigned long)mapview::s_areas.deleted());
|
||||
label(list, t, THEME_FONT_SMALL, theme::ACCENT);
|
||||
}
|
||||
if (n) {
|
||||
lv_obj_t* g = group(list, nullptr);
|
||||
for (int i = n - 1; i >= 0; i--) { // newest first
|
||||
const mapview::MapArea& m = mapview::s_areas.at(i);
|
||||
char sub[64], date[16];
|
||||
fmtDate(date, sizeof(date), m.created, _prefs);
|
||||
snprintf(sub, sizeof(sub), "z%d-%d%s%s%s%s", m.box.zmin, m.box.zmax,
|
||||
(m.flags & mapview::AreaStore::F_TRAILS) ? " - trails" : "",
|
||||
(m.flags & mapview::AreaStore::F_COMPLETE) ? "" : " - unfinished", date[0] ? " - " : "", date);
|
||||
listRow(g, m.name, sub, onArea, (void*)(intptr_t)i);
|
||||
}
|
||||
} else {
|
||||
lv_obj_t* l = label(list, "No areas yet. Maps downloaded before this list are still on the card.", THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
lv_label_set_long_mode(l, LV_LABEL_LONG_WRAP);
|
||||
lv_obj_set_width(l, LV_PCT(100));
|
||||
}
|
||||
lv_obj_t* add = lv_button_create(list);
|
||||
lv_obj_set_size(add, LV_PCT(100), 38);
|
||||
lv_obj_set_style_radius(add, theme::RADIUS, 0);
|
||||
lv_obj_set_style_shadow_width(add, 0, 0);
|
||||
lv_obj_add_event_cb(add, onNewArea, LV_EVENT_CLICKED, NULL);
|
||||
lv_obj_center(label(add, LV_SYMBOL_PLUS " New area", THEME_FONT_BODY, theme::TEXT));
|
||||
stylePrimary(add);
|
||||
}
|
||||
|
||||
void UITask::mapAreaPopup(int idx) {
|
||||
using namespace areas;
|
||||
if (idx < 0 || idx >= mapview::s_areas.count()) return;
|
||||
s_idx = idx;
|
||||
const mapview::MapArea& m = mapview::s_areas.at(idx);
|
||||
lv_obj_t* panel = navPopupPanel(m.name, false);
|
||||
char info[128], date[16];
|
||||
fmtDate(date, sizeof(date), m.created, _prefs);
|
||||
uint32_t n = mapview::countTiles(m.box);
|
||||
snprintf(info, sizeof(info), "Zoom %d-%d, %lu tiles%s%s%s\n%s", m.box.zmin, m.box.zmax, (unsigned long)n,
|
||||
(m.flags & mapview::AreaStore::F_TRAILS) ? ", trails" : "", date[0] ? " - " : "", date,
|
||||
(m.flags & mapview::AreaStore::F_COMPLETE) ? "Downloaded" : "Unfinished - Fill gaps completes it");
|
||||
lv_obj_t* l = label(panel, info, THEME_FONT_SMALL, theme::TEXT);
|
||||
lv_label_set_long_mode(l, LV_LABEL_LONG_WRAP);
|
||||
lv_obj_set_width(l, LV_PCT(100));
|
||||
lv_obj_t* r = toolRow(panel);
|
||||
actButton(r, UI_SYMBOL_MAP " Show", A_SHOW, true);
|
||||
actButton(r, LV_SYMBOL_EDIT " Rename", A_RENAME, false);
|
||||
r = toolRow(panel);
|
||||
actButton(r, LV_SYMBOL_DOWNLOAD " Fill gaps", A_FILL, false);
|
||||
actButton(r, LV_SYMBOL_REFRESH " Refresh", A_REFRESH, false);
|
||||
r = toolRow(panel);
|
||||
actButton(r, (m.flags & mapview::AreaStore::F_TRAILS) ? "Remove trails" : LV_SYMBOL_PLUS " Trails", A_TRAILS, false);
|
||||
s_del_lbl = actButton(r, LV_SYMBOL_TRASH " Delete", A_DELETE, false);
|
||||
s_del_armed_ms = 0;
|
||||
}
|
||||
|
||||
void UITask::mapAreaAction(uint8_t act) {
|
||||
using namespace areas;
|
||||
if (s_idx < 0 || s_idx >= mapview::s_areas.count()) return;
|
||||
mapview::MapArea m = mapview::s_areas.at(s_idx);
|
||||
bool trails = m.flags & mapview::AreaStore::F_TRAILS;
|
||||
switch (act) {
|
||||
case A_SHOW: {
|
||||
navClosePopup();
|
||||
double x0, y0, x1, y1;
|
||||
boxWorld(m.box, x0, y0, x1, y1);
|
||||
int w = _map_area ? lv_obj_get_width(_map_area) : 320, h = _map_area ? lv_obj_get_height(_map_area) : 218;
|
||||
_map_z = fitZoom(x1 - x0, y1 - y0, w - 40, h - 60);
|
||||
double s = (double)(1 << _map_z);
|
||||
_map_cx = (x0 + x1) / 2 * s;
|
||||
_map_cy = (y0 + y1) / 2 * s;
|
||||
_map_follow = false;
|
||||
s_show = s_idx;
|
||||
layoutMap();
|
||||
return;
|
||||
}
|
||||
case A_RENAME: {
|
||||
lv_obj_t* panel = navPopupPanel("Area name", false);
|
||||
lv_obj_align(panel, LV_ALIGN_TOP_MID, 0, theme::STATUS_H + 4); // above the keyboard
|
||||
_nav_ta = textField(panel);
|
||||
lv_textarea_set_max_length(_nav_ta, sizeof(m.name) - 1);
|
||||
lv_textarea_set_text(_nav_ta, m.name);
|
||||
lv_obj_add_state(_nav_ta, LV_STATE_FOCUSED);
|
||||
_nav_kb = kb::create(_nav_overlay, _prefs);
|
||||
lv_obj_set_size(_nav_kb, LV_PCT(100), 124);
|
||||
lv_obj_align(_nav_kb, LV_ALIGN_BOTTOM_MID, 0, 0);
|
||||
lv_keyboard_set_textarea(_nav_kb, _nav_ta);
|
||||
lv_obj_add_event_cb(_nav_kb, onRenameKb, LV_EVENT_READY, NULL);
|
||||
lv_obj_add_event_cb(_nav_kb, onRenameKb, LV_EVENT_CANCEL, NULL);
|
||||
return;
|
||||
}
|
||||
case A_FILL:
|
||||
case A_REFRESH:
|
||||
if (!areaStartDownload(m.box, act == A_REFRESH, trails)) return;
|
||||
navClosePopup();
|
||||
showToast(act == A_REFRESH ? "Downloading it all again" : "Fetching what's missing");
|
||||
return;
|
||||
case A_TRAILS:
|
||||
if (trails) { // off: its trail tiles go (unless another area has them)
|
||||
if (mapview::s_areas.deleting()) { showToast("Still deleting - try again shortly"); return; }
|
||||
mapview::s_areas.remove(s_idx, false, true);
|
||||
navClosePopup();
|
||||
showToast("Removing its trails");
|
||||
return;
|
||||
}
|
||||
if (!areaStartDownload(m.box, false, true)) return;
|
||||
mapview::s_areas.setFlag(s_idx, mapview::AreaStore::F_TRAILS, true);
|
||||
navClosePopup();
|
||||
showToast("Fetching its trails");
|
||||
return;
|
||||
case A_DELETE:
|
||||
if (!s_del_armed_ms || millis() - s_del_armed_ms > 3000) { // a second tap within 3 s confirms
|
||||
s_del_armed_ms = millis() | 1;
|
||||
if (s_del_lbl) lv_label_set_text(s_del_lbl, "Delete?");
|
||||
return;
|
||||
}
|
||||
if (mapview::s_areas.deleting()) { showToast("Still deleting - try again shortly"); return; }
|
||||
if (mapview::s_dl.active() && mapview::s_areas.find(mapview::s_dl.area()) == s_idx) mapview::s_dl.cancel();
|
||||
if (s_show == s_idx) s_show = -1;
|
||||
else if (s_show > s_idx) s_show--;
|
||||
mapview::s_areas.remove(s_idx, true, true);
|
||||
navClosePopup();
|
||||
showToast("Deleting the area");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void UITask::mapAreaRenameDone(bool ok) {
|
||||
using namespace areas;
|
||||
if (ok && _nav_ta) mapview::s_areas.rename(s_idx, lv_textarea_get_text(_nav_ta));
|
||||
navClosePopup();
|
||||
mapAreasPopup();
|
||||
}
|
||||
@@ -19,6 +19,7 @@
|
||||
#include "map/TileCache.h"
|
||||
#include "map/LiveCache.h"
|
||||
#include "map/TileDownloader.h"
|
||||
#include "map/AreaStore.h"
|
||||
|
||||
namespace mapview {
|
||||
|
||||
@@ -26,6 +27,7 @@ static RasterTileProvider s_raster("/sdcard/maps", LIVE_ROOT);
|
||||
static TileProvider* s_provider = &s_raster; // the one place to swap in a vector renderer
|
||||
static TileCache& s_cache = *new (psramBuf<TileCache>(1)) TileCache(); // decoded tiles, in PSRAM
|
||||
static TileDownloader s_dl("/sdcard/maps");
|
||||
static AreaStore s_areas("/sdcard/maps");
|
||||
static const uint32_t AVG_TILE_BYTES = 22 * 1024; // OpenTopoMap-ish, for the size estimate
|
||||
|
||||
// Decode a tile into the cache; with live tiles on, a base tile shown without
|
||||
@@ -329,6 +331,8 @@ void UITask::buildMap() {
|
||||
lv_obj_remove_flag(_map_me, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_add_flag(_map_me, LV_OBJ_FLAG_HIDDEN);
|
||||
|
||||
areaBuildLayer(body); // download frame / area outlines, over the tiles (MapAreas.h)
|
||||
|
||||
// Controls
|
||||
lv_obj_t* back = mapButton(body, LV_SYMBOL_LEFT, onBack);
|
||||
lv_obj_align(back, LV_ALIGN_TOP_LEFT, 6, 6);
|
||||
@@ -486,8 +490,9 @@ void UITask::layoutMap() {
|
||||
: mapview::s_dl.liveQueued() > 0 ? nullptr // being fetched
|
||||
: (!_map_pending && shown == 0 && missing > 0)
|
||||
? "No map here at this zoom -\nzoom out or download the area" : nullptr;
|
||||
if (hint) { lv_label_set_text(_map_hint, hint); lv_obj_remove_flag(_map_hint, LV_OBJ_FLAG_HIDDEN); }
|
||||
if (hint && !areaSelecting()) { 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);
|
||||
areaLayout();
|
||||
}
|
||||
|
||||
// One tile decode per call, nearest to the centre first.
|
||||
@@ -677,8 +682,12 @@ static mapview::TileArea visibleArea(double cx, double cy, int z, int w, int h,
|
||||
|
||||
static int dlZmin(int z) { return z - 4 < 5 ? (z < 5 ? z : 5) : z - 4; } // a few overview levels (cheap)
|
||||
|
||||
// The download button: progress / an unfinished job in a popup, else the
|
||||
// frame to pick a new area (MapAreas.h).
|
||||
void UITask::mapDownloadPopup() {
|
||||
if (_dl_overlay) return;
|
||||
mapview::TileArea job;
|
||||
if (!mapview::s_dl.active() && !mapview::s_dl.savedJob(job)) { areaSelectBegin(); return; }
|
||||
int src_max = mapview::s_dl.sourceMaxZ();
|
||||
if (_dl_zmax < _map_z) _dl_zmax = _map_z + 3;
|
||||
if (_dl_zmax > src_max) _dl_zmax = src_max; // the server has nothing finer
|
||||
@@ -848,20 +857,13 @@ void UITask::refreshDownloadPopup() {
|
||||
snprintf(last, sizeof(last), "\nLast: %s (%lu new)", dl.message(), (unsigned long)dl.downloaded());
|
||||
lv_label_set_text_fmt(_dl_info, "z%d-%d: %lu tiles, about %s - %s\n%s%s",
|
||||
a.zmin, a.zmax, (unsigned long)n, size, host, net, last);
|
||||
lv_label_set_text(_dl_start_lbl, n > mapview::TileDownloader::MAX_TILES ? "Too large - zoom in"
|
||||
: LV_SYMBOL_DOWNLOAD " Download");
|
||||
(void)n;
|
||||
lv_label_set_text(_dl_start_lbl, LV_SYMBOL_PLUS " New area");
|
||||
}
|
||||
|
||||
void UITask::mapDownloadStart() {
|
||||
if (mapview::s_dl.active()) { mapDownloadStop(); return; }
|
||||
char ssid[33], pass[65];
|
||||
if (!lvport::wifiAllowed()) { showToast("WiFi is off - Settings > WiFi"); return; }
|
||||
if (!lvport::loadWifi(ssid, sizeof(ssid), pass, sizeof(pass))) { showToast("Pick a WiFi network first - Settings > WiFi", 3000); return; }
|
||||
if (!lvport::mountStorage()) { showToast("No SD card"); return; }
|
||||
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);
|
||||
if (!mapview::s_dl.start(a, ssid, pass)) { showToast(mapview::s_dl.message()); return; }
|
||||
refreshDownloadPopup();
|
||||
areaSelectBegin(); // a new area: picked with the frame
|
||||
}
|
||||
|
||||
void UITask::mapDownloadResume() {
|
||||
@@ -928,6 +930,14 @@ void UITask::mapDownloadTick() {
|
||||
mapview::TileDownloader& dl = mapview::s_dl;
|
||||
dl.loop();
|
||||
mapview::s_live_cache.service(6); // trims live tiles past the Storage limit
|
||||
if (mapview::s_areas.service(8)) { // a deleted area's files are gone
|
||||
char t[48];
|
||||
snprintf(t, sizeof(t), "Area deleted (%lu files)", (unsigned long)mapview::s_areas.deleted());
|
||||
showToast(t);
|
||||
mapview::s_cache.invalidate();
|
||||
mapview::s_available = lvport::mountStorage() && mapview::s_provider->available();
|
||||
if (_screen == SCR_MAP) layoutMap();
|
||||
}
|
||||
if (dl.liveOn()) {
|
||||
int z, x, y;
|
||||
bool got = false;
|
||||
@@ -944,6 +954,8 @@ void UITask::mapDownloadTick() {
|
||||
bool was_active = _dl_last_state == mapview::TileDownloader::CONNECTING ||
|
||||
_dl_last_state == mapview::TileDownloader::RUNNING;
|
||||
if (was_active && !dl.active()) { // just finished
|
||||
if (st == mapview::TileDownloader::DONE)
|
||||
mapview::s_areas.setFlag(mapview::s_areas.find(dl.area()), mapview::AreaStore::F_COMPLETE, true);
|
||||
char t[64];
|
||||
snprintf(t, sizeof(t), "Map: %s, %lu new tiles", dl.message(), (unsigned long)dl.downloaded());
|
||||
showToast(t, 4000);
|
||||
|
||||
@@ -779,7 +779,7 @@ void UITask::shareToMessage(const char* text) {
|
||||
|
||||
namespace navmap {
|
||||
enum : uint8_t { TL_TRAIL_TOGGLE, TL_TRAIL_SAVE, TL_TRAIL_LOAD, TL_TRAIL_RESET, TL_TRAIL_GPX, TL_TRACKBACK,
|
||||
TL_SHARE_TOGGLE, TL_SHARE_ONCE, TL_DOWNLOAD, TL_OPTIONS,
|
||||
TL_SHARE_TOGGLE, TL_SHARE_ONCE, TL_DOWNLOAD, TL_AREAS, TL_OPTIONS,
|
||||
TL_WP_HERE, TL_WP_COORDS, TL_SPOT_ADD, TL_SPOT_GO,
|
||||
TL_ST_LOAD, TL_ST_GPX, TL_ST_DELETE };
|
||||
|
||||
@@ -973,7 +973,11 @@ void UITask::navToolsPopup() {
|
||||
lv_obj_t* tsw = switchRow(g, "Hiking trails", "Marked routes in their colours", nullptr);
|
||||
if (lvport::trailsOn()) lv_obj_add_state(tsw, LV_STATE_CHECKED);
|
||||
lv_obj_add_event_cb(tsw, onTrails, LV_EVENT_VALUE_CHANGED, NULL);
|
||||
listRow(g, "Download this area", "For use offline", onNavTool, (void*)(uintptr_t)navmap::TL_DOWNLOAD);
|
||||
listRow(g, "Download an area", "Pick it with a frame, for use offline", onNavTool, (void*)(uintptr_t)navmap::TL_DOWNLOAD);
|
||||
char ar[40];
|
||||
int na = mapview::s_areas.count();
|
||||
snprintf(ar, sizeof(ar), na ? "%d on the card - rename, refresh, delete" : "None yet", na);
|
||||
listRow(g, "Map areas", ar, onNavTool, (void*)(uintptr_t)navmap::TL_AREAS);
|
||||
listRow(g, "Map options", "Trail, live sharing, arrival alert", onNavTool, (void*)(uintptr_t)navmap::TL_OPTIONS);
|
||||
|
||||
_nav_reset_armed_ms = 0;
|
||||
@@ -1265,6 +1269,9 @@ void UITask::navToolAction(uint8_t act) {
|
||||
navClosePopup();
|
||||
mapDownloadPopup();
|
||||
return;
|
||||
case navmap::TL_AREAS:
|
||||
mapAreasPopup();
|
||||
return;
|
||||
case navmap::TL_OPTIONS: // back returns to the map
|
||||
showSchemaSettings(settings::PG_NAV);
|
||||
return;
|
||||
|
||||
@@ -1389,6 +1389,7 @@ void UITask::back() {
|
||||
break;
|
||||
case SCR_MAP:
|
||||
if (_nav_overlay) navClosePopup();
|
||||
else if (areaSelecting()) areaSelectEnd();
|
||||
else if (_dl_overlay) mapDownloadClose();
|
||||
else if (!_map_nav) showNearby(); // the Nodes map opens from Nearby
|
||||
else showHome();
|
||||
@@ -2181,6 +2182,7 @@ void UITask::nodeAction(uint8_t action) {
|
||||
static lv_obj_t* switchRow(lv_obj_t* parent, const char* text, const char* sub, uint8_t* pref); // below
|
||||
#include "MapScreen.h"
|
||||
#include "NavMap.h"
|
||||
#include "MapAreas.h"
|
||||
#include "HomeScreen.h"
|
||||
#include "ClockScreen.h"
|
||||
#include "RadioScreen.h"
|
||||
@@ -3099,6 +3101,8 @@ extern "C" EMSCRIPTEN_KEEPALIVE void sim_open(const char* name) {
|
||||
if (!strncmp(name, "page", 4)) { s_ui->showSchemaSettings(atoi(name + 4)); return; }
|
||||
if (!strcmp(name, "map")) { s_ui->openMap(true); return; }
|
||||
if (!strcmp(name, "maptools")) { s_ui->navToolsPopup(); return; }
|
||||
if (!strcmp(name, "areasel")) { s_ui->areaSelectBegin(); return; }
|
||||
if (!strcmp(name, "areas")) { s_ui->mapAreasPopup(); return; }
|
||||
if (!strcmp(name, "advert")) { s_ui->advertPopup(); return; }
|
||||
}
|
||||
// Scrolls the screen's (or a popup's) main list by dy; returns what was left to scroll.
|
||||
|
||||
@@ -21,6 +21,8 @@ class UiCore;
|
||||
class NearbyModel;
|
||||
struct UiEvent;
|
||||
|
||||
namespace mapview { struct TileArea; }
|
||||
|
||||
class UITask : public UITaskBase, public UiCoreHost {
|
||||
public:
|
||||
UITask(mesh::MainBoard* board, BaseSerialInterface* serial) : UITaskBase(board, serial) {}
|
||||
@@ -66,6 +68,21 @@ public:
|
||||
void mapPan(int dx, int dy);
|
||||
void mapOpenMarker(int idx);
|
||||
void mapDownloadPopup();
|
||||
// Map areas (MapAreas.h)
|
||||
void areaBuildLayer(lv_obj_t* body);
|
||||
void areaSelectBegin();
|
||||
void areaSelectEnd();
|
||||
bool areaSelecting() const;
|
||||
void areaSelectDownload();
|
||||
void areaSelectZmax(int d);
|
||||
void areaHandleDrag(int corner, int dx, int dy);
|
||||
void areaLayout();
|
||||
bool areaStartDownload(const mapview::TileArea& box, bool force, bool trails);
|
||||
void mapAreasPopup();
|
||||
void mapAreaPopup(int idx);
|
||||
void mapAreaAction(uint8_t act);
|
||||
void mapAreaRenameDone(bool ok);
|
||||
int mapZoomLevel() const;
|
||||
void setTapWake(bool on);
|
||||
void mapDownloadClose();
|
||||
void mapDownloadStart();
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
#pragma once
|
||||
// Downloaded map areas: what was fetched where, so it can be shown, named,
|
||||
// refreshed, given trails or deleted later (Map tools > Map areas). One text
|
||||
// line per area in <root>/areas.txt:
|
||||
// A lon0 lat0 lon1 lat1 zmin zmax flags created name
|
||||
// An area is a box and a zoom range, whatever the tiles are; a vector source
|
||||
// would keep its regions here too (a kind in `flags`).
|
||||
//
|
||||
// Deleting runs in steps (an area can be tens of thousands of files): a tile
|
||||
// is removed only if no other area covers it at that zoom, so overlapping
|
||||
// areas keep what they share. Packed columns (.pak, the PC tool) stay.
|
||||
//
|
||||
// Single-TU fragment: included by ui-lvgl/MapScreen.h after TileDownloader.h.
|
||||
|
||||
namespace mapview {
|
||||
|
||||
struct MapArea {
|
||||
TileArea box;
|
||||
uint8_t flags;
|
||||
uint32_t created; // RTC seconds, 0 unknown
|
||||
char name[32];
|
||||
};
|
||||
|
||||
class AreaStore {
|
||||
public:
|
||||
static const int MAX = 32;
|
||||
enum : uint8_t { F_TRAILS = 1, F_COMPLETE = 2 };
|
||||
|
||||
explicit AreaStore(const char* root) : _root(root) {}
|
||||
|
||||
int count() { load(); return _n; }
|
||||
MapArea& at(int i) { load(); return _a[i]; }
|
||||
|
||||
// A new area (the oldest goes when full); returns its index.
|
||||
int add(const TileArea& box, bool trails, uint32_t now) {
|
||||
load();
|
||||
if (_n >= MAX) { memmove(_a, _a + 1, sizeof(MapArea) * (MAX - 1)); _n--; }
|
||||
MapArea& m = _a[_n];
|
||||
m.box = box;
|
||||
m.flags = trails ? F_TRAILS : 0;
|
||||
m.created = now;
|
||||
int k = 1; // "Area n": the first number not taken
|
||||
for (bool taken = true; taken; k++) {
|
||||
taken = false;
|
||||
char want[16];
|
||||
snprintf(want, sizeof(want), "Area %d", k);
|
||||
for (int i = 0; i < _n; i++) if (!strcmp(_a[i].name, want)) taken = true;
|
||||
if (!taken) snprintf(m.name, sizeof(m.name), "%s", want);
|
||||
}
|
||||
_n++;
|
||||
save();
|
||||
return _n - 1;
|
||||
}
|
||||
|
||||
// The area a download job is for (same box and zooms).
|
||||
int find(const TileArea& b) {
|
||||
load();
|
||||
for (int i = _n - 1; i >= 0; i--) {
|
||||
const TileArea& a = _a[i].box;
|
||||
if (fabs(a.lon0 - b.lon0) < 1e-5 && fabs(a.lat0 - b.lat0) < 1e-5 && fabs(a.lon1 - b.lon1) < 1e-5 &&
|
||||
fabs(a.lat1 - b.lat1) < 1e-5 && a.zmin == b.zmin && a.zmax == b.zmax) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void setFlag(int i, uint8_t f, bool on) {
|
||||
if (i < 0 || i >= count()) return;
|
||||
if (on) _a[i].flags |= f; else _a[i].flags &= ~f;
|
||||
save();
|
||||
}
|
||||
void rename(int i, const char* name) {
|
||||
if (i < 0 || i >= count()) return;
|
||||
while (*name == ' ') name++;
|
||||
if (!*name) return;
|
||||
snprintf(_a[i].name, sizeof(_a[i].name), "%s", name);
|
||||
for (char* p = _a[i].name; *p; p++) if (*p == '\n' || *p == '\r') *p = ' ';
|
||||
save();
|
||||
}
|
||||
|
||||
// Removes area i from the list and starts deleting its files: the base
|
||||
// tiles (`base`) and / or its trails, except what other areas cover.
|
||||
void remove(int i, bool base, bool trails) {
|
||||
if (i < 0 || i >= count()) return;
|
||||
_del = _a[i].box;
|
||||
_del_base = base;
|
||||
_del_trails = trails;
|
||||
if (base) { // gone from the list; only trails: the area stays, without them
|
||||
memmove(_a + i, _a + i + 1, sizeof(MapArea) * (_n - i - 1));
|
||||
_n--;
|
||||
} else {
|
||||
_a[i].flags &= ~F_TRAILS;
|
||||
}
|
||||
save();
|
||||
_del_z = _del.zmin;
|
||||
tileRange(_del, _del_z, _dx0, _dy0, _dx1, _dy1);
|
||||
_dx = _dx0; _dy = _dy0;
|
||||
_deleting = true;
|
||||
_deleted = 0;
|
||||
}
|
||||
bool deleting() const { return _deleting; }
|
||||
uint32_t deleted() const { return _deleted; }
|
||||
|
||||
// A slice of the delete; true when it just finished.
|
||||
bool service(uint32_t budget_ms) {
|
||||
if (!_deleting) return false;
|
||||
uint32_t t0 = millis();
|
||||
while (millis() - t0 < budget_ms) {
|
||||
if (_del_z > _del.zmax) { _deleting = false; return true; }
|
||||
char path[64];
|
||||
if (_del_base && !covered(_del_z, _dx, _dy, false)) {
|
||||
snprintf(path, sizeof(path), "%s/%d/%d/%d.png", _root, _del_z, _dx, _dy);
|
||||
if (::remove(path) == 0) _deleted++;
|
||||
}
|
||||
if (_del_trails && !covered(_del_z, _dx, _dy, true)) {
|
||||
snprintf(path, sizeof(path), "%s/%d/%d/%d.png", TRAILS_ROOT, _del_z, _dx, _dy);
|
||||
if (::remove(path) == 0) _deleted++;
|
||||
}
|
||||
if (++_dy > _dy1) {
|
||||
_dy = _dy0;
|
||||
if (++_dx > _dx1) {
|
||||
if (++_del_z <= _del.zmax) { tileRange(_del, _del_z, _dx0, _dy0, _dx1, _dy1); _dx = _dx0; _dy = _dy0; }
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private:
|
||||
const char* _root;
|
||||
MapArea _a[MAX];
|
||||
int _n = 0;
|
||||
bool _loaded = false;
|
||||
TileArea _del = {0, 0, 0, 0, 0, 0};
|
||||
bool _deleting = false, _del_base = false, _del_trails = false;
|
||||
int _del_z = 0, _dx = 0, _dy = 0, _dx0 = 0, _dy0 = 0, _dx1 = 0, _dy1 = 0;
|
||||
uint32_t _deleted = 0;
|
||||
|
||||
// Another listed area has this tile (its trails, with `trails`).
|
||||
bool covered(int z, int x, int y, bool trails) {
|
||||
for (int i = 0; i < _n; i++) {
|
||||
const MapArea& m = _a[i];
|
||||
if (trails && !(m.flags & F_TRAILS)) continue;
|
||||
if (z < m.box.zmin || z > m.box.zmax) continue;
|
||||
int x0, y0, x1, y1;
|
||||
tileRange(m.box, z, x0, y0, x1, y1);
|
||||
if (x >= x0 && x <= x1 && y >= y0 && y <= y1) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void path(char* out, size_t n) const { snprintf(out, n, "%s/areas.txt", _root); }
|
||||
|
||||
void load() {
|
||||
if (_loaded) return;
|
||||
_loaded = true;
|
||||
_n = 0;
|
||||
char p[64];
|
||||
path(p, sizeof(p));
|
||||
FILE* f = fopen(p, "r");
|
||||
if (!f) return;
|
||||
char line[160];
|
||||
while (_n < MAX && fgets(line, sizeof(line), f)) {
|
||||
MapArea m;
|
||||
int flags = 0, off = 0;
|
||||
unsigned long created = 0;
|
||||
if (sscanf(line, "A %lf %lf %lf %lf %d %d %d %lu %n", &m.box.lon0, &m.box.lat0, &m.box.lon1, &m.box.lat1,
|
||||
&m.box.zmin, &m.box.zmax, &flags, &created, &off) < 8 || off <= 0) continue;
|
||||
if (m.box.zmin < 0 || m.box.zmax > 22 || m.box.zmin > m.box.zmax) continue;
|
||||
m.flags = (uint8_t)flags;
|
||||
m.created = (uint32_t)created;
|
||||
snprintf(m.name, sizeof(m.name), "%s", line + off);
|
||||
size_t k = strlen(m.name);
|
||||
while (k > 0 && (m.name[k - 1] == '\n' || m.name[k - 1] == '\r' || m.name[k - 1] == ' ')) m.name[--k] = '\0';
|
||||
if (!m.name[0]) snprintf(m.name, sizeof(m.name), "Area");
|
||||
_a[_n++] = m;
|
||||
}
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
void save() {
|
||||
char p[64], tmp[70];
|
||||
path(p, sizeof(p));
|
||||
makeParents(p);
|
||||
snprintf(tmp, sizeof(tmp), "%s.tmp", p);
|
||||
FILE* f = fopen(tmp, "w");
|
||||
if (!f) return;
|
||||
for (int i = 0; i < _n; i++) {
|
||||
const MapArea& m = _a[i];
|
||||
fprintf(f, "A %.6f %.6f %.6f %.6f %d %d %d %lu %s\n", m.box.lon0, m.box.lat0, m.box.lon1, m.box.lat1,
|
||||
m.box.zmin, m.box.zmax, m.flags, (unsigned long)m.created, m.name);
|
||||
}
|
||||
bool ok = fclose(f) == 0;
|
||||
if (!ok) { ::remove(tmp); return; }
|
||||
::remove(p);
|
||||
::rename(tmp, p);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace mapview
|
||||
@@ -86,6 +86,8 @@ public:
|
||||
explicit TileDownloader(const char* root) : _root(root) {}
|
||||
|
||||
State state() const { return _state; }
|
||||
const TileArea& area() const { return _area; } // of the current / last job
|
||||
bool withTrails() const { return _trails; }
|
||||
bool active() const { return _state == CONNECTING || _state == RUNNING; }
|
||||
uint32_t total() const { return _total; }
|
||||
uint32_t processed() const { return _done + _skipped + _failed; }
|
||||
@@ -110,7 +112,9 @@ public:
|
||||
_job_checked = true;
|
||||
}
|
||||
|
||||
bool start(const TileArea& a, const char* ssid, const char* pass) {
|
||||
// `force`: fetch every tile again (a refresh), not only the missing ones.
|
||||
// `trails`: 1 / 0 with or without the trails overlay, -1 as the map's toggle.
|
||||
bool start(const TileArea& a, const char* ssid, const char* pass, bool force = false, int trails = -1) {
|
||||
if (active()) return false;
|
||||
if (_lv_fetching) { lvport::fetchAbandon(); _lv_fetching = false; } // the job takes the fetcher
|
||||
_lv_state = LV_IDLE;
|
||||
@@ -118,7 +122,8 @@ public:
|
||||
_area = a;
|
||||
if (_area.zmax > _src_zmax) _area.zmax = _src_zmax; // the server has nothing finer
|
||||
if (_area.zmin > _area.zmax) { fail("The server has no finer zoom: zoom out"); return false; }
|
||||
_trails = s_trails_on;
|
||||
_trails = trails < 0 ? s_trails_on : trails > 0;
|
||||
_force = force;
|
||||
_layer = 0;
|
||||
_total = countTiles(_area);
|
||||
if (_trails && _area.zmin <= TRAILS_MAX_Z) {
|
||||
@@ -245,7 +250,7 @@ public:
|
||||
char path[96];
|
||||
tilePath(path, sizeof(path), _z, _x, _y, _layer);
|
||||
struct stat st;
|
||||
if (stat(path, &st) == 0 && (st.st_size > 0 || _layer == 1)) { _skipped++; advance(); continue; } // an empty trails file counts
|
||||
if (!_force && stat(path, &st) == 0 && (st.st_size > 0 || _layer == 1)) { _skipped++; advance(); continue; } // an empty trails file counts
|
||||
if ((int32_t)(millis() - _last_start) < 120) return; // be gentle with the server (a retry waits longer)
|
||||
char url[200];
|
||||
buildUrl(url, sizeof(url), _z, _x, _y, _layer);
|
||||
@@ -265,6 +270,7 @@ private:
|
||||
int _z = 0, _x = 0, _y = 0, _x0 = 0, _y0 = 0, _x1 = 0, _y1 = 0;
|
||||
bool _fetching = false;
|
||||
bool _trails = false; // this job fetches the trails overlay too
|
||||
bool _force = false; // refetches tiles already on the card
|
||||
uint8_t _layer = 0; // of the current tile: 0 the base map, 1 its trails
|
||||
bool _retried = false; // the current tile already failed once
|
||||
uint32_t _last_start = 0, _connect_started = 0;
|
||||
|
||||
Reference in New Issue
Block a user