mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-11-02 10:51:57 +00:00
feat(map): base layers - Topo, Light, Dark, Satellite, Esri Topo
Map tools > Layers > Map picks what the map is drawn from: - Topo: OpenTopoMap, as before. - Light and Dark: Esri's grey canvases, no labels, up to z16. - Satellite: Esri World Imagery. - Esri Topo. Each layer has its own folder for downloaded areas (/sdcard/maps-light, -dark, -sat, -esritopo; Topo stays in /sdcard/maps) and its own live-tile cache. Live-cache records carry the layer. Esri serves JPEG: decoded with LVGL's tjpgd (LV_USE_TJPGD), whatever the file name says. The layer is kept in NVS (mc_ui/layer); switching waits while a download or a deletion runs in the current folder. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -27,6 +27,7 @@ public:
|
||||
enum : uint8_t { F_TRAILS = 1, F_COMPLETE = 2 };
|
||||
|
||||
explicit AreaStore(const char* root) : _root(root) {}
|
||||
void setRoot(const char* root) { if (_root != root) { _root = root; _loaded = false; } } // another base layer
|
||||
|
||||
int count() { load(); return _n; }
|
||||
MapArea& at(int i) { load(); return _a[i]; }
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
// Live map tiles (fetched while browsing online) live apart from downloaded
|
||||
// maps, under LIVE_ROOT/{z}/{x}/{y}.png, and only up to a size limit
|
||||
// maps, under LIVE_ROOT/{z}/{x}/{y}.png (another base layer: a folder of
|
||||
// its own in it, MapLayers.h), and only up to a size limit
|
||||
// (Settings > Storage): an index remembers the order they came in, and the
|
||||
// oldest are deleted once the tiles add up to more than the limit. Maps
|
||||
// downloaded on purpose (TileDownloader jobs, the PC tool) are never touched.
|
||||
@@ -34,11 +35,11 @@ public:
|
||||
uint32_t count() { load(); return _h.count; }
|
||||
bool clearing() const { return _clearing; }
|
||||
|
||||
// A tile was written to LIVE_ROOT: remember it (eviction follows in service()).
|
||||
void add(int z, int x, int y, uint32_t size) {
|
||||
// A tile was written to `layer`'s live folder: remember it (eviction follows in service()).
|
||||
void add(int z, int x, int y, uint32_t size, uint8_t layer) {
|
||||
load();
|
||||
if (_h.count >= RING) evictOne(); // the ring is full: make room right away
|
||||
Rec r = { (int32_t)z, x, y, size };
|
||||
Rec r = { (int32_t)z | (int32_t)layer << 8, x, y, size }; // (records from before layers: 0, Topo)
|
||||
uint32_t slot = (_h.head + _h.count) % RING;
|
||||
_h.count++;
|
||||
_h.bytes += size;
|
||||
@@ -63,8 +64,8 @@ public:
|
||||
if (_clearing && _h.count == 0) { _clearing = false; _h.bytes = 0; writeHdr(); }
|
||||
}
|
||||
|
||||
static void tilePath(char* out, size_t n, int z, int x, int y) {
|
||||
snprintf(out, n, "%s/%d/%d/%d.png", LIVE_ROOT, z, x, y);
|
||||
static void tilePath(char* out, size_t n, int z, int x, int y, uint8_t layer) {
|
||||
snprintf(out, n, "%s/%d/%d/%d.png", mapview::layer(layer).live, z, x, y);
|
||||
}
|
||||
|
||||
private:
|
||||
@@ -124,7 +125,7 @@ private:
|
||||
Rec r;
|
||||
if (readRec(_h.head, r)) {
|
||||
char path[128];
|
||||
tilePath(path, sizeof(path), r.z, r.x, r.y);
|
||||
tilePath(path, sizeof(path), r.z & 0xFF, r.x, r.y, (uint8_t)(r.z >> 8));
|
||||
remove(path);
|
||||
_h.bytes = _h.bytes > r.size ? _h.bytes - r.size : 0;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
#pragma once
|
||||
// Base map layers (Map tools > Map): the tile servers the map can show, each
|
||||
// in a folder of its own on the card -- its downloaded areas, its job, its
|
||||
// attribution -- and its live tiles in a folder of their own under LIVE_ROOT
|
||||
// (LiveCache.h; one size limit for them all). Topo is the original layout:
|
||||
// /sdcard/maps and the top of /sdcard/maps-live, with <root>/source.txt still
|
||||
// able to name another server (TileDownloader.h).
|
||||
//
|
||||
// Tiles keep the .png name whatever the server sends: the Esri layers are
|
||||
// JPEG (all but Topo), told apart by their first bytes when decoded (TileProvider.h).
|
||||
//
|
||||
// Single-TU fragment: included by ui-lvgl/UITask.cpp only.
|
||||
|
||||
namespace mapview {
|
||||
|
||||
struct MapLayer {
|
||||
const char* name; // in Map tools
|
||||
const char* root; // downloaded areas
|
||||
const char* live; // live tiles (LIVE_ROOT or a folder in it)
|
||||
const char* url; // {z} {x} {y}
|
||||
const char* attr;
|
||||
int8_t zmax; // the server's last zoom level
|
||||
};
|
||||
|
||||
static const MapLayer LAYERS[] = {
|
||||
{ "Topo", "/sdcard/maps", "/sdcard/maps-live",
|
||||
"https://a.tile.opentopomap.org/{z}/{x}/{y}.png",
|
||||
"\xC2\xA9 OpenStreetMap contributors, SRTM | \xC2\xA9 OpenTopoMap (CC-BY-SA)", 17 },
|
||||
// Monochrome: Esri's grey canvases (CARTO's Positron / Dark Matter now want
|
||||
// an API key). Nothing past z16: the server answers "Map data not yet available".
|
||||
{ "Light", "/sdcard/maps-light", "/sdcard/maps-live/light",
|
||||
"https://server.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Light_Gray_Base/MapServer/tile/{z}/{y}/{x}",
|
||||
"\xC2\xA9 Esri, HERE, Garmin, \xC2\xA9 OpenStreetMap contributors", 16 },
|
||||
{ "Dark", "/sdcard/maps-dark", "/sdcard/maps-live/dark",
|
||||
"https://server.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Dark_Gray_Base/MapServer/tile/{z}/{y}/{x}",
|
||||
"\xC2\xA9 Esri, HERE, Garmin, \xC2\xA9 OpenStreetMap contributors", 16 },
|
||||
{ "Satellite", "/sdcard/maps-sat", "/sdcard/maps-live/sat",
|
||||
"https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",
|
||||
"Imagery \xC2\xA9 Esri, Maxar, Earthstar Geographics", 19 },
|
||||
{ "Esri Topo", "/sdcard/maps-esritopo", "/sdcard/maps-live/esritopo",
|
||||
"https://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{z}/{y}/{x}",
|
||||
"\xC2\xA9 Esri, HERE, Garmin, \xC2\xA9 OpenStreetMap contributors", 19 },
|
||||
};
|
||||
static const int LAYER_COUNT = sizeof(LAYERS) / sizeof(LAYERS[0]);
|
||||
static const char* const LAYER_OPTS = "Topo\nLight\nDark\nSatellite\nEsri Topo"; // as LAYERS
|
||||
static uint8_t s_layer = 0; // the one shown (lvport::mapLayer())
|
||||
|
||||
static const MapLayer& layer(int i = -1) { return LAYERS[i < 0 || i >= LAYER_COUNT ? s_layer : i]; }
|
||||
|
||||
} // namespace mapview
|
||||
@@ -12,9 +12,10 @@
|
||||
// itself runs asynchronously in the board's fetcher (lvport::fetch*), so the
|
||||
// mesh keeps being serviced during a long download.
|
||||
//
|
||||
// The server comes from <root>/source.txt (line 1: URL template with {z} {x}
|
||||
// {y}, line 2: attribution, optional line 3: the server's last zoom level,
|
||||
// default 19 -- or 17 for OpenTopoMap); without it, OpenTopoMap. Whatever the source,
|
||||
// The server is the base layer's (MapLayers.h; setLayer()); <root>/source.txt
|
||||
// can name another (line 1: URL template with {z} {x} {y}, line 2:
|
||||
// attribution, optional line 3: the server's last zoom level, default 19 --
|
||||
// or 17 for OpenTopoMap). Whatever the source,
|
||||
// its attribution is written to <root>/attribution.txt for the map to show.
|
||||
//
|
||||
// Hiking trails (s_trails_on): each tile's trails overlay is fetched right
|
||||
@@ -37,8 +38,6 @@
|
||||
|
||||
namespace mapview {
|
||||
|
||||
static const char* const DEFAULT_TILE_URL = "https://a.tile.opentopomap.org/{z}/{x}/{y}.png";
|
||||
static const char* const DEFAULT_TILE_ATTR = "\xC2\xA9 OpenStreetMap contributors, SRTM | \xC2\xA9 OpenTopoMap (CC-BY-SA)";
|
||||
|
||||
struct TileArea {
|
||||
double lon0, lat0, lon1, lat1; // west, south, east, north
|
||||
@@ -92,6 +91,16 @@ public:
|
||||
static const int RETRY_ROUNDS = 3; // goes at the tiles that failed, after the first pass
|
||||
|
||||
explicit TileDownloader(const char* root) : _root(root) {}
|
||||
// Another base layer: its folder, its server. Not while a job runs (the caller checks).
|
||||
void setLayer(uint8_t layer) {
|
||||
if (active()) return;
|
||||
if (_live) liveEnd(); // the caller starts it again, for the new folder
|
||||
_layer_id = layer;
|
||||
_root = mapview::layer(layer).root;
|
||||
_job_checked = false; // its own unfinished job
|
||||
_lv_failed_next = 0;
|
||||
for (LiveTile& t : _lv_failed) t = LiveTile();
|
||||
}
|
||||
|
||||
State state() const { return _state; }
|
||||
const TileArea& area() const { return _area; } // of the current / last job
|
||||
@@ -232,7 +241,7 @@ public:
|
||||
if (r > 0) {
|
||||
size_t len = 0;
|
||||
const uint8_t* data = lvport::fetchData(len);
|
||||
if (!looksLikeImage(data, len)) reason = "server didn't send a PNG tile"; // an error page: passing too
|
||||
if (!looksLikeImage(data, len)) reason = "server didn't send a tile"; // an error page: passing too
|
||||
else {
|
||||
if (_layer == 1) { // trails: the overlay, or an empty marker
|
||||
if (!writeTrails(_z, _x, _y, data, len)) { lvport::fetchRelease(); finish(FAILED, "Can't write to the SD card"); return; }
|
||||
@@ -317,6 +326,7 @@ public:
|
||||
|
||||
private:
|
||||
const char* _root;
|
||||
uint8_t _layer_id = 0; // the base layer (MapLayers.h)
|
||||
State _state = IDLE;
|
||||
TileArea _area = {0, 0, 0, 0, 0, 0};
|
||||
uint32_t _total = 0, _done = 0, _skipped = 0, _failed = 0;
|
||||
@@ -416,8 +426,8 @@ private:
|
||||
_lv_fetching = false;
|
||||
size_t len = 0;
|
||||
const uint8_t* data = r > 0 ? lvport::fetchData(len) : nullptr;
|
||||
char lpath[64];
|
||||
LiveCache::tilePath(lpath, sizeof(lpath), _lv_cur.z, _lv_cur.x, _lv_cur.y);
|
||||
char lpath[80];
|
||||
LiveCache::tilePath(lpath, sizeof(lpath), _lv_cur.z, _lv_cur.x, _lv_cur.y, _layer_id);
|
||||
if (_lv_cur.layer == 1) { // trails over a tile already shown
|
||||
if (r > 0 && looksLikeImage(data, len) && writeTrails(_lv_cur.z, _lv_cur.x, _lv_cur.y, data, len)) {
|
||||
_lv_consec_fail = 0;
|
||||
@@ -426,7 +436,7 @@ private:
|
||||
liveFailed(_lv_cur);
|
||||
}
|
||||
} else if (r > 0 && looksLikeImage(data, len) && !isNoTilePicture(data, len) && writeFile(lpath, data, len)) {
|
||||
s_live_cache.add(_lv_cur.z, _lv_cur.x, _lv_cur.y, (uint32_t)len); // kept apart, within the Storage limit
|
||||
s_live_cache.add(_lv_cur.z, _lv_cur.x, _lv_cur.y, (uint32_t)len, _layer_id); // kept apart, within the Storage limit
|
||||
_lv_consec_fail = 0;
|
||||
if (_lv_done_n < LV_DONE) _lv_done[_lv_done_n++] = _lv_cur;
|
||||
} else {
|
||||
@@ -560,14 +570,15 @@ private:
|
||||
}
|
||||
|
||||
void loadSource() {
|
||||
snprintf(_url_tpl, sizeof(_url_tpl), "%s", DEFAULT_TILE_URL);
|
||||
snprintf(_attr, sizeof(_attr), "%s", DEFAULT_TILE_ATTR);
|
||||
_src_zmax = -1;
|
||||
const MapLayer& L = mapview::layer(_layer_id);
|
||||
snprintf(_url_tpl, sizeof(_url_tpl), "%s", L.url);
|
||||
snprintf(_attr, sizeof(_attr), "%s", L.attr);
|
||||
_src_zmax = L.zmax;
|
||||
char path[64];
|
||||
snprintf(path, sizeof(path), "%s/source.txt", _root);
|
||||
if (FILE* f = fopen(path, "r")) {
|
||||
char line[256];
|
||||
if (fgets(line, sizeof(line), f)) { trim(line); if (strstr(line, "{z}")) snprintf(_url_tpl, sizeof(_url_tpl), "%s", line); }
|
||||
if (fgets(line, sizeof(line), f)) { trim(line); if (strstr(line, "{z}")) { snprintf(_url_tpl, sizeof(_url_tpl), "%s", line); _src_zmax = -1; } }
|
||||
if (fgets(line, sizeof(line), f)) { trim(line); if (line[0]) snprintf(_attr, sizeof(_attr), "%s", line); }
|
||||
if (fgets(line, sizeof(line), f)) { int z = atoi(line); if (z >= 1 && z <= 22) _src_zmax = z; }
|
||||
fclose(f);
|
||||
@@ -593,9 +604,9 @@ private:
|
||||
if (FILE* f = fopen(path, "w")) { fprintf(f, "%s\n", _attr); fclose(f); }
|
||||
}
|
||||
|
||||
// PNG only for now: RasterTileProvider has no JPEG decoder yet.
|
||||
// PNG, or JPEG (the Esri layers; RasterTileProvider tells them apart).
|
||||
static bool looksLikeImage(const uint8_t* d, size_t n) {
|
||||
return d && n >= 8 && d[0] == 0x89 && d[1] == 'P' && d[2] == 'N' && d[3] == 'G';
|
||||
return d && n >= 8 && ((d[0] == 0x89 && d[1] == 'P' && d[2] == 'N' && d[3] == 'G') || isJpeg(d, n));
|
||||
}
|
||||
|
||||
// A blocked / key-required server answers every tile with the same picture:
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#define LODEPNG_NO_COMPILE_CPP
|
||||
extern "C" {
|
||||
#include "src/libs/lodepng/lodepng.h"
|
||||
#include "src/libs/tjpgd/tjpgd.h" // LVGL's TJpgDec (LV_USE_TJPGD), for JPEG tiles
|
||||
}
|
||||
|
||||
#if defined(ESP32)
|
||||
@@ -92,6 +93,41 @@ static bool pngHasInk(const uint8_t* png, size_t len) {
|
||||
return ink;
|
||||
}
|
||||
|
||||
// A JPEG tile (the Esri layers) into out[TILE_PX * TILE_PX] RGB565, with
|
||||
// LVGL's TJpgDec: RGB888 out of it a block at a time. One decode at a time
|
||||
// (the tile cache's), so one work pool.
|
||||
static bool isJpeg(const uint8_t* d, size_t n) { return n > 3 && d[0] == 0xFF && d[1] == 0xD8 && d[2] == 0xFF; }
|
||||
struct JpegSrc { const uint8_t* d; size_t n, at; uint16_t* out; };
|
||||
static size_t jpegIn(JDEC* jd, uint8_t* buf, size_t n) {
|
||||
JpegSrc* s = (JpegSrc*)jd->device;
|
||||
if (n > s->n - s->at) n = s->n - s->at;
|
||||
if (buf) memcpy(buf, s->d + s->at, n);
|
||||
s->at += n;
|
||||
return n;
|
||||
}
|
||||
static int jpegOut(JDEC* jd, void* bitmap, JRECT* r) {
|
||||
JpegSrc* s = (JpegSrc*)jd->device;
|
||||
const uint8_t* p = (const uint8_t*)bitmap;
|
||||
const int w = r->right - r->left + 1;
|
||||
for (int y = r->top; y <= r->bottom; y++) {
|
||||
if (y >= TILE_PX) { p += w * 3; continue; }
|
||||
uint16_t* o = s->out + y * TILE_PX + r->left;
|
||||
for (int x = 0; x < w; x++, p += 3)
|
||||
if (r->left + x < TILE_PX) o[x] = (uint16_t)(((p[0] & 0xF8) << 8) | ((p[1] & 0xFC) << 3) | (p[2] >> 3));
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
static bool decodeJpeg(const uint8_t* d, size_t n, uint16_t* out) {
|
||||
static const size_t POOL = 8192;
|
||||
static void* pool = nullptr;
|
||||
if (!pool && !(pool = lv_malloc(POOL))) return false;
|
||||
JDEC jd;
|
||||
JpegSrc src = { d, n, 0, out };
|
||||
if (jd_prepare(&jd, jpegIn, pool, POOL, &src) != JDR_OK) return false;
|
||||
if (jd.width != TILE_PX || jd.height != TILE_PX) return false;
|
||||
return jd_decomp(&jd, jpegOut, 0) == JDR_OK;
|
||||
}
|
||||
|
||||
class TileProvider {
|
||||
public:
|
||||
virtual ~TileProvider() {}
|
||||
@@ -111,6 +147,7 @@ public:
|
||||
// `live`: a second folder in the same layout (live tiles, LiveCache.h), read
|
||||
// when <root> has no tile.
|
||||
explicit RasterTileProvider(const char* root, const char* live = nullptr) : _root(root), _live(live) {}
|
||||
void setRoots(const char* root, const char* live) { _root = root; _live = live; } // another base layer
|
||||
|
||||
// Also reads <root>/attribution.txt (written by tools/maps/fetch_tiles.py and
|
||||
// the on-device downloader): the tile set's credit line, shown on the map.
|
||||
@@ -148,8 +185,12 @@ public:
|
||||
#endif
|
||||
|
||||
bool ok = false;
|
||||
if (isJpeg(png, len)) { // (under the .png name: MapLayers.h)
|
||||
ok = decodeJpeg(png, len, out);
|
||||
if (!ok) { lv_free(png); return false; }
|
||||
}
|
||||
#if defined(ESP32)
|
||||
ok = pngdecRgb565(png, len, out, TILE_PX); // lodepng below if it can't
|
||||
if (!ok) ok = pngdecRgb565(png, len, out, TILE_PX); // lodepng below if it can't
|
||||
#endif
|
||||
if (!ok) {
|
||||
lv_draw_buf_t* db = decodePng(png, len);
|
||||
@@ -180,7 +221,7 @@ public:
|
||||
bool overlayMissed() const override { return _overlay_missed; }
|
||||
|
||||
const char* attribution() const override {
|
||||
const char* base = _attr[0] ? _attr : "\xC2\xA9 OpenStreetMap contributors";
|
||||
const char* base = _attr[0] ? _attr : layer().attr; // nothing on the card yet: the layer's own
|
||||
if (!s_trails_on) return base;
|
||||
snprintf(_attr_full, sizeof(_attr_full), "%s | %s", base, TRAILS_ATTR);
|
||||
return _attr_full;
|
||||
|
||||
Reference in New Issue
Block a user