mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-10-11 16:12:01 +00:00
- 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>
490 lines
22 KiB
C++
490 lines
22 KiB
C++
#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();
|
|
}
|