An OpenStreetMap App written in PicoLisp (Pt. 3)
7 min read
This is the third part of a little series which explains how to write a small app in PicoLisp that shows the nearest ice cream shops nearby. You might want to read the first two parts first:
At the end of the last post, we had a little app that displays our current location on Open Street Maps. Now we want to find all icecream shops nearby with help of the Open Street Map API.
Working with the Open Street Map API
OpenStreetMap has a convenient API that lets you search for specific facilities. There is also a frontend on overpass-turbo.eu to play around and understand how it works. For example, this query returns all drinking water positions:
If you navigate to "Export", you can download the raw query data, which looks like this:
node [amenity=drinking_water] (52.50882953708777,13.415980339050291,52.53089536100559,13.46125602722168); out
The actual query is surrounded by
out. The first parameter is
amenity is the "key" and
drinking_water is the "value". Besides "drinking_water", there are of course also other amenities we can look for which are listed here. There is also an amenity called "Ice Cream" - exactly what we are looking for! The second parameter in the example is the area we are searching, defined by the North, East, South and West borders.
With this information, we can already form a little CURL request to play around with the queries:
And we get back an xml-file with one ice cream shop per node:
<?xml version="1.0" encoding="UTF-8"?> <osm version="0.6" generator="Overpass API 0.7.57 93a4d346"> <note>The data included in this document is from www.openstreetmap.org. The data is made available u nder ODbL.</note> <meta osm_base="2022-05-18T09:40:07Z"/> <node id="753361477" lat="52.5699404" lon="13.4106207"> <tag k="addr:city" v="Berlin"/> <tag k="addr:country" v="DE"/> <tag k="addr:housenumber" v="4"/> <tag k="addr:postcode" v="13187"/> <tag k="addr:street" v="Berliner Straße"/> <tag k="addr:suburb" v="Pankow"/> <tag k="amenity" v="ice_cream"/> <tag k="contact:facebook" v="https://www.facebook.com/Eisspatz/"/> <tag k="cuisine" v="ice_cream"/> <tag k="name" v="Eisspatz"/> <tag k="opening_hours" v="Sa-Mo 12:00-20:00; Tu-Fr 12:00-19:00"/> </node> <node id="2936989595" lat="52.5682129" lon="13.3993821"> ... </node> ... </osm>
Now we don't only want to find ice cream shops, but also supermarkets (because these might sell icecream, too). The key-value pair for supermarkets in the OpenStreetMap API is
[shop=supermarket]. With this we can get the data we need, but there is still some work to do:
- Call the API from within PicoLisp,
- Convert the XML data into a nicer data format.
Calling the API from PicoLisp
This task could actually be easy - simply use the
call function to invoke a system command and use
(call "curl" "https://<URL>"). Unfortunately, it doesn't work, because the Android system does not give our PilBox app permissions to execute
Luckily we can use the built-in ``ssl``` function for this as well. The syntax is easy:
(ssl 'host 'path . prg) -> any
prgin an input stream (using
in) from "@bin/ssl" requesting
The host is "overpass-api.de" and the path is
/api/.... So our request looks like this:
(ssl "overpass-api.de" (pack "/api/interpreter?data=node++[amenity=ice_cream]++(" <Location-Borders> ");out;" ) ( <what to do with the data> )
How do we get the four borders of our map? This is actually easy because it is already tracked within the app. In order to use this data, let's define another global variable
(local) (*IceCreamOsm *Latitude *Longitude)
and set the OpenStreetMap
*Osm variable to it (for example within
(start (scl 6) (setq *Osm '*IceCreamOsm) (task -6000 1000 ...
If you now type
*IceCreamOsm in the pty REPL, you will see a list with four values (representing North, South, West, East) and the map's zoom factor.
icecream: *IceCreamOsm -> ((142536613 . 142609643) (193327006 . 193468454) . 12)
Now all we need to do is to convert these data back to the format that the overpass API is expecting. This is simple math as explained in this post: subtracting 90° from the latitude and 180° from the longitude, and format the output according to the scale. For example, we can get the formatted North coordinate with this:
(format (- (caar *IceCreamOsm) 90.0) *Scl )
However we should also catch the case that
*IceCreamOsm is empty because the map is not drawn yet. Let's add a fallback solution so that we canstill fetch data in the background while the map is loading.
(format (- (or (caar Osm) (- *Latitude 0.1)) 90.0) *Scl )
This sets the North boundary to 0.1° distance from the current location, which corresponds roughly to a 11 km radius around the current location.
Converting from XML to list format
Now we need to format the XML output on based on our needs. As we could see above, the output looks somewhat like this:
<node id="753361477" lat="52.5699404" lon="13.4106207"> <tag k="addr:city" v="Berlin"/> <tag k="addr:country" v="DE"/> <tag k="addr:housenumber" v="4"/> <tag k="addr:postcode" v="13187"/> <tag k="addr:street" v="Berliner Straße"/> <tag k="addr:suburb" v="Pankow"/> <tag k="amenity" v="ice_cream"/> <tag k="contact:facebook" v="https://www.facebook.com/Eisspatz/"/> <tag k="cuisine" v="ice_cream"/> <tag k="name" v="Eisspatz"/> <tag k="opening_hours" v="Sa-Mo 12:00-20:00; Tu-Fr 12:00-19:00"/> </node>
This is a lot of information we don't actually need. Let's keep it simple and only extract the shop name and the GPS position in the following format:
(<name> <latitude> . <longitude>). This will form a list similar to this one:
(("Delabuu" 142545086 . 193362049) ("Eiscafé Gelato degli Angeli" 142587148 . 193401989) ("Eisspatz" 142569940 . 193410621) ("Tribeca" 142537430 . 193421053) ("Eis Henri" 142545990 . 193390389) ("Hijís Gelateria" 142551831 . 193413717) ("Eis & Back" 142551954 . 193366761) ("Eislabor" 142541762 . 193422135) ...)
In order to work conveniently with XML data, we can import the
xm.l library in the `once`` function.
(once (load "@lib/xm.l") )
Similarly to the json-library, the
xml function takes XML data and provides some functions to transform it to a list. First we check if the xml output is valid (
xml?), and if yes, we travel through the whole XML-tree and visit each valid node.
(when (xml?) (for L (xml) (when (== 'node (car L))
For each valid node, we want to take the
lon attributes from the root as well as the
name attribute, and
cons these three items to a list. Then we form a final output list with
(make (cons (attr (find '((X) (= '(k . "name") (caadr X))) (cddr L)) 'v ) (+ 90.0 (format (attr L 'lat) *Scl)) (+ 180.0 (format (attr L 'lon) *Scl)) )
Fetching the data on button press
We still need to define when the data should actually be fetched. Let's put the
ssh function from above into a function
iceCream and define it in a separate file
lib.l to keep our code more readable. Also we can replace the API variables by local variables:
(de iceCream (Osm Category Type) (ssl "overpass-api.de"
Osm is the current map data and
Type are the key/value pairs from the Overpass API. Then we load the
lib.l file in the
(once (scl 6) (load "@lib/xm.l" "icecream/lib.l") )
Next, we define the output lists as
*IceList (for ice cream shops) and
*SupermarketList (for the supermarkets):
(local) (*IceCreamOsm *Latitude *Longitude *IceList *SupermarketList)
Now we can add the
iceCream function as action to our button and store the output in the global variables.
(gui '(+Style +Button) "button-icon bg-white mb-5 mx-3" T "icecream/img/ice.png" '(setq *IceList (iceCream *IceCreamOsm "amenity" "ice_cream")) ) (gui '(+Style +Button) "button-icon bg-white mb-5 mx-3" T "icecream/img/supermarket.png" '(setq *SupermarketList (iceCream IceCreamOsm "shop" "supermarket")) ) )
Displaying the data on the map
Finally we can display the data in our map by looping over the
*SupermarketList respectively and add a little icon and the shop name with the
(for Ice *IceList (<poi> (cadr Ice) (cddr Ice) "icecream/img/ice-mini.png" "0.1" "1.0" (car Ice) 0 "red") )
Unfortunately the Overpass API sometimes seem a bit unreliable. It can take quite a long time to receive back the data, and sometimes there is no valid data at all. In the last (and final) post of this series, we will add Server-Side Events to let the user know what is going on.
You can find the final code for this app (including server-sent events) under this link and the zip file here.