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
279 lines
5.8 KiB
Go
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
|
|
}
|