mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-10-09 03:06:39 +00:00
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:
@@ -17,6 +17,7 @@
|
||||
#include <math.h>
|
||||
#include "map/TileProvider.h"
|
||||
#include "map/VectorTileProvider.h"
|
||||
#include "map/VectorLabels.h"
|
||||
#include "map/TileCache.h"
|
||||
#include "map/LiveCache.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)
|
||||
}
|
||||
|
||||
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.
|
||||
_map_marks = lv_obj_create(body);
|
||||
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
|
||||
int32_t lat, lon;
|
||||
if (_core->course.currentLocation(lat, lon)) {
|
||||
@@ -527,6 +533,7 @@ void UITask::mapLoop() {
|
||||
if (!_map_area) return;
|
||||
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 (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);
|
||||
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
|
||||
|
||||
@@ -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,
|
||||
// {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
|
||||
// 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.
|
||||
//
|
||||
// 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(); }
|
||||
bool hasData() const { return _have; }
|
||||
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; }
|
||||
@@ -130,7 +131,13 @@ private:
|
||||
}
|
||||
|
||||
// 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) {
|
||||
switch (c) {
|
||||
case 1: return rgb(0xE6DED6); // residential
|
||||
@@ -148,6 +155,7 @@ private:
|
||||
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_PATH_HARD: col = rgb(0x8A3A1E); w = 1.2f; return true; // dotted
|
||||
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
|
||||
@@ -186,21 +194,22 @@ private:
|
||||
_end = p;
|
||||
_bx = bb[0]; _by = bb[1];
|
||||
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 (_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.
|
||||
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; }
|
||||
if (cls == L_ROUTES) { strokeRoutes(parts, nparts, col); _t_line += 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);
|
||||
strokeFeature(parts, nparts, c, w, cls == L_PATH_HARD);
|
||||
_t_line += micros() - t0;
|
||||
}
|
||||
}
|
||||
@@ -323,11 +332,12 @@ private:
|
||||
}
|
||||
|
||||
// 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;
|
||||
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) {
|
||||
if (dots && (_dash++ & 3) >= 2) continue; // 2 px on, 2 off, carried across segments
|
||||
int px = x >> 12, py = y >> 12;
|
||||
for (int oy = 0; oy < pen; oy++)
|
||||
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
|
||||
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;
|
||||
});
|
||||
for (int i = 1; i < n; i++) {
|
||||
int32_t ax = pts[2 * i - 2], ay = pts[2 * i - 1], bx = pts[2 * i], by = pts[2 * i + 1];
|
||||
if (vmax(ax, bx) < -h || vmin(ax, bx) > S + h || vmax(ay, by) < -h || vmin(ay, by) > S + h) continue;
|
||||
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, dots);
|
||||
continue;
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user