StaticMapShellScript

From OpenStreetMap Wiki
Jump to navigation Jump to search

Problem

We want to scriptably (headlessly) convert a set of co-ordinates + zoom level into a PNG static map.

As of Jan 2020, there seems to be no extant free service that allows you to input co-ordinates, and get back an image (png/jpg). All the systems are either:

  • unmaintained/broken
  • very heavily rate limited (a few per day)
  • need an API key (and payment)

Summary

So... here's how to do it (on Linux). There are a number of parts to the recipe:

  1. To turn the slippymap data into a PNG, the JS needs to be evaluated. The best way to do this is a headless web browser. Of these, wkhtml2pdf, phantomjs, casperjs, are all deprecated, while chromium-browser is no longer packaged for ubuntu in an automation-friendly way (snaps will not run for users without $HOME in /home). This leaves firefox, which does work.
  2. We need to load the slippymap, wait a suitable length of time for the JS to run, then screenshot.
  3. However, firefox's --screenshot tends to trigger too early. A trick here is to embed a slow-loading iframe to force firefox to wait. But we don't want to have to create temporary web-pages, so use a data: url.
  4. And if we're running firefox automated from a daemon, we need to make that work ($HOME, xvirtual-framebuffer, file-locking)
  5. Then, we need to crop the resulting image.
  6. Also...while the normal maps are parameterised by center+zoom, the exportable maps (the only ones that can be embedded in iframes) are parameterised by bounding-box.

The script

Here is a recipe, you may need to adapt it...

#1. Assume you have the latitude,longitude and zoom level.
$lat    = 52.0
$lon    = 0.1
$zoom   = 14
#2. Compute the correct URL for the slippymap. %2C is a comma. 
$scale  = round(360 * (0.5**$zoom),6);  #https://wiki.openstreetmap.org/wiki/Zoom_levels
$left   = $lon - $scale;
$right  = $lon + $scale;
$top    = $lat - $scale;
$bottom = $lat + $scale;
$url="https://www.openstreetmap.org/export/embed.html?bbox=$left%2C$top%2C$right%2C$bottom&layer=mapnik&marker=$lat%2C$lon";
#3. Generate the webpage, and convert to a data-url. 
#Note that, in some earlier versions of firefox, you may have to set the iframe width/height in pixels explicitly.
#Note that delay.php is a trivial script that sleeps for 4 seconds before responding. It means that the main iframe's JS has
#time to complete, before firefox thinks that the page is "ready" and screenshots it. Or use a site such as slowwly
$delay_ms = 4000;  	#experimentally, this gives enough time for openstreetmap servers to respond in full.
$page = "<html><body>".
        "<iframe style='width:100vw; height:100vh' frameborder=0 scrolling=no marginheight=0 marginwidth=0 src='$url'></iframe>". 
        "<iframe src='http://localhost/delay.php?sleep=$sleep_ms' style='display:none' width=1 height=1></iframe>".
        "</body></html>";
$data = "data:text/html;base64,".base64_encode($page);	//Make this "micro webpage" a data-url.
#4. Now, pass that data-url to firefox screenshot. The dimensions here set the dimensions of the final result.
firefox --headless --window-size 1600,1200 --screenshot output.png '$data'
#5. If you are running firefox as a daemon user (e.g. from a PHP script as www-data), you may have to wrap this, 
#or you will run afoul of restrictions from systemd.
#Note that apache's /tmp is a systemd private tmp, and is not the "real" /tmp. 
#The flock forces serialisation if there should be multiple competing processes.
$home="/tmp/staticmap_home" 
export HOME=$home; timeout 20 xvfb-run -a flock -w 50 $home firefox --headless etc....
#6. Finally, you may want to crop this, to remove for example the [+/-] control. Use convert (from ImageMagick)
#Optionally, add the "-normalize" flag to make the colours stronger.
convert output.png -gravity center -crop 1400x1000 final.png

Summary

This works, it's a mix of PHP and bash, and works on a server I control (currently running Ubuntu 18.04). I wrote the above script, and I hereby release it into the public domain.

Alternative: use Midori

In Ubuntu 22.04, Firefox is also packaged within Snap. We need a web browser which is modern enough to run current javascript, but obscure enough to escape the snap-ocalypse. Instead of firefox, use midori, within the xvbf-run, as above:

 sh -c "midori -p $url & sleep 2; midori -e fullscreen; sleep 3; xwd -root | convert xwd:- $output; killall midori; "

Notes:

  • The timeout is now explicit (via the sleeps) rather than trying to somehow make Firefox wait "till it's ready".
  • You can use the same data-url hack as above, but it's not necessary, and you can simply use the normal URL.
  • This is still capable of running within the xvfb-run subsystem above; if you don't want to, then omit the "sh -c" outer layer.
  • It uses xwd (x window dump), part of the x11-apps package, to take the screenshot, and then convert (ImageMagick) to read the xwd format and turn it into a pdf or png.
  • Don't forget the sleep before making midori go fullscreen.
  • If you are not running this within xvfb-run, and therefore have a window-manager, then xwd needs to be invoked with -id $win_id rather than -root, where $win_id is the window id of the Midori window. You can find this with, for example:
 win_id=$(wmctrl -lx | grep midori.Midori | cut -f 1 -d ' ')