feat(ui-lvgl): vector map route stripes and point labels

Hiking routes are stored per stretch with the waymark colours of every
route along it and drawn as side-by-side stripes on a white band (half
the route ways in the Tatra sample carry two or more routes), less
simplified; demanding / alpine paths are dotted. Named points (places,
peaks with height, huts, passes, springs, viewpoints, caves) come in
small .vp files and are placed by priority as a layer over the tiles,
clear of each other and of the map controls.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-09-27 11:25:08 +02:00
co-authored by Claude Opus 5.5
parent ba4e13e2b2
commit 117b7513bc
5 changed files with 614 additions and 47 deletions
+7 -3
View File
@@ -259,9 +259,13 @@ Ideas to come back to (2026-09-26):
scanline polygons + thick lines drawn into the 256 px tile; behind Map scanline polygons + thick lines drawn into the 256 px tile; behind Map
tools > Vector map (test), raster where no vector data. Measured on the 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). L2: 8-60 ms drawing, SD read ~30 ms per new data file (cached after).
- [ ] Vector maps next: delta / varint coordinates (smaller files, faster - [x] Vector maps: VT3 (delta / varint points, ~half the size); hiking
read), labels and POIs (peaks, shelters, springs), contours, a PBF routes per stretch, the routes sharing it as side-by-side stripes on a
pipeline for regions + download on the device, styling. white band, less simplified; demanding / alpine paths dotted; labels of
named points (places, peaks with height, huts, passes, springs...) as
a layer over the tiles, placed by priority. No street names (raster).
- [ ] Vector maps next: contours, a PBF pipeline for regions + download on
the device, styling.
User list, second batch (2026-09-26): User list, second batch (2026-09-26):
@@ -17,6 +17,7 @@
#include <math.h> #include <math.h>
#include "map/TileProvider.h" #include "map/TileProvider.h"
#include "map/VectorTileProvider.h" #include "map/VectorTileProvider.h"
#include "map/VectorLabels.h"
#include "map/TileCache.h" #include "map/TileCache.h"
#include "map/LiveCache.h" #include "map/LiveCache.h"
#include "map/TileDownloader.h" #include "map/TileDownloader.h"
@@ -313,6 +314,8 @@ void UITask::buildMap() {
lv_image_set_pivot(_map_tiles[i], 0, 0); // magnify from the top-left (overzoom) lv_image_set_pivot(_map_tiles[i], 0, 0); // magnify from the top-left (overzoom)
} }
mapview::labels::buildLayer(body); // vector map names, over the tiles, under the markers
// Marker layer: same size as the map, lets presses through to it. // Marker layer: same size as the map, lets presses through to it.
_map_marks = lv_obj_create(body); _map_marks = lv_obj_create(body);
lv_obj_remove_style_all(_map_marks); lv_obj_remove_style_all(_map_marks);
@@ -467,6 +470,9 @@ void UITask::layoutMap() {
} }
} }
mapview::labels::layout(left, top, w, h, _map_z, have_provider && mapview::s_provider == &mapview::s_vector && mapview::s_vector.hasData(),
_map_nav ? navmap::BAR_H : 0);
// Own position // Own position
int32_t lat, lon; int32_t lat, lon;
if (_core->course.currentLocation(lat, lon)) { if (_core->course.currentLocation(lat, lon)) {
@@ -527,6 +533,7 @@ void UITask::mapLoop() {
if (!_map_area) return; if (!_map_area) return;
uint32_t since_pan = millis() - mapview::s_last_pan_ms; uint32_t since_pan = millis() - mapview::s_last_pan_ms;
if (since_pan < mapview::PAN_SETTLE_MS) return; // mid-drag: keep it smooth, decode after if (since_pan < mapview::PAN_SETTLE_MS) return; // mid-drag: keep it smooth, decode after
if (mapview::labels::loadOne()) { layoutMap(); return; } // names of the view first: a small file
int w = lv_obj_get_width(_map_area), h = lv_obj_get_height(_map_area); int w = lv_obj_get_width(_map_area), h = lv_obj_get_height(_map_area);
double left = _map_cx * mapview::TILE_PX - w / 2.0, top = _map_cy * mapview::TILE_PX - h / 2.0; double left = _map_cx * mapview::TILE_PX - w / 2.0, top = _map_cy * mapview::TILE_PX - h / 2.0;
if (!_map_pending) { // view complete: when idle, decode the next tile a pan would reveal if (!_map_pending) { // view complete: when idle, decode the next tile a pan would reveal
@@ -0,0 +1,302 @@
#pragma once
// Labels of the vector map: named points (places, peaks with their height,
// huts, springs...) from the {dz}/{x}/{y}.vp files beside the vector tiles
// (tools/maps/osm_vector.py). Drawn over the tiles, not into them, so a name
// is never cut at a tile edge and stays crisp at any zoom: a layer under the
// markers, placed again on every layout -- by priority (towns, the highest
// peaks, huts first), each where it doesn't cover one already placed or the
// map's controls. No street names (the raster map has those).
//
// Files are read one per map loop pass while the map rests (they're small),
// never inside a layout, so panning doesn't wait for the card.
//
// Single-TU fragment: included by ui-lvgl/MapScreen.h after VectorTileProvider.h.
namespace mapview {
namespace labels {
enum : uint8_t { T_TOWN = 60, T_VILLAGE, T_HUT, T_PEAK, T_LAKE, T_HAMLET, T_PASS, T_SHELTER, T_SPRING, T_VIEW, T_CAVE };
static const int LABEL_MAX = 40;
static const int16_t NO_ELE = -32768;
struct Poi { uint8_t cls; int16_t ele; uint16_t u, v; char name[LABEL_MAX + 1]; };
// Points of the data tiles read last (PSRAM), least recently used replaced.
struct PoiTile { int dz = -1, x = 0, y = 0; Poi* p = nullptr; int n = 0, cap = 0; uint32_t used = 0; };
static const int SLOTS = 12;
static PoiTile s_tiles[SLOTS];
static uint32_t s_tick = 0;
static PoiTile* find(int dz, int x, int y) {
for (PoiTile& t : s_tiles) if (t.dz == dz && t.x == x && t.y == y) { t.used = ++s_tick; return &t; }
return nullptr;
}
// Reads a tile's points (none if it has no file: remembered too).
static void load(int dz, int x, int y) {
PoiTile* t = &s_tiles[0];
for (PoiTile& s : s_tiles) if (s.used < t->used) t = &s;
t->dz = dz; t->x = x; t->y = y; t->n = 0; t->used = ++s_tick;
char path[64];
snprintf(path, sizeof(path), "%s/%d/%d/%d.vp", VECTOR_ROOT, dz, x, y);
FILE* f = fopen(path, "rb");
if (!f) return;
uint8_t h[6];
if (fread(h, 1, 6, f) != 6 || memcmp(h, "VP1", 3) != 0) { fclose(f); return; }
int count = h[4] | (h[5] << 8);
if (count > t->cap) {
free(t->p);
t->p = psramBuf<Poi>(count);
t->cap = t->p ? count : 0;
}
while (t->n < t->cap && t->n < count) {
uint8_t r[8];
if (fread(r, 1, 8, f) != 8) break;
Poi& p = t->p[t->n];
p.cls = r[0];
p.u = r[1] | (r[2] << 8);
p.v = r[3] | (r[4] << 8);
p.ele = (int16_t)(r[5] | (r[6] << 8));
int len = r[7] > LABEL_MAX ? LABEL_MAX : r[7];
if (fread(p.name, 1, len, f) != (size_t)len) break;
if (r[7] > len) fseek(f, r[7] - len, SEEK_CUR);
p.name[len] = '\0';
t->n++;
}
fclose(f);
}
// What layout() placed, for the draw callback (names copied: a later read
// may reuse the tile's memory before LVGL draws).
struct Placed {
int16_t px, py; // the point, layer px
int16_t tx, ty; // the text's top-left
uint8_t cls;
char text[LABEL_MAX + 8];
};
static const int MAX_PLACED = 40;
static Placed* s_placed = psramBuf<Placed>(MAX_PLACED);
static int s_n = 0;
static int s_want_dz = -1, s_want_x = 0, s_want_y = 0; // a tile to read next, dz -1: none
static lv_obj_t* s_layer = nullptr;
static const lv_font_t* fontFor(uint8_t cls) { return cls == T_TOWN || cls == T_VILLAGE ? THEME_FONT_BODY : THEME_FONT_SMALL; }
static uint32_t colourFor(uint8_t cls) {
switch (cls) {
case T_LAKE: case T_SPRING: return 0x2A6A9A;
case T_HUT: case T_SHELTER: return 0x8A2A1A;
case T_PEAK: case T_PASS: return 0x5A3A20;
case T_HAMLET: return 0x55524C;
}
return 0x2A2824;
}
static bool hasIcon(uint8_t cls) { return cls >= T_HUT && cls != T_LAKE && cls != T_HAMLET; }
// Candidates of the view, highest priority first.
struct Cand { const Poi* p; int16_t x, y; };
static const int MAX_CAND = 512;
static Cand* s_cand = psramBuf<Cand>(MAX_CAND);
// Placing order: places, then peaks (the highest first), then huts... -- in
// the mountains the peaks are what you find your way by.
static int priority(uint8_t cls) { return cls == T_PEAK ? 2 * T_VILLAGE + 1 : cls == T_HUT ? 2 * T_VILLAGE + 2 : 2 * cls; }
static int cmpCand(const void* a, const void* b) {
const Poi* p = ((const Cand*)a)->p;
const Poi* q = ((const Cand*)b)->p;
if (p->cls != q->cls) return priority(p->cls) - priority(q->cls);
return q->ele - p->ele; // higher peaks first
}
static void clear() {
s_n = 0;
s_want_dz = -1;
}
// Places the labels for a view (world px of its top-left at zoom z, size;
// `bottom` px taken by a bar along the bottom).
static void layout(double left, double top, int w, int h, int z, bool on, int bottom) {
clear();
if (!on || z < 10 || !s_layer || !s_placed || !s_cand) { if (s_layer) lv_obj_invalidate(s_layer); return; }
int dz = z >= 14 ? 14 : z >= 12 ? 12 : 10, k = z - dz;
double span = (double)(TILE_PX << k); // a data tile, in px at zoom z
int x0 = (int)floor(left / span), x1 = (int)floor((left + w) / span);
int y0 = (int)floor(top / span), y1 = (int)floor((top + h) / span);
int nc = 0;
for (int ty = y0; ty <= y1; ty++)
for (int tx = x0; tx <= x1; tx++) {
PoiTile* t = find(dz, tx, ty);
if (!t) { // read in mapLoop, one per pass
if (s_want_dz < 0) { s_want_dz = dz; s_want_x = tx; s_want_y = ty; }
continue;
}
for (int i = 0; i < t->n && nc < MAX_CAND; i++) {
const Poi& p = t->p[i];
double px = (tx + p.u / 4096.0) * span - left, py = (ty + p.v / 4096.0) * span - top;
if (px < 0 || py < 0 || px >= w || py >= h) continue;
s_cand[nc++] = { &p, (int16_t)lround(px), (int16_t)lround(py) };
}
}
qsort(s_cand, nc, sizeof(Cand), cmpCand);
// Greedy placement: the icon, then the text right / left / above / below it.
struct Box { int16_t x1, y1, x2, y2; };
static Box used[MAX_PLACED * 2 + 8];
int nu = 0;
// The map's controls: button columns, the zoom pill between them, scale /
// credit and the centre button above the bottom bar, the bar.
const int16_t W = (int16_t)w, H = (int16_t)(h - bottom);
used[nu++] = { 0, 0, 50, 144 };
used[nu++] = { (int16_t)(W - 50), 0, W, 144 };
used[nu++] = { 0, 0, W, 36 };
used[nu++] = { 0, (int16_t)(H - 34), 180, (int16_t)h };
used[nu++] = { (int16_t)(W - 50), (int16_t)(H - 50), W, (int16_t)h };
if (bottom) used[nu++] = { 0, H, W, (int16_t)h };
auto clearOf = [&](const Box& b) {
if (b.x1 < 2 || b.y1 < 2 || b.x2 > w - 2 || b.y2 > h - 2) return false;
for (int i = 0; i < nu; i++)
if (b.x1 <= used[i].x2 && b.x2 >= used[i].x1 && b.y1 <= used[i].y2 && b.y2 >= used[i].y1) return false;
return true;
};
for (int c = 0; c < nc && s_n < MAX_PLACED; c++) {
const Poi& p = *s_cand[c].p;
int px = s_cand[c].x, py = s_cand[c].y;
Placed& L = s_placed[s_n];
if (p.name[0] && p.ele != NO_ELE && (p.cls == T_PEAK || p.cls == T_PASS)) snprintf(L.text, sizeof(L.text), "%s %d", p.name, p.ele);
else if (p.name[0]) snprintf(L.text, sizeof(L.text), "%s", p.name);
else if (p.ele != NO_ELE) snprintf(L.text, sizeof(L.text), "%d", p.ele);
else continue;
const lv_font_t* font = fontFor(p.cls);
lv_point_t sz;
lv_text_get_size(&sz, L.text, font, 0, 0, LV_COORD_MAX, LV_TEXT_FLAG_NONE);
int tw = sz.x, th = sz.y;
bool icon = hasIcon(p.cls);
Box ib = { (int16_t)(px - 5), (int16_t)(py - 5), (int16_t)(px + 5), (int16_t)(py + 5) };
if (icon && !clearOf(ib)) continue;
struct { int x, y; } at[4];
int na = 0;
if (icon) {
at[na++] = { px + 7, py - th / 2 };
at[na++] = { px - 7 - tw, py - th / 2 };
at[na++] = { px - tw / 2, py - 6 - th };
at[na++] = { px - tw / 2, py + 6 };
} else {
at[na++] = { px - tw / 2, py - th / 2 }; // a place name sits on the place
}
int pick = -1;
Box tb = {};
for (int i = 0; i < na && pick < 0; i++) {
tb = { (int16_t)(at[i].x - 1), (int16_t)(at[i].y), (int16_t)(at[i].x + tw + 1), (int16_t)(at[i].y + th - 1) };
if (clearOf(tb)) pick = i;
}
if (pick < 0) {
if (!icon) continue;
if (p.cls != T_HUT && dz < 14) continue; // far out: a bare icon is just clutter
if (p.cls >= T_SPRING) continue; // minor: no room, not shown
L.text[0] = '\0'; // the icon alone still tells there's a peak / hut
}
L.px = px; L.py = py;
L.tx = pick < 0 ? 0 : at[pick].x;
L.ty = pick < 0 ? 0 : at[pick].y;
L.cls = p.cls;
if (icon) used[nu++] = ib;
if (pick >= 0) used[nu++] = tb;
s_n++;
}
lv_obj_invalidate(s_layer);
}
// A tile layout() found missing, read now; true if one was.
static bool loadOne() {
if (s_want_dz < 0) return false;
load(s_want_dz, s_want_x, s_want_y);
s_want_dz = -1;
return true;
}
static void icon(lv_layer_t* layer, uint8_t cls, int32_t x, int32_t y) {
lv_draw_rect_dsc_t r;
lv_draw_rect_dsc_init(&r);
r.bg_opa = LV_OPA_COVER;
r.border_color = lv_color_hex(0xFFFFFF);
r.border_opa = LV_OPA_COVER;
lv_draw_triangle_dsc_t t;
lv_draw_triangle_dsc_init(&t);
t.bg_opa = LV_OPA_COVER;
lv_area_t a;
auto tri = [&](uint32_t col, int32_t x0, int32_t y0, int32_t x1, int32_t y1, int32_t x2, int32_t y2) {
t.bg_color = lv_color_hex(col);
t.p[0] = { (lv_value_precise_t)x0, (lv_value_precise_t)y0 };
t.p[1] = { (lv_value_precise_t)x1, (lv_value_precise_t)y1 };
t.p[2] = { (lv_value_precise_t)x2, (lv_value_precise_t)y2 };
lv_draw_triangle(layer, &t);
};
switch (cls) {
case T_PEAK: // a triangle with a light rim
tri(0xFFFFFF, x, y - 6, x - 6, y + 4, x + 6, y + 4);
tri(0x5A3A20, x, y - 4, x - 4, y + 3, x + 4, y + 3);
return;
case T_PASS: // a short bar
r.bg_color = lv_color_hex(0x5A3A20);
r.border_width = 1;
a = { x - 4, y - 2, x + 4, y + 1 };
lv_draw_rect(layer, &r, &a);
return;
case T_HUT: case T_SHELTER: { // a house: roof over a square
uint32_t c = cls == T_HUT ? 0xC0301E : 0x8A5A2A;
r.bg_color = lv_color_hex(c);
r.border_width = 1;
a = { x - 4, y - 1, x + 4, y + 5 };
lv_draw_rect(layer, &r, &a);
tri(c, x, y - 6, x - 6, y, x + 6, y);
return;
}
case T_SPRING: case T_VIEW: case T_CAVE:
r.bg_color = lv_color_hex(cls == T_SPRING ? 0x2A7AC8 : cls == T_VIEW ? 0xE08A1C : 0x302A26);
r.radius = LV_RADIUS_CIRCLE;
r.border_width = 1;
a = { x - 3, y - 3, x + 3, y + 3 };
lv_draw_rect(layer, &r, &a);
return;
}
}
// LV_EVENT_DRAW_MAIN of the layer.
static void draw(lv_event_t* e) {
if (!s_n) return;
lv_obj_t* o = (lv_obj_t*)lv_event_get_target(e);
lv_layer_t* layer = lv_event_get_layer(e);
lv_area_t a;
lv_obj_get_coords(o, &a);
for (int i = 0; i < s_n; i++) {
const Placed& L = s_placed[i];
if (hasIcon(L.cls)) icon(layer, L.cls, a.x1 + L.px, a.y1 + L.py);
if (!L.text[0]) continue;
lv_draw_label_dsc_t d;
lv_draw_label_dsc_init(&d);
d.font = fontFor(L.cls);
d.text = L.text;
lv_area_t t = { a.x1 + L.tx, a.y1 + L.ty, a.x1 + L.tx + 400, a.y1 + L.ty + 40 };
d.color = lv_color_hex(0xF7F4EE); // a halo: the text in paper colour around it
static const int8_t OFF[4][2] = { { -1, 0 }, { 1, 0 }, { 0, -1 }, { 0, 1 } };
for (const auto& off : OFF) {
lv_area_t h = t;
lv_area_move(&h, off[0], off[1]);
lv_draw_label(layer, &d, &h);
}
d.color = lv_color_hex(colourFor(L.cls));
lv_draw_label(layer, &d, &t);
}
}
static void buildLayer(lv_obj_t* body) {
clear();
s_layer = lv_obj_create(body);
lv_obj_remove_style_all(s_layer);
lv_obj_set_size(s_layer, LV_PCT(100), LV_PCT(100));
lv_obj_remove_flag(s_layer, LV_OBJ_FLAG_CLICKABLE);
lv_obj_remove_flag(s_layer, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_add_event_cb(s_layer, draw, LV_EVENT_DRAW_MAIN, NULL);
}
} // namespace labels
} // namespace mapview
@@ -2,7 +2,7 @@
// Vector map (spike): tiles from tools/maps/osm_vector.py under VECTOR_ROOT, // Vector map (spike): tiles from tools/maps/osm_vector.py under VECTOR_ROOT,
// {dz}/{x}/{y}.vt (format VT3) at data zooms 10, 12 and 14, drawn here into the 256x256 RGB565 // {dz}/{x}/{y}.vt (format VT3) 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 // 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 // with round joins. No anti-aliasing; names of points in VectorLabels.h. Where there is no vector
// data (or below zoom 10) the raster provider draws instead, so the two mix. // 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 // renderTile() times itself (lastMs()); the map shows it while this provider
@@ -48,6 +48,7 @@ public:
} }
const char* attribution() const override { return _have ? "\xC2\xA9 OpenStreetMap contributors (ODbL)" : _fb.attribution(); } const char* attribution() const override { return _have ? "\xC2\xA9 OpenStreetMap contributors (ODbL)" : _fb.attribution(); }
bool hasData() const { return _have; }
uint32_t lastMs() const { return _last_ms; } uint32_t lastMs() const { return _last_ms; }
// The last tile's time split: reading the data, areas, lines (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; } void lastSplit(uint32_t& load, uint32_t& fill, uint32_t& line) const { load = _t_load / 1000; fill = _t_fill / 1000; line = _t_line / 1000; }
@@ -130,7 +131,13 @@ private:
} }
// Styles. Widths in px at zoom 14, scaled with the zoom. // 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 }; enum : uint8_t { L_STREAM = 20, L_RIVER = 21, L_PATH_HARD = 29, L_PATH = 30, L_TRACK = 31, L_SERVICE = 32, L_TRUNK = 37,
L_ROUTE = 50, L_ROUTES = 51 };
// Waymark colours of L_ROUTES (index 1.., osm_vector.py PALETTE).
static uint16_t routeColour(int i) {
static const uint32_t P[] = { 0xE0302A, 0x2A5FE0, 0x2EA043, 0xE8C20E, 0x202020, 0xF08A1C, 0x9040C0, 0xF0F0F0, 0x8B5A2B, 0xD04040 };
return rgb(i >= 1 && i <= 10 ? P[i - 1] : 0xD04040);
}
static uint16_t polyColour(uint8_t c) { static uint16_t polyColour(uint8_t c) {
switch (c) { switch (c) {
case 1: return rgb(0xE6DED6); // residential case 1: return rgb(0xE6DED6); // residential
@@ -148,6 +155,7 @@ private:
case L_STREAM: col = rgb(0x86B6D8); w = 1.0f; return true; case L_STREAM: col = rgb(0x86B6D8); w = 1.0f; return true;
case L_RIVER: col = rgb(0x86B6D8); w = 3.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_PATH: col = rgb(0xA8502A); w = 1.2f; return true;
case L_PATH_HARD: col = rgb(0x8A3A1E); w = 1.2f; return true; // dotted
case L_TRACK: col = rgb(0x94683A); w = 1.6f; return true; case L_TRACK: col = rgb(0x94683A); w = 1.6f; return true;
case 32: col = 0xFFFF; w = 2.2f; return true; // service case 32: col = 0xFFFF; w = 2.2f; return true; // service
case 33: col = 0xFFFF; w = 3.2f; return true; // minor case 33: col = 0xFFFF; w = 3.2f; return true; // minor
@@ -186,21 +194,22 @@ private:
_end = p; _end = p;
_bx = bb[0]; _by = bb[1]; _bx = bb[0]; _by = bb[1];
bool road = cls >= L_SERVICE && cls <= L_TRUNK; bool road = cls >= L_SERVICE && cls <= L_TRUNK;
bool want = pass == 0 ? (cls < L_SERVICE) : pass == 3 ? cls == L_ROUTE : road; bool want = pass == 0 ? (cls < L_SERVICE) : pass == 3 ? (cls == L_ROUTE || cls == L_ROUTES) : road;
if (!want) continue; if (!want) continue;
if (_z < 13 && (cls == L_PATH || cls == 7)) continue; // paths from z13, buildings from z14 (data) if (_z < 13 && (cls == L_PATH || cls == L_PATH_HARD || cls == 7)) continue; // paths from z13, buildings from z14 (data)
// Off the drawn tile (with a margin for the widest line): not even read. // Off the drawn tile (with a margin for the widest line): not even read.
const int32_t M = 12 * 16, S = TILE_PX * 16; 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; if (sx(bb[2]) < -M || sx(bb[0]) > S + M || sy(bb[3]) < -M || sy(bb[1]) > S + M) continue;
uint32_t t0 = micros(); uint32_t t0 = micros();
if (cls < 10) { fillFeature(parts, nparts, polyColour(cls)); _t_fill += micros() - t0; continue; } if (cls < 10) { fillFeature(parts, nparts, polyColour(cls)); _t_fill += micros() - t0; continue; }
if (cls == L_ROUTES) { strokeRoutes(parts, nparts, col); _t_line += micros() - t0; continue; }
uint16_t c; uint16_t c;
float w; float w;
if (!lineStyle(cls, c, w)) continue; if (!lineStyle(cls, c, w)) continue;
w *= zoomScale(); w *= zoomScale();
if (cls == L_ROUTE) { c = col ? col : rgb(0xD04040); w = w < 2.5f ? 2.5f : w; } 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 if (pass == 1) { c = rgb(0xB4ACA2); w += 1.6f; } // casing
strokeFeature(parts, nparts, c, w); strokeFeature(parts, nparts, c, w, cls == L_PATH_HARD);
_t_line += micros() - t0; _t_line += micros() - t0;
} }
} }
@@ -323,11 +332,12 @@ private:
} }
// A thin line: a DDA with a 1 or 2 px pen. // 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) { void thinSegment(int32_t ax, int32_t ay, int32_t bx, int32_t by, int pen, uint16_t col, bool dots = false) {
int32_t dx = bx - ax, dy = by - ay; int32_t dx = bx - ax, dy = by - ay;
int steps = (vmax(abs(dx), abs(dy)) >> 4) + 1; 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 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) { for (int i = 0; i <= steps; i++, x += ix, y += iy) {
if (dots && (_dash++ & 3) >= 2) continue; // 2 px on, 2 off, carried across segments
int px = x >> 12, py = y >> 12; int px = x >> 12, py = y >> 12;
for (int oy = 0; oy < pen; oy++) for (int oy = 0; oy < pen; oy++)
for (int ox = 0; ox < pen; ox++) { for (int ox = 0; ox < pen; ox++) {
@@ -337,33 +347,112 @@ private:
} }
} }
void strokeFeature(const uint8_t* p, int nparts, uint16_t col, float w) { // A part's points (1/16 px) for the stroke, and an offset copy of them.
static const int PTS = 2048;
int32_t* _pts = nullptr; // x, y pairs
int32_t* _off = nullptr;
uint32_t _dash = 0;
bool reservePts() {
if (!_pts) _pts = psramBuf<int32_t>(2 * PTS);
if (!_off) _off = psramBuf<int32_t>(2 * PTS);
return _pts && _off;
}
// Strokes each part: its points gathered (in runs of PTS), then `fn(n)`.
template <typename F> void forParts(const uint8_t* p, int nparts, F fn) {
for (int i = 0; i < nparts; i++) {
int n = 0;
forPoints(p, true, [&](int32_t x, int32_t y, bool) {
if (n == PTS) { // a very long part: draw what's gathered, go on from its end
fn(n);
_pts[0] = _pts[2 * (n - 1)]; _pts[1] = _pts[2 * (n - 1) + 1];
n = 1;
}
_pts[2 * n] = x; _pts[2 * n + 1] = y;
n++;
});
if (n >= 2) fn(n);
}
}
void strokeFeature(const uint8_t* p, int nparts, uint16_t col, float w, bool dots = false) {
if (!reservePts()) return;
_dash = 0;
forParts(p, nparts, [&](int n) { strokePoints(_pts, n, col, w, dots); });
}
// `pts` moved sideways by `off` (1/16 px, + to the left of the direction),
// corners mitred (limited, so a hairpin doesn't shoot out) into _off.
void offsetPoints(const int32_t* pts, int n, float off) {
float pnx = 0, pny = 0;
for (int i = 0; i < n; i++) {
float nx = 0, ny = 0; // the next segment's normal
if (i + 1 < n) {
float dx = pts[2 * i + 2] - pts[2 * i], dy = pts[2 * i + 3] - pts[2 * i + 1], len = sqrtf(dx * dx + dy * dy);
if (len > 0) { nx = dy / len; ny = -dx / len; }
} else { nx = pnx; ny = pny; }
if (i == 0) { pnx = nx; pny = ny; }
float mx = pnx + nx, my = pny + ny, m2 = mx * mx + my * my;
float ox = nx, oy = ny;
if (m2 > 0.01f) {
float k = 2.0f / m2; // mitre: (n1 + n2) / (1 + n1.n2)
if (k > 4.0f) k = 4.0f;
ox = mx * k; oy = my * k;
}
_off[2 * i] = pts[2 * i] + (int32_t)(ox * off);
_off[2 * i + 1] = pts[2 * i + 1] + (int32_t)(oy * off);
pnx = nx; pny = ny;
}
}
// Hiking routes along a stretch: a stripe per route, side by side on a
// white band, in PALETTE order.
void strokeRoutes(const uint8_t* p, int nparts, uint16_t packed) {
if (!reservePts()) return;
uint16_t cols[4];
int k = 0;
for (int i = 0; i < 4; i++) {
int c = (packed >> (4 * i)) & 15;
if (c) cols[k++] = routeColour(c);
}
if (!k) return;
float sw = 2.2f * zoomScale(); // a stripe
sw = sw < 1.6f ? 1.6f : sw > 4.5f ? 4.5f : sw;
forParts(p, nparts, [&](int n) {
strokePoints(_pts, n, 0xFFFF, k * sw + 1.6f, false);
for (int i = 0; i < k; i++) {
if (k == 1) { strokePoints(_pts, n, cols[0], sw, false); break; }
offsetPoints(_pts, n, (i - (k - 1) / 2.0f) * sw * 16);
strokePoints(_off, n, cols[i], sw, false);
}
});
}
void strokePoints(const int32_t* pts, int n, uint16_t col, float w, bool dots) {
int32_t h = (int32_t)(w * 8); // half width, 1/16 px int32_t h = (int32_t)(w * 8); // half width, 1/16 px
const int32_t S = TILE_PX * 16; const int32_t S = TILE_PX * 16;
bool thin = w < 2.2f; bool thin = w < 2.2f;
int pen = w < 1.8f ? 1 : 2; int pen = w < 1.8f ? 1 : 2;
for (int i = 0; i < nparts; i++) { for (int i = 1; i < n; i++) {
int32_t ax = 0, ay = 0; int32_t ax = pts[2 * i - 2], ay = pts[2 * i - 1], bx = pts[2 * i], by = pts[2 * i + 1];
forPoints(p, true, [&](int32_t bx, int32_t by, bool first) { if (vmax(ax, bx) < -h || vmin(ax, bx) > S + h || vmax(ay, by) < -h || vmin(ay, by) > S + h) continue;
if (!first && !(vmax(ax, bx) < -h || vmin(ax, bx) > S + h || vmax(ay, by) < -h || vmin(ay, by) > S + h)) { if (thin) {
if (thin) { if (pen == 2) { ax -= 8; ay -= 8; bx -= 8; by -= 8; } // a 2 px pen centred on the line
thinSegment(ax, ay, bx, by, pen, col); thinSegment(ax, ay, bx, by, pen, col, dots);
} else { continue;
float dx = bx - ax, dy = by - ay, len = sqrtf(dx * dx + dy * dy); }
if (len > 0) { float dx = bx - ax, dy = by - ay, len = sqrtf(dx * dx + dy * dy);
int32_t nx = (int32_t)(-dy / len * h), ny = (int32_t)(dx / len * h); if (len > 0) {
_ne = 0; int32_t nx = (int32_t)(-dy / len * h), ny = (int32_t)(dx / len * h);
addEdge(ax + nx, ay + ny, bx + nx, by + ny); _ne = 0;
addEdge(bx + nx, by + ny, bx - nx, by - ny); addEdge(ax + nx, ay + ny, bx + nx, by + ny);
addEdge(bx - nx, by - ny, ax - nx, ay - ny); addEdge(bx + nx, by + ny, bx - nx, by - ny);
addEdge(ax - nx, ay - ny, ax + nx, ay + ny); addEdge(bx - nx, by - ny, ax - nx, ay - ny);
fillEdges(col); addEdge(ax - nx, ay - ny, ax + nx, ay + ny);
} fillEdges(col);
if (w >= 2.8f) disc(bx, by, h, col); // round join }
} if (w >= 2.8f) disc(bx, by, h, col); // round join
}
ax = bx; ay = by;
});
} }
} }
}; };
+180 -15
View File
@@ -4,9 +4,10 @@
Reads an Overpass JSON export (`out geom;`) and writes small binary tiles the Reads an Overpass JSON export (`out geom;`) and writes small binary tiles the
device rasterises itself (ui-lvgl/map/VectorTileProvider.h): roads, paths, device rasterises itself (ui-lvgl/map/VectorTileProvider.h): roads, paths,
marked hiking routes in their waymark colours, water, forest, meadows, rock, marked hiking routes in their waymark colours, water, forest, meadows, rock,
buildings. No labels yet. buildings; and named points for the labels (places, peaks, huts, springs...).
No street names (the raster map has them).
tools/maps/osm_vector.py area.json --out vmap/ tools/maps/osm_vector.py area.json [points.json] --out vmap/
Three data zooms: 10 (drawn at z10-11), 12 (z12-13), 14 (z14-18); the device picks the 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 data tile covering the tile it draws and scales it. Copy the output folder to
@@ -22,6 +23,16 @@ Tile format 'VT3' (little-endian):
Points are deltas from the previous one; a part's first from (x0, y0). Points are deltas from the previous one; a part's first from (x0, y0).
Coordinates: 0..4096 across the data tile, a little past its edges. Coordinates: 0..4096 across the data tile, a little past its edges.
Polygons: rings, even-odd. Lines: polylines. Polygons: rings, even-odd. Lines: polylines.
Hiking routes (51) are per stretch of path, not per route: `colour` packs
the waymark colours of every route along it (4 bits each, PALETTE index,
lowest nibble first), drawn side by side.
Points 'VP1' beside each tile ({y}.vp, little-endian), every point whose class
starts at or below that data zoom (so a zoom's file is complete on its own):
'V' 'P' '1' dz:u8 count:u16
count x (cls:u8 x:u16 y:u16 ele:i16 (-32768 none) namelen:u8 name (UTF-8))
cls: 60 town, 61 village, 62 hut, 63 peak, 64 lake, 65 hamlet, 66 pass,
67 shelter, 68 spring, 69 viewpoint, 70 cave.
Get the data for a box (south, west, north, east) from Overpass, e.g.: Get the data for a box (south, west, north, east) from Overpass, e.g.:
[out:json][timeout:120]; [out:json][timeout:120];
@@ -33,6 +44,15 @@ Get the data for a box (south, west, north, east) from Overpass, e.g.:
relation["landuse"~"forest|meadow"](S,W,N,E); relation["landuse"~"forest|meadow"](S,W,N,E);
relation["route"="hiking"](S,W,N,E); ); relation["route"="hiking"](S,W,N,E); );
out geom; out geom;
and the points (a second file):
[out:json][timeout:90];
( node["natural"~"^(peak|saddle|spring|cave_entrance)$"](S,W,N,E);
node["tourism"~"^(alpine_hut|wilderness_hut|viewpoint)$"](S,W,N,E);
node["amenity"="shelter"](S,W,N,E);
node["place"~"^(city|town|village|hamlet)$"](S,W,N,E);
way["tourism"~"^(alpine_hut|wilderness_hut)$"](S,W,N,E);
nwr["natural"="water"]["name"](S,W,N,E); );
out center tags;
Map data (c) OpenStreetMap contributors, ODbL. Map data (c) OpenStreetMap contributors, ODbL.
""" """
import argparse, json, math, os, struct, sys import argparse, json, math, os, struct, sys
@@ -46,7 +66,9 @@ DATA_ZOOMS = (10, 12, 14) # drawn at z10-11, z12-13, z14-18
P_RESIDENTIAL, P_MEADOW, P_SCRUB, P_FOREST, P_ROCK, P_WATER, P_BUILDING = 1, 2, 3, 4, 5, 6, 7 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_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_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 L_PATH_HARD = 29 # a path of demanding / alpine difficulty (sac_scale)
L_ROUTE = 50 # (old: one route, its RGB565 colour)
L_ROUTES = 51 # the routes along a stretch
HIGHWAY = { HIGHWAY = {
'path': L_PATH, 'footway': L_PATH, 'steps': L_PATH, 'bridleway': L_PATH, 'cycleway': L_PATH, 'path': L_PATH, 'footway': L_PATH, 'steps': L_PATH, 'bridleway': L_PATH, 'cycleway': L_PATH,
@@ -59,7 +81,69 @@ HIGHWAY = {
'trunk': L_TRUNK, 'trunk_link': L_TRUNK, 'motorway': L_TRUNK, 'motorway_link': L_TRUNK, '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). # 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} MIN_DZ = {P_BUILDING: 14, L_SERVICE: 14, L_PATH: 12, L_PATH_HARD: 12, L_TRACK: 12, L_STREAM: 12, L_MINOR: 12}
HARD_SAC = ('demanding_mountain_hiking', 'alpine_hiking', 'demanding_alpine_hiking', 'difficult_alpine_hiking')
# Points (labels): class, lowest data zoom.
T_TOWN, T_VILLAGE, T_HUT, T_PEAK, T_LAKE, T_HAMLET, T_PASS, T_SHELTER, T_SPRING, T_VIEW, T_CAVE = range(60, 71)
POINT_DZ = {T_TOWN: 10, T_VILLAGE: 10, T_HUT: 10, T_PEAK: 10, T_LAKE: 12, T_HAMLET: 12, T_PASS: 12,
T_SHELTER: 12, T_SPRING: 14, T_VIEW: 14, T_CAVE: 14}
NAME_MAX = 40 # bytes
# Long words the labels shorten (map convention; the rest of the name stays).
ABBREV = (('Schronisko', 'Schr.'), ('Przełęcz', 'Przeł.'), ('Schutzhütte', 'Sch.'), ('Chata', 'Ch.'))
def point_class(t):
n, tour, pl = t.get('natural'), t.get('tourism'), t.get('place')
if pl in ('city', 'town'):
return T_TOWN
if pl == 'village':
return T_VILLAGE
if pl == 'hamlet':
return T_HAMLET
if tour in ('alpine_hut', 'wilderness_hut'):
return T_HUT
if n == 'peak':
return T_PEAK
if n == 'saddle':
return T_PASS
if n == 'water':
return T_LAKE
if t.get('amenity') == 'shelter':
return T_SHELTER
if n == 'spring':
return T_SPRING
if tour == 'viewpoint':
return T_VIEW
if n == 'cave_entrance':
return T_CAVE
return None
def elevation(t):
v = t.get('ele', '').replace(',', '.').split(' ')[0].rstrip('m')
try:
return max(-32767, min(32767, int(round(float(v)))))
except ValueError:
return None
def short_name(name):
if ' / ' in name: # "Świnica / Svinica" on a border: the first one
name = name.split(' / ')[0]
for a, b in ABBREV:
if name.startswith(a + ' ') and len(name) > 16:
name = b + name[len(a):]
b = name.encode('utf-8')
if len(b) <= NAME_MAX:
return b
b = b[:NAME_MAX]
while b and (b[-1] & 0xC0) == 0x80: # a cut multi-byte character
b = b[:-1]
if b and b[-1] >= 0xC0:
b = b[:-1]
return b
# Natural land cover (not roads, buildings, water): its edges are vague # Natural land cover (not roads, buildings, water): its edges are vague
# anyway, so simplified harder -- most of the points are here. # anyway, so simplified harder -- most of the points are here.
@@ -70,6 +154,9 @@ WAYMARK = { # osmc:symbol / colour -> RGB888
'red': 0xE0302A, 'blue': 0x2A5FE0, 'green': 0x2EA043, 'yellow': 0xE8C20E, 'black': 0x202020, 'red': 0xE0302A, 'blue': 0x2A5FE0, 'green': 0x2EA043, 'yellow': 0xE8C20E, 'black': 0x202020,
'orange': 0xF08A1C, 'purple': 0x9040C0, 'white': 0xF0F0F0, 'brown': 0x8B5A2B, 'orange': 0xF08A1C, 'purple': 0x9040C0, 'white': 0xF0F0F0, 'brown': 0x8B5A2B,
} }
# Route colours by index (1..), the device has the same table; also the
# order the stripes go side by side.
PALETTE = ['red', 'blue', 'green', 'yellow', 'black', 'orange', 'purple', 'white', 'brown', 'other']
def varint(v): def varint(v):
@@ -120,20 +207,21 @@ def line_class(t):
def route_colour(t): def route_colour(t):
"""The waymark colour's PALETTE index (1..)."""
c = t.get('colour', '').lower() c = t.get('colour', '').lower()
if c in WAYMARK: if c in WAYMARK:
return WAYMARK[c] return PALETTE.index(c) + 1
sym = t.get('osmc:symbol', '') sym = t.get('osmc:symbol', '')
if sym: if sym:
first = sym.split(':')[0].lower() first = sym.split(':')[0].lower()
if first in WAYMARK: if first in WAYMARK:
return WAYMARK[first] return PALETTE.index(first) + 1
# "white:red_bar" style: the bar's colour # "white:red_bar" style: the bar's colour
for part in sym.split(':')[1:]: for part in sym.split(':')[1:]:
k = part.split('_')[0].lower() k = part.split('_')[0].lower()
if k in WAYMARK and k != 'white': if k in WAYMARK and k != 'white':
return WAYMARK[k] return PALETTE.index(k) + 1
return 0xD04040 return len(PALETTE)
def world(lon, lat): def world(lon, lat):
@@ -174,6 +262,35 @@ def join_rings(segments):
return rings return rings
def join_lines(ways):
"""Polylines -> as few as possible, joined at shared ends."""
segs = [list(w) for w in ways if len(w) >= 2]
out = []
while segs:
line = segs.pop()
changed = True
while changed:
changed = False
for i, s in enumerate(segs):
if s[0] == line[-1]:
line += s[1:]
elif s[-1] == line[-1]:
line += s[-2::-1]
elif s[-1] == line[0]:
line = s[:-1] + line
elif s[0] == line[0]:
line = s[:0:-1] + line
else:
continue
segs.pop(i)
changed = True
break
if line[0][0] > line[-1][0]: # west to east: the stripes keep their sides between stretches
line.reverse()
out.append(line)
return out
def simplify(pts, tol): def simplify(pts, tol):
"""Douglas-Peucker on tile units.""" """Douglas-Peucker on tile units."""
if len(pts) < 3 or tol <= 0: if len(pts) < 3 or tol <= 0:
@@ -271,14 +388,29 @@ def clip_ring(pts, lo, hi):
def main(): def main():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument('json') ap.add_argument('json', nargs='+')
ap.add_argument('--out', default='vmap') ap.add_argument('--out', default='vmap')
a = ap.parse_args() a = ap.parse_args()
data = json.load(open(a.json)) elements = []
for fn in a.json:
elements += json.load(open(fn))['elements']
feats = [] # (cls, colour, 'poly'|'line', [parts in world coords]) feats = [] # (cls, colour, 'poly'|'line', [parts in world coords])
for e in data['elements']: way_routes = defaultdict(set) # way id -> route colours along it
way_geom = {}
points = {} # (cls, name, rounded pos) -> (cls, x, y, ele, name bytes): a node and a way can both be there
for e in elements:
t = e.get('tags', {}) t = e.get('tags', {})
pt = point_class(t)
if pt is not None:
pos = (e['lon'], e['lat']) if 'lat' in e else (e['center']['lon'], e['center']['lat']) if 'center' in e else None
name = t.get('name', '')
ele = elevation(t)
if pos and (name or (pt == T_PEAK and ele is not None)): # unnamed: just peaks, by their height
x, y = world(*pos)
points[(pt, name, round(x * 2e5), round(y * 2e5))] = (pt, x, y, ele, short_name(name))
if 'geometry' not in e and 'members' not in e:
continue
if e['type'] == 'way' and 'geometry' in e: if e['type'] == 'way' and 'geometry' in e:
pts = geom_pts(e['geometry']) pts = geom_pts(e['geometry'])
closed = len(pts) >= 4 and pts[0] == pts[-1] closed = len(pts) >= 4 and pts[0] == pts[-1]
@@ -286,14 +418,18 @@ def main():
if pc: if pc:
feats.append((pc, 0, 'poly', [pts])) feats.append((pc, 0, 'poly', [pts]))
lc = line_class(t) lc = line_class(t)
if lc == L_PATH and t.get('sac_scale') in HARD_SAC:
lc = L_PATH_HARD
if lc and not pc: if lc and not pc:
feats.append((lc, 0, 'line', [pts])) feats.append((lc, 0, 'line', [pts]))
elif e['type'] == 'relation': elif e['type'] == 'relation':
members = e.get('members', []) members = e.get('members', [])
if t.get('route') == 'hiking': if t.get('route') == 'hiking':
col = rgb565(route_colour(t)) col = route_colour(t)
parts = [geom_pts(m['geometry']) for m in members if m.get('type') == 'way' and 'geometry' in m] for m in members:
feats.append((L_ROUTE, col, 'line', [p for p in parts if len(p) >= 2])) if m.get('type') == 'way' and 'geometry' in m:
way_routes[m['ref']].add(col)
way_geom[m['ref']] = geom_pts(m['geometry'])
else: else:
pc = poly_class(t) pc = poly_class(t)
if not pc: if not pc:
@@ -303,7 +439,14 @@ def main():
rings = join_rings(outer) + join_rings(inner) rings = join_rings(outer) + join_rings(inner)
if rings: if rings:
feats.append((pc, 0, 'poly', rings)) feats.append((pc, 0, 'poly', rings))
print(f'{len(feats)} features', file=sys.stderr) bundles = defaultdict(list) # the same routes along adjacent ways: one line
for wid, cols in way_routes.items():
if len(way_geom[wid]) >= 2:
bundles[tuple(sorted(cols))[:4]].append(way_geom[wid])
for cols, ways in bundles.items():
packed = sum(c << (4 * i) for i, c in enumerate(cols))
feats.append((L_ROUTES, packed, 'line', join_lines(ways)))
print(f'{len(feats)} features, {len(way_routes)} route ways', file=sys.stderr)
total_bytes = total_tiles = 0 total_bytes = total_tiles = 0
for dz in DATA_ZOOMS: for dz in DATA_ZOOMS:
@@ -314,6 +457,8 @@ def main():
if MIN_DZ.get(cls, 0) > dz: if MIN_DZ.get(cls, 0) > dz:
continue continue
tol = base_tol * (NATURAL_TOL if cls in NATURAL else 1) tol = base_tol * (NATURAL_TOL if cls in NATURAL else 1)
if cls == L_ROUTES: # routes: close to the ground at every zoom
tol = min(tol, 1.0 if dz >= 12 else 2.0)
allp = [p for part in parts for p in part] allp = [p for part in parts for p in part]
if not allp: if not allp:
continue continue
@@ -372,6 +517,26 @@ def main():
f.write(buf) f.write(buf)
total_bytes += len(buf) total_bytes += len(buf)
total_tiles += 1 total_tiles += 1
for dz in DATA_ZOOMS:
n = 1 << dz
tiles = defaultdict(list)
for pt, x, y, ele, name in points.values():
if POINT_DZ[pt] > dz or (not name and dz < 14): # unnamed peaks only close up
continue
tx, ty = int(x * n), int(y * n)
tiles[(tx, ty)].append((pt, -(ele or 0), min(EXTENT - 1, int((x * n - tx) * EXTENT)),
min(EXTENT - 1, int((y * n - ty) * EXTENT)), ele, name))
for (tx, ty), pl in tiles.items():
pl.sort()
buf = bytearray(b'VP1' + bytes([dz]) + struct.pack('<H', len(pl)))
for pt, _, px, py, ele, name in pl:
buf += struct.pack('<BHHhB', pt, px, py, -32768 if ele is None else ele, len(name)) + name
d = os.path.join(a.out, str(dz), str(tx))
os.makedirs(d, exist_ok=True)
with open(os.path.join(d, f'{ty}.vp'), 'wb') as f:
f.write(buf)
total_bytes += len(buf)
print(f'{len(points)} points', file=sys.stderr)
with open(os.path.join(a.out, 'attribution.txt'), 'w') as f: with open(os.path.join(a.out, 'attribution.txt'), 'w') as f:
f.write('© OpenStreetMap contributors (ODbL)\n') f.write('© OpenStreetMap contributors (ODbL)\n')
print(f'{total_tiles} tiles, {total_bytes / 1024:.0f} KB', file=sys.stderr) print(f'{total_tiles} tiles, {total_bytes / 1024:.0f} KB', file=sys.stderr)