Files
mg4/server/mvt.go
T
nilsandClaude Opus 5.5 6be2ba5193 App 2.39.1, server 2.8.0: map roads from OpenFreeMap, on demand
The public Overpass answered 429/502 and needs ~40 s a tile while the
car gives up after 8 s, so the server cancelled its queries and the
car's retries added load. Now the server fetches only the tiles a car
asks for, as OpenStreetMap vector tiles from OpenFreeMap (MAP_TILES_URL),
decodes the transportation layer with a small reader of its own, and
keeps each tile 30 days on a volume (MAP_CACHE_DIR). A fetch finishes
even after the car has stopped waiting; the car hears 503 with
Retry-After and gets the tile on its next try.

App: OpenFreeMap/OpenMapTiles attribution on the map, retry after 15 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LhzAVaUyqcw5PkGjz4mWW6
2026-10-26 23:54:30 +01:00

279 lines
5.8 KiB
Go

package main
import (
"errors"
"math"
)
// The roads of a Mapbox Vector Tile (spec 2.1) in the OpenMapTiles schema: the line features
// of the "transportation" layer whose "class" the car draws (roadClasses), scaled to
// mapExtent. A minimal protobuf reader, enough for that and nothing more; anything malformed
// is an error, never a guess.
type roadLine struct {
class int
points [][2]int
}
var errBadTile = errors.New("vector tile is malformed")
// pbReader walks protobuf fields.
type pbReader struct {
b []byte
i int
}
func (r *pbReader) done() bool { return r.i >= len(r.b) }
func (r *pbReader) varint() (uint64, error) {
var v uint64
for shift := uint(0); shift < 64; shift += 7 {
if r.i >= len(r.b) {
return 0, errBadTile
}
c := r.b[r.i]
r.i++
v |= uint64(c&0x7f) << shift
if c < 0x80 {
return v, nil
}
}
return 0, errBadTile
}
// field reads one field's key and, for length-delimited ones, its bytes.
func (r *pbReader) field() (num int, wire int, val uint64, data []byte, err error) {
key, err := r.varint()
if err != nil {
return 0, 0, 0, nil, err
}
num, wire = int(key>>3), int(key&7)
switch wire {
case 0:
val, err = r.varint()
case 1:
if r.i+8 > len(r.b) {
return 0, 0, 0, nil, errBadTile
}
r.i += 8
case 2:
var n uint64
n, err = r.varint()
if err == nil {
if n > uint64(len(r.b)-r.i) {
return 0, 0, 0, nil, errBadTile
}
data = r.b[r.i : r.i+int(n)]
r.i += int(n)
}
case 5:
if r.i+4 > len(r.b) {
return 0, 0, 0, nil, errBadTile
}
r.i += 4
default:
return 0, 0, 0, nil, errBadTile
}
return num, wire, val, data, err
}
// packed reads a repeated uint32 field, packed or not.
func packed(wire int, val uint64, data []byte, into []uint32) ([]uint32, error) {
if wire == 0 {
return append(into, uint32(val)), nil
}
if wire != 2 {
return nil, errBadTile
}
r := pbReader{b: data}
for !r.done() {
v, err := r.varint()
if err != nil {
return nil, err
}
into = append(into, uint32(v))
}
return into, nil
}
// decodeRoads returns the drawable roads of the tile.
func decodeRoads(tile []byte) ([]roadLine, error) {
r := pbReader{b: tile}
for !r.done() {
num, _, _, data, err := r.field()
if err != nil {
return nil, err
}
if num != 3 { // layers
continue
}
lines, isRoads, err := decodeLayer(data)
if err != nil {
return nil, err
}
if isRoads {
return lines, nil
}
}
return nil, nil // a tile without roads (sea, desert) is an empty one
}
type rawFeature struct {
tags []uint32
geomType uint64
geometry []uint32
}
func decodeLayer(layer []byte) ([]roadLine, bool, error) {
r := pbReader{b: layer}
var name string
var keys []string
var values []string // only string values matter for "class"
var features []rawFeature
extent := uint64(4096)
for !r.done() {
num, wire, val, data, err := r.field()
if err != nil {
return nil, false, err
}
switch num {
case 1:
name = string(data)
case 2:
f, err := decodeFeature(data)
if err != nil {
return nil, false, err
}
features = append(features, f)
case 3:
keys = append(keys, string(data))
case 4:
values = append(values, valueString(data))
case 5:
if wire != 0 || val == 0 {
return nil, false, errBadTile
}
extent = val
}
}
if name != "transportation" {
return nil, false, nil
}
scale := float64(mapExtent) / float64(extent)
var lines []roadLine
for _, f := range features {
if f.geomType != 2 { // LINESTRING
continue
}
class, ok := -1, false
for i := 0; i+1 < len(f.tags); i += 2 {
k, v := int(f.tags[i]), int(f.tags[i+1])
if k >= len(keys) || v >= len(values) {
return nil, false, errBadTile
}
if keys[k] == "class" {
class, ok = roadClasses[values[v]]
if !ok {
class = -1
}
}
}
if class < 0 {
continue
}
parts, err := lineGeometry(f.geometry, scale)
if err != nil {
return nil, false, err
}
for _, p := range parts {
lines = append(lines, roadLine{class: class, points: p})
}
}
return lines, true, nil
}
func decodeFeature(data []byte) (rawFeature, error) {
var f rawFeature
r := pbReader{b: data}
for !r.done() {
num, wire, val, d, err := r.field()
if err != nil {
return f, err
}
switch num {
case 2:
if f.tags, err = packed(wire, val, d, f.tags); err != nil {
return f, err
}
case 3:
f.geomType = val
case 4:
if f.geometry, err = packed(wire, val, d, f.geometry); err != nil {
return f, err
}
}
}
if len(f.tags)%2 != 0 {
return f, errBadTile
}
return f, nil
}
// valueString is a Value's string_value, "" for any other kind.
func valueString(data []byte) string {
r := pbReader{b: data}
for !r.done() {
num, _, _, d, err := r.field()
if err != nil {
return ""
}
if num == 1 {
return string(d)
}
}
return ""
}
// lineGeometry follows the MoveTo/LineTo commands of a (multi)line string.
func lineGeometry(cmds []uint32, scale float64) ([][][2]int, error) {
var parts [][][2]int
var cur [][2]int
x, y := 0, 0
zig := func(v uint32) int { return int(int32(v>>1) ^ -int32(v&1)) }
at := func() [2]int {
return [2]int{int(math.Round(float64(x) * scale)), int(math.Round(float64(y) * scale))}
}
for i := 0; i < len(cmds); {
id, count := cmds[i]&7, int(cmds[i]>>3)
i++
switch id {
case 1, 2: // MoveTo, LineTo
if i+2*count > len(cmds) {
return nil, errBadTile
}
for k := 0; k < count; k++ {
x += zig(cmds[i])
y += zig(cmds[i+1])
i += 2
if id == 1 {
if len(cur) >= 2 {
parts = append(parts, cur)
}
cur = [][2]int{at()}
} else {
if p := at(); len(cur) == 0 || cur[len(cur)-1] != p {
cur = append(cur, p)
}
}
}
case 7: // ClosePath, not in a line; ignored
default:
return nil, errBadTile
}
}
if len(cur) >= 2 {
parts = append(parts, cur)
}
return parts, nil
}