mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-10-09 03:06:39 +00:00
feat(ui-lvgl): hiking trails overlay on the map
Waymarked Trails' transparent hiking layer, kept in /sdcard/maps-trails and blended over the base tile at render time (Map tools > Hiking trails, NVS). An area download fetches each tile's trails after its base tile, so re-running an area on the card adds just the trails; with live tiles on, a shown tile missing its trails has them fetched. Tiles without a trail are stored as 0-byte files (no cluster on FAT). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -397,6 +397,7 @@ void UITask::buildHomeMap(lv_obj_t* box) {
|
||||
lv_obj_set_style_pad_ver(s_caption, 2, 0);
|
||||
lv_obj_align(s_caption, LV_ALIGN_BOTTOM_LEFT, 6, -6);
|
||||
mapview::s_available = lvport::mountStorage() && mapview::s_provider->available();
|
||||
mapview::s_trails_on = lvport::trailsOn();
|
||||
lv_obj_update_layout(s_area);
|
||||
homeMapFit();
|
||||
homeMapLayout();
|
||||
|
||||
@@ -185,6 +185,9 @@ static void setWifiAllowed(bool on) { s_wifi_allowed = on; nvs::putBool("mc_wifi
|
||||
// Map tools > Live tiles: missing tiles fetched over WiFi while the map is open.
|
||||
static bool liveTiles() { return nvs::getBool("mc_wifi", "live", true); }
|
||||
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); }
|
||||
|
||||
// Screen-lock PIN (Settings > Display & power > Screen PIN): digits, "" = none.
|
||||
static void loadPin(char* out, size_t n) { nvs::getStr("mc_lock", "pin", out, n); }
|
||||
@@ -516,6 +519,9 @@ static void setWifiAllowed(bool on) { s_wifi_on = on; }
|
||||
static bool s_live_tiles = true;
|
||||
static bool liveTiles() { return s_live_tiles; }
|
||||
static void setLiveTiles(bool on) { s_live_tiles = on; }
|
||||
static bool s_trails = false;
|
||||
static bool trailsOn() { return s_trails; }
|
||||
static void setTrailsOn(bool on) { s_trails = on; }
|
||||
static bool s_net_on = false;
|
||||
static void netBegin(const char*, const char*) { s_net_on = true; }
|
||||
static int netState() { return NET_UP; }
|
||||
|
||||
@@ -28,6 +28,14 @@ static TileCache& s_cache = *new (psramBuf<TileCache>(1)) TileCache();
|
||||
static TileDownloader s_dl("/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
|
||||
// its trails overlay has the overlay fetched.
|
||||
static TileCache::Slot* loadTile(int z, int x, int y) {
|
||||
TileCache::Slot* s = s_cache.load(*s_provider, z, x, y);
|
||||
if (s_trails_on && s_provider == &s_raster && s_raster.overlayMissed()) s_dl.liveRequest(z, x, y, true);
|
||||
return s;
|
||||
}
|
||||
|
||||
static const int MIN_Z = 3, MAX_Z = 18;
|
||||
static const int GRID_COLS = 3, GRID_ROWS = 2; // covers 320x218 at any offset
|
||||
|
||||
@@ -110,7 +118,7 @@ static bool prefetchOne(double left, double top, int w, int h, int z) {
|
||||
}
|
||||
}
|
||||
if (best >= 1e18) return false;
|
||||
s_cache.load(*s_provider, z, bx, by);
|
||||
loadTile(z, bx, by);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -198,6 +206,7 @@ void UITask::openMap(bool nav) {
|
||||
|
||||
void UITask::showMap() {
|
||||
_screen = SCR_MAP;
|
||||
mapview::s_trails_on = lvport::trailsOn();
|
||||
mapLiveBegin();
|
||||
mapview::s_available = lvport::mountStorage() && mapview::s_provider->available();
|
||||
if (_map_z == 0) { // first open: own position, else the node's own advert position, else Poland
|
||||
@@ -436,7 +445,7 @@ void UITask::mapLoop() {
|
||||
}
|
||||
}
|
||||
if (best_d < 1e18) {
|
||||
mapview::s_cache.load(*mapview::s_provider, _map_z, best_x, best_y);
|
||||
mapview::loadTile(_map_z, best_x, best_y);
|
||||
} else { // every tile at this zoom looked up: decode a coarser one for a missing tile
|
||||
best_d = 1e18;
|
||||
int bz = -1;
|
||||
@@ -455,7 +464,7 @@ void UITask::mapLoop() {
|
||||
if (d < best_d) { best_d = d; bz = wz; best_x = ax; best_y = ay; }
|
||||
}
|
||||
}
|
||||
if (bz >= 0) mapview::s_cache.load(*mapview::s_provider, bz, best_x, best_y);
|
||||
if (bz >= 0) mapview::loadTile(bz, best_x, best_y);
|
||||
}
|
||||
layoutMap();
|
||||
}
|
||||
@@ -797,6 +806,17 @@ void UITask::mapLiveBegin() {
|
||||
mapview::s_dl.liveBegin(ssid, pass);
|
||||
}
|
||||
|
||||
// Map tools > Hiking trails: the overlay on / off, every decoded tile redrawn.
|
||||
void UITask::setTrails(bool on) {
|
||||
lvport::setTrailsOn(on);
|
||||
mapview::s_trails_on = on;
|
||||
mapview::s_cache.invalidate();
|
||||
if (_screen == SCR_MAP) layoutMap();
|
||||
if (!on) showToast("Hiking trails off");
|
||||
else if (lvport::liveTiles() && lvport::wifiAllowed()) showToast("Trails load with the map over WiFi", 3000);
|
||||
else showToast("Download an area again to add its trails", 3000);
|
||||
}
|
||||
|
||||
void UITask::setLiveTiles(bool on) {
|
||||
lvport::setLiveTiles(on);
|
||||
if (on) {
|
||||
@@ -820,7 +840,7 @@ void UITask::mapDownloadTick() {
|
||||
if (dl.liveOn()) {
|
||||
int z, x, y;
|
||||
bool got = false;
|
||||
while (dl.liveTake(z, x, y)) { mapview::s_cache.forgetMissing(z, x, y); got = true; }
|
||||
while (dl.liveTake(z, x, y)) { mapview::s_cache.drop(z, x, y); got = true; } // a new tile, or its trails
|
||||
if (got && _screen == SCR_MAP) layoutMap();
|
||||
// The WiFi goes 30 s after the map was left or the screen went off (a
|
||||
// quick look elsewhere keeps it).
|
||||
|
||||
@@ -873,6 +873,9 @@ 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 onTrails(lv_event_t* e) {
|
||||
s_ui->setTrails(lv_obj_has_state((lv_obj_t*)lv_event_get_target(e), LV_STATE_CHECKED));
|
||||
}
|
||||
static void onLiveTiles(lv_event_t* e) {
|
||||
s_ui->setLiveTiles(lv_obj_has_state((lv_obj_t*)lv_event_get_target(e), LV_STATE_CHECKED));
|
||||
}
|
||||
@@ -967,6 +970,9 @@ void UITask::navToolsPopup() {
|
||||
lv_obj_t* lsw = switchRow(g, "Live tiles", "Load missing tiles over WiFi", nullptr);
|
||||
if (lvport::liveTiles()) lv_obj_add_state(lsw, LV_STATE_CHECKED);
|
||||
lv_obj_add_event_cb(lsw, onLiveTiles, LV_EVENT_VALUE_CHANGED, NULL);
|
||||
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, "Map options", "Trail, live sharing, arrival alert", onNavTool, (void*)(uintptr_t)navmap::TL_OPTIONS);
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ static void startWalk() {
|
||||
|
||||
// What a top-level folder counts as.
|
||||
static uint8_t topCat(const char* name) {
|
||||
if (!strcmp(name, "maps")) return C_MAPS;
|
||||
if (!strcmp(name, "maps") || !strcmp(name, "maps-trails")) return C_MAPS; // trails: the maps' overlay
|
||||
if (!strcmp(name, "maps-live")) return C_LIVE;
|
||||
if (!strcmp(name, "meshcore")) return C_MSGS;
|
||||
if (!strcmp(name, "trails")) return C_TRAILS;
|
||||
|
||||
@@ -73,7 +73,8 @@ public:
|
||||
void mapDownloadResume();
|
||||
void mapDownloadDiscard();
|
||||
void mapDownloadZmax(int delta);
|
||||
void setLiveTiles(bool on); // Map tools > Live tiles
|
||||
void setLiveTiles(bool on);
|
||||
void setTrails(bool on); // Map tools > Live tiles
|
||||
// Navigation map (NavMap.h)
|
||||
void navTargetsPopup();
|
||||
void navClosePopup();
|
||||
|
||||
@@ -78,6 +78,12 @@ public:
|
||||
for (Missing& m : _missing) if (m.z == z && m.x == x && m.y == y) m.z = -1;
|
||||
}
|
||||
|
||||
// Forget one tile, decoded or missing (it changed on the card: redrawn next time).
|
||||
void drop(int z, int x, int y) {
|
||||
for (Slot& s : _slots) if (s.z == z && s.x == x && s.y == y) { s.z = -1; s.used = 0; }
|
||||
forgetMissing(z, x, y);
|
||||
}
|
||||
|
||||
// Forget everything (e.g. the card was swapped); keeps the buffers.
|
||||
void invalidate() { for (Slot& s : _slots) { s.z = -1; s.used = 0; } forgetMissing(); }
|
||||
|
||||
|
||||
@@ -17,6 +17,11 @@
|
||||
// default 19 -- or 17 for OpenTopoMap); without it, 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
|
||||
// after its base tile, into TRAILS_ROOT (TileProvider.h) -- empty ones as
|
||||
// 0-byte files, so re-running an area already on the card adds just the
|
||||
// trails.
|
||||
//
|
||||
// Live tiles: while the map is open, single tiles it is missing are fetched
|
||||
// the same way (one at a time, between the job's steps when there is no job),
|
||||
// the WiFi connected on the first miss and dropped by the caller (liveEnd)
|
||||
@@ -113,12 +118,19 @@ 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;
|
||||
_layer = 0;
|
||||
_total = countTiles(_area);
|
||||
if (_trails && _area.zmin <= TRAILS_MAX_Z) {
|
||||
TileArea t = _area;
|
||||
if (t.zmax > TRAILS_MAX_Z) t.zmax = TRAILS_MAX_Z;
|
||||
_total += countTiles(t);
|
||||
}
|
||||
_done = _skipped = _failed = _consec_fail = 0;
|
||||
_retried = false;
|
||||
_placeholder_hash = 0; _placeholder_hits = 0;
|
||||
_msg[0] = '\0';
|
||||
if (_total == 0 || _total > MAX_TILES) { fail("Area too large: use the PC tool"); return false; }
|
||||
if (_total == 0 || _total > (_trails ? 2 : 1) * MAX_TILES) { fail("Area too large: use the PC tool"); return false; }
|
||||
writeAttribution();
|
||||
writeJob(_area);
|
||||
_z = _area.zmin;
|
||||
@@ -157,14 +169,17 @@ public:
|
||||
bool liveConnecting() const { return _live && _lv_state == LV_CONNECTING; }
|
||||
int liveQueued() const { return _live ? _lv_n + (_lv_fetching ? 1 : 0) : 0; }
|
||||
// A tile the map is missing (duplicates, recent failures and a full queue ignored).
|
||||
void liveRequest(int z, int x, int y) {
|
||||
if (!_live || active() || z > _src_zmax) return; // past the server's zoom: the map magnifies
|
||||
if (_lv_fetching && _lv_cur.z == z && _lv_cur.x == x && _lv_cur.y == y) return;
|
||||
for (int i = 0; i < _lv_n; i++) if (_lv_q[i].z == z && _lv_q[i].x == x && _lv_q[i].y == y) return;
|
||||
for (const LiveTile& f : _lv_failed) if (f.z == z && f.x == x && f.y == y) return;
|
||||
// `trails`: the tile's trails overlay (its base is on the card already).
|
||||
void liveRequest(int z, int x, int y, bool trails = false) {
|
||||
if (!_live || active() || z > (trails ? TRAILS_MAX_Z : _src_zmax)) return; // past the server's zoom: the map magnifies
|
||||
uint8_t layer = trails ? 1 : 0;
|
||||
auto same = [&](const LiveTile& t) { return t.z == z && t.x == x && t.y == y && t.layer == layer; };
|
||||
if (_lv_fetching && same(_lv_cur)) return;
|
||||
for (int i = 0; i < _lv_n; i++) if (same(_lv_q[i])) return;
|
||||
for (const LiveTile& f : _lv_failed) if (same(f)) return;
|
||||
if (_lv_n >= LV_QUEUE) return;
|
||||
LiveTile& t = _lv_q[_lv_n++];
|
||||
t.z = (int16_t)z; t.x = x; t.y = y;
|
||||
t.z = (int16_t)z; t.x = x; t.y = y; t.layer = layer;
|
||||
}
|
||||
// Next tile written since the last call (the map forgets it was missing).
|
||||
bool liveTake(int& z, int& x, int& y) {
|
||||
@@ -199,6 +214,10 @@ public:
|
||||
size_t len = 0;
|
||||
const uint8_t* data = lvport::fetchData(len);
|
||||
if (!looksLikeImage(data, len)) onFailure("Server didn't send a PNG tile");
|
||||
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; }
|
||||
_done++; _consec_fail = 0;
|
||||
}
|
||||
else if (isNoTilePicture(data, len)) { _skipped++; _consec_fail = 0; } // "no tile here": nothing to keep
|
||||
else if (isPlaceholder(data, len)) { lvport::fetchRelease(); finish(FAILED, "Server sends a placeholder (blocked / key?)"); return; }
|
||||
else if (!writeTile(_z, _x, _y, data, len)) { lvport::fetchRelease(); finish(FAILED, "Can't write to the SD card"); return; }
|
||||
@@ -224,12 +243,12 @@ public:
|
||||
for (int i = 0; i < 16 && _state == RUNNING; i++) {
|
||||
if (_z > _area.zmax) { finish(DONE, "Done"); return; }
|
||||
char path[96];
|
||||
tilePath(path, sizeof(path), _z, _x, _y);
|
||||
tilePath(path, sizeof(path), _z, _x, _y, _layer);
|
||||
struct stat st;
|
||||
if (stat(path, &st) == 0 && st.st_size > 0) { _skipped++; advance(); continue; }
|
||||
if (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);
|
||||
buildUrl(url, sizeof(url), _z, _x, _y, _layer);
|
||||
int r = lvport::fetchStart(url);
|
||||
if (r > 0) { _fetching = true; _last_start = millis(); }
|
||||
else if (r < 0) finish(FAILED, "Out of memory (download task)");
|
||||
@@ -245,6 +264,8 @@ private:
|
||||
uint8_t _consec_fail = 0;
|
||||
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
|
||||
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;
|
||||
uint32_t _placeholder_hash = 0;
|
||||
@@ -258,7 +279,7 @@ private:
|
||||
bool _has_job = false, _job_checked = false;
|
||||
|
||||
// Live tiles
|
||||
struct LiveTile { int16_t z = -1; int32_t x = 0, y = 0; };
|
||||
struct LiveTile { int16_t z = -1; uint8_t layer = 0; int32_t x = 0, y = 0; }; // layer: 0 base, 1 trails
|
||||
enum LiveState : uint8_t { LV_IDLE, LV_CONNECTING, LV_UP, LV_BACKOFF };
|
||||
static const int LV_QUEUE = 8, LV_FAILED = 16, LV_DONE = 8;
|
||||
bool _live = false;
|
||||
@@ -320,7 +341,14 @@ private:
|
||||
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);
|
||||
if (r > 0 && looksLikeImage(data, len) && !isNoTilePicture(data, len) && writeFile(lpath, data, len)) {
|
||||
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;
|
||||
if (_lv_done_n < LV_DONE) _lv_done[_lv_done_n++] = _lv_cur;
|
||||
} else {
|
||||
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
|
||||
_lv_consec_fail = 0;
|
||||
if (_lv_done_n < LV_DONE) _lv_done[_lv_done_n++] = _lv_cur;
|
||||
@@ -335,7 +363,7 @@ private:
|
||||
for (int i = 1; i < _lv_n; i++) _lv_q[i - 1] = _lv_q[i];
|
||||
_lv_n--;
|
||||
char url[200];
|
||||
buildUrl(url, sizeof(url), _lv_cur.z, _lv_cur.x, _lv_cur.y);
|
||||
buildUrl(url, sizeof(url), _lv_cur.z, _lv_cur.x, _lv_cur.y, _lv_cur.layer);
|
||||
if (lvport::fetchStart(url) > 0) { _lv_fetching = true; _last_start = millis(); }
|
||||
else _lv_q[_lv_n++] = _lv_cur; // fetcher busy: back in the queue
|
||||
}
|
||||
@@ -401,6 +429,8 @@ private:
|
||||
|
||||
void advance() {
|
||||
_retried = false;
|
||||
if (_layer == 0 && _trails && _z <= TRAILS_MAX_Z) { _layer = 1; return; } // this tile's trails next
|
||||
_layer = 0;
|
||||
if (++_y <= _y1) return;
|
||||
_y = _y0;
|
||||
if (++_x <= _x1) return;
|
||||
@@ -409,13 +439,13 @@ private:
|
||||
_x = _x0; _y = _y0;
|
||||
}
|
||||
|
||||
void tilePath(char* out, size_t n, int z, int x, int y) const {
|
||||
snprintf(out, n, "%s/%d/%d/%d.png", _root, z, x, y);
|
||||
void tilePath(char* out, size_t n, int z, int x, int y, uint8_t layer = 0) const {
|
||||
snprintf(out, n, "%s/%d/%d/%d.png", layer ? TRAILS_ROOT : _root, z, x, y);
|
||||
}
|
||||
|
||||
void buildUrl(char* out, size_t n, int z, int x, int y) const {
|
||||
void buildUrl(char* out, size_t n, int z, int x, int y, uint8_t layer = 0) const {
|
||||
size_t o = 0;
|
||||
for (const char* p = _url_tpl; *p && o + 12 < n; p++) {
|
||||
for (const char* p = layer ? TRAILS_URL : _url_tpl; *p && o + 12 < n; p++) {
|
||||
if (p[0] == '{' && p[1] && p[2] == '}') {
|
||||
int v = p[1] == 'z' ? z : p[1] == 'x' ? x : p[1] == 'y' ? y : -1;
|
||||
if (v >= 0) { o += snprintf(out + o, n - o, "%d", v); p += 2; continue; }
|
||||
@@ -479,6 +509,13 @@ private:
|
||||
return false;
|
||||
}
|
||||
|
||||
// A trails tile: as fetched when something is drawn on it, else 0 bytes.
|
||||
bool writeTrails(int z, int x, int y, const uint8_t* d, size_t n) {
|
||||
char path[96];
|
||||
tilePath(path, sizeof(path), z, x, y, 1);
|
||||
return pngHasInk(d, n) ? writeFile(path, d, n) : writeFile(path, d, 0);
|
||||
}
|
||||
|
||||
bool writeTile(int z, int x, int y, const uint8_t* d, size_t n) {
|
||||
char path[96];
|
||||
tilePath(path, sizeof(path), z, x, y);
|
||||
|
||||
@@ -29,9 +29,42 @@ static bool isNoTilePicture(const uint8_t* d, size_t n) {
|
||||
return h == OTM_NOTILE_FNV;
|
||||
}
|
||||
|
||||
|
||||
static const int TILE_PX = 256;
|
||||
|
||||
// Hiking trails: Waymarked Trails' transparent overlay (marked routes in their
|
||||
// waymark colours), kept apart from the base map under TRAILS_ROOT in the same
|
||||
// {z}/{x}/{y}.png layout, drawn over whatever base tile is shown while
|
||||
// s_trails_on. A 0-byte file there means "fetched, no trail on this tile" --
|
||||
// most tiles, and an empty file takes no cluster on a FAT card.
|
||||
static const char* const TRAILS_ROOT = "/sdcard/maps-trails";
|
||||
static const char* const TRAILS_URL = "https://tile.waymarkedtrails.org/hiking/{z}/{x}/{y}.png";
|
||||
static const char* const TRAILS_ATTR = "Trails \xC2\xA9 waymarkedtrails.org (CC-BY-SA)";
|
||||
static const int TRAILS_MAX_Z = 18;
|
||||
static bool s_trails_on = false; // Map tools > Hiking trails (NVS), set when the map opens
|
||||
|
||||
// PNG -> RGBA (LVGL's lodepng: the result is an lv_draw_buf_t*, destroy it).
|
||||
static lv_draw_buf_t* decodePng(const uint8_t* png, size_t len) {
|
||||
unsigned char* res = nullptr;
|
||||
unsigned w = 0, h = 0;
|
||||
unsigned err = lodepng_decode32(&res, &w, &h, png, len);
|
||||
lv_draw_buf_t* db = (lv_draw_buf_t*)res;
|
||||
if (err || !db || w != TILE_PX || h != TILE_PX) { if (db) lv_draw_buf_destroy(db); return nullptr; }
|
||||
return db;
|
||||
}
|
||||
|
||||
// An overlay tile with anything drawn on it (not fully transparent).
|
||||
static bool pngHasInk(const uint8_t* png, size_t len) {
|
||||
lv_draw_buf_t* db = decodePng(png, len);
|
||||
if (!db) return false;
|
||||
bool ink = false;
|
||||
for (int row = 0; row < TILE_PX && !ink; row++) {
|
||||
const uint8_t* p = db->data + row * db->header.stride + 3;
|
||||
for (int i = 0; i < TILE_PX; i++, p += 4) if (*p) { ink = true; break; }
|
||||
}
|
||||
lv_draw_buf_destroy(db);
|
||||
return ink;
|
||||
}
|
||||
|
||||
class TileProvider {
|
||||
public:
|
||||
virtual ~TileProvider() {}
|
||||
@@ -69,19 +102,16 @@ public:
|
||||
}
|
||||
|
||||
bool renderTile(int z, int x, int y, uint16_t* out) override {
|
||||
_overlay_missed = false;
|
||||
uint32_t len = 0;
|
||||
uint8_t* png = readLoose(_root, z, x, y, len);
|
||||
if (!png) png = readPacked(z, x, y, len);
|
||||
if (!png && _live) png = readLoose(_live, z, x, y, len);
|
||||
if (!png) return false;
|
||||
|
||||
unsigned char* res = nullptr;
|
||||
unsigned w = 0, h = 0;
|
||||
unsigned err = lodepng_decode32(&res, &w, &h, png, len);
|
||||
lv_draw_buf_t* db = decodePng(png, len);
|
||||
lv_free(png);
|
||||
lv_draw_buf_t* db = (lv_draw_buf_t*)res;
|
||||
if (err || !db) { if (db) lv_draw_buf_destroy(db); return false; }
|
||||
if (w != TILE_PX || h != TILE_PX) { lv_draw_buf_destroy(db); return false; }
|
||||
if (!db) return false;
|
||||
|
||||
for (int row = 0; row < TILE_PX; row++) {
|
||||
const uint8_t* p = db->data + row * db->header.stride; // R, G, B, A
|
||||
@@ -90,15 +120,56 @@ public:
|
||||
o[i] = (uint16_t)(((p[0] & 0xF8) << 8) | ((p[1] & 0xFC) << 3) | (p[2] >> 3));
|
||||
}
|
||||
lv_draw_buf_destroy(db);
|
||||
if (s_trails_on && z <= TRAILS_MAX_Z) drawOverlay(z, x, y, out);
|
||||
return true;
|
||||
}
|
||||
|
||||
const char* attribution() const override { return _attr[0] ? _attr : "\xC2\xA9 OpenStreetMap contributors"; }
|
||||
// The last renderTile() found no trails file for its tile (not fetched yet).
|
||||
bool overlayMissed() const { return _overlay_missed; }
|
||||
|
||||
const char* attribution() const override {
|
||||
const char* base = _attr[0] ? _attr : "\xC2\xA9 OpenStreetMap contributors";
|
||||
if (!s_trails_on) return base;
|
||||
snprintf(_attr_full, sizeof(_attr_full), "%s | %s", base, TRAILS_ATTR);
|
||||
return _attr_full;
|
||||
}
|
||||
|
||||
private:
|
||||
const char* _root;
|
||||
const char* _live;
|
||||
char _attr[96] = "";
|
||||
mutable char _attr_full[160] = "";
|
||||
bool _overlay_missed = false;
|
||||
|
||||
// The trails tile over `out`, blended by its alpha.
|
||||
void drawOverlay(int z, int x, int y, uint16_t* out) {
|
||||
char path[64];
|
||||
snprintf(path, sizeof(path), "%s/%d/%d/%d.png", TRAILS_ROOT, z, x, y);
|
||||
FILE* f = fopen(path, "rb");
|
||||
if (!f) { _overlay_missed = true; return; }
|
||||
fseek(f, 0, SEEK_END);
|
||||
long sz = ftell(f);
|
||||
uint8_t* png = sz > 0 ? readRange(f, 0, (uint32_t)sz) : nullptr; // 0 bytes: no trail here
|
||||
fclose(f);
|
||||
if (!png) return;
|
||||
lv_draw_buf_t* db = decodePng(png, (size_t)sz);
|
||||
lv_free(png);
|
||||
if (!db) return;
|
||||
for (int row = 0; row < TILE_PX; row++) {
|
||||
const uint8_t* p = db->data + row * db->header.stride;
|
||||
uint16_t* o = out + row * TILE_PX;
|
||||
for (int i = 0; i < TILE_PX; i++, p += 4) {
|
||||
unsigned a = p[3];
|
||||
if (!a) continue;
|
||||
unsigned r = (o[i] >> 8) & 0xF8, g = (o[i] >> 3) & 0xFC, b = (o[i] << 3) & 0xF8;
|
||||
r = (p[0] * a + r * (255 - a)) / 255;
|
||||
g = (p[1] * a + g * (255 - a)) / 255;
|
||||
b = (p[2] * a + b * (255 - a)) / 255;
|
||||
o[i] = (uint16_t)(((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3));
|
||||
}
|
||||
}
|
||||
lv_draw_buf_destroy(db);
|
||||
}
|
||||
|
||||
static uint8_t* readRange(FILE* f, long off, uint32_t len) {
|
||||
if (len == 0 || len > 512 * 1024) return nullptr;
|
||||
|
||||
Reference in New Issue
Block a user