find-a-bench.com / View source on Github

Introduction

This project came about as I am an avid walker/hiker, and have stumbled across many a bench out in the countryside, seemingly in the middle of nowhere. Similarly there have been many times where I’ve been walking and longed for a bench to sit down on.

Image showing find-a-bench.com website.

I often use OpenStreetMap (OSM) (and tools that utilise it) to plan and map routes I wanted to go. Through this regular usage I learned that OSM stores data about far more than just where hills, rivers, streets, and addresses are. It has a plethora of information for all sorts of data points, such as the location of drinking water, fruit trees, and (you guessed it) benches.

Approaching this project I knew I wanted to keep it really light and simple, so decided to make it with pure HTML, CSS, and JavaScript.

Research

After a little bit of searching, I found out I’d need to be requesting data from OSM’s Overpass API. I used overpass turbo to learn how requests should be structured, and the kind of data you get back.

Having seen a few other websites that used the Overpass API, and how laggy they could be, I knew I wanted to try and keep my web app as performant as possible. (Especially since I wanted it to be usable on mobile devices too.)

There were 3 main things that other sites were often doing that I felt were the culprits for poor performance:

  1. Returning results for huge areas - e.g. allowing a search for all the street lights in a 10km city block.
  2. Displaying markers for every single result.
  3. Not clearing previous results when a new request is made.

My solutions:

  1. Only search a 1km radius.
  2. Wasn’t an issue for me due to the smaller search radius. (Though perhaps in future I’ll change this.)
  3. Clear the results and markers every time a new request was made. (This could be improved in future by not actually deleting old results, just hiding them, and then upon new requests checking if the hidden results are within the search radius and displaying them.)

For displaying results, I decided to try out Leaflet.js, as it looked very simple to set up and configure.

Implementation

When the page loads, it grabs the user’s location from their device. If permission is not granted for this, nothing happens other than a failure message being displayed, prompting the user to check location permissions. (I won’t go over all the code in the project here, just key interesting bits.)

function getGeoLocation(){
    if (!navigator.geolocation) {
        setTextContent(loc, `Location not supported by browser.`);
    } else {
        navigator.geolocation.getCurrentPosition(geoSuccess, geoError);
    }
}

function geoError() {
    setTextContent(loc, `Failed to get your location! \nDoes your device have location enabled? \nDid you allow permission?`);
}

If the request for location is successful however, we display the user’s coordinates, zoom the map to them, remove existing markers, and then make the call to the Overpass API via the getBenchesByCoordinates() function.

async function geoSuccess(position) {
    const lat = position.coords.latitude;
    const lon = position.coords.longitude;

    setTextContent(loc, `Your location: ${lat}, ${lon}`);
    map.setView(new L.LatLng(lat, lon), 14);
    removeMarkers(map, markers); 
    handleResult(await getBenchesByCoordinates(lat, lon, searchRadius), lat, lon); 
}

To allow the user to click/long tap on the map to search an area, I used one of Leaflet’s in-built functions,

map.on("contextmenu", async function (event) { /* more code */ });

and within the square brackets the code is basically identical to the last 4 lines of the geoSuccess function, but we use event.latlng values given by Leaflet in place of the values from navigator.geolocation.getCurrentPosition.

The API call function: All fairly self-explanatory, but the timeout could be shorter.

async function getBenchesByCoordinates(lat, lon, searchRadius){
    const api = await fetch('https://www.overpass-api.de/api/interpreter?', {
        method: 'POST',
        headers: {
            'Accept': 'application/json',
            'Content-Type': 'application/json'
        },
        body:
        `[out:json][timeout:25];node(around:${searchRadius},${lat},${lon})[amenity=bench];out center;`
    });

    return await api.json();
}

Once we receive a response from the API, we check whether it returned any results. If 0 results are returned, we inform the user via modifying some text and placing a red circle on the area they searched.

If we do receive results, we place a marker for each one, and again place a circle on the area they searched, this time in green.

function handleResult(result, lat, lon){
    if(result.elements.length == 0){
        setTextContent(benchless, `There are no mapped benches within ${searchRadius}m. :(`);
        placeCircle(lat, lon, "red", markers);
        return false;
    }else{
        setTextContent(benchless, "");
        placeMarkers(result);
        placeCircle(lat, lon, "green", markers);
        return true;
    }
}

By adding a ‘popup’ the markers could then be clicked on by the user to display exact coordinates, plus I generate Google Maps and OSM links to navigate to them.

function placeMarkers(result){
    result.elements.forEach(element => {
        markers.push(
            L.marker([element.lat, element.lon], {alt: `Bench at latitude ${element.lat}, longitude ${element.lon}.`}).addTo(map)
            .bindPopup(
            'Bench!<br>' 
            + element.lat + ', ' + element.lon +
            '<br><a href="https://www.google.co.uk/maps/dir//' + element.lat + ',' + element.lon + '" target="_blank">Google Maps Directions</a>' +
            '<br><a href="https://www.openstreetmap.org/directions?from=&to=' + element.lat + '%2C' + element.lon + '#map=17/' + element.lat + '/' + element.lon + '" target="_blank">OSM Directions</a>')
        );
    });
}

That’s pretty much it for how the website works.

Testing & Deployment

I also wanted this project to have automated testing and deployment. Both are handled via Github Actions. The website is deployed to https://eddierowe.github.io/find-a-bench/. The unit tests are run whenever code is pushed to the main branch or a pull request is made, and a coverage report is uploaded to Codecov.

Testing is very limited,( codecov ), and at the moment only the API function has full test coverage. The API should be mocked for this, but I haven’t got around to it yet.

Two tests are made on the API, one checks that we receive "Overpass API" from the results’ generator parameter. The second test checks a known city location that has multiple benches, to ensure we receive non-negative results.

Summary

A very small project, I only worked on it for around a week. Good fun and nice to have a useful application at the end of it.

  • Built with JavaScript, Leaflet.js, CSS, & HTML.
  • Uses Jest for unit CI testing, CD via GitHub Actions.
  • Requests user location then queries the Overpass API for nearby bench coordinates.
  • Manually searchable via right-clicking anywhere on map.