(local req (require :http.request)) (local { : dict_to_query } (require :http.util)) (local json (require :json)) (local cqueues (require :cqueues)) (import-macros { : define-tests : expect : expect= : expect-near } :assert) (local { : view } (require :fennel)) (local f% string.format) (fn sinh [x] (/ (- 1 (math.exp (* -2 x))) (* 2 (math.exp (- x))))) (expect (< (math.abs (- (sinh 2) 3.626860407847)) 0.001)) (fn tile->latlon [xtile ytile zoom] (let [n (^ 2 zoom) lon-deg (- (/ (* xtile 360) n) 180.0) lat-rad (math.atan (sinh (* math.pi (- 1 (/ (* 2 ytile) n)))) )] (values (/ (* lat-rad 180) math.pi) lon-deg))) (let [(lat lon) (tile->latlon 0 0 0)] (expect= lon -180) (expect-near lat 85.05112877) ) (let [(lat lon) (tile->latlon 232798 103246 18)] (expect-near lon 139.699401855) (expect-near lat 35.6595278648) ) (fn latlon->tile [lat lon zoom] (let [n (^ 2 zoom) x (* n (/ (+ lon 180) 360)) t1 (/ (* lat math.pi) 180) t (math.log (+ (math.tan t1) (/ 1 (math.cos t1)))) y (* (- 1 (/ t math.pi)) (/ n 2))] (values x y))) (let [(x y) (latlon->tile 52.1234 -0.53 17)] (expect= (math.floor x) 65343) (expect= (math.floor y) 43221)) (fn overpass [lat lon zoom] (let [width (/ 360 (^ 2 zoom)) n (+ lat width) ;XXX adjust for latitude w (- lon width) s lat e lon] (-> [ "[out:json];" (f% "way(%f,%f,%f,%f)['highway'];" s w n e) "(._;>;);" "out;" ] (table.concat "\n")))) (fn canvas [elements] (let [nodes {} lines {}] (each [_ e (ipairs elements)] (case e.type :node (tset nodes e.id e) :way (tset lines e.id (icollect [_ nd (ipairs e.nodes)] (let [node (. nodes nd) (tx ty) (latlon->tile node.lat node.lon 17)] ;;(print e.tags.name e.id e.name node.lat node.lon) [ tx ty ]))))) lines)) (fn file-exists? [name] (match (io.open name :r) f (do (f:close) true) _ false)) (fn unparsed-for-xyz [x y zoom] (let [(lat lon) (tile->latlon x y zoom) o (overpass lat lon zoom) r (req.new_from_uri "https://overpass-api.de/api/interpreter") query { :data o }] (tset r.headers ":method" "POST") (r:set_body (dict_to_query query)) (let [(headers stream) (r:go)] (if (= (headers:get ":status") "429") nil (stream:get_body_as_string))))) ;; if we have json in disk, return it ;; if we have an empty file on disk, that signifies a request in ;; flight, so return a "pending" sentinel ;; if we have no disk file, kick off a request and send "pending" sentinel ;; we'd like to have a way for completed background fetch to signal ;; so that the map can be redrawn (fn polylines [cq x y zoom] (let [k (.. x "_" y "_" zoom) pathname (.. "/tmp/tiles/" k ".json")] (if (file-exists? pathname) (let [data (with-open [i (io.open pathname :r)] (i:read "*a"))] (if (= data "") [] (canvas (. (json.decode data) :elements)))) (let [out (io.open pathname :w)] (cq:wrap (fn [] (print "getting " k) (var json nil) (with-open [f out] (while (not json) (set json (unparsed-for-xyz x y zoom)) (when (not json) (print "sleeping " k) (cqueues.sleep (math.random 2 6)))) (print "got " k) (f:write json) true))) [] ; return no lines for now )))) { : polylines : latlon->tile }