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
This commit is contained in:
+278
@@ -0,0 +1,278 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user