feat: workout-suggester, weather-aware workouts from a local Gemma
Go + Templ + HTMX app that checks the weather (Open-Meteo, MET Norway fallback), scores run/ride/walk with plain rules, and has a local open-weight model (Gemma 4 E2B via any OpenAI-compatible server) write the plan. Routes are loops from your door (BRouter) plus signposted OpenStreetMap routes (Overpass), with GPX export.
This commit is contained in:
42 files changed
+7219
No files matched your search
@@ -0,0 +1,233 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// BRouter's round trips come out about this many times longer than the
|
||||
// roundTripDistance asked for (it's closer to a radius than a length).
|
||||
const brouterStretch = 5.1
|
||||
|
||||
var brouterProfile = map[Kind]string{Foot: "hiking-mountain", Bike: "trekking"}
|
||||
|
||||
// Loops asks BRouter for round trips of about targetKm that start and end
|
||||
// at p, heading out in three directions. The first heads out into the wind
|
||||
// (windFrom, in degrees) so the way home is the easy part. The start point
|
||||
// is rounded to ~100 m before it leaves the machine.
|
||||
func (c *Client) Loops(ctx context.Context, kind Kind, p Point, targetKm float64, windFrom int) ([]Suggestion, error) {
|
||||
if c.BRouterURL == "" || c.BRouterURL == "-" || targetKm <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
p = Point{math.Round(p.Lat*1000) / 1000, math.Round(p.Lon*1000) / 1000}
|
||||
key := fmt.Sprintf("%s_%.3f_%.3f_%.1f_%d", kind, p.Lat, p.Lon, targetKm, windFrom/30)
|
||||
c.mu.Lock()
|
||||
c.initLocked()
|
||||
if hit, ok := c.loops[key]; ok && time.Since(hit.at) < cacheFor {
|
||||
c.mu.Unlock()
|
||||
return hit.s, nil
|
||||
}
|
||||
c.mu.Unlock()
|
||||
|
||||
type result struct {
|
||||
r *Route
|
||||
err error
|
||||
}
|
||||
results := make([]result, 3)
|
||||
var wg sync.WaitGroup
|
||||
for i := range results {
|
||||
wg.Go(func() {
|
||||
dir := (windFrom + i*120) % 360
|
||||
r, err := c.roundTrip(ctx, kind, p, targetKm, dir)
|
||||
results[i] = result{r, err}
|
||||
})
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
var out []Suggestion
|
||||
var errs []error
|
||||
for _, res := range results {
|
||||
if res.err != nil {
|
||||
errs = append(errs, res.err)
|
||||
continue
|
||||
}
|
||||
out = append(out, Suggestion{Route: res.r, Plan: "loop", Laps: 1, TotalKm: res.r.LengthKm, JoinAt: p})
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil, fmt.Errorf("brouter: %w", errors.Join(errs...))
|
||||
}
|
||||
// Closest to the target first; into-the-wind wins ties within 10%.
|
||||
slices.SortStableFunc(out, func(a, b Suggestion) int {
|
||||
da, db := math.Abs(a.TotalKm-targetKm), math.Abs(b.TotalKm-targetKm)
|
||||
if math.Abs(da-db) < 0.1*targetKm {
|
||||
return 0
|
||||
}
|
||||
if da < db {
|
||||
return -1
|
||||
}
|
||||
return 1
|
||||
})
|
||||
|
||||
c.mu.Lock()
|
||||
c.loops[key] = cachedLoops{out, time.Now()}
|
||||
for _, s := range out {
|
||||
c.byID[s.Route.ID] = s.Route
|
||||
}
|
||||
c.mu.Unlock()
|
||||
return out, nil
|
||||
}
|
||||
|
||||
type cachedLoops struct {
|
||||
s []Suggestion
|
||||
at time.Time
|
||||
}
|
||||
|
||||
func (c *Client) roundTrip(ctx context.Context, kind Kind, p Point, targetKm float64, dir int) (*Route, error) {
|
||||
q := url.Values{
|
||||
"lonlats": {fmt.Sprintf("%.3f,%.3f", p.Lon, p.Lat)},
|
||||
"profile": {brouterProfile[kind]},
|
||||
"alternativeidx": {"0"},
|
||||
"format": {"geojson"},
|
||||
"engineMode": {"4"}, // round trip
|
||||
"roundTripDistance": {strconv.Itoa(int(targetKm * 1000 / brouterStretch))},
|
||||
"direction": {strconv.Itoa(dir)},
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.BRouterURL+"?"+q.Encode(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("User-Agent", userAgent)
|
||||
hc := c.HTTP
|
||||
if hc == nil {
|
||||
hc = http.DefaultClient
|
||||
}
|
||||
resp, err := hc.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
msg, _ := io.ReadAll(io.LimitReader(resp.Body, 256))
|
||||
return nil, fmt.Errorf("%s: %s", resp.Status, strings.TrimSpace(string(msg)))
|
||||
}
|
||||
r, err := parseBRouter(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.Kind, r.Heading = kind, dir
|
||||
r.ID = c.nextLoopID()
|
||||
r.Name = fmt.Sprintf("%.1f km loop heading %s", r.LengthKm, compass(dir))
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// nextLoopID hands out negative ids so generated loops never collide with
|
||||
// OpenStreetMap relation ids.
|
||||
func (c *Client) nextLoopID() int64 {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.loopSeq--
|
||||
return c.loopSeq
|
||||
}
|
||||
|
||||
func parseBRouter(r io.Reader) (*Route, error) {
|
||||
var out struct {
|
||||
Features []struct {
|
||||
Properties struct {
|
||||
Length string `json:"track-length"`
|
||||
Ascent string `json:"filtered ascend"`
|
||||
Messages [][]string `json:"messages"`
|
||||
} `json:"properties"`
|
||||
Geometry struct {
|
||||
Coordinates [][]float64 `json:"coordinates"` // lon, lat, elevation
|
||||
} `json:"geometry"`
|
||||
} `json:"features"`
|
||||
}
|
||||
if err := json.NewDecoder(r).Decode(&out); err != nil {
|
||||
return nil, fmt.Errorf("decode brouter: %w", err)
|
||||
}
|
||||
if len(out.Features) == 0 || len(out.Features[0].Geometry.Coordinates) < 2 {
|
||||
return nil, errors.New("brouter: empty route")
|
||||
}
|
||||
f := out.Features[0]
|
||||
m, _ := strconv.Atoi(f.Properties.Length)
|
||||
ascent, _ := strconv.Atoi(f.Properties.Ascent)
|
||||
pts := make([]Point, len(f.Geometry.Coordinates))
|
||||
for i, c := range f.Geometry.Coordinates {
|
||||
pts[i] = Point{c[1], c[0]}
|
||||
}
|
||||
return &Route{
|
||||
LengthKm: math.Round(float64(m)/100) / 10,
|
||||
Loop: true,
|
||||
Generated: true,
|
||||
AscentM: ascent,
|
||||
PavedPct: pavedPct(f.Properties.Messages),
|
||||
Ways: [][]Point{simplify(pts, 0.015)},
|
||||
}, nil
|
||||
}
|
||||
|
||||
var (
|
||||
pavedSurfaces = []string{"asphalt", "paved", "concrete", "concrete:plates", "concrete:lanes", "paving_stones", "sett", "metal", "wood"}
|
||||
unpavedHighways = []string{"track", "path", "bridleway"}
|
||||
)
|
||||
|
||||
// pavedPct estimates how much of the route is paved from BRouter's per-
|
||||
// segment way tags: the surface tag when there is one, else the road type.
|
||||
func pavedPct(msgs [][]string) int {
|
||||
if len(msgs) < 2 {
|
||||
return -1
|
||||
}
|
||||
dist, tags := slices.Index(msgs[0], "Distance"), slices.Index(msgs[0], "WayTags")
|
||||
if dist < 0 || tags < 0 {
|
||||
return -1
|
||||
}
|
||||
var paved, total float64
|
||||
for _, row := range msgs[1:] {
|
||||
if len(row) <= max(dist, tags) {
|
||||
continue
|
||||
}
|
||||
d, _ := strconv.ParseFloat(row[dist], 64)
|
||||
kv := map[string]string{}
|
||||
for _, f := range strings.Fields(row[tags]) {
|
||||
k, v, _ := strings.Cut(f, "=")
|
||||
kv[k] = v
|
||||
}
|
||||
total += d
|
||||
if s, ok := kv["surface"]; ok {
|
||||
if slices.Contains(pavedSurfaces, s) {
|
||||
paved += d
|
||||
}
|
||||
} else if !slices.Contains(unpavedHighways, kv["highway"]) {
|
||||
paved += d
|
||||
}
|
||||
}
|
||||
if total == 0 {
|
||||
return -1
|
||||
}
|
||||
return int(math.Round(100 * paved / total))
|
||||
}
|
||||
|
||||
func compass(deg int) string {
|
||||
dirs := []string{"north", "north-east", "east", "south-east", "south", "south-west", "west", "north-west"}
|
||||
return dirs[((deg%360+360)%360+22)/45%8]
|
||||
}
|
||||
|
||||
// IntoWind reports whether a loop heads out roughly into the wind, so the
|
||||
// way home has a tailwind.
|
||||
func (r *Route) IntoWind(windFrom int) bool {
|
||||
if !r.Generated {
|
||||
return false
|
||||
}
|
||||
d := ((r.Heading-windFrom)%360 + 360) % 360
|
||||
return d <= 45 || d >= 315
|
||||
}
|
||||
@@ -0,0 +1,367 @@
|
||||
// Package routes finds signposted cycling, walking and running routes near a
|
||||
// point using OpenStreetMap data from the Overpass API, and picks the ones
|
||||
// that fit a target distance.
|
||||
package routes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Kind is what a route is signposted for.
|
||||
type Kind string
|
||||
|
||||
const (
|
||||
Bike Kind = "bike"
|
||||
Foot Kind = "foot"
|
||||
)
|
||||
|
||||
// Point is a WGS84 coordinate.
|
||||
type Point struct {
|
||||
Lat float64 `json:"lat"`
|
||||
Lon float64 `json:"lon"`
|
||||
}
|
||||
|
||||
// Route is a named OSM route relation with its geometry.
|
||||
type Route struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Kind Kind `json:"kind"`
|
||||
LengthKm float64 `json:"length_km"`
|
||||
Loop bool `json:"loop"`
|
||||
Ways [][]Point `json:"ways"` // unordered pieces, as OSM stores them
|
||||
|
||||
// Set for loops generated by BRouter rather than signposted in OSM.
|
||||
Generated bool `json:"generated,omitempty"`
|
||||
Heading int `json:"heading,omitempty"` // direction it sets off in, degrees
|
||||
AscentM int `json:"ascent_m,omitempty"` // total climb
|
||||
PavedPct int `json:"paved_pct,omitempty"` // -1 when unknown
|
||||
}
|
||||
|
||||
// Junction is a numbered node of the Belgian/Dutch node networks
|
||||
// (fietsknooppunt / wandelknooppunt).
|
||||
type Junction struct {
|
||||
Ref string `json:"ref"`
|
||||
Kind Kind `json:"kind"`
|
||||
Point Point `json:"point"`
|
||||
}
|
||||
|
||||
// Area holds everything found around one spot.
|
||||
type Area struct {
|
||||
Routes []Route `json:"routes"`
|
||||
Junctions []Junction `json:"junctions"`
|
||||
Fetched time.Time `json:"fetched"`
|
||||
}
|
||||
|
||||
// Client queries Overpass for signposted routes, keeping results in memory
|
||||
// and on disk so an area you've loaded once keeps working without a
|
||||
// connection, and BRouter for loops generated from your door.
|
||||
type Client struct {
|
||||
URLs []string // Overpass endpoints, tried in order
|
||||
BRouterURL string // e.g. https://brouter.de/brouter; empty or "-" disables loops
|
||||
HTTP *http.Client
|
||||
CacheDir string // empty: no disk cache
|
||||
|
||||
mu sync.Mutex
|
||||
areas map[string]*Area
|
||||
byID map[int64]*Route
|
||||
locks map[string]*sync.Mutex
|
||||
loops map[string]cachedLoops
|
||||
loopSeq int64
|
||||
}
|
||||
|
||||
func (c *Client) initLocked() {
|
||||
if c.byID == nil {
|
||||
c.areas, c.byID, c.locks, c.loops = map[string]*Area{}, map[int64]*Route{}, map[string]*sync.Mutex{}, map[string]cachedLoops{}
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
bikeRadius = 15000 // m
|
||||
footRadius = 8000 // m
|
||||
junctionRadius = 3000 // m
|
||||
cacheFor = 7 * 24 * time.Hour
|
||||
userAgent = "workout-suggester/0.1 (+https://git.b0b.be/bdeb/workout-suggester)"
|
||||
)
|
||||
|
||||
// cell snaps a coordinate to a ~2 km grid so nearby requests share a cache
|
||||
// entry and the exact location never leaves the machine.
|
||||
func cell(lat, lon float64) (float64, float64, string) {
|
||||
lat, lon = math.Round(lat*50)/50, math.Round(lon*50)/50
|
||||
return lat, lon, fmt.Sprintf("%.2f_%.2f", lat, lon)
|
||||
}
|
||||
|
||||
// Near returns the routes and junctions around a point.
|
||||
func (c *Client) Near(ctx context.Context, lat, lon float64) (*Area, error) {
|
||||
clat, clon, key := cell(lat, lon)
|
||||
|
||||
// One fetch per cell at a time; concurrent callers wait for it.
|
||||
c.mu.Lock()
|
||||
c.initLocked()
|
||||
l, ok := c.locks[key]
|
||||
if !ok {
|
||||
l = &sync.Mutex{}
|
||||
c.locks[key] = l
|
||||
}
|
||||
c.mu.Unlock()
|
||||
l.Lock()
|
||||
defer l.Unlock()
|
||||
|
||||
c.mu.Lock()
|
||||
a := c.areas[key]
|
||||
c.mu.Unlock()
|
||||
if a == nil {
|
||||
a = c.readDisk(key)
|
||||
}
|
||||
if a != nil && time.Since(a.Fetched) < cacheFor {
|
||||
c.remember(key, a)
|
||||
return a, nil
|
||||
}
|
||||
|
||||
fresh, err := c.fetch(ctx, clat, clon)
|
||||
if err != nil {
|
||||
if a != nil { // stale beats nothing
|
||||
c.remember(key, a)
|
||||
return a, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
c.remember(key, fresh)
|
||||
c.writeDisk(key, fresh)
|
||||
return fresh, nil
|
||||
}
|
||||
|
||||
// Route looks up a route seen in an earlier Near call.
|
||||
func (c *Client) Route(id int64) (*Route, bool) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.initLocked()
|
||||
r, ok := c.byID[id]
|
||||
return r, ok
|
||||
}
|
||||
|
||||
func (c *Client) remember(key string, a *Area) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.areas[key] = a
|
||||
for i := range a.Routes {
|
||||
c.byID[a.Routes[i].ID] = &a.Routes[i]
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) readDisk(key string) *Area {
|
||||
if c.CacheDir == "" {
|
||||
return nil
|
||||
}
|
||||
b, err := os.ReadFile(filepath.Join(c.CacheDir, key+".json"))
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var a Area
|
||||
if json.Unmarshal(b, &a) != nil {
|
||||
return nil
|
||||
}
|
||||
return &a
|
||||
}
|
||||
|
||||
func (c *Client) writeDisk(key string, a *Area) {
|
||||
if c.CacheDir == "" {
|
||||
return
|
||||
}
|
||||
b, err := json.Marshal(a)
|
||||
if err != nil || os.MkdirAll(c.CacheDir, 0o755) != nil {
|
||||
return
|
||||
}
|
||||
tmp := filepath.Join(c.CacheDir, key+".json.tmp")
|
||||
if os.WriteFile(tmp, b, 0o644) == nil {
|
||||
os.Rename(tmp, filepath.Join(c.CacheDir, key+".json"))
|
||||
}
|
||||
}
|
||||
|
||||
func query(lat, lon float64) string {
|
||||
at := func(r int) string { return fmt.Sprintf("(around:%d,%.4f,%.4f)", r, lat, lon) }
|
||||
return `[out:json][timeout:25];(` +
|
||||
`relation["type"="route"]["route"="bicycle"]["network"~"^(lcn|rcn)$"]["network:type"!="node_network"]` + at(bikeRadius) + `;` +
|
||||
`relation["type"="route"]["route"~"^(hiking|foot|walking|running)$"]["network"~"^(lwn|rwn)$"]["network:type"!="node_network"]` + at(footRadius) + `;` +
|
||||
`);out geom;(` +
|
||||
`node["rcn_ref"]` + at(junctionRadius) + `;` +
|
||||
`node["rwn_ref"]` + at(junctionRadius) + `;` +
|
||||
`);out;`
|
||||
}
|
||||
|
||||
// fetch tries each Overpass endpoint, retrying busy servers a couple of times.
|
||||
func (c *Client) fetch(ctx context.Context, lat, lon float64) (*Area, error) {
|
||||
hc := c.HTTP
|
||||
if hc == nil {
|
||||
hc = http.DefaultClient
|
||||
}
|
||||
body := url.Values{"data": {query(lat, lon)}}.Encode()
|
||||
var errs []error
|
||||
for attempt := range 3 {
|
||||
for _, u := range c.URLs {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, u, strings.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("Accept", "application/json") // overpass-api.de answers 406 without it
|
||||
req.Header.Set("User-Agent", userAgent)
|
||||
resp, err := hc.Do(req)
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
errs = append(errs, err)
|
||||
continue
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
resp.Body.Close()
|
||||
errs = append(errs, fmt.Errorf("%s: %s", u, resp.Status))
|
||||
continue
|
||||
}
|
||||
a, err := parse(resp.Body)
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
continue
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
select { // busy servers (429/504) usually recover within seconds
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case <-time.After(time.Duration(attempt+1) * 2 * time.Second):
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("overpass: %w", errors.Join(errs...))
|
||||
}
|
||||
|
||||
func parse(r io.Reader) (*Area, error) {
|
||||
var out struct {
|
||||
Elements []struct {
|
||||
Type string `json:"type"`
|
||||
ID int64 `json:"id"`
|
||||
Lat float64 `json:"lat"`
|
||||
Lon float64 `json:"lon"`
|
||||
Tags map[string]string `json:"tags"`
|
||||
Members []struct {
|
||||
Type string `json:"type"`
|
||||
Role string `json:"role"`
|
||||
Geometry []Point `json:"geometry"`
|
||||
} `json:"members"`
|
||||
} `json:"elements"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
if err := json.NewDecoder(r).Decode(&out); err != nil {
|
||||
return nil, fmt.Errorf("decode overpass: %w", err)
|
||||
}
|
||||
if strings.Contains(out.Remark, "runtime error") {
|
||||
return nil, fmt.Errorf("overpass: %s", out.Remark)
|
||||
}
|
||||
a := &Area{Fetched: time.Now()}
|
||||
for _, e := range out.Elements {
|
||||
t := e.Tags
|
||||
switch e.Type {
|
||||
case "node":
|
||||
if ref := t["rcn_ref"]; ref != "" {
|
||||
a.Junctions = append(a.Junctions, Junction{Ref: ref, Kind: Bike, Point: Point{e.Lat, e.Lon}})
|
||||
}
|
||||
if ref := t["rwn_ref"]; ref != "" {
|
||||
a.Junctions = append(a.Junctions, Junction{Ref: ref, Kind: Foot, Point: Point{e.Lat, e.Lon}})
|
||||
}
|
||||
case "relation":
|
||||
name := t["name"]
|
||||
if name == "" {
|
||||
name = t["ref"]
|
||||
}
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
r := Route{ID: e.ID, Name: name, Kind: Foot, Loop: t["roundtrip"] == "yes"}
|
||||
if t["route"] == "bicycle" {
|
||||
r.Kind = Bike
|
||||
}
|
||||
for _, m := range e.Members {
|
||||
if m.Type != "way" || len(m.Geometry) < 2 {
|
||||
continue
|
||||
}
|
||||
r.LengthKm += pathKm(m.Geometry)
|
||||
r.Ways = append(r.Ways, simplify(m.Geometry, 0.015))
|
||||
}
|
||||
if r.LengthKm < 0.5 {
|
||||
continue
|
||||
}
|
||||
r.LengthKm = math.Round(r.LengthKm*10) / 10
|
||||
a.Routes = append(a.Routes, r)
|
||||
}
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
// Km is the great-circle distance between two points.
|
||||
func Km(a, b Point) float64 {
|
||||
const R = 6371.0
|
||||
la1, la2 := a.Lat*math.Pi/180, b.Lat*math.Pi/180
|
||||
dla, dlo := la2-la1, (b.Lon-a.Lon)*math.Pi/180
|
||||
h := math.Sin(dla/2)*math.Sin(dla/2) + math.Cos(la1)*math.Cos(la2)*math.Sin(dlo/2)*math.Sin(dlo/2)
|
||||
return 2 * R * math.Asin(math.Sqrt(h))
|
||||
}
|
||||
|
||||
func pathKm(pts []Point) float64 {
|
||||
var d float64
|
||||
for i := 1; i < len(pts); i++ {
|
||||
d += Km(pts[i-1], pts[i])
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
// simplify drops points closer than minKm to the last kept one, which is
|
||||
// plenty for a map or a GPX track and keeps the page small.
|
||||
func simplify(pts []Point, minKm float64) []Point {
|
||||
out := []Point{pts[0]}
|
||||
for i := 1; i < len(pts)-1; i++ {
|
||||
if Km(out[len(out)-1], pts[i]) >= minKm {
|
||||
out = append(out, pts[i])
|
||||
}
|
||||
}
|
||||
return append(out, pts[len(pts)-1])
|
||||
}
|
||||
|
||||
// Closest returns the point of the route nearest to p and its distance.
|
||||
func (r *Route) Closest(p Point) (Point, float64) {
|
||||
best, bestKm := Point{}, math.Inf(1)
|
||||
for _, w := range r.Ways {
|
||||
for _, q := range w {
|
||||
if d := Km(p, q); d < bestKm {
|
||||
best, bestKm = q, d
|
||||
}
|
||||
}
|
||||
}
|
||||
return best, bestKm
|
||||
}
|
||||
|
||||
// NearestJunction finds the closest node-network junction of a kind.
|
||||
func (a *Area) NearestJunction(kind Kind, p Point) (Junction, float64, bool) {
|
||||
var best Junction
|
||||
bestKm := math.Inf(1)
|
||||
for _, j := range a.Junctions {
|
||||
if j.Kind != kind {
|
||||
continue
|
||||
}
|
||||
if d := Km(p, j.Point); d < bestKm {
|
||||
best, bestKm = j, d
|
||||
}
|
||||
}
|
||||
return best, bestKm, !math.IsInf(bestKm, 1)
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// line returns a straight path of about km kilometres going east from p.
|
||||
func line(p Point, km float64) []Point {
|
||||
const kmPerDegLon = 70.0 // at ~51°N
|
||||
return []Point{p, {p.Lat, p.Lon + km/kmPerDegLon/2}, {p.Lat, p.Lon + km/kmPerDegLon}}
|
||||
}
|
||||
|
||||
const overpassJSON = `{"elements":[
|
||||
{"type":"relation","id":1,"tags":{"route":"foot","name":"Park loop","roundtrip":"yes"},
|
||||
"members":[{"type":"way","geometry":[{"lat":51.05,"lon":3.70},{"lat":51.05,"lon":3.7357}]}]},
|
||||
{"type":"relation","id":2,"tags":{"route":"bicycle","name":"River ride"},
|
||||
"members":[{"type":"way","geometry":[{"lat":51.05,"lon":3.70},{"lat":51.05,"lon":3.9857}]},{"type":"node"}]},
|
||||
{"type":"relation","id":3,"tags":{"route":"foot"},"members":[]},
|
||||
{"type":"node","id":9,"lat":51.051,"lon":3.701,"tags":{"rcn_ref":"56"}}
|
||||
]}`
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
a, err := parse(strings.NewReader(overpassJSON))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(a.Routes) != 2 {
|
||||
t.Fatalf("got %d routes, want 2 (unnamed one skipped): %+v", len(a.Routes), a.Routes)
|
||||
}
|
||||
park, ride := a.Routes[0], a.Routes[1]
|
||||
if park.Kind != Foot || !park.Loop || park.LengthKm < 2.4 || park.LengthKm > 2.6 {
|
||||
t.Errorf("park = %+v", park)
|
||||
}
|
||||
if ride.Kind != Bike || ride.Loop || ride.LengthKm < 19.5 || ride.LengthKm > 20.5 {
|
||||
t.Errorf("ride = %+v", ride)
|
||||
}
|
||||
j, km, ok := a.NearestJunction(Bike, Point{51.05, 3.70})
|
||||
if !ok || j.Ref != "56" || km > 0.2 {
|
||||
t.Errorf("junction = %+v %.2f %v", j, km, ok)
|
||||
}
|
||||
if _, _, ok := a.NearestJunction(Foot, Point{51.05, 3.70}); ok {
|
||||
t.Error("found a walking junction that isn't there")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSuggest(t *testing.T) {
|
||||
home := Point{51.05, 3.70}
|
||||
a := &Area{Routes: []Route{
|
||||
{ID: 1, Name: "short loop", Kind: Foot, Loop: true, LengthKm: 5, Ways: [][]Point{line(home, 5)}},
|
||||
{ID: 2, Name: "perfect loop far away", Kind: Foot, Loop: true, LengthKm: 10, Ways: [][]Point{line(Point{51.2, 3.7}, 10)}},
|
||||
{ID: 3, Name: "long path", Kind: Foot, LengthKm: 30, Ways: [][]Point{line(home, 30)}},
|
||||
{ID: 4, Name: "bike loop", Kind: Bike, Loop: true, LengthKm: 10, Ways: [][]Point{line(home, 10)}},
|
||||
}}
|
||||
got := Suggest(a, Foot, 10, home, 3)
|
||||
if len(got) != 2 {
|
||||
t.Fatalf("got %d suggestions, want 2 (one too far, one for bikes): %+v", len(got), got)
|
||||
}
|
||||
if got[0].Route.ID != 1 || got[0].Plan != "laps" || got[0].Laps != 2 || got[0].TotalKm != 10 {
|
||||
t.Errorf("first = %+v, want 2 laps of the short loop", got[0])
|
||||
}
|
||||
if got[1].Plan != "section" || got[1].TotalKm != 10 {
|
||||
t.Errorf("second = %+v, want a 10 km section", got[1])
|
||||
}
|
||||
if s := got[0].Describe(); s != "2 laps of the 5.0 km loop" {
|
||||
t.Errorf("describe = %q", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNearRetriesAndCaches(t *testing.T) {
|
||||
var calls atomic.Int32
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Header.Get("Accept") != "application/json" {
|
||||
http.Error(w, "406", http.StatusNotAcceptable)
|
||||
return
|
||||
}
|
||||
if calls.Add(1) == 1 {
|
||||
http.Error(w, "busy", http.StatusGatewayTimeout)
|
||||
return
|
||||
}
|
||||
fmt.Fprint(w, overpassJSON)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
dir := t.TempDir()
|
||||
c := &Client{URLs: []string{srv.URL}, CacheDir: dir}
|
||||
a, err := c.Near(context.Background(), 51.05, 3.70)
|
||||
if err != nil || len(a.Routes) != 2 {
|
||||
t.Fatalf("Near = %v, %v", a, err)
|
||||
}
|
||||
if _, ok := c.Route(2); !ok {
|
||||
t.Error("route 2 not remembered for GPX")
|
||||
}
|
||||
// Same ~2 km cell: no new request. A fresh client reads the disk cache.
|
||||
c.Near(context.Background(), 51.051, 3.701)
|
||||
(&Client{URLs: []string{srv.URL}, CacheDir: dir}).Near(context.Background(), 51.05, 3.70)
|
||||
if n := calls.Load(); n != 2 {
|
||||
t.Errorf("overpass called %d times, want 2 (one busy, one ok)", n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteGPX(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
r := &Route{Name: "Park & loop", Ways: [][]Point{{{51, 3}, {51.1, 3.1}}, {{51.2, 3.2}, {51.3, 3.3}}}}
|
||||
if err := WriteGPX(&buf, r); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var g struct {
|
||||
Name string `xml:"trk>name"`
|
||||
Segs []struct {
|
||||
Pts []struct{} `xml:"trkpt"`
|
||||
} `xml:"trk>trkseg"`
|
||||
}
|
||||
if err := xml.Unmarshal(buf.Bytes(), &g); err != nil {
|
||||
t.Fatal(err, buf.String())
|
||||
}
|
||||
if g.Name != "Park & loop" || len(g.Segs) != 2 || len(g.Segs[1].Pts) != 2 {
|
||||
t.Errorf("gpx = %+v\n%s", g, buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoops(t *testing.T) {
|
||||
var dirs []string
|
||||
var mu sync.Mutex
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query()
|
||||
mu.Lock()
|
||||
dirs = append(dirs, q.Get("direction"))
|
||||
mu.Unlock()
|
||||
if q.Get("engineMode") != "4" || q.Get("profile") != "hiking-mountain" || q.Get("lonlats") != "4.700,50.880" {
|
||||
http.Error(w, "bad query "+r.URL.RawQuery, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if q.Get("roundTripDistance") != "1960" { // 10 km / 5.1
|
||||
http.Error(w, "bad distance "+q.Get("roundTripDistance"), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if q.Get("direction") == "330" {
|
||||
http.Error(w, "no route", http.StatusInternalServerError) // one direction failing is fine
|
||||
return
|
||||
}
|
||||
http.ServeFile(w, r, "testdata/brouter.json")
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := &Client{BRouterURL: srv.URL}
|
||||
// The exact spot is rounded to ~100 m before it's sent.
|
||||
got, err := c.Loops(context.Background(), Foot, Point{50.88012, 4.70044}, 10, 90)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
slices.Sort(dirs)
|
||||
if !slices.Equal(dirs, []string{"210", "330", "90"}) {
|
||||
t.Errorf("directions asked = %v, want into the wind (90) and ±120°", dirs)
|
||||
}
|
||||
if len(got) != 2 {
|
||||
t.Fatalf("got %d loops, want 2", len(got))
|
||||
}
|
||||
r := got[0].Route
|
||||
if !r.Generated || !r.Loop || r.LengthKm != 5.9 || r.AscentM != 17 || r.PavedPct < 0 || r.PavedPct > 100 || r.ID >= 0 {
|
||||
t.Errorf("loop = %+v", r)
|
||||
}
|
||||
if !strings.HasPrefix(r.Name, "5.9 km loop heading ") {
|
||||
t.Errorf("name = %q", r.Name)
|
||||
}
|
||||
if _, ok := c.Route(r.ID); !ok {
|
||||
t.Error("loop not remembered for GPX")
|
||||
}
|
||||
n := len(dirs)
|
||||
c.Loops(context.Background(), Foot, Point{50.88012, 4.70044}, 10, 90)
|
||||
if len(dirs) != n {
|
||||
t.Error("second identical request should hit the cache")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPavedPct(t *testing.T) {
|
||||
msgs := [][]string{
|
||||
{"Longitude", "Latitude", "Elevation", "Distance", "WayTags"},
|
||||
{"0", "0", "0", "300", "highway=residential"},
|
||||
{"0", "0", "0", "100", "highway=track surface=asphalt"},
|
||||
{"0", "0", "0", "400", "highway=path"},
|
||||
{"0", "0", "0", "200", "highway=footway surface=gravel"},
|
||||
}
|
||||
if got := pavedPct(msgs); got != 40 {
|
||||
t.Errorf("paved = %d%%, want 40%%", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntoWind(t *testing.T) {
|
||||
r := &Route{Generated: true, Heading: 270}
|
||||
if !r.IntoWind(250) || r.IntoWind(90) || (&Route{Heading: 270}).IntoWind(270) {
|
||||
t.Error("IntoWind wrong")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSuggestSkipsLongTrails(t *testing.T) {
|
||||
home := Point{51.05, 3.70}
|
||||
a := &Area{Routes: []Route{
|
||||
{ID: 1, Name: "Streek-GR", Kind: Foot, LengthKm: 130, Ways: [][]Point{line(home, 130)}},
|
||||
{ID: 3, Name: "Streek-GR loop", Kind: Foot, Loop: true, LengthKm: 130, Ways: [][]Point{line(home, 130)}},
|
||||
{ID: 2, Name: "Park loop", Kind: Foot, Loop: true, LengthKm: 8, Ways: [][]Point{line(Point{51.06, 3.70}, 8)}},
|
||||
}}
|
||||
got := Suggest(a, Foot, 10, home, 3)
|
||||
if len(got) != 1 || got[0].Route.ID != 2 { // loop or not, 130 km is not a 10 km workout
|
||||
t.Errorf("got %+v, want only the park loop", got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"slices"
|
||||
)
|
||||
|
||||
// Suggestion is a way to use a route for a workout of a given distance.
|
||||
type Suggestion struct {
|
||||
Route *Route
|
||||
Plan string // "loop", "laps", "out-and-back" or "section"
|
||||
Laps int // for "laps"
|
||||
TotalKm float64 // what the user will actually cover on the route
|
||||
FromYouKm float64 // to the nearest point of the route
|
||||
JoinAt Point
|
||||
}
|
||||
|
||||
// Describe says in a few words how to use the route.
|
||||
func (s Suggestion) Describe() string {
|
||||
switch s.Plan {
|
||||
case "laps":
|
||||
return fmt.Sprintf("%d laps of the %.1f km loop", s.Laps, s.Route.LengthKm)
|
||||
case "out-and-back":
|
||||
return fmt.Sprintf("out and back, %.1f km each way", s.Route.LengthKm)
|
||||
case "section":
|
||||
return fmt.Sprintf("a %.0f km stretch (out and back) of this %.0f km route", s.TotalKm, s.Route.LengthKm)
|
||||
}
|
||||
if s.Route.Generated {
|
||||
return "from your door and back"
|
||||
}
|
||||
if s.Route.Loop {
|
||||
return fmt.Sprintf("the full %.1f km loop", s.Route.LengthKm)
|
||||
}
|
||||
return fmt.Sprintf("the full %.1f km route", s.Route.LengthKm)
|
||||
}
|
||||
|
||||
// maxApproachKm is how far we'd send someone to reach a route's start.
|
||||
var maxApproachKm = map[Kind]float64{Bike: 6, Foot: 3}
|
||||
|
||||
// Suggest picks up to n routes of a kind that fit targetKm, starting from p.
|
||||
func Suggest(a *Area, kind Kind, targetKm float64, p Point, n int) []Suggestion {
|
||||
type scored struct {
|
||||
Suggestion
|
||||
cost float64
|
||||
}
|
||||
var all []scored
|
||||
for i := range a.Routes {
|
||||
r := &a.Routes[i]
|
||||
if r.Kind != kind {
|
||||
continue
|
||||
}
|
||||
if r.LengthKm > 3*targetKm {
|
||||
continue // long-distance trails (Streek-GR & co) aren't a workout route
|
||||
}
|
||||
join, from := r.Closest(p)
|
||||
if from > maxApproachKm[kind] {
|
||||
continue
|
||||
}
|
||||
s := Suggestion{Route: r, Plan: "loop", Laps: 1, TotalKm: r.LengthKm, FromYouKm: from, JoinAt: join}
|
||||
switch {
|
||||
case r.Loop && r.LengthKm < 0.75*targetKm:
|
||||
s.Laps = min(4, max(1, int(math.Round(targetKm/r.LengthKm))))
|
||||
if s.Laps > 1 {
|
||||
s.Plan = "laps"
|
||||
}
|
||||
s.TotalKm = r.LengthKm * float64(s.Laps)
|
||||
case !r.Loop && 2*r.LengthKm <= 1.3*targetKm:
|
||||
s.Plan, s.TotalKm = "out-and-back", 2*r.LengthKm
|
||||
case r.LengthKm > 1.3*targetKm:
|
||||
s.Plan, s.TotalKm = "section", max(targetKm/2, targetKm-2*from)
|
||||
}
|
||||
// Getting to the start and back is part of the workout.
|
||||
cost := math.Abs(s.TotalKm+2*from-targetKm)/targetKm + 0.03*from
|
||||
if !r.Loop {
|
||||
cost += 0.3 // loops bring you home
|
||||
}
|
||||
if s.Plan == "section" {
|
||||
cost += 0.15
|
||||
}
|
||||
if s.Laps > 2 {
|
||||
cost += 0.1 // laps get dull
|
||||
}
|
||||
all = append(all, scored{s, cost})
|
||||
}
|
||||
slices.SortFunc(all, func(x, y scored) int {
|
||||
switch {
|
||||
case x.cost < y.cost:
|
||||
return -1
|
||||
case x.cost > y.cost:
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
var out []Suggestion
|
||||
for _, s := range all[:min(n, len(all))] {
|
||||
out = append(out, s.Suggestion)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// WriteGPX writes a route as a GPX track, one segment per OSM way, ready for
|
||||
// a watch, bike computer or OsmAnd.
|
||||
func WriteGPX(w io.Writer, r *Route) error {
|
||||
type trkpt struct {
|
||||
Lat float64 `xml:"lat,attr"`
|
||||
Lon float64 `xml:"lon,attr"`
|
||||
}
|
||||
type trkseg struct {
|
||||
Points []trkpt `xml:"trkpt"`
|
||||
}
|
||||
type gpx struct {
|
||||
XMLName xml.Name `xml:"gpx"`
|
||||
Version string `xml:"version,attr"`
|
||||
Creator string `xml:"creator,attr"`
|
||||
Xmlns string `xml:"xmlns,attr"`
|
||||
Name string `xml:"trk>name"`
|
||||
Segs []trkseg `xml:"trk>trkseg"`
|
||||
}
|
||||
g := gpx{Version: "1.1", Creator: "workout-suggester (data © OpenStreetMap contributors, ODbL)",
|
||||
Xmlns: "http://www.topografix.com/GPX/1/1", Name: r.Name}
|
||||
for _, way := range r.Ways {
|
||||
var seg trkseg
|
||||
for _, p := range way {
|
||||
seg.Points = append(seg.Points, trkpt{p.Lat, p.Lon})
|
||||
}
|
||||
g.Segs = append(g.Segs, seg)
|
||||
}
|
||||
io.WriteString(w, xml.Header)
|
||||
enc := xml.NewEncoder(w)
|
||||
enc.Indent("", " ")
|
||||
return enc.Encode(g)
|
||||
}
|
||||
+453
@@ -0,0 +1,453 @@
|
||||
{
|
||||
"type": "FeatureCollection",
|
||||
"features": [
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"creator": "BRouter-1.7.10",
|
||||
"name": "brouter_hiking-mountain_0",
|
||||
"track-length": "5872",
|
||||
"filtered ascend": "17",
|
||||
"plain-ascend": "0",
|
||||
"total-time": "4214",
|
||||
"total-energy": "64086",
|
||||
"cost": "10944",
|
||||
"messages": [
|
||||
["Longitude", "Latitude", "Elevation", "Distance", "CostPerKm", "ElevCost", "TurnCost", "NodeCost", "InitialCost", "WayTags", "NodeTags", "Time", "Energy"],
|
||||
["4700029", "50879984", "23", "3", "1260", "0", "0", "0", "0", "highway=footway surface=paving_stones", "", "2", "34"],
|
||||
["4700045", "50880001", "22", "2", "1200", "0", "0", "0", "0", "highway=pedestrian surface=paving_stones bicycle=yes", "", "3", "52"],
|
||||
["4700077", "50880030", "22", "4", "1260", "0", "0", "0", "0", "highway=footway surface=asphalt bicycle=yes", "highway=crossing crossing=uncontrolled bicycle=yes", "6", "89"],
|
||||
["4700117", "50880063", "22", "5", "1260", "0", "0", "0", "0", "highway=footway surface=asphalt bicycle=yes", "", "10", "135"],
|
||||
["4700293", "50880210", "22", "20", "1200", "0", "0", "0", "0", "highway=pedestrian surface=sett bicycle=yes", "", "23", "203"],
|
||||
["4700423", "50880308", "21", "14", "1200", "0", "0", "0", "0", "highway=pedestrian surface=paving_stones bicycle=yes", "", "33", "233"],
|
||||
["4700964", "50880166", "22", "48", "1000", "0", "0", "0", "0", "highway=pedestrian surface=sett bicycle=yes route_hiking_nwn=yes route_hiking_rwn=yes", "", "67", "617"],
|
||||
["4700995", "50880188", "22", "3", "1260", "0", "0", "0", "0", "reversedirection=yes highway=footway surface=sett", "", "70", "646"],
|
||||
["4701350", "50880466", "22", "40", "1260", "0", "0", "0", "0", "reversedirection=yes highway=footway surface=paving_stones", "", "98", "890"],
|
||||
["4701426", "50880513", "22", "7", "1260", "0", "0", "0", "0", "reversedirection=yes highway=footway surface=sett", "highway=crossing crossing=unmarked", "103", "937"],
|
||||
["4701462", "50880534", "22", "3", "1260", "0", "0", "0", "0", "reversedirection=yes highway=footway surface=sett", "", "105", "958"],
|
||||
["4701820", "50880705", "21", "31", "1260", "0", "0", "0", "0", "highway=footway surface=compacted access=no foot=yes", "", "126", "1050"],
|
||||
["4701864", "50880726", "21", "4", "1260", "0", "0", "0", "0", "reversedirection=yes highway=footway surface=sett", "highway=crossing crossing=unmarked", "129", "1060"],
|
||||
["4701911", "50880749", "21", "4", "1260", "0", "0", "0", "0", "reversedirection=yes highway=footway surface=sett", "", "132", "1070"],
|
||||
["4702081", "50880815", "21", "14", "1260", "0", "0", "0", "0", "reversedirection=yes highway=footway surface=paving_stones", "", "142", "1087"],
|
||||
["4702141", "50880842", "21", "5", "1260", "0", "0", "0", "0", "reversedirection=yes highway=footway surface=asphalt", "crossing=unknown", "145", "1096"],
|
||||
["4702201", "50880864", "21", "5", "1260", "0", "0", "0", "0", "reversedirection=yes highway=footway surface=asphalt", "", "149", "1108"],
|
||||
["4703896", "50881681", "20", "159", "1260", "0", "0", "0", "0", "reversedirection=yes highway=footway surface=paving_stones", "", "260", "1725"],
|
||||
["4703929", "50881704", "20", "3", "1260", "0", "0", "0", "0", "reversedirection=yes highway=footway surface=asphalt", "highway=crossing", "262", "1732"],
|
||||
["4703985", "50881728", "20", "5", "1260", "0", "0", "0", "0", "reversedirection=yes highway=footway surface=asphalt", "", "265", "1745"],
|
||||
["4704773", "50882093", "20", "70", "1260", "0", "0", "0", "0", "highway=footway surface=paving_stones", "", "315", "2066"],
|
||||
["4704838", "50882125", "20", "6", "1260", "0", "0", "0", "0", "highway=footway surface=asphalt", "crossing=unknown", "319", "2102"],
|
||||
["4704885", "50882149", "20", "4", "1260", "0", "0", "0", "0", "highway=footway surface=asphalt", "", "322", "2128"],
|
||||
["4704219", "50882691", "20", "76", "1260", "0", "0", "0", "0", "reversedirection=yes highway=footway surface=paving_stones", "", "375", "2676"],
|
||||
["4704763", "50883960", "19", "147", "1260", "0", "0", "0", "0", "reversedirection=yes highway=footway surface=concrete", "", "480", "3854"],
|
||||
["4705107", "50884096", "19", "29", "1260", "0", "0", "0", "0", "reversedirection=yes highway=footway surface=paving_stones", "", "500", "3913"],
|
||||
["4705080", "50884119", "19", "3", "1260", "0", "0", "0", "0", "reversedirection=yes highway=footway surface=asphalt", "highway=crossing crossing=unmarked", "502", "3923"],
|
||||
["4705033", "50884156", "19", "5", "1260", "0", "0", "0", "0", "reversedirection=yes highway=footway surface=asphalt", "", "506", "3940"],
|
||||
["4705152", "50884205", "18", "10", "1260", "0", "0", "0", "0", "highway=footway", "", "513", "3957"],
|
||||
["4705390", "50884781", "18", "91", "1260", "0", "0", "0", "0", "reversedirection=yes highway=footway", "", "576", "4195"],
|
||||
["4705499", "50884835", "18", "9", "1260", "0", "0", "0", "0", "highway=footway surface=asphalt", "", "582", "4231"],
|
||||
["4705441", "50884869", "18", "6", "1100", "0", "0", "0", "0", "highway=cycleway surface=paving_stones foot=designated bicycle=designated route_hiking_nwn=yes route_hiking_rwn=yes", "", "587", "4257"],
|
||||
["4705465", "50884885", "18", "2", "1100", "0", "0", "0", "0", "highway=cycleway surface=concrete foot=yes route_hiking_nwn=yes route_hiking_rwn=yes", "", "588", "4266"],
|
||||
["4705560", "50884949", "18", "10", "1100", "0", "0", "0", "0", "reversedirection=yes highway=cycleway surface=asphalt foot=designated bicycle=designated route_hiking_nwn=yes route_hiking_rwn=yes", "", "595", "4293"],
|
||||
["4705824", "50886096", "19", "129", "1260", "0", "0", "0", "0", "reversedirection=yes highway=footway surface=asphalt foot=yes", "crossing=unknown", "688", "5661"],
|
||||
["4705976", "50886872", "18", "87", "1260", "0", "0", "0", "0", "reversedirection=yes highway=footway surface=asphalt foot=yes", "", "749", "6310"],
|
||||
["4706015", "50886870", "18", "3", "1260", "0", "0", "0", "0", "reversedirection=yes highway=footway surface=asphalt", "highway=crossing", "751", "6326"],
|
||||
["4706055", "50886868", "18", "3", "1260", "0", "0", "0", "0", "reversedirection=yes highway=footway surface=asphalt", "barrier=kerb kerb=flush", "754", "6342"],
|
||||
["4706588", "50887183", "18", "54", "1260", "0", "0", "0", "0", "highway=footway surface=paving_stones", "", "792", "6729"],
|
||||
["4706710", "50887317", "19", "17", "1260", "0", "0", "0", "0", "reversedirection=yes highway=footway", "highway=crossing estimated_crossing_class=0 crossing=traffic_signals", "804", "6940"],
|
||||
["4707067", "50887727", "20", "52", "1260", "0", "0", "0", "0", "reversedirection=yes highway=footway", "", "843", "7844"],
|
||||
["4707080", "50887764", "20", "4", "1260", "0", "0", "0", "0", "reversedirection=yes highway=footway surface=asphalt", "highway=crossing estimated_crossing_class=0 crossing=traffic_signals bicycle=yes", "845", "7912"],
|
||||
["4707124", "50887747", "20", "4", "16800", "0", "0", "0", "0", "reversedirection=yes highway=primary_link surface=asphalt foot=use_sidepath bicycle=use_sidepath", "highway=traffic_signals traffic_signals:direction=forward", "848", "7978"],
|
||||
["4708324", "50887251", "21", "101", "16800", "0", "0", "0", "0", "reversedirection=yes highway=primary_link surface=asphalt foot=use_sidepath bicycle=use_sidepath", "", "923", "9656"],
|
||||
["4707124", "50887747", "20", "101", "16800", "0", "0", "0", "0", "highway=primary_link surface=asphalt foot=use_sidepath bicycle=use_sidepath", "highway=traffic_signals traffic_signals:direction=forward", "994", "10312"],
|
||||
["4707080", "50887764", "20", "4", "16800", "0", "0", "0", "0", "highway=primary_link surface=asphalt foot=use_sidepath bicycle=use_sidepath", "highway=crossing estimated_crossing_class=0 crossing=traffic_signals bicycle=yes", "997", "10343"],
|
||||
["4707067", "50887727", "20", "4", "1260", "0", "0", "0", "0", "highway=footway surface=asphalt", "", "1000", "10374"],
|
||||
["4706710", "50887317", "19", "52", "1260", "0", "0", "0", "0", "highway=footway", "highway=crossing estimated_crossing_class=0 crossing=traffic_signals", "1036", "10496"],
|
||||
["4706588", "50887183", "18", "17", "1260", "0", "0", "0", "0", "highway=footway", "", "1048", "10515"],
|
||||
["4711122", "50884603", "21", "441", "1260", "0", "0", "0", "0", "highway=footway surface=paving_stones", "", "1366", "15770"],
|
||||
["4711170", "50884572", "21", "5", "1260", "0", "0", "0", "0", "highway=footway surface=asphalt", "highway=crossing crossing=uncontrolled", "1370", "15819"],
|
||||
["4711194", "50884557", "21", "2", "1260", "0", "0", "0", "0", "highway=footway surface=asphalt", "barrier=kerb kerb=flush", "1371", "15839"],
|
||||
["4713706", "50882320", "24", "306", "1260", "0", "0", "0", "0", "highway=footway surface=paving_stones", "", "1591", "19160"],
|
||||
["4713716", "50882311", "24", "1", "1260", "0", "0", "0", "0", "reversedirection=yes highway=footway surface=paving_stones", "", "1592", "19170"],
|
||||
["4714413", "50881554", "28", "98", "1260", "0", "0", "0", "0", "highway=footway surface=paving_stones", "", "1668", "22011"],
|
||||
["4714572", "50881225", "29", "39", "1200", "0", "0", "0", "0", "highway=pedestrian surface=paving_stones", "", "1698", "23027"],
|
||||
["4714622", "50881234", "29", "4", "1260", "0", "0", "0", "0", "highway=footway surface=asphalt", "crossing=unknown", "1701", "23119"],
|
||||
["4714653", "50881159", "29", "9", "1320", "0", "0", "0", "0", "highway=residential surface=concrete bicycle=yes sidewalk=both route_bicycle_lcn=yes", "", "1707", "23317"],
|
||||
["4714681", "50881090", "29", "8", "1320", "0", "0", "0", "0", "highway=residential surface=concrete bicycle=yes route_bicycle_ncn=yes route_bicycle_rcn=yes route_bicycle_lcn=yes", "crossing=unknown", "1713", "23484"],
|
||||
["4714797", "50880817", "29", "31", "1320", "0", "0", "0", "0", "highway=residential surface=concrete bicycle=yes route_bicycle_ncn=yes route_bicycle_rcn=yes route_bicycle_lcn=yes", "", "1736", "24071"],
|
||||
["4714876", "50880590", "29", "26", "1320", "0", "0", "0", "0", "reversedirection=yes highway=residential surface=concrete bicycle=yes route_bicycle_lcn=yes", "", "1756", "24593"],
|
||||
["4715160", "50879831", "30", "87", "1320", "0", "0", "0", "0", "highway=residential surface=asphalt bicycle=use_sidepath", "", "1820", "26149"],
|
||||
["4715101", "50879883", "30", "7", "1440", "0", "0", "0", "0", "reversedirection=yes highway=service", "highway=give_way direction=forward", "1825", "26274"],
|
||||
["4714381", "50881499", "26", "187", "1440", "0", "0", "0", "0", "reversedirection=yes highway=service", "", "1965", "30005"],
|
||||
["4714614", "50881537", "26", "17", "1440", "0", "0", "0", "0", "reversedirection=yes highway=service surface=concrete", "", "1977", "30222"],
|
||||
["4713938", "50882450", "24", "114", "1440", "0", "0", "0", "0", "highway=service", "", "2057", "30932"],
|
||||
["4713540", "50882794", "24", "47", "1440", "0", "0", "0", "0", "highway=service surface=asphalt", "", "2090", "31017"],
|
||||
["4713938", "50882450", "24", "47", "1440", "0", "0", "0", "0", "reversedirection=yes highway=service surface=asphalt", "", "2124", "31453"],
|
||||
["4714614", "50881537", "27", "114", "1440", "0", "0", "0", "0", "reversedirection=yes highway=service", "", "2205", "32510"],
|
||||
["4714381", "50881499", "27", "17", "1440", "0", "0", "0", "0", "highway=service surface=concrete", "", "2217", "32667"],
|
||||
["4715101", "50879883", "30", "187", "1440", "0", "0", "0", "0", "highway=service", "highway=give_way direction=forward", "2357", "36467"],
|
||||
["4715160", "50879831", "30", "7", "1440", "0", "0", "0", "0", "highway=service", "", "2362", "36570"],
|
||||
["4714907", "50880454", "29", "72", "1320", "0", "0", "0", "0", "reversedirection=yes highway=residential surface=asphalt bicycle=use_sidepath", "", "2417", "38328"],
|
||||
["4714636", "50880436", "30", "19", "1320", "0", "0", "0", "0", "reversedirection=yes highway=cycleway surface=paving_stones route_bicycle_lcn=yes", "", "2431", "38835"],
|
||||
["4712836", "50879070", "34", "198", "1320", "0", "0", "0", "0", "reversedirection=yes highway=residential surface=asphalt foot=yes sidewalk=both", "", "2578", "42607"],
|
||||
["4712905", "50879038", "34", "6", "1320", "0", "0", "0", "0", "highway=residential surface=asphalt", "highway=give_way direction=backward", "2583", "42716"],
|
||||
["4714481", "50878358", "34", "134", "1320", "0", "0", "0", "0", "highway=residential surface=asphalt", "highway=crossing crossing=uncontrolled traffic_calming=table", "2680", "44525"],
|
||||
["4714517", "50878296", "35", "10", "1320", "0", "0", "0", "0", "highway=residential surface=asphalt", "highway=crossing crossing=unmarked kerb=flush", "2688", "44730"],
|
||||
["4713125", "50877237", "36", "153", "1320", "0", "0", "0", "0", "highway=residential surface=asphalt", "", "2802", "47900"],
|
||||
["4713457", "50877115", "36", "29", "1440", "0", "0", "0", "0", "reversedirection=yes highway=service access=permissive", "", "2823", "48359"],
|
||||
["4713125", "50877237", "36", "29", "1440", "0", "0", "0", "0", "highway=service access=permissive", "", "2845", "48838"],
|
||||
["4712507", "50876767", "36", "68", "1320", "0", "0", "0", "0", "highway=residential surface=asphalt", "highway=give_way direction=forward", "2895", "49693"],
|
||||
["4712479", "50876746", "37", "3", "1320", "0", "0", "0", "0", "highway=residential surface=asphalt", "", "2897", "49741"],
|
||||
["4712501", "50876618", "37", "14", "1320", "0", "0", "0", "0", "reversedirection=yes highway=residential surface=paving_stones junction=roundabout", "", "2907", "49956"],
|
||||
["4712445", "50876614", "37", "4", "1260", "0", "0", "0", "0", "highway=footway surface=paving_stones", "barrier=bollard bicycle=yes foot=yes motor_vehicle=no", "2910", "50016"],
|
||||
["4712448", "50876594", "37", "2", "1200", "0", "0", "0", "0", "highway=pedestrian surface=paving_stones bicycle=yes", "barrier=bollard bicycle=yes foot=yes motor_vehicle=no", "2911", "50045"],
|
||||
["4712345", "50876516", "37", "16", "1200", "0", "0", "0", "0", "highway=pedestrian surface=paving_stones bicycle=yes", "barrier=bollard bicycle=yes foot=yes motor_vehicle=no", "2923", "50289"],
|
||||
["4712316", "50876514", "37", "2", "1200", "0", "0", "0", "0", "highway=pedestrian surface=paving_stones bicycle=yes", "barrier=bollard bicycle=yes foot=yes motor_vehicle=no", "2925", "50316"],
|
||||
["4712323", "50876470", "37", "5", "1260", "0", "0", "0", "0", "reversedirection=yes highway=footway surface=paving_stones", "", "2928", "50394"],
|
||||
["4712121", "50876456", "37", "14", "1320", "0", "0", "0", "0", "reversedirection=yes highway=residential surface=paving_stones junction=roundabout", "", "2938", "50576"],
|
||||
["4712096", "50876437", "37", "3", "1320", "0", "0", "0", "0", "highway=residential surface=asphalt sidewalk=both", "highway=give_way direction=backward", "2941", "50614"],
|
||||
["4711050", "50875639", "37", "116", "1320", "0", "0", "0", "0", "highway=residential surface=asphalt sidewalk=both", "highway=crossing crossing=uncontrolled kerb=lowered", "3024", "51921"],
|
||||
["4711033", "50875627", "37", "2", "1320", "0", "0", "0", "0", "highway=residential surface=asphalt sidewalk=both", "highway=give_way direction=forward", "3026", "51941"],
|
||||
["4710922", "50875545", "37", "12", "1320", "0", "0", "0", "0", "highway=residential surface=asphalt sidewalk=both", "highway=crossing crossing=uncontrolled kerb=lowered", "3034", "52064"],
|
||||
["4710881", "50875514", "37", "4", "1320", "0", "0", "0", "0", "highway=residential surface=asphalt sidewalk=both", "highway=give_way direction=backward", "3037", "52105"],
|
||||
["4710354", "50875078", "37", "61", "1320", "0", "0", "0", "0", "highway=residential surface=asphalt sidewalk=both", "", "3081", "52710"],
|
||||
["4710315", "50875054", "37", "4", "1320", "0", "0", "0", "0", "highway=residential surface=paving_stones sidewalk=both", "highway=crossing crossing=uncontrolled kerb=lowered", "3084", "52749"],
|
||||
["4710276", "50875029", "37", "4", "1320", "0", "0", "0", "0", "highway=residential surface=paving_stones sidewalk=both", "highway=stop direction=forward", "3087", "52788"],
|
||||
["4710191", "50874972", "37", "8", "1320", "0", "0", "0", "0", "highway=residential surface=paving_stones sidewalk=both", "highway=stop direction=backward", "3092", "52866"],
|
||||
["4710148", "50874938", "37", "5", "1320", "0", "0", "0", "0", "highway=residential surface=paving_stones sidewalk=both", "highway=crossing crossing=uncontrolled kerb=lowered", "3096", "52914"],
|
||||
["4710115", "50874913", "37", "4", "1320", "0", "0", "0", "0", "highway=residential surface=paving_stones sidewalk=both", "", "3099", "52953"],
|
||||
["4709794", "50874663", "38", "36", "1320", "0", "0", "0", "0", "highway=residential surface=asphalt", "", "3125", "53524"],
|
||||
["4709764", "50874640", "38", "3", "1320", "0", "0", "0", "0", "highway=residential surface=asphalt sidewalk=both", "barrier=bollard bicycle=yes foot=yes motor_vehicle=no", "3127", "53572"],
|
||||
["4709735", "50874620", "38", "3", "1320", "0", "0", "0", "0", "reversedirection=yes highway=residential surface=asphalt sidewalk=both", "", "3130", "53619"],
|
||||
["4708064", "50872679", "38", "253", "1320", "0", "0", "0", "0", "highway=residential surface=asphalt sidewalk=both", "", "3313", "56842"],
|
||||
["4708332", "50872658", "37", "19", "1440", "0", "0", "0", "0", "reversedirection=yes highway=service", "", "3326", "56977"],
|
||||
["4708064", "50872679", "38", "19", "1440", "0", "0", "0", "0", "highway=service", "", "3340", "57267"],
|
||||
["4708625", "50874109", "38", "184", "1320", "0", "0", "0", "0", "reversedirection=yes highway=residential surface=asphalt sidewalk=both", "traffic_calming=choker", "3471", "58852"],
|
||||
["4708058", "50874404", "37", "51", "1320", "0", "0", "0", "0", "reversedirection=yes highway=residential surface=asphalt sidewalk=both", "traffic_calming=choker", "3507", "59141"],
|
||||
["4707935", "50874469", "38", "11", "1320", "0", "0", "0", "0", "reversedirection=yes highway=residential surface=asphalt sidewalk=both", "highway=crossing", "3515", "59262"],
|
||||
["4707611", "50874644", "38", "30", "1320", "0", "0", "0", "0", "reversedirection=yes highway=residential surface=asphalt sidewalk=both", "traffic_calming=choker", "3537", "59585"],
|
||||
["4707340", "50874790", "38", "25", "1320", "0", "0", "0", "0", "reversedirection=yes highway=residential surface=asphalt sidewalk=both", "highway=give_way direction=backward", "3555", "59845"],
|
||||
["4707325", "50874798", "38", "1", "1320", "0", "0", "0", "0", "reversedirection=yes highway=residential surface=asphalt sidewalk=both", "highway=crossing crossing=unmarked", "3556", "59855"],
|
||||
["4707260", "50874830", "38", "6", "1320", "0", "0", "0", "0", "reversedirection=yes highway=residential surface=asphalt sidewalk=both", "", "3560", "59916"],
|
||||
["4707818", "50875257", "38", "62", "1320", "0", "0", "0", "0", "highway=residential surface=asphalt sidewalk=both", "", "3604", "60533"],
|
||||
["4708336", "50875628", "38", "55", "1320", "0", "0", "0", "0", "highway=residential surface=asphalt foot=yes sidewalk=both", "highway=stop direction=forward", "3644", "61052"],
|
||||
["4708350", "50875638", "38", "1", "1320", "0", "0", "0", "0", "highway=residential surface=asphalt foot=yes sidewalk=both", "highway=crossing crossing=uncontrolled kerb=lowered", "3644", "61062"],
|
||||
["4708412", "50875685", "38", "7", "1320", "0", "0", "0", "0", "highway=residential surface=asphalt foot=yes sidewalk=both", "", "3649", "61129"],
|
||||
["4708322", "50875717", "37", "7", "1440", "0", "0", "0", "0", "reversedirection=yes highway=tertiary surface=asphalt sidewalk=both", "highway=crossing crossing=uncontrolled kerb=lowered", "3654", "61182"],
|
||||
["4706094", "50876714", "38", "193", "1440", "0", "0", "0", "0", "reversedirection=yes highway=tertiary surface=asphalt sidewalk=both", "highway=crossing crossing=unmarked kerb=lowered", "3793", "63402"],
|
||||
["4706058", "50876727", "38", "3", "1440", "0", "0", "0", "0", "reversedirection=yes highway=tertiary surface=asphalt sidewalk=both", "", "3796", "63438"],
|
||||
["4705972", "50876755", "38", "7", "1320", "0", "0", "0", "0", "reversedirection=yes highway=living_street surface=asphalt bicycle=yes sidewalk=both", "", "3801", "63506"],
|
||||
["4705423", "50876932", "38", "43", "1320", "0", "0", "0", "0", "reversedirection=yes highway=living_street surface=paving_stones bicycle=yes sidewalk=both", "", "3831", "63776"],
|
||||
["4705400", "50876939", "38", "2", "1320", "0", "0", "0", "0", "reversedirection=yes highway=residential surface=asphalt", "highway=crossing crossing=unmarked kerb=flush", "3832", "63789"],
|
||||
["4704857", "50877116", "36", "43", "1320", "0", "0", "0", "0", "reversedirection=yes highway=residential surface=asphalt", "", "3862", "63789"],
|
||||
["4704683", "50877183", "36", "14", "1320", "0", "0", "0", "0", "reversedirection=yes highway=residential surface=asphalt sidewalk=both", "highway=crossing crossing=unmarked", "3871", "63789"],
|
||||
["4704644", "50877203", "36", "3", "1320", "0", "0", "0", "0", "reversedirection=yes highway=residential surface=asphalt sidewalk=both", "", "3873", "63789"],
|
||||
["4703626", "50877883", "32", "105", "1200", "0", "0", "0", "0", "reversedirection=yes highway=pedestrian surface=sett bicycle=yes", "", "3946", "64086"],
|
||||
["4702846", "50878849", "29", "126", "1200", "0", "0", "0", "0", "reversedirection=yes highway=pedestrian surface=sett access=delivery foot=yes bicycle=yes", "barrier=bollard", "4030", "64086"],
|
||||
["4702447", "50879023", "27", "34", "1400", "0", "0", "0", "0", "reversedirection=yes highway=pedestrian surface=paving_stones access=delivery foot=yes bicycle=yes", "", "4052", "64086"],
|
||||
["4702453", "50879159", "26", "15", "1200", "0", "0", "0", "0", "reversedirection=yes highway=pedestrian access=delivery foot=yes bicycle=yes", "", "4062", "64086"],
|
||||
["4702176", "50879240", "28", "22", "1320", "0", "0", "0", "0", "highway=residential surface=asphalt foot=yes bicycle=yes route_bicycle_ncn=yes route_bicycle_rcn=yes", "highway=crossing", "4076", "64086"],
|
||||
["4702137", "50879274", "28", "5", "1320", "0", "0", "0", "0", "highway=residential surface=asphalt foot=yes bicycle=yes sidewalk=both route_bicycle_ncn=yes route_bicycle_rcn=yes", "", "4080", "64086"],
|
||||
["4701863", "50879520", "27", "33", "1100", "0", "0", "0", "0", "highway=residential surface=asphalt foot=yes bicycle=yes sidewalk=both route_bicycle_lcn=yes route_hiking_nwn=yes route_hiking_rwn=yes", "nodeaccessgranted=yes highway=crossing", "4102", "64086"],
|
||||
["4701074", "50879938", "24", "73", "1300", "0", "0", "0", "0", "highway=residential surface=asphalt foot=yes bicycle=yes sidewalk=both route_bicycle_lcn=yes route_hiking_nwn=yes route_hiking_rwn=yes", "nodeaccessgranted=yes highway=crossing crossing=uncontrolled", "4150", "64086"],
|
||||
["4700933", "50880001", "23", "12", "1300", "0", "0", "0", "0", "highway=residential surface=asphalt foot=yes bicycle=yes sidewalk=both route_bicycle_lcn=yes route_hiking_nwn=yes route_hiking_rwn=yes", "", "4157", "64086"],
|
||||
["4700731", "50879935", "23", "16", "1520", "0", "0", "0", "0", "highway=residential surface=asphalt foot=yes bicycle=yes sidewalk=both route_bicycle_lcn=yes", "", "4168", "64086"],
|
||||
["4700609", "50879880", "23", "11", "1520", "0", "0", "0", "0", "highway=residential surface=asphalt bicycle=yes route_bicycle_lcn=yes", "crossing=unknown", "4175", "64086"],
|
||||
["4700567", "50879908", "23", "4", "1460", "0", "0", "0", "0", "reversedirection=yes highway=footway surface=asphalt", "", "4178", "64086"],
|
||||
["4700436", "50879922", "23", "12", "1400", "0", "0", "0", "0", "highway=pedestrian surface=paving_stones", "", "4185", "64086"],
|
||||
["4700399", "50879888", "23", "5", "1460", "0", "0", "0", "0", "reversedirection=yes highway=footway surface=asphalt", "highway=crossing crossing=unmarked", "4189", "64086"],
|
||||
["4700077", "50880030", "22", "28", "1431", "0", "0", "0", "0", "highway=residential surface=asphalt bicycle=yes route_bicycle_ncn=yes route_bicycle_rcn=yes route_bicycle_lcn=yes", "highway=crossing crossing=uncontrolled bicycle=yes", "4207", "64086"],
|
||||
["4700045", "50880001", "22", "4", "1260", "0", "0", "0", "0", "reversedirection=yes highway=footway surface=asphalt bicycle=yes", "", "4210", "64086"],
|
||||
["4700029", "50879984", "23", "2", "1200", "0", "0", "0", "0", "reversedirection=yes highway=pedestrian surface=paving_stones bicycle=yes", "", "4212", "64086"],
|
||||
["4699997", "50879997", "22", "3", "1260", "0", "0", "0", "0", "reversedirection=yes highway=footway surface=paving_stones", "", "4214", "64086"]
|
||||
],
|
||||
"times": [0,2.163,3.592,6.45,10.023,10.731,23.989,33.714,36.5,39.264,42.056,67.852,70.001,70.711,74.936,98.233,103.186,105.31,122.058,126.223,126.918,129.7,132.485,142.174,145.645,149.121,153.297,155.388,216.549,217.964,230.002,256.888,260.361,262.446,265.926,268.017,315.035,319.27,322.095,341.906,375.975,422.96,434.367,456.478,466.468,480.519,500.66,502.752,506.243,509.709,513.179,520.135,540.402,541.103,576.523,577.222,578.621,582.822,587.027,588.43,595.394,599.581,657.224,665.125,677.329,688.09,735.792,749.828,751.937,754.048,757.568,761.759,768.057,789.447,792.325,794.483,804.63,816.298,843.016,845.964,848.908,923.235,994.634,997.475,1000.317,1025.363,1036.474,1046.156,1048.236,1054.488,1057.969,1188.031,1216.246,1217.69,1219.854,1224.861,1270.5,1294.282,1366.788,1370.37,1371.803,1373.236,1373.959,1394.884,1442.953,1591.905,1592.62,1595.505,1647.111,1651.672,1668.393,1669.148,1698.194,1701.212,1707.979,1713.969,1736.995,1740.685,1747.368,1756.393,1767.421,1771.745,1820.746,1825.924,1829.647,1871.099,1965.407,1977.726,2021.74,2057.81,2090.421,2124.015,2161.896,2205.496,2217.646,2312.819,2353.622,2357.313,2362.428,2412.539,2417.085,2423.174,2431.638,2509.766,2578.798,2583.241,2669.58,2680.645,2685.133,2688.118,2691.843,2775.31,2782.71,2802.574,2806.969,2817.116,2820.704,2823.859,2827.488,2831.152,2841.471,2845.88,2863.9,2884.197,2895.129,2897.33,2907.583,2910.506,2911.966,2913.439,2917.814,2922.222,2923.677,2925.131,2928.797,2938.952,2941.126,2959.208,2964.983,2990.204,3021.826,3024.697,3026.132,3029.003,3034.743,3036.895,3037.612,3050.519,3055.536,3081.326,3084.191,3087.055,3090.635,3092.782,3096.362,3099.225,3116.077,3121.231,3125.641,3127.843,3130.043,3197.625,3232.684,3242.78,3252.782,3262.867,3313.393,3326.857,3340.766,3389.922,3399.826,3409.824,3419.743,3454.78,3465.504,3471.938,3484.501,3502.874,3507.871,3515.79,3537.358,3555.307,3556.024,3560.329,3574.671,3604.764,3611.155,3644.101,3644.817,3649.828,3654.796,3670.425,3726.304,3745.056,3748.627,3780.958,3789.644,3793.981,3796.149,3801.162,3803.31,3820.933,3831.533,3832.948,3862.153,3868.914,3871.629,3872.309,3873.669,3887.58,3914.583,3946.206,3950.333,3988.384,4008.297,4016.443,4030.473,4035.778,4052.43,4057.557,4062.002,4067.258,4074.098,4076.843,4080.28,4102.608,4113.217,4121.783,4125.044,4150.016,4157.703,4168.1,4175.395,4178.04,4183.308,4185.978,4189.325,4207.966,4210.658,4212.018,4214.043]
|
||||
},
|
||||
"geometry": {
|
||||
"type": "LineString",
|
||||
"coordinates": [
|
||||
[4.699997, 50.879997, 22.75],
|
||||
[4.700029, 50.879984, 23.0],
|
||||
[4.700045, 50.880001, 22.75],
|
||||
[4.700077, 50.880030, 22.75],
|
||||
[4.700117, 50.880063, 22.75],
|
||||
[4.700126, 50.880071, 22.5],
|
||||
[4.700293, 50.880210, 22.0],
|
||||
[4.700423, 50.880308, 21.75],
|
||||
[4.700458, 50.880337, 21.75],
|
||||
[4.700497, 50.880366, 21.5],
|
||||
[4.700547, 50.880349, 21.75],
|
||||
[4.700964, 50.880166, 22.5],
|
||||
[4.700995, 50.880188, 22.5],
|
||||
[4.701006, 50.880197, 22.25],
|
||||
[4.701045, 50.880243, 22.0],
|
||||
[4.701350, 50.880466, 22.0],
|
||||
[4.701426, 50.880513, 22.0],
|
||||
[4.701462, 50.880534, 22.0],
|
||||
[4.701734, 50.880664, 21.5],
|
||||
[4.701806, 50.880698, 21.25],
|
||||
[4.701820, 50.880705, 21.25],
|
||||
[4.701864, 50.880726, 21.25],
|
||||
[4.701911, 50.880749, 21.25],
|
||||
[4.702081, 50.880815, 21.0],
|
||||
[4.702141, 50.880842, 21.0],
|
||||
[4.702201, 50.880864, 21.0],
|
||||
[4.702262, 50.880829, 21.0],
|
||||
[4.702299, 50.880840, 21.0],
|
||||
[4.703275, 50.881325, 21.0],
|
||||
[4.703293, 50.881333, 21.0],
|
||||
[4.703486, 50.881429, 21.0],
|
||||
[4.703923, 50.881642, 20.0],
|
||||
[4.703896, 50.881681, 20.0],
|
||||
[4.703929, 50.881704, 20.0],
|
||||
[4.703985, 50.881728, 20.0],
|
||||
[4.704023, 50.881729, 20.0],
|
||||
[4.704773, 50.882093, 20.0],
|
||||
[4.704838, 50.882125, 20.0],
|
||||
[4.704885, 50.882149, 20.0],
|
||||
[4.704640, 50.882349, 20.0],
|
||||
[4.704219, 50.882691, 20.0],
|
||||
[4.704474, 50.883260, 20.0],
|
||||
[4.704538, 50.883397, 20.0],
|
||||
[4.704650, 50.883664, 20.0],
|
||||
[4.704707, 50.883786, 20.0],
|
||||
[4.704763, 50.883960, 19.5],
|
||||
[4.705107, 50.884096, 19.0],
|
||||
[4.705080, 50.884119, 19.0],
|
||||
[4.705033, 50.884156, 19.0],
|
||||
[4.705090, 50.884178, 18.75],
|
||||
[4.705152, 50.884205, 18.75],
|
||||
[4.705069, 50.884275, 18.75],
|
||||
[4.704819, 50.884487, 18.75],
|
||||
[4.704823, 50.884496, 18.75],
|
||||
[4.705390, 50.884781, 18.25],
|
||||
[4.705406, 50.884788, 18.25],
|
||||
[4.705432, 50.884800, 18.25],
|
||||
[4.705499, 50.884835, 18.25],
|
||||
[4.705441, 50.884869, 18.25],
|
||||
[4.705465, 50.884885, 18.25],
|
||||
[4.705560, 50.884949, 18.0],
|
||||
[4.705589, 50.884999, 18.0],
|
||||
[4.705744, 50.885709, 19.0],
|
||||
[4.705767, 50.885811, 19.0],
|
||||
[4.705800, 50.885964, 19.0],
|
||||
[4.705824, 50.886096, 19.0],
|
||||
[4.705941, 50.886695, 18.75],
|
||||
[4.705976, 50.886872, 18.25],
|
||||
[4.706015, 50.886870, 18.25],
|
||||
[4.706055, 50.886868, 18.25],
|
||||
[4.706128, 50.886863, 18.25],
|
||||
[4.706211, 50.886883, 18.0],
|
||||
[4.706328, 50.886925, 18.0],
|
||||
[4.706555, 50.887151, 18.5],
|
||||
[4.706588, 50.887183, 18.75],
|
||||
[4.706610, 50.887205, 18.75],
|
||||
[4.706710, 50.887317, 19.0],
|
||||
[4.706819, 50.887439, 19.25],
|
||||
[4.707067, 50.887727, 20.0],
|
||||
[4.707080, 50.887764, 20.0],
|
||||
[4.707124, 50.887747, 20.0],
|
||||
[4.708324, 50.887251, 21.0],
|
||||
[4.707124, 50.887747, 20.0],
|
||||
[4.707080, 50.887764, 20.0],
|
||||
[4.707067, 50.887727, 20.0],
|
||||
[4.706819, 50.887439, 19.25],
|
||||
[4.706710, 50.887317, 19.0],
|
||||
[4.706610, 50.887205, 18.75],
|
||||
[4.706588, 50.887183, 18.75],
|
||||
[4.706719, 50.887194, 18.75],
|
||||
[4.706785, 50.887176, 18.75],
|
||||
[4.708918, 50.886307, 20.5],
|
||||
[4.709376, 50.886112, 20.75],
|
||||
[4.709399, 50.886102, 20.75],
|
||||
[4.709430, 50.886089, 20.75],
|
||||
[4.709508, 50.886055, 20.5],
|
||||
[4.710023, 50.885591, 21.0],
|
||||
[4.710294, 50.885347, 21.0],
|
||||
[4.711122, 50.884603, 21.0],
|
||||
[4.711170, 50.884572, 21.0],
|
||||
[4.711194, 50.884557, 21.0],
|
||||
[4.711216, 50.884540, 21.0],
|
||||
[4.711223, 50.884534, 21.25],
|
||||
[4.711459, 50.884324, 21.25],
|
||||
[4.711997, 50.883841, 21.75],
|
||||
[4.713706, 50.882320, 24.5],
|
||||
[4.713716, 50.882311, 24.5],
|
||||
[4.713749, 50.882281, 24.75],
|
||||
[4.714302, 50.881798, 28.0],
|
||||
[4.714327, 50.881744, 28.0],
|
||||
[4.714413, 50.881554, 28.25],
|
||||
[4.714431, 50.881557, 28.25],
|
||||
[4.714572, 50.881225, 29.0],
|
||||
[4.714622, 50.881234, 29.0],
|
||||
[4.714653, 50.881159, 29.0],
|
||||
[4.714681, 50.881090, 29.0],
|
||||
[4.714797, 50.880817, 29.0],
|
||||
[4.714814, 50.880777, 29.0],
|
||||
[4.714846, 50.880695, 29.25],
|
||||
[4.714876, 50.880590, 29.75],
|
||||
[4.714907, 50.880454, 29.25],
|
||||
[4.714921, 50.880405, 28.75],
|
||||
[4.715160, 50.879831, 30.0],
|
||||
[4.715101, 50.879883, 30.25],
|
||||
[4.715059, 50.879920, 30.5],
|
||||
[4.714850, 50.880390, 29.25],
|
||||
[4.714381, 50.881499, 26.5],
|
||||
[4.714614, 50.881537, 26.25],
|
||||
[4.714384, 50.882068, 25.0],
|
||||
[4.713938, 50.882450, 24.0],
|
||||
[4.713540, 50.882794, 24.0],
|
||||
[4.713938, 50.882450, 24.75],
|
||||
[4.714384, 50.882068, 26.0],
|
||||
[4.714614, 50.881537, 27.25],
|
||||
[4.714381, 50.881499, 27.75],
|
||||
[4.714850, 50.880390, 30.5],
|
||||
[4.715059, 50.879920, 30.5],
|
||||
[4.715101, 50.879883, 30.25],
|
||||
[4.715160, 50.879831, 30.0],
|
||||
[4.714921, 50.880405, 28.75],
|
||||
[4.714907, 50.880454, 29.25],
|
||||
[4.714793, 50.880442, 29.5],
|
||||
[4.714636, 50.880436, 30.0],
|
||||
[4.713651, 50.879700, 32.0],
|
||||
[4.712836, 50.879070, 34.25],
|
||||
[4.712905, 50.879038, 34.25],
|
||||
[4.714304, 50.878435, 34.0],
|
||||
[4.714481, 50.878358, 34.75],
|
||||
[4.714557, 50.878326, 35.25],
|
||||
[4.714517, 50.878296, 35.25],
|
||||
[4.714471, 50.878260, 35.25],
|
||||
[4.713463, 50.877494, 36.5],
|
||||
[4.713369, 50.877427, 36.75],
|
||||
[4.713125, 50.877237, 36.75],
|
||||
[4.713185, 50.877205, 36.75],
|
||||
[4.713340, 50.877128, 36.5],
|
||||
[4.713403, 50.877117, 36.25],
|
||||
[4.713457, 50.877115, 36.0],
|
||||
[4.713403, 50.877117, 36.25],
|
||||
[4.713340, 50.877128, 36.5],
|
||||
[4.713185, 50.877205, 36.75],
|
||||
[4.713125, 50.877237, 36.75],
|
||||
[4.712896, 50.877063, 36.25],
|
||||
[4.712645, 50.876872, 36.5],
|
||||
[4.712507, 50.876767, 36.75],
|
||||
[4.712479, 50.876746, 37.0],
|
||||
[4.712501, 50.876618, 37.0],
|
||||
[4.712445, 50.876614, 37.0],
|
||||
[4.712448, 50.876594, 37.0],
|
||||
[4.712451, 50.876576, 37.25],
|
||||
[4.712371, 50.876570, 37.0],
|
||||
[4.712379, 50.876518, 37.25],
|
||||
[4.712345, 50.876516, 37.0],
|
||||
[4.712316, 50.876514, 37.0],
|
||||
[4.712323, 50.876470, 37.25],
|
||||
[4.712121, 50.876456, 37.0],
|
||||
[4.712096, 50.876437, 37.0],
|
||||
[4.711873, 50.876267, 37.0],
|
||||
[4.711800, 50.876211, 37.0],
|
||||
[4.711483, 50.875969, 37.0],
|
||||
[4.711086, 50.875667, 37.0],
|
||||
[4.711050, 50.875639, 37.0],
|
||||
[4.711033, 50.875627, 37.0],
|
||||
[4.711000, 50.875602, 37.0],
|
||||
[4.710922, 50.875545, 37.0],
|
||||
[4.710893, 50.875523, 37.0],
|
||||
[4.710881, 50.875514, 37.0],
|
||||
[4.710719, 50.875385, 37.0],
|
||||
[4.710670, 50.875332, 37.0],
|
||||
[4.710354, 50.875078, 37.0],
|
||||
[4.710315, 50.875054, 37.0],
|
||||
[4.710276, 50.875029, 37.0],
|
||||
[4.710221, 50.874995, 37.0],
|
||||
[4.710191, 50.874972, 37.0],
|
||||
[4.710148, 50.874938, 37.0],
|
||||
[4.710115, 50.874913, 37.0],
|
||||
[4.709913, 50.874756, 37.75],
|
||||
[4.709846, 50.874704, 38.0],
|
||||
[4.709794, 50.874663, 38.0],
|
||||
[4.709764, 50.874640, 38.0],
|
||||
[4.709735, 50.874620, 38.0],
|
||||
[4.708889, 50.873974, 38.0],
|
||||
[4.708457, 50.873625, 37.75],
|
||||
[4.708350, 50.873523, 38.0],
|
||||
[4.708263, 50.873409, 37.75],
|
||||
[4.708197, 50.873294, 38.0],
|
||||
[4.708064, 50.872679, 38.75],
|
||||
[4.708332, 50.872658, 37.5],
|
||||
[4.708064, 50.872679, 38.75],
|
||||
[4.708197, 50.873294, 38.0],
|
||||
[4.708263, 50.873409, 37.75],
|
||||
[4.708350, 50.873523, 38.0],
|
||||
[4.708457, 50.873625, 37.75],
|
||||
[4.708889, 50.873974, 38.0],
|
||||
[4.708719, 50.874060, 38.0],
|
||||
[4.708625, 50.874109, 38.0],
|
||||
[4.708423, 50.874214, 37.25],
|
||||
[4.708132, 50.874365, 37.5],
|
||||
[4.708058, 50.874404, 37.75],
|
||||
[4.707935, 50.874469, 38.0],
|
||||
[4.707611, 50.874644, 38.0],
|
||||
[4.707340, 50.874790, 38.0],
|
||||
[4.707325, 50.874798, 38.0],
|
||||
[4.707260, 50.874830, 38.0],
|
||||
[4.707439, 50.874966, 38.0],
|
||||
[4.707818, 50.875257, 38.0],
|
||||
[4.707898, 50.875319, 37.75],
|
||||
[4.708336, 50.875628, 38.0],
|
||||
[4.708350, 50.875638, 38.0],
|
||||
[4.708412, 50.875685, 38.0],
|
||||
[4.708322, 50.875717, 37.75],
|
||||
[4.708046, 50.875813, 37.75],
|
||||
[4.707083, 50.876156, 38.0],
|
||||
[4.706788, 50.876294, 38.25],
|
||||
[4.706735, 50.876321, 38.0],
|
||||
[4.706291, 50.876602, 39.0],
|
||||
[4.706168, 50.876681, 38.75],
|
||||
[4.706094, 50.876714, 38.75],
|
||||
[4.706058, 50.876727, 38.75],
|
||||
[4.705972, 50.876755, 38.5],
|
||||
[4.705931, 50.876768, 38.5],
|
||||
[4.705614, 50.876870, 38.0],
|
||||
[4.705423, 50.876932, 38.0],
|
||||
[4.705400, 50.876939, 38.0],
|
||||
[4.704857, 50.877116, 36.5],
|
||||
[4.704725, 50.877159, 36.0],
|
||||
[4.704683, 50.877183, 36.0],
|
||||
[4.704666, 50.877191, 36.0],
|
||||
[4.704644, 50.877203, 36.0],
|
||||
[4.704412, 50.877323, 35.0],
|
||||
[4.704041, 50.877569, 33.75],
|
||||
[4.703626, 50.877883, 32.5],
|
||||
[4.703643, 50.877935, 32.25],
|
||||
[4.703401, 50.878420, 30.75],
|
||||
[4.703191, 50.878656, 29.75],
|
||||
[4.703075, 50.878735, 30.0],
|
||||
[4.702846, 50.878849, 29.25],
|
||||
[4.702758, 50.878898, 28.75],
|
||||
[4.702447, 50.879023, 27.25],
|
||||
[4.702440, 50.879099, 26.75],
|
||||
[4.702453, 50.879159, 26.25],
|
||||
[4.702343, 50.879173, 27.0],
|
||||
[4.702217, 50.879215, 28.0],
|
||||
[4.702176, 50.879240, 28.0],
|
||||
[4.702137, 50.879274, 28.0],
|
||||
[4.701863, 50.879520, 27.25],
|
||||
[4.701712, 50.879630, 26.25],
|
||||
[4.701582, 50.879708, 25.75],
|
||||
[4.701521, 50.879736, 25.25],
|
||||
[4.701074, 50.879938, 24.0],
|
||||
[4.700933, 50.880001, 23.25],
|
||||
[4.700731, 50.879935, 23.25],
|
||||
[4.700609, 50.879880, 23.5],
|
||||
[4.700567, 50.879908, 23.25],
|
||||
[4.700470, 50.879953, 23.0],
|
||||
[4.700436, 50.879922, 23.25],
|
||||
[4.700399, 50.879888, 23.25],
|
||||
[4.700077, 50.880030, 22.75],
|
||||
[4.700045, 50.880001, 22.75],
|
||||
[4.700029, 50.879984, 23.0],
|
||||
[4.699997, 50.879997, 22.75]
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in new issue
Block a user