mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-11-01 10:21:59 +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:
@@ -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.
|
> Downloaded areas can be renamed, refreshed or deleted.
|
||||||
> - **Live tiles**: with WiFi on, tiles for where you look are fetched and
|
> - **Live tiles**: with WiFi on, tiles for where you look are fetched and
|
||||||
> cached, up to the size set in Settings › Storage.
|
> 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):
|
> - **Vector regions** (only in a build with `-D MAP_VECTOR`, unfinished):
|
||||||
> whole regions as packs made with `tools/maps/osm_vector.py`, copied to
|
> whole regions as packs made with `tools/maps/osm_vector.py`, copied to
|
||||||
> the card.
|
> 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.
|
// Map tools > Hiking trails: the Waymarked Trails overlay over the map.
|
||||||
static bool trailsOn() { return nvs::getBool("mc_ui", "trails", false); }
|
static bool trailsOn() { return nvs::getBool("mc_ui", "trails", false); }
|
||||||
static void setTrailsOn(bool on) { nvs::putBool("mc_ui", "trails", on); }
|
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,
|
// Map tools > Vector map (test). Unfinished: on only in a -D MAP_VECTOR build,
|
||||||
// off (and its switch gone) otherwise, whatever was saved.
|
// off (and its switch gone) otherwise, whatever was saved.
|
||||||
#ifdef MAP_VECTOR
|
#ifdef MAP_VECTOR
|
||||||
|
|||||||
@@ -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 TileCache& s_cache = *new (psramBuf<TileCache>(1)) TileCache(); // decoded tiles, in PSRAM
|
||||||
static PSRAM_BSS TileDownloader s_dl("/sdcard/maps");
|
static PSRAM_BSS TileDownloader s_dl("/sdcard/maps");
|
||||||
static PSRAM_BSS AreaStore s_areas("/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
|
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.
|
// 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) {
|
void UITask::showMap(bool frame) {
|
||||||
_screen = SCR_MAP;
|
_screen = SCR_MAP;
|
||||||
mapview::s_trails_on = lvport::trailsOn();
|
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;
|
mapview::s_provider = lvport::vectorOn() ? (mapview::TileProvider*)&mapview::s_vector : &mapview::s_raster;
|
||||||
mapLiveBegin();
|
mapLiveBegin();
|
||||||
mapview::s_available = lvport::mountStorage() && mapview::s_provider->available();
|
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);
|
(unsigned long)ld, (unsigned long)fl, (unsigned long)ln);
|
||||||
}
|
}
|
||||||
else setTextFmt(_map_zoom_lbl, "z%d", _map_z);
|
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
|
: mapview::s_dl.liveQueued() > 0 ? nullptr // being fetched
|
||||||
: (!_map_pending && shown == 0 && missing > 0)
|
: (!_map_pending && shown == 0 && missing > 0)
|
||||||
? "No map here at this zoom -\nzoom out or download the area" : nullptr;
|
? "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);
|
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.
|
// Map tools > Vector map (test): /sdcard/vmap drawn on the device where it has data.
|
||||||
void UITask::setVectorMap(bool on) {
|
void UITask::setVectorMap(bool on) {
|
||||||
lvport::setVectorOn(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 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 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) {
|
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));
|
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);
|
lv_obj_add_event_cb(lsw, onLiveTiles, LV_EVENT_VALUE_CHANGED, NULL);
|
||||||
|
|
||||||
g = group(list, "LAYERS");
|
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);
|
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);
|
if (lvport::trailsOn()) lv_obj_add_state(tsw, LV_STATE_CHECKED);
|
||||||
lv_obj_add_event_cb(tsw, onTrails, LV_EVENT_VALUE_CHANGED, NULL);
|
lv_obj_add_event_cb(tsw, onTrails, LV_EVENT_VALUE_CHANGED, NULL);
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ static bool s_chats_stale = false; // Messages: a refresh waits for a held row
|
|||||||
#include "LvglPort.h"
|
#include "LvglPort.h"
|
||||||
#include "lv_mem_psram.h"
|
#include "lv_mem_psram.h"
|
||||||
#include "HistoryStore.h"
|
#include "HistoryStore.h"
|
||||||
|
#include "map/MapLayers.h"
|
||||||
#include "map/LiveCache.h"
|
#include "map/LiveCache.h"
|
||||||
static histstore::SdArchive s_archive; // message history on the SD card
|
static histstore::SdArchive s_archive; // message history on the SD card
|
||||||
namespace storeview { static void stopWalk(); } // StorageScreen.h
|
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__)
|
#if defined(SIM_PLATFORM) && defined(__EMSCRIPTEN__)
|
||||||
// The simulator page opens a screen by name (screenshots of the layout):
|
// The simulator page opens a screen by name (screenshots of the layout):
|
||||||
// home, chats, contacts, settings, settings:<page>, nearby, map, navmap, places, clock, radio,
|
// 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) {
|
extern "C" EMSCRIPTEN_KEEPALIVE int sim_ui_show(const char* name) {
|
||||||
if (!s_ui || !name) return 0;
|
if (!s_ui || !name) return 0;
|
||||||
UITask& u = *s_ui;
|
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, "map")) u.openMap(false);
|
||||||
else if (!strcmp(name, "navmap")) u.openMap(true);
|
else if (!strcmp(name, "navmap")) u.openMap(true);
|
||||||
else if (!strcmp(name, "places")) { u.openMap(true); u.navTargetsPopup(); }
|
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, "clock")) u.showClock();
|
||||||
else if (!strcmp(name, "radio")) u.showRadio();
|
else if (!strcmp(name, "radio")) u.showRadio();
|
||||||
else if (!strcmp(name, "gps")) u.showGps(false);
|
else if (!strcmp(name, "gps")) u.showGps(false);
|
||||||
|
|||||||
@@ -169,6 +169,7 @@ public:
|
|||||||
void setLiveTiles(bool on);
|
void setLiveTiles(bool on);
|
||||||
void setTrails(bool on);
|
void setTrails(bool on);
|
||||||
void setVectorMap(bool on);
|
void setVectorMap(bool on);
|
||||||
|
void setMapLayer(int i);
|
||||||
void simMapAt(const char* spec); // Map tools > Live tiles
|
void simMapAt(const char* spec); // Map tools > Live tiles
|
||||||
void simCompose(const char* text); // the open thread's field: this text, keyboard up
|
void simCompose(const char* text); // the open thread's field: this text, keyboard up
|
||||||
void simDm(); // the first chat contact's thread
|
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
|
// 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.
|
// PNGdec (ui-lvgl/TilePng.cpp). map/TileProvider.h calls lodepng directly.
|
||||||
#define LV_USE_LODEPNG 1
|
#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).
|
// Spans: message text with @[nick] mentions picked out (UITask.cpp msgText).
|
||||||
#define LV_USE_SPAN 1
|
#define LV_USE_SPAN 1
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ public:
|
|||||||
enum : uint8_t { F_TRAILS = 1, F_COMPLETE = 2 };
|
enum : uint8_t { F_TRAILS = 1, F_COMPLETE = 2 };
|
||||||
|
|
||||||
explicit AreaStore(const char* root) : _root(root) {}
|
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; }
|
int count() { load(); return _n; }
|
||||||
MapArea& at(int i) { load(); return _a[i]; }
|
MapArea& at(int i) { load(); return _a[i]; }
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
// Live map tiles (fetched while browsing online) live apart from downloaded
|
// 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
|
// (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
|
// 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.
|
// downloaded on purpose (TileDownloader jobs, the PC tool) are never touched.
|
||||||
@@ -34,11 +35,11 @@ public:
|
|||||||
uint32_t count() { load(); return _h.count; }
|
uint32_t count() { load(); return _h.count; }
|
||||||
bool clearing() const { return _clearing; }
|
bool clearing() const { return _clearing; }
|
||||||
|
|
||||||
// A tile was written to LIVE_ROOT: remember it (eviction follows in service()).
|
// 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) {
|
void add(int z, int x, int y, uint32_t size, uint8_t layer) {
|
||||||
load();
|
load();
|
||||||
if (_h.count >= RING) evictOne(); // the ring is full: make room right away
|
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;
|
uint32_t slot = (_h.head + _h.count) % RING;
|
||||||
_h.count++;
|
_h.count++;
|
||||||
_h.bytes += size;
|
_h.bytes += size;
|
||||||
@@ -63,8 +64,8 @@ public:
|
|||||||
if (_clearing && _h.count == 0) { _clearing = false; _h.bytes = 0; writeHdr(); }
|
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) {
|
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", LIVE_ROOT, z, x, y);
|
snprintf(out, n, "%s/%d/%d/%d.png", mapview::layer(layer).live, z, x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -124,7 +125,7 @@ private:
|
|||||||
Rec r;
|
Rec r;
|
||||||
if (readRec(_h.head, r)) {
|
if (readRec(_h.head, r)) {
|
||||||
char path[128];
|
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);
|
remove(path);
|
||||||
_h.bytes = _h.bytes > r.size ? _h.bytes - r.size : 0;
|
_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
|
// itself runs asynchronously in the board's fetcher (lvport::fetch*), so the
|
||||||
// mesh keeps being serviced during a long download.
|
// mesh keeps being serviced during a long download.
|
||||||
//
|
//
|
||||||
// The server comes from <root>/source.txt (line 1: URL template with {z} {x}
|
// The server is the base layer's (MapLayers.h; setLayer()); <root>/source.txt
|
||||||
// {y}, line 2: attribution, optional line 3: the server's last zoom level,
|
// can name another (line 1: URL template with {z} {x} {y}, line 2:
|
||||||
// default 19 -- or 17 for OpenTopoMap); without it, OpenTopoMap. Whatever the source,
|
// 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.
|
// 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
|
// Hiking trails (s_trails_on): each tile's trails overlay is fetched right
|
||||||
@@ -37,8 +38,6 @@
|
|||||||
|
|
||||||
namespace mapview {
|
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 {
|
struct TileArea {
|
||||||
double lon0, lat0, lon1, lat1; // west, south, east, north
|
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
|
static const int RETRY_ROUNDS = 3; // goes at the tiles that failed, after the first pass
|
||||||
|
|
||||||
explicit TileDownloader(const char* root) : _root(root) {}
|
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; }
|
State state() const { return _state; }
|
||||||
const TileArea& area() const { return _area; } // of the current / last job
|
const TileArea& area() const { return _area; } // of the current / last job
|
||||||
@@ -232,7 +241,7 @@ public:
|
|||||||
if (r > 0) {
|
if (r > 0) {
|
||||||
size_t len = 0;
|
size_t len = 0;
|
||||||
const uint8_t* data = lvport::fetchData(len);
|
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 {
|
else {
|
||||||
if (_layer == 1) { // trails: the overlay, or an empty marker
|
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; }
|
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:
|
private:
|
||||||
const char* _root;
|
const char* _root;
|
||||||
|
uint8_t _layer_id = 0; // the base layer (MapLayers.h)
|
||||||
State _state = IDLE;
|
State _state = IDLE;
|
||||||
TileArea _area = {0, 0, 0, 0, 0, 0};
|
TileArea _area = {0, 0, 0, 0, 0, 0};
|
||||||
uint32_t _total = 0, _done = 0, _skipped = 0, _failed = 0;
|
uint32_t _total = 0, _done = 0, _skipped = 0, _failed = 0;
|
||||||
@@ -416,8 +426,8 @@ private:
|
|||||||
_lv_fetching = false;
|
_lv_fetching = false;
|
||||||
size_t len = 0;
|
size_t len = 0;
|
||||||
const uint8_t* data = r > 0 ? lvport::fetchData(len) : nullptr;
|
const uint8_t* data = r > 0 ? lvport::fetchData(len) : nullptr;
|
||||||
char lpath[64];
|
char lpath[80];
|
||||||
LiveCache::tilePath(lpath, sizeof(lpath), _lv_cur.z, _lv_cur.x, _lv_cur.y);
|
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 (_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)) {
|
if (r > 0 && looksLikeImage(data, len) && writeTrails(_lv_cur.z, _lv_cur.x, _lv_cur.y, data, len)) {
|
||||||
_lv_consec_fail = 0;
|
_lv_consec_fail = 0;
|
||||||
@@ -426,7 +436,7 @@ private:
|
|||||||
liveFailed(_lv_cur);
|
liveFailed(_lv_cur);
|
||||||
}
|
}
|
||||||
} else if (r > 0 && looksLikeImage(data, len) && !isNoTilePicture(data, len) && writeFile(lpath, data, len)) {
|
} 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;
|
_lv_consec_fail = 0;
|
||||||
if (_lv_done_n < LV_DONE) _lv_done[_lv_done_n++] = _lv_cur;
|
if (_lv_done_n < LV_DONE) _lv_done[_lv_done_n++] = _lv_cur;
|
||||||
} else {
|
} else {
|
||||||
@@ -560,14 +570,15 @@ private:
|
|||||||
}
|
}
|
||||||
|
|
||||||
void loadSource() {
|
void loadSource() {
|
||||||
snprintf(_url_tpl, sizeof(_url_tpl), "%s", DEFAULT_TILE_URL);
|
const MapLayer& L = mapview::layer(_layer_id);
|
||||||
snprintf(_attr, sizeof(_attr), "%s", DEFAULT_TILE_ATTR);
|
snprintf(_url_tpl, sizeof(_url_tpl), "%s", L.url);
|
||||||
_src_zmax = -1;
|
snprintf(_attr, sizeof(_attr), "%s", L.attr);
|
||||||
|
_src_zmax = L.zmax;
|
||||||
char path[64];
|
char path[64];
|
||||||
snprintf(path, sizeof(path), "%s/source.txt", _root);
|
snprintf(path, sizeof(path), "%s/source.txt", _root);
|
||||||
if (FILE* f = fopen(path, "r")) {
|
if (FILE* f = fopen(path, "r")) {
|
||||||
char line[256];
|
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)) { 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; }
|
if (fgets(line, sizeof(line), f)) { int z = atoi(line); if (z >= 1 && z <= 22) _src_zmax = z; }
|
||||||
fclose(f);
|
fclose(f);
|
||||||
@@ -593,9 +604,9 @@ private:
|
|||||||
if (FILE* f = fopen(path, "w")) { fprintf(f, "%s\n", _attr); fclose(f); }
|
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) {
|
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:
|
// A blocked / key-required server answers every tile with the same picture:
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
#define LODEPNG_NO_COMPILE_CPP
|
#define LODEPNG_NO_COMPILE_CPP
|
||||||
extern "C" {
|
extern "C" {
|
||||||
#include "src/libs/lodepng/lodepng.h"
|
#include "src/libs/lodepng/lodepng.h"
|
||||||
|
#include "src/libs/tjpgd/tjpgd.h" // LVGL's TJpgDec (LV_USE_TJPGD), for JPEG tiles
|
||||||
}
|
}
|
||||||
|
|
||||||
#if defined(ESP32)
|
#if defined(ESP32)
|
||||||
@@ -92,6 +93,41 @@ static bool pngHasInk(const uint8_t* png, size_t len) {
|
|||||||
return ink;
|
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 {
|
class TileProvider {
|
||||||
public:
|
public:
|
||||||
virtual ~TileProvider() {}
|
virtual ~TileProvider() {}
|
||||||
@@ -111,6 +147,7 @@ public:
|
|||||||
// `live`: a second folder in the same layout (live tiles, LiveCache.h), read
|
// `live`: a second folder in the same layout (live tiles, LiveCache.h), read
|
||||||
// when <root> has no tile.
|
// when <root> has no tile.
|
||||||
explicit RasterTileProvider(const char* root, const char* live = nullptr) : _root(root), _live(live) {}
|
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
|
// 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.
|
// the on-device downloader): the tile set's credit line, shown on the map.
|
||||||
@@ -148,8 +185,12 @@ public:
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
bool ok = false;
|
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)
|
#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
|
#endif
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
lv_draw_buf_t* db = decodePng(png, len);
|
lv_draw_buf_t* db = decodePng(png, len);
|
||||||
@@ -180,7 +221,7 @@ public:
|
|||||||
bool overlayMissed() const override { return _overlay_missed; }
|
bool overlayMissed() const override { return _overlay_missed; }
|
||||||
|
|
||||||
const char* attribution() const override {
|
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;
|
if (!s_trails_on) return base;
|
||||||
snprintf(_attr_full, sizeof(_attr_full), "%s | %s", base, TRAILS_ATTR);
|
snprintf(_attr_full, sizeof(_attr_full), "%s | %s", base, TRAILS_ATTR);
|
||||||
return _attr_full;
|
return _attr_full;
|
||||||
|
|||||||
Reference in New Issue
Block a user