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:
Jakub
2026-10-14 17:25:16 +02:00
co-authored by Claude Opus 5.5
parent 85ef06b6be
commit b4ec5dcb54
12 changed files with 177 additions and 26 deletions
+7
View File
@@ -127,6 +127,13 @@ it: GPS, the trail's distance, live shares, and the distance to the target
> Downloaded areas can be renamed, refreshed or deleted.
> - **Live tiles**: with WiFi on, tiles for where you look are fetched and
> cached, up to the size set in Settings › Storage.
> - **Map** (Map tools › Layers): what the map is drawn from. **Topo**
> (OpenTopoMap, the default), **Light** and **Dark** (Esri's grey
> canvases, no labels, up to zoom 16), **Satellite** (Esri World Imagery)
> and **Esri Topo**. Each keeps its own downloaded areas and live tiles
> (`/sdcard/maps-light`, `-dark`, `-sat`, `-esritopo`; Topo stays in
> `/sdcard/maps`). The Esri layers are meant for personal use: their terms
> don't cover downloading large areas.
> - **Vector regions** (only in a build with `-D MAP_VECTOR`, unfinished):
> whole regions as packs made with `tools/maps/osm_vector.py`, copied to
> the card.
@@ -993,6 +993,9 @@ static void setLiveTiles(bool on) { nvs::putBool("mc_wifi", "live", on); }
// Map tools > Hiking trails: the Waymarked Trails overlay over the map.
static bool trailsOn() { return nvs::getBool("mc_ui", "trails", false); }
static void setTrailsOn(bool on) { nvs::putBool("mc_ui", "trails", on); }
// Map tools > Map: the base layer (map/MapLayers.h).
static int mapLayer() { return nvs::getU8("mc_ui", "layer", 0); }
static void setMapLayer(int i) { nvs::putU8("mc_ui", "layer", i); }
// Map tools > Vector map (test). Unfinished: on only in a -D MAP_VECTOR build,
// off (and its switch gone) otherwise, whatever was saved.
#ifdef MAP_VECTOR
+28 -1
View File
@@ -32,6 +32,20 @@ static TileProvider* s_provider = &s_raster; // Map tools > Vector map (t
static TileCache& s_cache = *new (psramBuf<TileCache>(1)) TileCache(); // decoded tiles, in PSRAM
static PSRAM_BSS TileDownloader s_dl("/sdcard/maps");
static PSRAM_BSS AreaStore s_areas("/sdcard/maps");
// The base layer `i` (MapLayers.h) for everything that reads or writes tiles;
// false while a download or an area's deletion works in the current one's folder.
static bool applyLayer(int i) {
if (i < 0 || i >= LAYER_COUNT) i = 0;
if (i == s_layer) return true;
if (s_dl.active() || s_areas.deleting()) return false;
s_layer = (uint8_t)i;
s_raster.setRoots(LAYERS[i].root, LAYERS[i].live);
s_dl.setLayer(s_layer);
s_areas.setRoot(LAYERS[i].root);
s_cache.invalidate();
return true;
}
static const uint32_t AVG_TILE_BYTES = 22 * 1024; // OpenTopoMap-ish, for the size estimate
// Hand a tile to the decoder (on the other core); false while one is in flight.
@@ -720,6 +734,7 @@ void UITask::openMap(bool nav) {
void UITask::showMap(bool frame) {
_screen = SCR_MAP;
mapview::s_trails_on = lvport::trailsOn();
mapview::applyLayer(lvport::mapLayer()); // (a download running in another: that one, until it's done)
mapview::s_provider = lvport::vectorOn() ? (mapview::TileProvider*)&mapview::s_vector : &mapview::s_raster;
mapLiveBegin();
mapview::s_available = lvport::mountStorage() && mapview::s_provider->available();
@@ -983,7 +998,8 @@ void UITask::layoutMap() {
(unsigned long)ld, (unsigned long)fl, (unsigned long)ln);
}
else setTextFmt(_map_zoom_lbl, "z%d", _map_z);
const char* hint = !have_provider ? "No map on the SD card.\nPut tiles in /maps (tools/maps)."
const char* hint = !have_provider ? (mapview::s_layer ? "Nothing of this map on the card -\ndownload an area or turn Live tiles on"
: "No map on the SD card.\nPut tiles in /maps (tools/maps).")
: 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;
@@ -1585,6 +1601,17 @@ void UITask::setTrails(bool on) {
else showToast("Download an area again to add its trails", 3000);
}
// Map tools > Map: another base layer, its tiles from its own folder.
void UITask::setMapLayer(int i) {
if (!mapview::applyLayer(i)) { showToast("Wait for the download to finish"); return; }
lvport::setMapLayer(i);
mapLiveBegin(); // live tiles: from the new layer's server
mapview::s_available = lvport::mountStorage() && mapview::s_provider->available();
if (_screen == SCR_MAP) layoutMap();
if (!lvport::liveTiles() || !lvport::wifiAllowed())
showToast("Shows what is downloaded for this map - or turn Live tiles on", 3000);
}
// Map tools > Vector map (test): /sdcard/vmap drawn on the device where it has data.
void UITask::setVectorMap(bool on) {
lvport::setVectorOn(on);
@@ -880,6 +880,11 @@ static void scanTrails() {
static void onSavedTrail(lv_event_t* e) { s_ui->savedTrailPopup((int)(intptr_t)lv_event_get_user_data(e)); }
static void onNavTools(lv_event_t* e) { (void)e; s_ui->navToolsPopup(); }
static void onMapLayer(lv_event_t* e) { // the map shows it at once: Map tools out of the way
int i = choiceSelected((lv_obj_t*)lv_event_get_target(e));
s_ui->navClosePopup();
s_ui->setMapLayer(i);
}
static void onTrails(lv_event_t* e) {
s_ui->setTrails(lv_obj_has_state((lv_obj_t*)lv_event_get_target(e), LV_STATE_CHECKED));
}
@@ -976,6 +981,7 @@ void UITask::navToolsPopup(bool back) {
lv_obj_add_event_cb(lsw, onLiveTiles, LV_EVENT_VALUE_CHANGED, NULL);
g = group(list, "LAYERS");
choiceRow(g, "Map", "Topo, light, dark or satellite", mapview::LAYER_OPTS, mapview::s_layer, onMapLayer);
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);
+3 -1
View File
@@ -95,6 +95,7 @@ static bool s_chats_stale = false; // Messages: a refresh waits for a held row
#include "LvglPort.h"
#include "lv_mem_psram.h"
#include "HistoryStore.h"
#include "map/MapLayers.h"
#include "map/LiveCache.h"
static histstore::SdArchive s_archive; // message history on the SD card
namespace storeview { static void stopWalk(); } // StorageScreen.h
@@ -1429,7 +1430,7 @@ extern "C" EMSCRIPTEN_KEEPALIVE int sim_scroll(int dy) {
#if defined(SIM_PLATFORM) && defined(__EMSCRIPTEN__)
// The simulator page opens a screen by name (screenshots of the layout):
// home, chats, contacts, settings, settings:<page>, nearby, map, navmap, places, clock, radio,
// gps, diag, compass, storage, repeater, wifi, bot, quick, node, channel.
// gps, diag, compass, storage, repeater, wifi, bot, quick, node, channel, layer:<n>.
extern "C" EMSCRIPTEN_KEEPALIVE int sim_ui_show(const char* name) {
if (!s_ui || !name) return 0;
UITask& u = *s_ui;
@@ -1442,6 +1443,7 @@ extern "C" EMSCRIPTEN_KEEPALIVE int sim_ui_show(const char* name) {
else if (!strcmp(name, "map")) u.openMap(false);
else if (!strcmp(name, "navmap")) u.openMap(true);
else if (!strcmp(name, "places")) { u.openMap(true); u.navTargetsPopup(); }
else if (!strncmp(name, "layer:", 6)) u.setMapLayer(atoi(name + 6)); // the map's base layer (MapLayers.h)
else if (!strcmp(name, "clock")) u.showClock();
else if (!strcmp(name, "radio")) u.showRadio();
else if (!strcmp(name, "gps")) u.showGps(false);
@@ -169,6 +169,7 @@ public:
void setLiveTiles(bool on);
void setTrails(bool on);
void setVectorMap(bool on);
void setMapLayer(int i);
void simMapAt(const char* spec); // Map tools > Live tiles
void simCompose(const char* text); // the open thread's field: this text, keyboard up
void simDm(); // the first chat contact's thread
@@ -49,6 +49,7 @@
// PNG decoder for raster map tiles: the sim's, and the fallback on the L2 behind
// PNGdec (ui-lvgl/TilePng.cpp). map/TileProvider.h calls lodepng directly.
#define LV_USE_LODEPNG 1
#define LV_USE_TJPGD 1 // map tiles: the Esri layers are JPEG (map/TileProvider.h)
// Spans: message text with @[nick] mentions picked out (UITask.cpp msgText).
#define LV_USE_SPAN 1
@@ -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;