mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-10-09 11:16:39 +00:00
feat(ui-lvgl): vector map spike (own VT2 format from OSM)
tools/maps/osm_vector.py turns Overpass JSON into VT2 tiles (data zooms 10/12/14, per-feature bbox, simplified); VectorTileProvider draws them into 256 px tiles (scanline polygons, thick lines with round joins) with a small PSRAM cache of data files. Behind Map tools > Vector map (test), raster where there is no vector data. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -254,8 +254,14 @@ Ideas to come back to (2026-09-26):
|
||||
- [x] A grid under the map (and the minimap), its step a round distance in
|
||||
the metric / imperial unit, with a scale bar: position and scale where
|
||||
no tile is loaded.
|
||||
- [ ] Vector maps: spike first (own compact format from OSM, roads / trails
|
||||
/ water / forest, no labels; measure render time).
|
||||
- [x] Vector maps spike: own VT2 format from OSM (tools/maps/osm_vector.py,
|
||||
Overpass JSON -> /sdcard/vmap, data zooms 10/12/14, bbox per feature),
|
||||
scanline polygons + thick lines drawn into the 256 px tile; behind Map
|
||||
tools > Vector map (test), raster where no vector data. Measured on the
|
||||
L2: 8-60 ms drawing, SD read ~30 ms per new data file (cached after).
|
||||
- [ ] Vector maps next: delta / varint coordinates (smaller files, faster
|
||||
read), labels and POIs (peaks, shelters, springs), contours, a PBF
|
||||
pipeline for regions + download on the device, styling.
|
||||
|
||||
User list, second batch (2026-09-26):
|
||||
|
||||
|
||||
@@ -188,6 +188,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 > Vector map (test).
|
||||
static bool vectorOn() { return nvs::getBool("mc_ui", "vector", false); }
|
||||
static void setVectorOn(bool on) { nvs::putBool("mc_ui", "vector", 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); }
|
||||
@@ -522,6 +525,9 @@ 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_vector = false;
|
||||
static bool vectorOn() { return s_vector; }
|
||||
static void setVectorOn(bool on) { s_vector = on; }
|
||||
static bool s_net_on = false;
|
||||
static void netBegin(const char*, const char*) { s_net_on = true; }
|
||||
static int netState() { return NET_UP; }
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
#include <math.h>
|
||||
#include "map/TileProvider.h"
|
||||
#include "map/VectorTileProvider.h"
|
||||
#include "map/TileCache.h"
|
||||
#include "map/LiveCache.h"
|
||||
#include "map/TileDownloader.h"
|
||||
@@ -24,7 +25,8 @@
|
||||
namespace mapview {
|
||||
|
||||
static RasterTileProvider s_raster("/sdcard/maps", LIVE_ROOT);
|
||||
static TileProvider* s_provider = &s_raster; // the one place to swap in a vector renderer
|
||||
static VectorTileProvider s_vector(s_raster); // spike: vector data where there is some, raster elsewhere
|
||||
static TileProvider* s_provider = &s_raster; // Map tools > Vector map (test) swaps it
|
||||
static TileCache& s_cache = *new (psramBuf<TileCache>(1)) TileCache(); // decoded tiles, in PSRAM
|
||||
static TileDownloader s_dl("/sdcard/maps");
|
||||
static AreaStore s_areas("/sdcard/maps");
|
||||
@@ -267,6 +269,7 @@ void UITask::openMap(bool nav) {
|
||||
void UITask::showMap() {
|
||||
_screen = SCR_MAP;
|
||||
mapview::s_trails_on = lvport::trailsOn();
|
||||
mapview::s_provider = lvport::vectorOn() ? (mapview::TileProvider*)&mapview::s_vector : &mapview::s_raster;
|
||||
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
|
||||
@@ -485,6 +488,13 @@ void UITask::layoutMap() {
|
||||
if (_map_nav) layoutNav();
|
||||
|
||||
if (over) lv_label_set_text_fmt(_map_zoom_lbl, "z%d (map z%d)", _map_z, _map_z - over); // magnified
|
||||
else if (mapview::s_provider == &mapview::s_vector) // spike: how long a tile takes
|
||||
{
|
||||
uint32_t ld, fl, ln;
|
||||
mapview::s_vector.lastSplit(ld, fl, ln);
|
||||
lv_label_set_text_fmt(_map_zoom_lbl, "z%d vt %lu ms (r%lu a%lu l%lu)", _map_z, (unsigned long)mapview::s_vector.lastMs(),
|
||||
(unsigned long)ld, (unsigned long)fl, (unsigned long)ln);
|
||||
}
|
||||
else lv_label_set_text_fmt(_map_zoom_lbl, "z%d", _map_z);
|
||||
const char* hint = !have_provider ? "No map on the SD card.\nPut tiles in /maps (tools/maps)."
|
||||
: mapview::s_dl.liveQueued() > 0 ? nullptr // being fetched
|
||||
@@ -910,6 +920,16 @@ void UITask::setTrails(bool on) {
|
||||
else showToast("Download an area again to add its trails", 3000);
|
||||
}
|
||||
|
||||
// Map tools > Vector map (test): /sdcard/vmap drawn on the device where it has data.
|
||||
void UITask::setVectorMap(bool on) {
|
||||
lvport::setVectorOn(on);
|
||||
mapview::s_provider = on ? (mapview::TileProvider*)&mapview::s_vector : &mapview::s_raster;
|
||||
mapview::s_cache.invalidate();
|
||||
mapview::s_available = lvport::mountStorage() && mapview::s_provider->available();
|
||||
if (_screen == SCR_MAP) layoutMap();
|
||||
showToast(on ? "Vector map on (where /vmap has data)" : "Vector map off");
|
||||
}
|
||||
|
||||
void UITask::setLiveTiles(bool on) {
|
||||
lvport::setLiveTiles(on);
|
||||
if (on) {
|
||||
@@ -998,6 +1018,19 @@ void UITask::mapDownloadTick() {
|
||||
if (dl.active() && _screen == SCR_MAP && dl.downloaded() > 0) { mapview::s_cache.forgetMissing(); layoutMap(); }
|
||||
}
|
||||
|
||||
// Sim / tests: the Navigation map at "lat,lon,z", not following the GPS.
|
||||
void UITask::simMapAt(const char* spec) {
|
||||
double lat = 0, lon = 0;
|
||||
int z = 14;
|
||||
if (sscanf(spec, "%lf,%lf,%d", &lat, &lon, &z) < 2) return;
|
||||
openMap(true);
|
||||
_map_z = z;
|
||||
_map_cx = mapview::lonToTileX(lon, z);
|
||||
_map_cy = mapview::latToTileY(lat, z);
|
||||
_map_follow = false;
|
||||
layoutMap();
|
||||
}
|
||||
|
||||
#if defined(SIM_PLATFORM) && defined(__EMSCRIPTEN__)
|
||||
// Sim page / tests: a saved WiFi network without going through Settings > WiFi.
|
||||
extern "C" EMSCRIPTEN_KEEPALIVE void sim_wifi_save(const char* ssid, const char* pass) { lvport::saveWifi(ssid, pass); }
|
||||
|
||||
@@ -876,6 +876,9 @@ 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 onVectorMap(lv_event_t* e) {
|
||||
s_ui->setVectorMap(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));
|
||||
}
|
||||
@@ -973,6 +976,9 @@ void UITask::navToolsPopup() {
|
||||
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);
|
||||
lv_obj_t* vsw = switchRow(g, "Vector map (test)", "Drawn on the device from /vmap", nullptr);
|
||||
if (lvport::vectorOn()) lv_obj_add_state(vsw, LV_STATE_CHECKED);
|
||||
lv_obj_add_event_cb(vsw, onVectorMap, LV_EVENT_VALUE_CHANGED, NULL);
|
||||
listRow(g, "Download an area", "Pick it with a frame, for use offline", onNavTool, (void*)(uintptr_t)navmap::TL_DOWNLOAD);
|
||||
char ar[40];
|
||||
int na = mapview::s_areas.count();
|
||||
|
||||
@@ -3100,6 +3100,8 @@ extern "C" EMSCRIPTEN_KEEPALIVE void sim_open(const char* name) {
|
||||
for (auto& s : SCREENS) if (!strcmp(s.n, name)) { (s_ui->*s.fn)(); return; }
|
||||
if (!strncmp(name, "page", 4)) { s_ui->showSchemaSettings(atoi(name + 4)); return; }
|
||||
if (!strcmp(name, "map")) { s_ui->openMap(true); return; }
|
||||
if (!strncmp(name, "map@", 4)) { s_ui->simMapAt(name + 4); return; } // "map@lat,lon,z"
|
||||
if (!strcmp(name, "vector")) { s_ui->setVectorMap(true); return; }
|
||||
if (!strcmp(name, "maptools")) { s_ui->navToolsPopup(); return; }
|
||||
if (!strcmp(name, "areasel")) { s_ui->areaSelectBegin(); return; }
|
||||
if (!strcmp(name, "areas")) { s_ui->mapAreasPopup(); return; }
|
||||
|
||||
@@ -91,7 +91,9 @@ public:
|
||||
void mapDownloadDiscard();
|
||||
void mapDownloadZmax(int delta);
|
||||
void setLiveTiles(bool on);
|
||||
void setTrails(bool on); // Map tools > Live tiles
|
||||
void setTrails(bool on);
|
||||
void setVectorMap(bool on);
|
||||
void simMapAt(const char* spec); // Map tools > Live tiles
|
||||
// Navigation map (NavMap.h)
|
||||
void navTargetsPopup();
|
||||
void navClosePopup();
|
||||
|
||||
@@ -42,6 +42,28 @@ static const char* const TRAILS_ATTR = "Trails \xC2\xA9 waymarkedtrails.org (CC-
|
||||
static const int TRAILS_MAX_Z = 18;
|
||||
static bool s_trails_on = false; // Map tools > Hiking trails (NVS), set when the map opens
|
||||
|
||||
// fread into a PSRAM buffer: the SD card can't DMA there, so the driver
|
||||
// falls back to one 512-byte sector per transfer. Read in chunks through
|
||||
// internal (DMA-capable) memory instead -- several times faster.
|
||||
static bool readFast(FILE* f, uint8_t* dst, size_t len) {
|
||||
#if defined(ESP32)
|
||||
const size_t CHUNK = 16 * 1024;
|
||||
uint8_t* bounce = (uint8_t*)heap_caps_malloc(len < CHUNK ? len : CHUNK, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL);
|
||||
if (bounce) {
|
||||
size_t done = 0;
|
||||
while (done < len) {
|
||||
size_t n = len - done < CHUNK ? len - done : CHUNK;
|
||||
if (fread(bounce, 1, n, f) != n) break;
|
||||
memcpy(dst + done, bounce, n);
|
||||
done += n;
|
||||
}
|
||||
heap_caps_free(bounce);
|
||||
return done == len;
|
||||
}
|
||||
#endif
|
||||
return fread(dst, 1, len, f) == len;
|
||||
}
|
||||
|
||||
// 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;
|
||||
@@ -175,7 +197,7 @@ private:
|
||||
if (len == 0 || len > 512 * 1024) return nullptr;
|
||||
uint8_t* buf = (uint8_t*)lv_malloc(len);
|
||||
if (!buf) return nullptr;
|
||||
if (fseek(f, off, SEEK_SET) != 0 || fread(buf, 1, len, f) != len) { lv_free(buf); return nullptr; }
|
||||
if (fseek(f, off, SEEK_SET) != 0 || !readFast(f, buf, len)) { lv_free(buf); return nullptr; }
|
||||
return buf;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,360 @@
|
||||
#pragma once
|
||||
// Vector map (spike): tiles from tools/maps/osm_vector.py under VECTOR_ROOT,
|
||||
// {dz}/{x}/{y}.vt at data zooms 10, 12 and 14, drawn here into the 256x256 RGB565
|
||||
// buffer the map asks for -- polygons by scanline (even-odd), lines as quads
|
||||
// with round joins. No anti-aliasing, no labels yet. Where there is no vector
|
||||
// data (or below zoom 10) the raster provider draws instead, so the two mix.
|
||||
//
|
||||
// renderTile() times itself (lastMs()); the map shows it while this provider
|
||||
// is on, to judge whether the approach holds on the ESP32-S3.
|
||||
//
|
||||
// Single-TU fragment: included by ui-lvgl/MapScreen.h after TileProvider.h.
|
||||
|
||||
namespace mapview {
|
||||
|
||||
static inline int32_t vmin(int32_t a, int32_t b) { return a < b ? a : b; }
|
||||
static inline int32_t vmax(int32_t a, int32_t b) { return a > b ? a : b; }
|
||||
|
||||
static const char* const VECTOR_ROOT = "/sdcard/vmap";
|
||||
|
||||
class VectorTileProvider : public TileProvider {
|
||||
public:
|
||||
explicit VectorTileProvider(TileProvider& fallback) : _fb(fallback) {}
|
||||
|
||||
bool available() override {
|
||||
struct stat st;
|
||||
_have = stat(VECTOR_ROOT, &st) == 0 && S_ISDIR(st.st_mode);
|
||||
bool fb = _fb.available();
|
||||
return _have || fb;
|
||||
}
|
||||
|
||||
bool renderTile(int z, int x, int y, uint16_t* out) override {
|
||||
if (!_have || z < 10) return _fb.renderTile(z, x, y, out);
|
||||
uint32_t t0 = micros();
|
||||
int dz = z >= 14 ? 14 : z >= 12 ? 12 : 10, k = z - dz;
|
||||
_t_fill = _t_line = 0;
|
||||
if (!loadData(dz, x >> k, y >> k)) return _fb.renderTile(z, x, y, out);
|
||||
_t_load = micros() - t0;
|
||||
int span = EXTENT >> k;
|
||||
_ox = (x & ((1 << k) - 1)) * span;
|
||||
_oy = (y & ((1 << k) - 1)) * span;
|
||||
_k = k;
|
||||
_z = z;
|
||||
_out = out;
|
||||
for (int i = 0; i < TILE_PX * TILE_PX; i++) out[i] = PAPER;
|
||||
for (int pass = 0; pass < 4; pass++) drawPass(pass);
|
||||
_last_ms = (micros() - t0 + 500) / 1000;
|
||||
return true;
|
||||
}
|
||||
|
||||
const char* attribution() const override { return _have ? "\xC2\xA9 OpenStreetMap contributors (ODbL)" : _fb.attribution(); }
|
||||
uint32_t lastMs() const { return _last_ms; }
|
||||
// The last tile's time split: reading the data, areas, lines (ms).
|
||||
void lastSplit(uint32_t& load, uint32_t& fill, uint32_t& line) const { load = _t_load / 1000; fill = _t_fill / 1000; line = _t_line / 1000; }
|
||||
|
||||
private:
|
||||
static const int EXTENT = 4096;
|
||||
static const uint16_t PAPER = 0xF77C; // #F2EFE9-ish
|
||||
|
||||
TileProvider& _fb;
|
||||
bool _have = false;
|
||||
const uint8_t* _data = nullptr; // the data tile being drawn
|
||||
size_t _data_len = 0;
|
||||
int _ox = 0, _oy = 0, _k = 0, _z = 0;
|
||||
uint16_t* _out = nullptr;
|
||||
uint32_t _last_ms = 0;
|
||||
|
||||
// Scratch for the polygon filler: edges (in 1/16 px), crossings of a row.
|
||||
struct Edge { int16_t r0, r1; int64_t x, dx; }; // rows [r0, r1), x / step 16.16 in 1/16 px
|
||||
static const int ACTIVE = 512;
|
||||
Edge* _edges = nullptr;
|
||||
int _edge_cap = 0, _ne = 0;
|
||||
Edge* _act[ACTIVE];
|
||||
int32_t _xs[ACTIVE];
|
||||
uint32_t _t_load = 0, _t_fill = 0, _t_line = 0; // us, last tile
|
||||
|
||||
static uint16_t rgb(uint32_t c) { return (uint16_t)(((c >> 8) & 0xF800) | ((c >> 5) & 0x07E0) | ((c >> 3) & 0x1F)); }
|
||||
|
||||
// The data tiles last read (PSRAM), least recently used replaced: the map
|
||||
// draws several tiles out of each.
|
||||
struct Data { uint8_t* buf = nullptr; size_t len = 0, cap = 0; int dz = -1, x = -1, y = -1; bool ok = false; uint32_t used = 0; };
|
||||
static const int DATA_SLOTS = 4;
|
||||
Data _slot[DATA_SLOTS];
|
||||
uint32_t _tick = 0;
|
||||
|
||||
bool loadData(int dz, int x, int y) {
|
||||
Data* d = nullptr;
|
||||
for (Data& s : _slot) if (s.dz == dz && s.x == x && s.y == y) d = &s;
|
||||
if (d) { d->used = ++_tick; _t_load = 0; _data = d->buf; _data_len = d->len; return d->ok; }
|
||||
d = &_slot[0];
|
||||
for (Data& s : _slot) if (s.used < d->used) d = &s;
|
||||
d->dz = dz; d->x = x; d->y = y; d->ok = false; d->used = ++_tick;
|
||||
char path[64];
|
||||
snprintf(path, sizeof(path), "%s/%d/%d/%d.vt", VECTOR_ROOT, dz, x, y);
|
||||
FILE* f = fopen(path, "rb");
|
||||
if (!f) return false;
|
||||
fseek(f, 0, SEEK_END);
|
||||
long sz = ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
if (sz < 6 || sz > 2 * 1024 * 1024) { fclose(f); return false; }
|
||||
if ((size_t)sz > d->cap) {
|
||||
if (d->buf) free(d->buf);
|
||||
#if defined(ESP32)
|
||||
d->buf = (uint8_t*)heap_caps_malloc(sz, MALLOC_CAP_SPIRAM);
|
||||
#else
|
||||
d->buf = (uint8_t*)malloc(sz);
|
||||
#endif
|
||||
d->cap = d->buf ? sz : 0;
|
||||
if (!d->buf) { fclose(f); return false; }
|
||||
}
|
||||
d->len = readFast(f, d->buf, sz) ? sz : 0;
|
||||
fclose(f);
|
||||
d->ok = d->len == (size_t)sz && memcmp(d->buf, "VT2", 3) == 0;
|
||||
_data = d->buf;
|
||||
_data_len = d->len;
|
||||
return d->ok;
|
||||
}
|
||||
|
||||
// Styles. Widths in px at zoom 14, scaled with the zoom.
|
||||
enum : uint8_t { L_STREAM = 20, L_RIVER = 21, L_PATH = 30, L_TRACK = 31, L_SERVICE = 32, L_TRUNK = 37, L_ROUTE = 50 };
|
||||
static uint16_t polyColour(uint8_t c) {
|
||||
switch (c) {
|
||||
case 1: return rgb(0xE6DED6); // residential
|
||||
case 2: return rgb(0xE1ECC6); // meadow
|
||||
case 3: return rgb(0xCFE2B2); // scrub
|
||||
case 4: return rgb(0xB6D59A); // forest
|
||||
case 5: return rgb(0xE2DED8); // rock / scree
|
||||
case 6: return rgb(0xA6CBE0); // water
|
||||
case 7: return rgb(0xD4C8BC); // building
|
||||
}
|
||||
return PAPER;
|
||||
}
|
||||
static bool lineStyle(uint8_t c, uint16_t& col, float& w) {
|
||||
switch (c) {
|
||||
case L_STREAM: col = rgb(0x86B6D8); w = 1.0f; return true;
|
||||
case L_RIVER: col = rgb(0x86B6D8); w = 3.0f; return true;
|
||||
case L_PATH: col = rgb(0xA8502A); w = 1.2f; return true;
|
||||
case L_TRACK: col = rgb(0x94683A); w = 1.6f; return true;
|
||||
case 32: col = 0xFFFF; w = 2.2f; return true; // service
|
||||
case 33: col = 0xFFFF; w = 3.2f; return true; // minor
|
||||
case 34: col = rgb(0xFFFBB0); w = 3.8f; return true; // tertiary
|
||||
case 35: col = rgb(0xF7D38A); w = 4.2f; return true; // secondary
|
||||
case 36: col = rgb(0xF2A860); w = 4.8f; return true; // primary
|
||||
case 37: col = rgb(0xE8808A); w = 5.2f; return true; // trunk
|
||||
case L_ROUTE: col = 0; w = 3.0f; return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
float zoomScale() const {
|
||||
static const float S[] = { 0.4f, 0.45f, 0.6f, 0.8f, 1.0f, 1.3f, 1.7f, 2.2f, 2.8f }; // z10..z18
|
||||
int i = _z - 10;
|
||||
return S[i < 0 ? 0 : i > 8 ? 8 : i];
|
||||
}
|
||||
|
||||
// Passes: 0 areas + water / paths / tracks, 1 road casings, 2 road fills,
|
||||
// 3 hiking routes -- so roads join cleanly and routes stay on top.
|
||||
void drawPass(int pass) {
|
||||
const uint8_t* p = _data + 6;
|
||||
const uint8_t* end = _data + _data_len;
|
||||
uint16_t count;
|
||||
memcpy(&count, _data + 4, 2);
|
||||
for (int f = 0; f < count && p + 12 <= end; f++) {
|
||||
uint8_t cls = p[0], nparts = p[1];
|
||||
uint16_t col;
|
||||
int16_t bb[4];
|
||||
memcpy(&col, p + 2, 2);
|
||||
memcpy(bb, p + 4, 8);
|
||||
p += 12;
|
||||
const uint8_t* parts = p;
|
||||
for (int i = 0; i < nparts && p + 2 <= end; i++) { // skip to the next feature
|
||||
uint16_t n;
|
||||
memcpy(&n, p, 2);
|
||||
p += 2 + 4 * n;
|
||||
}
|
||||
if (p > end) return;
|
||||
bool road = cls >= L_SERVICE && cls <= L_TRUNK;
|
||||
bool want = pass == 0 ? (cls < L_SERVICE) : pass == 3 ? cls == L_ROUTE : road;
|
||||
if (!want) continue;
|
||||
if (_z < 13 && (cls == L_PATH || cls == 7)) continue; // paths from z13, buildings from z14 (data)
|
||||
// Off the drawn tile (with a margin for the widest line): not even read.
|
||||
const int32_t M = 12 * 16, S = TILE_PX * 16;
|
||||
if (sx(bb[2]) < -M || sx(bb[0]) > S + M || sy(bb[3]) < -M || sy(bb[1]) > S + M) continue;
|
||||
uint32_t t0 = micros();
|
||||
if (cls < 10) { fillFeature(parts, nparts, polyColour(cls)); _t_fill += micros() - t0; continue; }
|
||||
uint16_t c;
|
||||
float w;
|
||||
if (!lineStyle(cls, c, w)) continue;
|
||||
w *= zoomScale();
|
||||
if (cls == L_ROUTE) { c = col ? col : rgb(0xD04040); w = w < 2.5f ? 2.5f : w; }
|
||||
if (pass == 1) { c = rgb(0xB4ACA2); w += 1.6f; } // casing
|
||||
strokeFeature(parts, nparts, c, w);
|
||||
_t_line += micros() - t0;
|
||||
}
|
||||
}
|
||||
|
||||
// Tile units -> 1/16 px of the drawn tile.
|
||||
inline int32_t sx(int16_t u) const { return (int32_t)(u - _ox) << _k; }
|
||||
inline int32_t sy(int16_t u) const { return (int32_t)(u - _oy) << _k; }
|
||||
|
||||
bool reserveEdges(int n) {
|
||||
if (n <= _edge_cap) return true;
|
||||
int cap = n + 256;
|
||||
Edge* e = (Edge*)realloc(_edges, sizeof(Edge) * cap);
|
||||
if (!e) return false;
|
||||
_edges = e;
|
||||
_edge_cap = cap;
|
||||
return true;
|
||||
}
|
||||
// An edge, top to bottom, with its x at the first row centre it crosses and
|
||||
// its slope per row (both 16.16 px).
|
||||
void addEdge(int32_t x0, int32_t y0, int32_t x1, int32_t y1) {
|
||||
if (y0 == y1) return;
|
||||
if (y0 > y1) { int32_t t = x0; x0 = x1; x1 = t; t = y0; y0 = y1; y1 = t; }
|
||||
int r0 = (y0 - 8 + 15) >> 4, r1 = (y1 - 8 + 15) >> 4; // rows r0 .. r1-1 (centre in [y0, y1))
|
||||
if (r1 <= r0 || r1 <= 0 || r0 >= TILE_PX) return;
|
||||
if (_ne >= _edge_cap && !reserveEdges(_ne + 1)) return;
|
||||
int64_t slope = ((int64_t)(x1 - x0) << 16) / (y1 - y0); // 1/16 px of x per 1/16 px of y, 16.16
|
||||
int32_t yc = r0 * 16 + 8;
|
||||
Edge& e = _edges[_ne++];
|
||||
e.r0 = r0; e.r1 = r1;
|
||||
e.x = ((int64_t)x0 << 16) + slope * (yc - y0);
|
||||
e.dx = slope * 16;
|
||||
}
|
||||
|
||||
static int cmpEdge(const void* a, const void* b) { return ((const Edge*)a)->r0 - ((const Edge*)b)->r0; }
|
||||
|
||||
// Even-odd scanline fill of the edges collected (an active edge list).
|
||||
void fillEdges(uint16_t col) {
|
||||
if (!_ne) return;
|
||||
if (_ne > 4) qsort(_edges, _ne, sizeof(Edge), cmpEdge);
|
||||
else for (int i = 1; i < _ne; i++) { Edge v = _edges[i]; int j = i - 1; while (j >= 0 && _edges[j].r0 > v.r0) { _edges[j + 1] = _edges[j]; j--; } _edges[j + 1] = v; }
|
||||
int next = 0, na = 0;
|
||||
int row = _edges[0].r0 < 0 ? 0 : _edges[0].r0;
|
||||
int rmax = 0;
|
||||
for (int i = 0; i < _ne; i++) if (_edges[i].r1 > rmax) rmax = _edges[i].r1;
|
||||
if (rmax > TILE_PX) rmax = TILE_PX;
|
||||
for (; row < rmax; row++) {
|
||||
while (next < _ne && _edges[next].r0 <= row) { // edges starting by this row join
|
||||
Edge& e = _edges[next++];
|
||||
if (e.r1 <= row) continue;
|
||||
if (e.r0 < row) e.x += e.dx * (row - e.r0); // started above the tile
|
||||
if (na < ACTIVE) _act[na++] = &e;
|
||||
}
|
||||
int n = 0;
|
||||
for (int i = 0; i < na;) { // drop finished edges, collect crossings
|
||||
Edge* e = _act[i];
|
||||
if (e->r1 <= row) { _act[i] = _act[--na]; continue; }
|
||||
if (n < ACTIVE) _xs[n++] = (int32_t)(e->x >> 16);
|
||||
e->x += e->dx;
|
||||
i++;
|
||||
}
|
||||
if (n < 2) { if (!na && next >= _ne) break; continue; }
|
||||
for (int i = 1; i < n; i++) { // insertion sort: a handful per row
|
||||
int32_t v = _xs[i];
|
||||
int j = i - 1;
|
||||
while (j >= 0 && _xs[j] > v) { _xs[j + 1] = _xs[j]; j--; }
|
||||
_xs[j + 1] = v;
|
||||
}
|
||||
uint16_t* line = _out + row * TILE_PX;
|
||||
for (int i = 0; i + 1 < n; i += 2) {
|
||||
int a = (_xs[i] - 8 + 15) >> 4, b = (_xs[i + 1] - 8) >> 4; // centres inside
|
||||
if (a < 0) a = 0;
|
||||
if (b > TILE_PX - 1) b = TILE_PX - 1;
|
||||
for (int x = a; x <= b; x++) line[x] = col;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Points of a part, in 1/16 px, closer than half a pixel to the last one
|
||||
// dropped (the data is detailed enough for zoom 18).
|
||||
template <typename F> void forPoints(const uint8_t*& p, bool keep_last, F fn) {
|
||||
uint16_t n;
|
||||
memcpy(&n, p, 2);
|
||||
p += 2;
|
||||
int32_t lx = INT32_MIN, ly = 0;
|
||||
for (int j = 0; j < n; j++) {
|
||||
int16_t ux, uy;
|
||||
memcpy(&ux, p + 4 * j, 2); memcpy(&uy, p + 4 * j + 2, 2);
|
||||
int32_t x = sx(ux), y = sy(uy);
|
||||
if (lx != INT32_MIN && abs(x - lx) + abs(y - ly) < 8 && !(keep_last && j == n - 1)) continue;
|
||||
fn(x, y, lx == INT32_MIN);
|
||||
lx = x; ly = y;
|
||||
}
|
||||
p += 4 * n;
|
||||
}
|
||||
|
||||
void fillFeature(const uint8_t* p, int nparts, uint16_t col) {
|
||||
_ne = 0;
|
||||
for (int i = 0; i < nparts; i++) {
|
||||
int32_t fx = 0, fy = 0, px = 0, py = 0;
|
||||
forPoints(p, false, [&](int32_t x, int32_t y, bool first) {
|
||||
if (first) { fx = px = x; fy = py = y; return; }
|
||||
addEdge(px, py, x, y);
|
||||
px = x; py = y;
|
||||
});
|
||||
addEdge(px, py, fx, fy); // closed
|
||||
}
|
||||
fillEdges(col);
|
||||
}
|
||||
|
||||
void disc(int32_t cx, int32_t cy, int32_t r16, uint16_t col) { // radius in 1/16 px
|
||||
int x0 = (cx - r16) >> 4, x1 = (cx + r16) >> 4, y0 = (cy - r16) >> 4, y1 = (cy + r16) >> 4;
|
||||
if (x1 < 0 || y1 < 0 || x0 >= TILE_PX || y0 >= TILE_PX) return;
|
||||
int32_t r2 = r16 * r16;
|
||||
for (int y = vmax(0, y0); y <= vmin(TILE_PX - 1, y1); y++) {
|
||||
int32_t dy = y * 16 + 8 - cy;
|
||||
uint16_t* line = _out + y * TILE_PX;
|
||||
for (int x = vmax(0, x0); x <= vmin(TILE_PX - 1, x1); x++) {
|
||||
int32_t dx = x * 16 + 8 - cx;
|
||||
if (dx * dx + dy * dy <= r2) line[x] = col;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A thin line: a DDA with a 1 or 2 px pen.
|
||||
void thinSegment(int32_t ax, int32_t ay, int32_t bx, int32_t by, int pen, uint16_t col) {
|
||||
int32_t dx = bx - ax, dy = by - ay;
|
||||
int steps = (vmax(abs(dx), abs(dy)) >> 4) + 1;
|
||||
int32_t x = ax << 8, y = ay << 8, ix = (dx << 8) / steps, iy = (dy << 8) / steps; // 1/16 px << 8
|
||||
for (int i = 0; i <= steps; i++, x += ix, y += iy) {
|
||||
int px = x >> 12, py = y >> 12;
|
||||
for (int oy = 0; oy < pen; oy++)
|
||||
for (int ox = 0; ox < pen; ox++) {
|
||||
int qx = px + ox, qy = py + oy;
|
||||
if ((unsigned)qx < (unsigned)TILE_PX && (unsigned)qy < (unsigned)TILE_PX) _out[qy * TILE_PX + qx] = col;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void strokeFeature(const uint8_t* p, int nparts, uint16_t col, float w) {
|
||||
int32_t h = (int32_t)(w * 8); // half width, 1/16 px
|
||||
const int32_t S = TILE_PX * 16;
|
||||
bool thin = w < 2.2f;
|
||||
int pen = w < 1.8f ? 1 : 2;
|
||||
for (int i = 0; i < nparts; i++) {
|
||||
int32_t ax = 0, ay = 0;
|
||||
forPoints(p, true, [&](int32_t bx, int32_t by, bool first) {
|
||||
if (!first && !(vmax(ax, bx) < -h || vmin(ax, bx) > S + h || vmax(ay, by) < -h || vmin(ay, by) > S + h)) {
|
||||
if (thin) {
|
||||
thinSegment(ax, ay, bx, by, pen, col);
|
||||
} else {
|
||||
float dx = bx - ax, dy = by - ay, len = sqrtf(dx * dx + dy * dy);
|
||||
if (len > 0) {
|
||||
int32_t nx = (int32_t)(-dy / len * h), ny = (int32_t)(dx / len * h);
|
||||
_ne = 0;
|
||||
addEdge(ax + nx, ay + ny, bx + nx, by + ny);
|
||||
addEdge(bx + nx, by + ny, bx - nx, by - ny);
|
||||
addEdge(bx - nx, by - ny, ax - nx, ay - ny);
|
||||
addEdge(ax - nx, ay - ny, ax + nx, ay + ny);
|
||||
fillEdges(col);
|
||||
}
|
||||
if (w >= 2.8f) disc(bx, by, h, col); // round join
|
||||
}
|
||||
}
|
||||
ax = bx; ay = by;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace mapview
|
||||
@@ -0,0 +1,355 @@
|
||||
#!/usr/bin/env python3
|
||||
"""OSM data -> vector map tiles for the Wio Tracker L2 (spike).
|
||||
|
||||
Reads an Overpass JSON export (`out geom;`) and writes small binary tiles the
|
||||
device rasterises itself (ui-lvgl/map/VectorTileProvider.h): roads, paths,
|
||||
marked hiking routes in their waymark colours, water, forest, meadows, rock,
|
||||
buildings. No labels yet.
|
||||
|
||||
tools/maps/osm_vector.py area.json --out vmap/
|
||||
|
||||
Three data zooms: 10 (drawn at z10-11), 12 (z12-13), 14 (z14-18); the device picks the
|
||||
data tile covering the tile it draws and scales it. Copy the output folder to
|
||||
the card as /sdcard/vmap.
|
||||
|
||||
Tile format 'VT2' (little-endian):
|
||||
'V' 'T' '2' dz:u8 count:u16
|
||||
count x feature, in drawing order:
|
||||
cls:u8 nparts:u8 colour:u16 (RGB565, 0 = the class's own)
|
||||
bbox: x0 y0 x1 y1 (i16) -- the device skips what's off the tile unread
|
||||
nparts x (npts:u16, npts x (x:i16, y:i16))
|
||||
Coordinates: 0..4096 across the data tile, a little past its edges.
|
||||
Polygons: rings, even-odd. Lines: polylines.
|
||||
|
||||
Get the data for a box (south, west, north, east) from Overpass, e.g.:
|
||||
[out:json][timeout:120];
|
||||
( way["highway"](S,W,N,E); way["waterway"~"river|stream|canal"](S,W,N,E);
|
||||
way["natural"~"water|wood|scrub|grassland|heath|scree|bare_rock"](S,W,N,E);
|
||||
way["landuse"~"forest|meadow|grass|residential|farmland"](S,W,N,E);
|
||||
way["building"](S,W,N,E);
|
||||
relation["natural"~"water|wood|scrub"](S,W,N,E);
|
||||
relation["landuse"~"forest|meadow"](S,W,N,E);
|
||||
relation["route"="hiking"](S,W,N,E); );
|
||||
out geom;
|
||||
Map data (c) OpenStreetMap contributors, ODbL.
|
||||
"""
|
||||
import argparse, json, math, os, struct, sys
|
||||
from collections import defaultdict
|
||||
|
||||
EXTENT = 4096
|
||||
BUFFER = 128
|
||||
DATA_ZOOMS = (10, 12, 14) # drawn at z10-11, z12-13, z14-18
|
||||
|
||||
# Classes: the draw order is the number's order (the device styles them).
|
||||
P_RESIDENTIAL, P_MEADOW, P_SCRUB, P_FOREST, P_ROCK, P_WATER, P_BUILDING = 1, 2, 3, 4, 5, 6, 7
|
||||
L_STREAM, L_RIVER = 20, 21
|
||||
L_PATH, L_TRACK, L_SERVICE, L_MINOR, L_TERTIARY, L_SECONDARY, L_PRIMARY, L_TRUNK = 30, 31, 32, 33, 34, 35, 36, 37
|
||||
L_ROUTE = 50
|
||||
|
||||
HIGHWAY = {
|
||||
'path': L_PATH, 'footway': L_PATH, 'steps': L_PATH, 'bridleway': L_PATH, 'cycleway': L_PATH,
|
||||
'track': L_TRACK,
|
||||
'service': L_SERVICE, 'living_street': L_SERVICE, 'pedestrian': L_SERVICE,
|
||||
'residential': L_MINOR, 'unclassified': L_MINOR, 'road': L_MINOR,
|
||||
'tertiary': L_TERTIARY, 'tertiary_link': L_TERTIARY,
|
||||
'secondary': L_SECONDARY, 'secondary_link': L_SECONDARY,
|
||||
'primary': L_PRIMARY, 'primary_link': L_PRIMARY,
|
||||
'trunk': L_TRUNK, 'trunk_link': L_TRUNK, 'motorway': L_TRUNK, 'motorway_link': L_TRUNK,
|
||||
}
|
||||
# Lowest data zoom a class goes in (smaller ones would be clutter / weight).
|
||||
MIN_DZ = {P_BUILDING: 14, L_SERVICE: 14, L_PATH: 12, L_TRACK: 12, L_STREAM: 12, L_MINOR: 12}
|
||||
|
||||
WAYMARK = { # osmc:symbol / colour -> RGB888
|
||||
'red': 0xE0302A, 'blue': 0x2A5FE0, 'green': 0x2EA043, 'yellow': 0xE8C20E, 'black': 0x202020,
|
||||
'orange': 0xF08A1C, 'purple': 0x9040C0, 'white': 0xF0F0F0, 'brown': 0x8B5A2B,
|
||||
}
|
||||
|
||||
|
||||
def rgb565(c):
|
||||
return ((c >> 8) & 0xF800) | ((c >> 5) & 0x07E0) | ((c >> 3) & 0x001F)
|
||||
|
||||
|
||||
def poly_class(t):
|
||||
n, lu = t.get('natural'), t.get('landuse')
|
||||
if t.get('building'):
|
||||
return P_BUILDING
|
||||
if n == 'water' or t.get('water'):
|
||||
return P_WATER
|
||||
if n == 'wood' or lu == 'forest':
|
||||
return P_FOREST
|
||||
if n == 'scrub':
|
||||
return P_SCRUB
|
||||
if n in ('scree', 'bare_rock'):
|
||||
return P_ROCK
|
||||
if n in ('grassland', 'heath') or lu in ('meadow', 'grass', 'farmland'):
|
||||
return P_MEADOW
|
||||
if lu == 'residential':
|
||||
return P_RESIDENTIAL
|
||||
return None
|
||||
|
||||
|
||||
def line_class(t):
|
||||
if 'highway' in t:
|
||||
return HIGHWAY.get(t['highway'])
|
||||
w = t.get('waterway')
|
||||
if w in ('river', 'canal'):
|
||||
return L_RIVER
|
||||
if w == 'stream':
|
||||
return L_STREAM
|
||||
return None
|
||||
|
||||
|
||||
def route_colour(t):
|
||||
c = t.get('colour', '').lower()
|
||||
if c in WAYMARK:
|
||||
return WAYMARK[c]
|
||||
sym = t.get('osmc:symbol', '')
|
||||
if sym:
|
||||
first = sym.split(':')[0].lower()
|
||||
if first in WAYMARK:
|
||||
return WAYMARK[first]
|
||||
# "white:red_bar" style: the bar's colour
|
||||
for part in sym.split(':')[1:]:
|
||||
k = part.split('_')[0].lower()
|
||||
if k in WAYMARK and k != 'white':
|
||||
return WAYMARK[k]
|
||||
return 0xD04040
|
||||
|
||||
|
||||
def world(lon, lat):
|
||||
"""Web Mercator 0..1."""
|
||||
s = math.sin(math.radians(lat))
|
||||
return (lon + 180.0) / 360.0, 0.5 - math.log((1 + s) / (1 - s)) / (4 * math.pi)
|
||||
|
||||
|
||||
def geom_pts(g):
|
||||
return [world(p['lon'], p['lat']) for p in g if p]
|
||||
|
||||
|
||||
def join_rings(segments):
|
||||
"""Member ways of a multipolygon -> closed rings (by shared end points)."""
|
||||
segs = [list(s) for s in segments if len(s) >= 2]
|
||||
rings = []
|
||||
while segs:
|
||||
ring = segs.pop()
|
||||
changed = True
|
||||
while ring[0] != ring[-1] and changed:
|
||||
changed = False
|
||||
for i, s in enumerate(segs):
|
||||
if s[0] == ring[-1]:
|
||||
ring += s[1:]
|
||||
elif s[-1] == ring[-1]:
|
||||
ring += s[-2::-1]
|
||||
elif s[-1] == ring[0]:
|
||||
ring = s[:-1] + ring
|
||||
elif s[0] == ring[0]:
|
||||
ring = s[:0:-1] + ring
|
||||
else:
|
||||
continue
|
||||
segs.pop(i)
|
||||
changed = True
|
||||
break
|
||||
if len(ring) >= 4 and ring[0] == ring[-1]:
|
||||
rings.append(ring)
|
||||
return rings
|
||||
|
||||
|
||||
def simplify(pts, tol):
|
||||
"""Douglas-Peucker on tile units."""
|
||||
if len(pts) < 3 or tol <= 0:
|
||||
return pts
|
||||
keep = [False] * len(pts)
|
||||
keep[0] = keep[-1] = True
|
||||
stack = [(0, len(pts) - 1)]
|
||||
t2 = tol * tol
|
||||
while stack:
|
||||
a, b = stack.pop()
|
||||
ax, ay = pts[a]
|
||||
bx, by = pts[b]
|
||||
dx, dy = bx - ax, by - ay
|
||||
L = dx * dx + dy * dy
|
||||
best, bi = -1, -1
|
||||
for i in range(a + 1, b):
|
||||
px, py = pts[i]
|
||||
if L == 0:
|
||||
d = (px - ax) ** 2 + (py - ay) ** 2
|
||||
else:
|
||||
t = max(0, min(1, ((px - ax) * dx + (py - ay) * dy) / L))
|
||||
d = (ax + t * dx - px) ** 2 + (ay + t * dy - py) ** 2
|
||||
if d > best:
|
||||
best, bi = d, i
|
||||
if best > t2:
|
||||
keep[bi] = True
|
||||
stack += [(a, bi), (bi, b)]
|
||||
return [p for p, k in zip(pts, keep) if k]
|
||||
|
||||
|
||||
def clip_line(pts, lo, hi):
|
||||
"""Polyline -> pieces inside the box (Liang-Barsky per segment)."""
|
||||
out, cur = [], []
|
||||
for (x0, y0), (x1, y1) in zip(pts, pts[1:]):
|
||||
t0, t1, dx, dy = 0.0, 1.0, x1 - x0, y1 - y0
|
||||
ok = True
|
||||
for p, q in ((-dx, x0 - lo), (dx, hi - x0), (-dy, y0 - lo), (dy, hi - y0)):
|
||||
if p == 0:
|
||||
if q < 0:
|
||||
ok = False
|
||||
break
|
||||
else:
|
||||
r = q / p
|
||||
if p < 0:
|
||||
t0 = max(t0, r)
|
||||
else:
|
||||
t1 = min(t1, r)
|
||||
if not ok or t0 > t1:
|
||||
if len(cur) >= 2:
|
||||
out.append(cur)
|
||||
cur = []
|
||||
continue
|
||||
a = (x0 + t0 * dx, y0 + t0 * dy)
|
||||
b = (x0 + t1 * dx, y0 + t1 * dy)
|
||||
if not cur or cur[-1] != a:
|
||||
if len(cur) >= 2:
|
||||
out.append(cur)
|
||||
cur = [a]
|
||||
cur.append(b)
|
||||
if t1 < 1.0:
|
||||
out.append(cur)
|
||||
cur = []
|
||||
if len(cur) >= 2:
|
||||
out.append(cur)
|
||||
return out
|
||||
|
||||
|
||||
def clip_ring(pts, lo, hi):
|
||||
"""Sutherland-Hodgman against the box."""
|
||||
def edge(pts, inside, cross):
|
||||
out = []
|
||||
for i in range(len(pts)):
|
||||
cur, prev = pts[i], pts[i - 1]
|
||||
if inside(cur):
|
||||
if not inside(prev):
|
||||
out.append(cross(prev, cur))
|
||||
out.append(cur)
|
||||
elif inside(prev):
|
||||
out.append(cross(prev, cur))
|
||||
return out
|
||||
|
||||
def cx(v):
|
||||
return lambda a, b: (v, a[1] + (b[1] - a[1]) * (v - a[0]) / (b[0] - a[0]))
|
||||
|
||||
def cy(v):
|
||||
return lambda a, b: (a[0] + (b[0] - a[0]) * (v - a[1]) / (b[1] - a[1]), v)
|
||||
|
||||
for inside, cross in ((lambda p: p[0] >= lo, cx(lo)), (lambda p: p[0] <= hi, cx(hi)),
|
||||
(lambda p: p[1] >= lo, cy(lo)), (lambda p: p[1] <= hi, cy(hi))):
|
||||
pts = edge(pts, inside, cross)
|
||||
if not pts:
|
||||
return []
|
||||
return pts
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument('json')
|
||||
ap.add_argument('--out', default='vmap')
|
||||
a = ap.parse_args()
|
||||
data = json.load(open(a.json))
|
||||
|
||||
feats = [] # (cls, colour, 'poly'|'line', [parts in world coords])
|
||||
for e in data['elements']:
|
||||
t = e.get('tags', {})
|
||||
if e['type'] == 'way' and 'geometry' in e:
|
||||
pts = geom_pts(e['geometry'])
|
||||
closed = len(pts) >= 4 and pts[0] == pts[-1]
|
||||
pc = poly_class(t) if closed and t.get('area') != 'no' and 'highway' not in t else None
|
||||
if pc:
|
||||
feats.append((pc, 0, 'poly', [pts]))
|
||||
lc = line_class(t)
|
||||
if lc and not pc:
|
||||
feats.append((lc, 0, 'line', [pts]))
|
||||
elif e['type'] == 'relation':
|
||||
members = e.get('members', [])
|
||||
if t.get('route') == 'hiking':
|
||||
col = rgb565(route_colour(t))
|
||||
parts = [geom_pts(m['geometry']) for m in members if m.get('type') == 'way' and 'geometry' in m]
|
||||
feats.append((L_ROUTE, col, 'line', [p for p in parts if len(p) >= 2]))
|
||||
else:
|
||||
pc = poly_class(t)
|
||||
if not pc:
|
||||
continue
|
||||
outer = [geom_pts(m['geometry']) for m in members if m.get('role') in ('outer', '') and 'geometry' in m]
|
||||
inner = [geom_pts(m['geometry']) for m in members if m.get('role') == 'inner' and 'geometry' in m]
|
||||
rings = join_rings(outer) + join_rings(inner)
|
||||
if rings:
|
||||
feats.append((pc, 0, 'poly', rings))
|
||||
print(f'{len(feats)} features', file=sys.stderr)
|
||||
|
||||
total_bytes = total_tiles = 0
|
||||
for dz in DATA_ZOOMS:
|
||||
n = 1 << dz
|
||||
tol = 1.0 if dz == DATA_ZOOMS[-1] else 4.0 # half a pixel at the deepest zoom drawn from it
|
||||
tiles = defaultdict(list)
|
||||
for cls, col, kind, parts in feats:
|
||||
if MIN_DZ.get(cls, 0) > dz:
|
||||
continue
|
||||
allp = [p for part in parts for p in part]
|
||||
if not allp:
|
||||
continue
|
||||
x0 = int(min(p[0] for p in allp) * n); x1 = int(max(p[0] for p in allp) * n)
|
||||
y0 = int(min(p[1] for p in allp) * n); y1 = int(max(p[1] for p in allp) * n)
|
||||
for tx in range(x0, x1 + 1):
|
||||
for ty in range(y0, y1 + 1):
|
||||
out = []
|
||||
for part in parts:
|
||||
loc = [((p[0] * n - tx) * EXTENT, (p[1] * n - ty) * EXTENT) for p in part]
|
||||
if kind == 'poly':
|
||||
r = clip_ring(loc, -BUFFER, EXTENT + BUFFER)
|
||||
r = simplify(r, tol)
|
||||
if len(r) >= 3 and (max(p[0] for p in r) - min(p[0] for p in r) > 4 * tol or
|
||||
max(p[1] for p in r) - min(p[1] for p in r) > 4 * tol): # not a speck
|
||||
out.append(r)
|
||||
else:
|
||||
for piece in clip_line(loc, -BUFFER, EXTENT + BUFFER):
|
||||
piece = simplify(piece, tol)
|
||||
if len(piece) >= 2:
|
||||
out.append(piece)
|
||||
if out:
|
||||
tiles[(tx, ty)].append((cls, col, out))
|
||||
for (tx, ty), fl in tiles.items():
|
||||
fl.sort(key=lambda f: f[0])
|
||||
buf = bytearray(b'VT2' + bytes([dz]) + struct.pack('<H', 0))
|
||||
count = 0
|
||||
for cls, col, parts in fl:
|
||||
for i in range(0, len(parts), 255): # nparts is a byte
|
||||
chunk = parts[i:i + 255]
|
||||
qs = []
|
||||
for part in chunk:
|
||||
q = []
|
||||
for x, y in part:
|
||||
p = (int(round(x)), int(round(y)))
|
||||
if not q or q[-1] != p:
|
||||
q.append(p)
|
||||
qs.append(q)
|
||||
allq = [p for q in qs for p in q]
|
||||
buf += struct.pack('<BBHhhhh', cls, len(chunk), col, min(p[0] for p in allq), min(p[1] for p in allq),
|
||||
max(p[0] for p in allq), max(p[1] for p in allq))
|
||||
for q in qs:
|
||||
buf += struct.pack('<H', len(q))
|
||||
for x, y in q:
|
||||
buf += struct.pack('<hh', x, y)
|
||||
count += 1
|
||||
struct.pack_into('<H', buf, 4, min(count, 65535))
|
||||
d = os.path.join(a.out, str(dz), str(tx))
|
||||
os.makedirs(d, exist_ok=True)
|
||||
with open(os.path.join(d, f'{ty}.vt'), 'wb') as f:
|
||||
f.write(buf)
|
||||
total_bytes += len(buf)
|
||||
total_tiles += 1
|
||||
with open(os.path.join(a.out, 'attribution.txt'), 'w') as f:
|
||||
f.write('© OpenStreetMap contributors (ODbL)\n')
|
||||
print(f'{total_tiles} tiles, {total_bytes / 1024:.0f} KB', file=sys.stderr)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user